一、开发构思
1.开发一款小超市用的进销存系统,开发环境是Windows,包括销售、采购、仓库、系统等模块。
采用vue3+SpringBoot + MySql进行开发,完成后来布署在docker容器。
2、新建MySql数据库,数据库密码:*******,数据库名:JXC。
二、开发过程
1、基础的项目架构设计
1、首先创建MySQL数据库:
-- 创建数据库
CREATE DATABASE JXC DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
-- 使用数据库
USE JXC;
2、主要数据表设计:
-- 用户表
CREATE TABLE sys_user (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) NOT NULL UNIQUE,
password VARCHAR(100) NOT NULL,
real_name VARCHAR(50),
phone VARCHAR(20),
status TINYINT DEFAULT 1,
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
-- 商品表
CREATE TABLE product (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
product_code VARCHAR(50) NOT NULL UNIQUE,
product_name VARCHAR(100) NOT NULL,
category_id BIGINT,
unit VARCHAR(20),
price DECIMAL(10,2),
stock INT DEFAULT 0,
status TINYINT DEFAULT 1,
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
-- 供应商表
CREATE TABLE supplier (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
supplier_name VARCHAR(100) NOT NULL,
contact_person VARCHAR(50),
phone VARCHAR(20),
address VARCHAR(200),
status TINYINT DEFAULT 1,
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
-- 采购单表
CREATE TABLE purchase_order (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
order_no VARCHAR(50) NOT NULL UNIQUE,
supplier_id BIGINT,
total_amount DECIMAL(10,2),
status TINYINT DEFAULT 0,
create_user_id BIGINT,
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
-- 采购单明细表
CREATE TABLE purchase_order_detail (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
purchase_order_id BIGINT,
product_id BIGINT,
quantity INT,
price DECIMAL(10,2),
amount DECIMAL(10,2),
create_time DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- 销售单表
CREATE TABLE sale_order (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
order_no VARCHAR(50) NOT NULL UNIQUE,
customer_name VARCHAR(100),
total_amount DECIMAL(10,2),
status TINYINT DEFAULT 0,
create_user_id BIGINT,
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
-- 销售单明细表
CREATE TABLE sale_order_detail (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
sale_order_id BIGINT,
product_id BIGINT,
quantity INT,
price DECIMAL(10,2),
amount DECIMAL(10,2),
create_time DATETIME DEFAULT CURRENT_TIMESTAMP
);
3、项目结构建议:
jxc-system/
├── frontend/ # Vue3前端项目
│ ├── src/
│ │ ├── api/ # API接口
│ │ ├── components/ # 组件
│ │ ├── router/ # 路由
│ │ ├── store/ # Pinia状态管理
│ │ └── views/ # 页面
│ └── package.json
│
├── backend/ # SpringBoot后端项目
│ ├── src/
│ │ ├── main/
│ │ │ ├── java/com/jxc/
│ │ │ │ ├── config/ # 配置类
│ │ │ │ ├── controller/ # 控制器
│ │ │ │ ├── service/ # 服务层
│ │ │ │ ├── dao/ # 数据访问层
│ │ │ │ ├── entity/ # 实体类
│ │ │ │ └── common/ # 公共类
│ │ │ └── resources/
│ │ └── test/
│ └── pom.xml
│
└── docker/ # Docker相关配置
├── docker-compose.yml
└── Dockerfile
4、后端主要依赖(pom.xml):
<dependencies>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- MyBatis Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3</version>
</dependency>
<!-- MySQL -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
<!-- JWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
5、前端主要依赖(package.json):
{
"dependencies": {
"vue": "^3.3.0",
"vue-router": "^4.2.0",
"pinia": "^2.1.0",
"axios": "^1.4.0",
"element-plus": "^2.3.0",
"@element-plus/icons-vue": "^2.1.0"
}
}
6、Docker部署配置(docker-compose.yml):
version: '3'
services:
mysql:
image: mysql:8.0
container_name: jxc-mysql
environment:
MYSQL_ROOT_PASSWORD: ******
MYSQL_DATABASE: JXC
ports:
- "3306:3306"
volumes:
- ./mysql-data:/var/lib/mysql
backend:
build:
context: ./backend
dockerfile: Dockerfile
container_name: jxc-backend
ports:
- "8080:8080"
depends_on:
- mysql
frontend:
build:
context: ./frontend
dockerfile: Dockerfile
container_name: jxc-frontend
ports:
- "80:80"
depends_on:
- backend
基础的项目架构设计完成。
2、各模块的开发
2.1开发用户管理模块
2.1.1在后端创建相关的代码:
1.实体类:
backend\src\main\java\com\jxc\entity\SysUser.java
@Data
@TableName("sys_user")
public class SysUser {
@TableId(type = IdType.AUTO)
private Long id;
private String username;
private String password;
private String realName;
private String phone;
private Integer status;
private LocalDateTime createTime;
private LocalDateTime updateTime;
}
2.登录请求和响应DTO:
backend\src\main\java\com\jxc\dto\LoginRequest.java
@Data
public class LoginRequest {
private String username;
private String password;
}
backend\src\main\java\com\jxc\dto\LoginResponse.java
@Data
public class LoginResponse {
private String token;
private SysUser user;
}
3. 统一响应对象:
backend\src\main\java\com\jxc\common\Result.java
@Data
public class Result<T> {
private Integer code;
private String message;
private T data;
public static <T> Result<T> success(T data) {
Result<T> result = new Result<>();
result.setCode(200);
result.setMessage("success");
result.setData(data);
return result;
}
public static <T> Result<T> error(String message) {
Result<T> result = new Result<>();
result.setCode(500);
result.setMessage(message);
return result;
}
}
4.JWT工具类:
backend\src\main\java\com\jxc\util\JwtUtil.java
@Component
public class JwtUtil {
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expiration}")
private Long expiration;
public String generateToken(String username) {
return Jwts.builder()
.setSubject(username)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + expiration))
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
public String getUsernameFromToken(String token) {
return Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody()
.getSubject();
}
public boolean validateToken(String token) {
try {
Jwts.parser().setSigningKey(secret).parseClaimsJws(token);
return true;
} catch (Exception e) {
return false;
}
}
}
5.Service层:
backend\src\main\java\com\jxc\service\UserService.java
@Service
@RequiredArgsConstructor
public class UserService {
private final SysUserMapper userMapper;
private final PasswordEncoder passwordEncoder;
private final JwtUtil jwtUtil;
public LoginResponse login(LoginRequest request) {
SysUser user = userMapper.selectOne(
new QueryWrapper<SysUser>().eq("username", request.getUsername())
);
if (user == null || !passwordEncoder.matches(request.getPassword(), user.getPassword())) {
throw new RuntimeException("用户名或密码错误");
}
if (user.getStatus() != 1) {
throw new RuntimeException("账号已被禁用");
}
String token = jwtUtil.generateToken(user.getUsername());
LoginResponse response = new LoginResponse();
response.setToken(token);
response.setUser(user);
return response;
}
}
6.Controller层:
backend\src\main\java\com\jxc\controller\AuthController.java
@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class AuthController {
private final UserService userService;
@PostMapping("/login")
public Result<LoginResponse> login(@RequestBody LoginRequest request) {
try {
LoginResponse response = userService.login(request);
return Result.success(response);
} catch (Exception e) {
return Result.error(e.getMessage());
}
}
}
2.1.2在前端创建相关代码:
1.创建相关代码:
frontend\src\api\auth.ts
import request from '@/utils/request'
export function login(data: {username: string, password: string}) {
return request({
url: '/api/auth/login',
method: 'post',
data
})
}
2.创建登录页面:
frontend\src\views\login\index.vue
<template>
<div class="login-container">
<el-form
ref="loginForm"
:model="loginForm"
:rules="loginRules"
class="login-form"
>
<h3 class="title">进销存管理系统</h3>
<el-form-item prop="username">
<el-input
v-model="loginForm.username"
placeholder="用户名"
type="text"
/>
</el-form-item>
<el-form-item prop="password">
<el-input
v-model="loginForm.password"
placeholder="密码"
type="password"
/>
</el-form-item>
<el-form-item>
<el-button
:loading="loading"
type="primary"
style="width: 100%"
@click.prevent="handleLogin"
>
登录
</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import { login } from '@/api/auth'
const router = useRouter()
const loading = ref(false)
const loginForm = reactive({
username: '',
password: ''
})
const loginRules = {
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
password: [{ required: true, message: '请输入密码', trigger: 'blur' }]
}
const handleLogin = async () => {
loading.value = true
try {
const { data } = await login(loginForm)
localStorage.setItem('token', data.token)
localStorage.setItem('user', JSON.stringify(data.user))
ElMessage.success('登录成功')
router.push('/')
} catch (error: any) {
ElMessage.error(error.message || '登录失败')
} finally {
loading.value = false
}
}
</script>
<style lang="scss" scoped>
.login-container {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background-color: #2d3a4b;
.login-form {
width: 400px;
padding: 40px;
background: #fff;
border-radius: 4px;
.title {
text-align: center;
margin-bottom: 30px;
}
}
}
</style>
3.添加路由配置:
frontend\src\router\index.ts
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(),
routes: [
{
path: '/login',
component: () => import('@/views/login/index.vue')
},
{
path: '/',
component: () => import('@/layout/index.vue'),
redirect: '/dashboard',
children: [
{
path: 'dashboard',
component: () => import('@/views/dashboard/index.vue')
}
]
}
]
})
router.beforeEach((to, from, next) => {
const token = localStorage.getItem('token')
if (to.path === '/login') {
if (token) {
next('/')
} else {
next()
}
} else {
if (token) {
next()
} else {
next('/login')
}
}
})
export default router
4.在application.yml中添加JWT配置:
backend\src\main\resources\application.yml
jwt:
secret: your-secret-key
expiration: 86400000 # 24小时
这样,基本的用户认证模块就完成了。主要功能包括:
- 用户登录
- JWT token生成和验证
- 路由守卫
- 登录状态持久化
2.1.3开发用户管理及权限控制
2.1.3.1创建初始管理员
1.先创建一个初始管理员用户,并实现基本的用户管理功能。
-- 角色表
CREATE TABLE sys_role (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
role_name VARCHAR(50) NOT NULL,
role_code VARCHAR(50) NOT NULL UNIQUE,
description VARCHAR(200),
status TINYINT DEFAULT 1,
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
-- 用户角色关联表
CREATE TABLE sys_user_role (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT NOT NULL,
role_id BIGINT NOT NULL,
create_time DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- 插入初始角色数据
INSERT INTO sys_role (role_name, role_code, description) VALUES
('系统管理员', 'ADMIN', '系统管理员,拥有所有权限'),
('普通用户', 'USER', '普通用户,拥有基本操作权限');
-- 插入初始管理员用户
INSERT INTO sys_user (username, password, real_name, status) VALUES
('admin', '$2a$10$X/uME6GXZWxn.Z2P.TgqsOYCvbt3qdUcHvVTQJgBM8yWc8t9YmQdq', '系统管理员', 1);
-- 关联管理员角色
INSERT INTO sys_user_role (user_id, role_id) VALUES (1, 1);
2.创建角色实体类:
backend\src\main\java\com\jxc\entity\SysRole.java
@Data
@TableName("sys_role")
public class SysRole {
@TableId(type = IdType.AUTO)
private Long id;
private String roleName;
private String roleCode;
private String description;
private Integer status;
private LocalDateTime createTime;
private LocalDateTime updateTime;
}
3.创建用户管理相关DTO:
backend\src\main\java\com\jxc\dto\UserDTO.java
@Data
public class UserDTO {
private Long id;
private String username;
private String password;
private String realName;
private String phone;
private Integer status;
private List<Long> roleIds;
}
4.扩展UserService,添加用户管理相关方法:
backend\src\main\java\com\jxc\service\UserService.java
@Service
@RequiredArgsConstructor
public class UserService {
// ... 之前的代码 ...
@Transactional
public void createUser(UserDTO userDTO) {
// 检查用户名是否存在
if (userMapper.selectCount(new QueryWrapper<SysUser>()
.eq("username", userDTO.getUsername())) > 0) {
throw new RuntimeException("用户名已存在");
}
// 创建用户
SysUser user = new SysUser();
BeanUtils.copyProperties(userDTO, user);
user.setPassword(passwordEncoder.encode(userDTO.getPassword()));
userMapper.insert(user);
// 保存用户角色关系
if (userDTO.getRoleIds() != null && !userDTO.getRoleIds().isEmpty()) {
userDTO.getRoleIds().forEach(roleId -> {
userRoleMapper.insert(new SysUserRole(user.getId(), roleId));
});
}
}
@Transactional
public void updateUser(UserDTO userDTO) {
SysUser user = userMapper.selectById(userDTO.getId());
if (user == null) {
throw new RuntimeException("用户不存在");
}
// 更新用户信息
BeanUtils.copyProperties(userDTO, user, "password");
if (StringUtils.hasText(userDTO.getPassword())) {
user.setPassword(passwordEncoder.encode(userDTO.getPassword()));
}
userMapper.updateById(user);
// 更新用户角色关系
if (userDTO.getRoleIds() != null) {
userRoleMapper.delete(new QueryWrapper<SysUserRole>()
.eq("user_id", user.getId()));
userDTO.getRoleIds().forEach(roleId -> {
userRoleMapper.insert(new SysUserRole(user.getId(), roleId));
});
}
}
public void deleteUser(Long id) {
userMapper.deleteById(id);
userRoleMapper.delete(new QueryWrapper<SysUserRole>()
.eq("user_id", id));
}
public IPage<SysUser> getUserList(Page<SysUser> page, String username, String realName) {
return userMapper.selectPage(page, new QueryWrapper<SysUser>()
.like(StringUtils.hasText(username), "username", username)
.like(StringUtils.hasText(realName), "real_name", realName));
}
}
5. 创建用户管理Controller:
backend\src\main\java\com\jxc\controller\UserController.java
@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
@GetMapping
public Result<IPage<SysUser>> list(
@RequestParam(defaultValue = "1") Integer current,
@RequestParam(defaultValue = "10") Integer size,
@RequestParam(required = false) String username,
@RequestParam(required = false) String realName) {
Page<SysUser> page = new Page<>(current, size);
return Result.success(userService.getUserList(page, username, realName));
}
@PostMapping
public Result<?> create(@RequestBody UserDTO userDTO) {
userService.createUser(userDTO);
return Result.success(null);
}
@PutMapping("/{id}")
public Result<?> update(@PathVariable Long id, @RequestBody UserDTO userDTO) {
userDTO.setId(id);
userService.updateUser(userDTO);
return Result.success(null);
}
@DeleteMapping("/{id}")
public Result<?> delete(@PathVariable Long id) {
userService.deleteUser(id);
return Result.success(null);
}
}
6. 创建用户管理页面:
frontend\src\views\system\user\index.vue
<template>
<div class="app-container">
<div class="filter-container">
<el-input
v-model="queryParams.username"
placeholder="用户名"
style="width: 200px"
class="filter-item"
/>
<el-input
v-model="queryParams.realName"
placeholder="姓名"
style="width: 200px"
class="filter-item"
/>
<el-button type="primary" @click="handleQuery">查询</el-button>
<el-button type="primary" @click="handleAdd">新增</el-button>
</div>
<el-table :data="userList" style="width: 100%">
<el-table-column prop="username" label="用户名" />
<el-table-column prop="realName" label="姓名" />
<el-table-column prop="phone" label="电话" />
<el-table-column prop="status" label="状态">
<template #default="scope">
<el-tag :type="scope.row.status === 1 ? 'success' : 'danger'">
{{ scope.row.status === 1 ? '启用' : '禁用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作">
<template #default="scope">
<el-button type="text" @click="handleEdit(scope.row)">编辑</el-button>
<el-button type="text" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
v-model:current-page="queryParams.current"
v-model:page-size="queryParams.size"
:total="total"
@current-change="handleQuery"
/>
<!-- 用户表单对话框 -->
<el-dialog
:title="dialogTitle"
v-model="dialogVisible"
width="500px"
>
<el-form ref="userForm" :model="userForm" :rules="rules" label-width="80px">
<el-form-item label="用户名" prop="username">
<el-input v-model="userForm.username" />
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input v-model="userForm.password" type="password" />
</el-form-item>
<el-form-item label="姓名" prop="realName">
<el-input v-model="userForm.realName" />
</el-form-item>
<el-form-item label="电话" prop="phone">
<el-input v-model="userForm.phone" />
</el-form-item>
<el-form-item label="状态">
<el-switch v-model="userForm.status" :active-value="1" :inactive-value="0" />
</el-form-item>
<el-form-item label="角色">
<el-select v-model="userForm.roleIds" multiple>
<el-option
v-for="role in roleOptions"
:key="role.id"
:label="role.roleName"
:value="role.id"
/>
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
// ... 组件逻辑代码将在下一条消息中提供
</script>
2.1.3.2实现用户管理功能(创建、修改、删除用户)
1.完成用户管理页面的组件逻辑代码:
frontend\src\views\system\user\index.vue
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import type { FormInstance } from 'element-plus'
// 查询参数
const queryParams = reactive({
current: 1,
size: 10,
username: '',
realName: ''
})
// 用户列表数据
const userList = ref([])
const total = ref(0)
const loading = ref(false)
// 角色选项
const roleOptions = ref([])
// 对话框控制
const dialogVisible = ref(false)
const dialogTitle = ref('')
const userForm = reactive({
id: undefined,
username: '',
password: '',
realName: '',
phone: '',
status: 1,
roleIds: []
})
// 表单校验规则
const rules = {
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 3, max: 20, message: '长度在 3 到 20 个字符', trigger: 'blur' }
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur', when: () => !userForm.id },
{ min: 6, max: 20, message: '长度在 6 到 20 个字符', trigger: 'blur' }
],
realName: [
{ required: true, message: '请输入姓名', trigger: 'blur' }
]
}
const userFormRef = ref<FormInstance>()
// 获取用户列表
const getUserList = async () => {
loading.value = true
try {
const { data } = await request({
url: '/api/users',
method: 'get',
params: queryParams
})
userList.value = data.records
total.value = data.total
} catch (error: any) {
ElMessage.error(error.message || '获取用户列表失败')
} finally {
loading.value = false
}
}
// 获取角色列表
const getRoleList = async () => {
try {
const { data } = await request({
url: '/api/roles',
method: 'get'
})
roleOptions.value = data
} catch (error: any) {
ElMessage.error(error.message || '获取角色列表失败')
}
}
// 查询按钮点击事件
const handleQuery = () => {
queryParams.current = 1
getUserList()
}
// 重置表单
const resetForm = () => {
userForm.id = undefined
userForm.username = ''
userForm.password = ''
userForm.realName = ''
userForm.phone = ''
userForm.status = 1
userForm.roleIds = []
userFormRef.value?.resetFields()
}
// 新增按钮点击事件
const handleAdd = () => {
resetForm()
dialogTitle.value = '新增用户'
dialogVisible.value = true
}
// 编辑按钮点击事件
const handleEdit = (row: any) => {
resetForm()
dialogTitle.value = '编辑用户'
Object.assign(userForm, row)
dialogVisible.value = true
}
// 删除按钮点击事件
const handleDelete = (row: any) => {
ElMessageBox.confirm('确认删除该用户吗?', '提示', {
type: 'warning'
}).then(async () => {
try {
await request({
url: `/api/users/${row.id}`,
method: 'delete'
})
ElMessage.success('删除成功')
getUserList()
} catch (error: any) {
ElMessage.error(error.message || '删除失败')
}
})
}
// 表单提交
const handleSubmit = async () => {
if (!userFormRef.value) return
await userFormRef.value.validate()
try {
const method = userForm.id ? 'put' : 'post'
const url = userForm.id ? `/api/users/${userForm.id}` : '/api/users'
await request({
url,
method,
data: userForm
})
ElMessage.success(`${userForm.id ? '更新' : '创建'}成功`)
dialogVisible.value = false
getUserList()
} catch (error: any) {
ElMessage.error(error.message || `${userForm.id ? '更新' : '创建'}失败`)
}
}
// 页面加载时获取数据
onMounted(() => {
getUserList()
getRoleList()
})
</script>
<style scoped>
.filter-container {
padding-bottom: 10px;
}
.filter-item {
margin-right: 10px;
}
</style>
2.还需要创建一个API请求工具:
frontend\src\utils\request.ts
import axios from 'axios'
import { ElMessage } from 'element-plus'
import router from '@/router'
const request = axios.create({
baseURL: import.meta.env.VITE_API_URL || '/api',
timeout: 5000
})
// 请求拦截器
request.interceptors.request.use(
config => {
const token = localStorage.getItem('token')
if (token) {
config.headers['Authorization'] = `Bearer ${token}`
}
return config
},
error => {
return Promise.reject(error)
}
)
// 响应拦截器
request.interceptors.response.use(
response => {
const res = response.data
if (res.code !== 200) {
ElMessage.error(res.message || '请求失败')
if (res.code === 401) {
localStorage.removeItem('token')
localStorage.removeItem('user')
router.push('/login')
}
return Promise.reject(new Error(res.message || '请求失败'))
}
return res
},
error => {
ElMessage.error(error.message || '请求失败')
return Promise.reject(error)
}
)
export default request
3.在环境配置文件中添加API地址:
frontend\.env.development
VITE_API_URL=http://localhost:8080
用户管理模块就完成了,主要功能包括:
- 用户列表展示(分页、搜索)
- 新增用户
- 编辑用户
- 删除用户
- 用户状态管理
- 用户角色分配
2.1.3.3实现权限控制功能
我们将使用RBAC(基于角色的访问控制)模型。
1.首先创建权限表和角色-权限关联表:
-- 权限表
CREATE TABLE sys_permission (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
parent_id BIGINT,
name VARCHAR(50) NOT NULL,
code VARCHAR(50) NOT NULL UNIQUE,
type TINYINT COMMENT '1:菜单 2:按钮',
path VARCHAR(200),
component VARCHAR(200),
icon VARCHAR(50),
sort_num INT DEFAULT 0,
status TINYINT DEFAULT 1,
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
-- 角色权限关联表
CREATE TABLE sys_role_permission (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
role_id BIGINT NOT NULL,
permission_id BIGINT NOT NULL,
create_time DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- 插入基础权限数据
INSERT INTO sys_permission (parent_id, name, code, type, path, component, icon, sort_num) VALUES
(0, '系统管理', 'system', 1, '/system', 'Layout', 'setting', 1),
(1, '用户管理', 'system:user', 1, 'user', 'system/user/index', 'user', 1),
(1, '角色管理', 'system:role', 1, 'role', 'system/role/index', 'peoples', 2),
(2, '用户新增', 'system:user:add', 2, NULL, NULL, NULL, 1),
(2, '用户编辑', 'system:user:edit', 2, NULL, NULL, NULL, 2),
(2, '用户删除', 'system:user:delete', 2, NULL, NULL, NULL, 3);
-- 为管理员角色分配所有权限
INSERT INTO sys_role_permission (role_id, permission_id)
SELECT 1, id FROM sys_permission;
2.创建权限实体类:
backend\src\main\java\com\jxc\entity\SysPermission.java
@Data
@TableName("sys_permission")
public class SysPermission {
@TableId(type = IdType.AUTO)
private Long id;
private Long parentId;
private String name;
private String code;
private Integer type;
private String path;
private String component;
private String icon;
private Integer sortNum;
private Integer status;
private LocalDateTime createTime;
private LocalDateTime updateTime;
@TableField(exist = false)
private List<SysPermission> children;
}
3. 创建权限注解:
backend\src\main\java\com\jxc\annotation\RequiresPermission.java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequiresPermission {
String value();
}
4.创建权限拦截器:
backend\src\main\java\com\jxc\interceptor\PermissionInterceptor.java
@Component
@RequiredArgsConstructor
public class PermissionInterceptor implements HandlerInterceptor {
private final JwtUtil jwtUtil;
private final UserService userService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
// 如果不是处理方法直接通过
if (!(handler instanceof HandlerMethod)) {
return true;
}
HandlerMethod handlerMethod = (HandlerMethod) handler;
RequiresPermission requiresPermission = handlerMethod.getMethodAnnotation(RequiresPermission.class);
// 如果没有权限注解直接通过
if (requiresPermission == null) {
return true;
}
// 获取token中的用户名
String token = request.getHeader("Authorization");
if (token != null && token.startsWith("Bearer ")) {
token = token.substring(7);
}
String username = jwtUtil.getUsernameFromToken(token);
// 检查用户权限
Set<String> permissions = userService.getUserPermissions(username);
if (!permissions.contains(requiresPermission.value())) {
throw new RuntimeException("没有操作权限");
}
return true;
}
}
5.扩展UserService,添加获取用户权限的方法:
backend\src\main\java\com\jxc\service\UserService.java
@Service
public class UserService {
// ... 之前的代码 ...
public Set<String> getUserPermissions(String username) {
return userMapper.getUserPermissions(username);
}
public List<SysPermission> getUserMenus(String username) {
List<SysPermission> permissions = userMapper.getUserPermissions(username)
.stream()
.filter(p -> p.getType() == 1)
.collect(Collectors.toList());
return buildTree(permissions);
}
private List<SysPermission> buildTree(List<SysPermission> permissions) {
List<SysPermission> tree = new ArrayList<>();
Map<Long, SysPermission> map = new HashMap<>();
// 构建节点Map
permissions.forEach(permission -> map.put(permission.getId(), permission));
// 构建树形结构
permissions.forEach(permission -> {
if (permission.getParentId() == 0) {
tree.add(permission);
} else {
SysPermission parent = map.get(permission.getParentId());
if (parent != null) {
if (parent.getChildren() == null) {
parent.setChildren(new ArrayList<>());
}
parent.getChildren().add(permission);
}
}
});
return tree;
}
}
6.在Controller中使用权限注解:
backend\src\main\java\com\jxc\controller\UserController.java
@RestController
@RequestMapping("/api/users")
public class UserController {
// ... 之前的代码 ...
@GetMapping
@RequiresPermission("system:user:list")
public Result<IPage<SysUser>> list(/* ... */) {
// ...
}
@PostMapping
@RequiresPermission("system:user:add")
public Result<?> create(@RequestBody UserDTO userDTO) {
// ...
}
@PutMapping("/{id}")
@RequiresPermission("system:user:edit")
public Result<?> update(@PathVariable Long id, @RequestBody UserDTO userDTO) {
// ...
}
@DeleteMapping("/{id}")
@RequiresPermission("system:user:delete")
public Result<?> delete(@PathVariable Long id) {
// ...
}
}
7.在前端添加权限指令:
frontend\src\directives\permission.ts
import { DirectiveBinding } from 'vue'
function checkPermission(el: HTMLElement, binding: DirectiveBinding) {
const { value } = binding
const permissions = JSON.parse(localStorage.getItem('permissions') || '[]')
if (value && value instanceof Array) {
const hasPermission = value.some(permission => permissions.includes(permission))
if (!hasPermission) {
el.parentNode?.removeChild(el)
}
} else {
throw new Error('need permissions! Like v-permission="[\'system:user:add\']"')
}
}
export default {
mounted(el: HTMLElement, binding: DirectiveBinding) {
checkPermission(el, binding)
},
updated(el: HTMLElement, binding: DirectiveBinding) {
checkPermission(el, binding)
}
}
8.在用户管理页面中使用权限指令:
frontend\src\views\system\user\index.vue
<template>
<div class="app-container">
<!-- ... 其他代码 ... -->
<el-button
v-permission="['system:user:add']"
type="primary"
@click="handleAdd"
>
新增
</el-button>
<el-table :data="userList">
<!-- ... 其他列 ... -->
<el-table-column label="操作">
<template #default="scope">
<el-button
v-permission="['system:user:edit']"
type="text"
@click="handleEdit(scope.row)"
>
编辑
</el-button>
<el-button
v-permission="['system:user:delete']"
type="text"
@click="handleDelete(scope.row)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
</div>
</template>
9.在登录成功后获取用户权限:
frontend\src\views\login\index.vue
const handleLogin = async () => {
try {
const { data } = await login(loginForm)
localStorage.setItem('token', data.token)
localStorage.setItem('user', JSON.stringify(data.user))
// 获取用户权限和菜单
const { data: permissions } = await getUserPermissions()
localStorage.setItem('permissions', JSON.stringify(permissions))
const { data: menus } = await getUserMenus()
store.commit('SET_MENUS', menus)
router.push('/')
} catch (error) {
ElMessage.error(error.message)
}
}
2.13.4实现角色管理功能
1.首先创建角色管理相关的DTO:
backend\src\main\java\com\jxc\dto\RoleDTO.java
@Data
public class RoleDTO {
private Long id;
private String roleName;
private String roleCode;
private String description;
private Integer status;
private List<Long> permissionIds;
}
2.创建角色Service:
backend\src\main\java\com\jxc\service\RoleService.java
@Service
@RequiredArgsConstructor
public class RoleService {
private final SysRoleMapper roleMapper;
private final SysRolePermissionMapper rolePermissionMapper;
public IPage<SysRole> getRoleList(Page<SysRole> page, String roleName) {
return roleMapper.selectPage(page, new QueryWrapper<SysRole>()
.like(StringUtils.hasText(roleName), "role_name", roleName));
}
@Transactional
public void createRole(RoleDTO roleDTO) {
// 检查角色编码是否存在
if (roleMapper.selectCount(new QueryWrapper<SysRole>()
.eq("role_code", roleDTO.getRoleCode())) > 0) {
throw new RuntimeException("角色编码已存在");
}
// 创建角色
SysRole role = new SysRole();
BeanUtils.copyProperties(roleDTO, role);
roleMapper.insert(role);
// 保存角色权限关系
if (roleDTO.getPermissionIds() != null && !roleDTO.getPermissionIds().isEmpty()) {
roleDTO.getPermissionIds().forEach(permissionId -> {
rolePermissionMapper.insert(new SysRolePermission(role.getId(), permissionId));
});
}
}
@Transactional
public void updateRole(RoleDTO roleDTO) {
SysRole role = roleMapper.selectById(roleDTO.getId());
if (role == null) {
throw new RuntimeException("角色不存在");
}
// 检查角色编码是否重复
if (!role.getRoleCode().equals(roleDTO.getRoleCode())) {
if (roleMapper.selectCount(new QueryWrapper<SysRole>()
.eq("role_code", roleDTO.getRoleCode())) > 0) {
throw new RuntimeException("角色编码已存在");
}
}
// 更新角色信息
BeanUtils.copyProperties(roleDTO, role);
roleMapper.updateById(role);
// 更新角色权限关系
if (roleDTO.getPermissionIds() != null) {
rolePermissionMapper.delete(new QueryWrapper<SysRolePermission>()
.eq("role_id", role.getId()));
roleDTO.getPermissionIds().forEach(permissionId -> {
rolePermissionMapper.insert(new SysRolePermission(role.getId(), permissionId));
});
}
}
@Transactional
public void deleteRole(Long id) {
// 检查是否有用户使用该角色
if (roleMapper.hasUsers(id) > 0) {
throw new RuntimeException("该角色已被用户使用,无法删除");
}
roleMapper.deleteById(id);
rolePermissionMapper.delete(new QueryWrapper<SysRolePermission>()
.eq("role_id", id));
}
public List<Long> getRolePermissions(Long roleId) {
return rolePermissionMapper.selectList(new QueryWrapper<SysRolePermission>()
.eq("role_id", roleId))
.stream()
.map(SysRolePermission::getPermissionId)
.collect(Collectors.toList());
}
}
3.创建角色Controller:
backend\src\main\java\com\jxc\controller\RoleController.java
@RestController
@RequestMapping("/api/roles")
@RequiredArgsConstructor
public class RoleController {
private final RoleService roleService;
@GetMapping
@RequiresPermission("system:role:list")
public Result<IPage<SysRole>> list(
@RequestParam(defaultValue = "1") Integer current,
@RequestParam(defaultValue = "10") Integer size,
@RequestParam(required = false) String roleName) {
Page<SysRole> page = new Page<>(current, size);
return Result.success(roleService.getRoleList(page, roleName));
}
@PostMapping
@RequiresPermission("system:role:add")
public Result<?> create(@RequestBody RoleDTO roleDTO) {
roleService.createRole(roleDTO);
return Result.success(null);
}
@PutMapping("/{id}")
@RequiresPermission("system:role:edit")
public Result<?> update(@PathVariable Long id, @RequestBody RoleDTO roleDTO) {
roleDTO.setId(id);
roleService.updateRole(roleDTO);
return Result.success(null);
}
@DeleteMapping("/{id}")
@RequiresPermission("system:role:delete")
public Result<?> delete(@PathVariable Long id) {
roleService.deleteRole(id);
return Result.success(null);
}
@GetMapping("/{id}/permissions")
@RequiresPermission("system:role:edit")
public Result<List<Long>> getRolePermissions(@PathVariable Long id) {
return Result.success(roleService.getRolePermissions(id));
}
}
4.创建角色管理页面:
frontend\src\views\system\role\index.vue
<template>
<div class="app-container">
<div class="filter-container">
<el-input
v-model="queryParams.roleName"
placeholder="角色名称"
style="width: 200px"
class="filter-item"
/>
<el-button type="primary" @click="handleQuery">查询</el-button>
<el-button
v-permission="['system:role:add']"
type="primary"
@click="handleAdd"
>
新增
</el-button>
</div>
<el-table :data="roleList" style="width: 100%">
<el-table-column prop="roleName" label="角色名称" />
<el-table-column prop="roleCode" label="角色编码" />
<el-table-column prop="description" label="描述" />
<el-table-column prop="status" label="状态">
<template #default="scope">
<el-tag :type="scope.row.status === 1 ? 'success' : 'danger'">
{{ scope.row.status === 1 ? '启用' : '禁用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作">
<template #default="scope">
<el-button
v-permission="['system:role:edit']"
type="text"
@click="handleEdit(scope.row)"
>
编辑
</el-button>
<el-button
v-permission="['system:role:delete']"
type="text"
@click="handleDelete(scope.row)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
v-model:current-page="queryParams.current"
v-model:page-size="queryParams.size"
:total="total"
@current-change="handleQuery"
/>
<!-- 角色表单对话框 -->
<el-dialog
:title="dialogTitle"
v-model="dialogVisible"
width="500px"
>
<el-form ref="roleForm" :model="roleForm" :rules="rules" label-width="80px">
<el-form-item label="角色名称" prop="roleName">
<el-input v-model="roleForm.roleName" />
</el-form-item>
<el-form-item label="角色编码" prop="roleCode">
<el-input v-model="roleForm.roleCode" />
</el-form-item>
<el-form-item label="描述">
<el-input v-model="roleForm.description" type="textarea" />
</el-form-item>
<el-form-item label="状态">
<el-switch v-model="roleForm.status" :active-value="1" :inactive-value="0" />
</el-form-item>
<el-form-item label="权限">
<el-tree
ref="permissionTree"
:data="permissionOptions"
:props="{ label: 'name', children: 'children' }"
show-checkbox
node-key="id"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
// ... 组件逻辑代码将在下一条消息中提供
</script>
5.组件逻辑代码:
frontend\src\views\system\role\index.vue
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import type { FormInstance, ElTree } from 'element-plus'
import request from '@/utils/request'
// 查询参数
const queryParams = reactive({
current: 1,
size: 10,
roleName: ''
})
// 角色列表数据
const roleList = ref([])
const total = ref(0)
const loading = ref(false)
// 权限树选项
const permissionOptions = ref([])
const permissionTree = ref<InstanceType<typeof ElTree>>()
// 对话框控制
const dialogVisible = ref(false)
const dialogTitle = ref('')
const roleForm = reactive({
id: undefined,
roleName: '',
roleCode: '',
description: '',
status: 1,
permissionIds: [] as number[]
})
// 表单校验规则
const rules = {
roleName: [
{ required: true, message: '请输入角色名称', trigger: 'blur' }
],
roleCode: [
{ required: true, message: '请输入角色编码', trigger: 'blur' },
{ pattern: /^[A-Z_]+$/, message: '角色编码只能包含大写字母和下划线', trigger: 'blur' }
]
}
const roleFormRef = ref<FormInstance>()
// 获取角色列表
const getRoleList = async () => {
loading.value = true
try {
const { data } = await request({
url: '/api/roles',
method: 'get',
params: queryParams
})
roleList.value = data.records
total.value = data.total
} catch (error: any) {
ElMessage.error(error.message || '获取角色列表失败')
} finally {
loading.value = false
}
}
// 获取权限树数据
const getPermissionTree = async () => {
try {
const { data } = await request({
url: '/api/permissions/tree',
method: 'get'
})
permissionOptions.value = data
} catch (error: any) {
ElMessage.error(error.message || '获取权限列表失败')
}
}
// 获取角色权限
const getRolePermissions = async (roleId: number) => {
try {
const { data } = await request({
url: `/api/roles/${roleId}/permissions`,
method: 'get'
})
permissionTree.value?.setCheckedKeys(data)
} catch (error: any) {
ElMessage.error(error.message || '获取角色权限失败')
}
}
// 查询按钮点击事件
const handleQuery = () => {
queryParams.current = 1
getRoleList()
}
// 重置表单
const resetForm = () => {
roleForm.id = undefined
roleForm.roleName = ''
roleForm.roleCode = ''
roleForm.description = ''
roleForm.status = 1
roleForm.permissionIds = []
roleFormRef.value?.resetFields()
permissionTree.value?.setCheckedKeys([])
}
// 新增按钮点击事件
const handleAdd = () => {
resetForm()
dialogTitle.value = '新增角色'
dialogVisible.value = true
}
// 编辑按钮点击事件
const handleEdit = async (row: any) => {
resetForm()
dialogTitle.value = '编辑角色'
Object.assign(roleForm, row)
dialogVisible.value = true
await getRolePermissions(row.id)
}
// 删除按钮点击事件
const handleDelete = (row: any) => {
ElMessageBox.confirm('确认删除该角色吗?', '提示', {
type: 'warning'
}).then(async () => {
try {
await request({
url: `/api/roles/${row.id}`,
method: 'delete'
})
ElMessage.success('删除成功')
getRoleList()
} catch (error: any) {
ElMessage.error(error.message || '删除失败')
}
})
}
// 表单提交
const handleSubmit = async () => {
if (!roleFormRef.value) return
await roleFormRef.value.validate()
// 获取选中的权限ID
const checkedKeys = permissionTree.value?.getCheckedKeys(false) as number[]
const halfCheckedKeys = permissionTree.value?.getHalfCheckedKeys() as number[]
roleForm.permissionIds = [...checkedKeys, ...halfCheckedKeys]
try {
const method = roleForm.id ? 'put' : 'post'
const url = roleForm.id ? `/api/roles/${roleForm.id}` : '/api/roles'
await request({
url,
method,
data: roleForm
})
ElMessage.success(`${roleForm.id ? '更新' : '创建'}成功`)
dialogVisible.value = false
getRoleList()
} catch (error: any) {
ElMessage.error(error.message || `${roleForm.id ? '更新' : '创建'}失败`)
}
}
// 页面加载时获取数据
onMounted(() => {
getRoleList()
getPermissionTree()
})
</script>
<style scoped>
.filter-container {
padding-bottom: 10px;
}
.filter-item {
margin-right: 10px;
}
</style>
6.创建一个权限树的接口:
backend\src\main\java\com\jxc\controller\PermissionController.java
@RestController
@RequestMapping("/api/permissions")
@RequiredArgsConstructor
public class PermissionController {
private final UserService userService;
@GetMapping("/tree")
@RequiresPermission("system:role:edit")
public Result<List<SysPermission>> getPermissionTree() {
List<SysPermission> permissions = userService.getAllPermissions();
return Result.success(permissions);
}
}
7.在UserService中添加获取所有权限的方法:
backend\src\main\java\com\jxc\service\UserService.java
@Service
public class UserService {
// ... 之前的代码 ...
public List<SysPermission> getAllPermissions() {
List<SysPermission> permissions = permissionMapper.selectList(new QueryWrapper<>());
return buildTree(permissions);
}
}
角色管理功能就完成了,主要包括:
- 角色的增删改查
- 角色权限分配
- 权限树形展示
- 表单验证
- 添加权限控制
您想继续哪个部分?
- 开发商品管理功能
- 开发采购管理功能
- 开发销售管理功能
- 开发库存管理功能
- 开发报表统计功能