项目简介
校园公告管理系统是一个基于Spring Boot + Vue.js的全栈Web应用,旨在为学校提供一个现代化的公告发布和管理平台。系统支持公告的发布、编辑、查询和分类管理,并提供用户权限控制等功能。
技术栈
后端技术
- Spring Boot 2.7.0
- Spring Security
- MyBatis-Plus
- MySQL 8.0
- Redis
- JWT认证
前端技术
- Vue 3
- Element Plus
- Axios
- Vue Router
- Pinia
核心功能模块
1. 用户管理
- 用户注册与登录
- 角色权限控制(管理员、教师、学生)
- 个人信息管理
2. 公告管理
- 公告发布与编辑
- 公告分类管理
- 公告置顶与下架
- 公告审核流程
3. 评论互动
- 公告评论功能
- 评论管理
- 点赞与收藏
4. 系统管理
- 系统参数配置
- 操作日志记录
- 数据备份恢复
数据库设计
用户表(sys_user)
CREATE TABLE sys_user (id BIGINT PRIMARY KEY AUTO_INCREMENT,username VARCHAR(50) NOT NULL,password VARCHAR(100) NOT NULL,real_name VARCHAR(50),role VARCHAR(20) NOT NULL,email VARCHAR(100),create_time DATETIME,update_time DATETIME,status TINYINT DEFAULT 1
);
公告表(announcement)
CREATE TABLE announcement (id BIGINT PRIMARY KEY AUTO_INCREMENT,title VARCHAR(200) NOT NULL,content TEXT NOT NULL,category_id BIGINT,author_id BIGINT,status TINYINT,is_top TINYINT DEFAULT 0,view_count INT DEFAULT 0,create_time DATETIME,update_time DATETIME
);
核心代码实现
后端接口示例
java">@RestController
@RequestMapping("/api/announcement")
public class AnnouncementController {@Autowiredprivate AnnouncementService announcementService;@PostMapping("/create")public Result createAnnouncement(@RequestBody AnnouncementDTO dto) {return announcementService.createAnnouncement(dto);}@GetMapping("/list")public Result getAnnouncementList(AnnouncementQuery query) {return announcementService.getAnnouncementList(query);}
}
前端组件示例
<template><div class="announcement-list"><el-table :data="announcements" style="width: 100%"><el-table-column prop="title" label="标题" /><el-table-column prop="createTime" label="发布时间" /><el-table-column label="操作"><template #default="scope"><el-button @click="viewDetail(scope.row)">查看</el-button><el-button type="primary" @click="editAnnouncement(scope.row)">编辑</el-button></template></el-table-column></el-table></div>
</template><script setup>
import { ref, onMounted } from 'vue'
import { getAnnouncementList } from '@/api/announcement'const announcements = ref([])onMounted(async () => {const { data } = await getAnnouncementList()announcements.value = data
})
</script>
项目亮点
-
权限控制
- 基于RBAC模型实现细粒度权限控制
- 使用JWT实现无状态认证
-
性能优化
- Redis缓存热点数据
- MyBatis-Plus分页查询优化
- 前端组件懒加载
-
用户体验
- 响应式设计,适配多端显示
- 富文本编辑器支持
- 实时消息提醒
项目部署
环境要求
- JDK 1.8+
- Maven 3.6+
- Node.js 14+
- MySQL 8.0
- Redis 6.0+
部署步骤
- 后端部署
# 打包
mvn clean package# 运行
java -jar campus-announcement.jar
- 前端部署
# 安装依赖
npm install# 构建
npm run build# 部署到Nginx
cp -r dist/* /usr/share/nginx/html/
总结与展望
本项目采用主流的Java全栈技术栈,实现了一个功能完整的校园公告管理系统。通过这个项目,不仅锻炼了全栈开发能力,也对项目架构设计和性能优化有了更深的理解。
后续优化方向
- 引入消息队列处理异步任务
- 添加微信小程序端
- 引入ElasticSearch提升搜索体验
- 优化系统监控和告警机制
校园公告管理系统完整实现
一、用户管理模块
1.1 用户注册与登录
数据库设计
-- 用户表
CREATE TABLE sys_user (id BIGINT PRIMARY KEY AUTO_INCREMENT,username VARCHAR(50) NOT NULL COMMENT '用户名',password VARCHAR(100) NOT NULL COMMENT '密码',real_name VARCHAR(50) COMMENT '真实姓名',role VARCHAR(20) NOT NULL COMMENT '角色',email VARCHAR(100) COMMENT '邮箱',phone VARCHAR(20) COMMENT '手机号',status TINYINT DEFAULT 1 COMMENT '状态(0:禁用,1:启用)',create_time DATETIME DEFAULT CURRENT_TIMESTAMP,update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,UNIQUE KEY uk_username (username)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
前端实现
<!-- src/views/auth/Login.vue -->
<template><div class="login-container"><el-form ref="loginForm" :model="loginForm" :rules="loginRules"><el-form-item prop="username"><el-input v-model="loginForm.username" placeholder="用户名"prefix-icon="User"/></el-form-item><el-form-item prop="password"><el-inputv-model="loginForm.password"type="password"placeholder="密码"prefix-icon="Lock"@keyup.enter="handleLogin"/></el-form-item><el-form-item><el-button type="primary" :loading="loading"@click="handleLogin"style="width: 100%">登录</el-button></el-form-item><div class="register-link"><router-link to="/register">注册账号</router-link></div></el-form></div>
</template><script setup>
import { ref, reactive } from 'vue'
import { useRouter } from 'vue-router'
import { useUserStore } from '@/stores/user'
import { login } from '@/api/auth'const router = useRouter()
const userStore = useUserStore()
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 = truetry {const { data } = await login(loginForm)userStore.setToken(data.token)await userStore.getUserInfo()router.push('/')} catch (error) {console.error('登录失败:', error)} finally {loading.value = false}
}
</script>
后端实现
java">@RestController
@RequestMapping("/api/auth")
public class AuthController {@Autowiredprivate AuthService authService;@PostMapping("/login")public Result login(@RequestBody @Valid LoginDTO loginDTO) {return authService.login(loginDTO);}@PostMapping("/register")public Result register(@RequestBody @Valid RegisterDTO registerDTO) {return authService.register(registerDTO);}
}@Service
public class AuthServiceImpl implements AuthService {@Autowiredprivate UserMapper userMapper;@Autowiredprivate PasswordEncoder passwordEncoder;@Autowiredprivate JwtUtils jwtUtils;@Override@Transactional(rollbackFor = Exception.class)public Result register(RegisterDTO dto) {// 验证用户名是否存在if (userMapper.selectByUsername(dto.getUsername()) != null) {return Result.fail("用户名已存在");}// 创建用户User user = new User();BeanUtils.copyProperties(dto, user);user.setPassword(passwordEncoder.encode(dto.getPassword()));userMapper.insert(user);return Result.success();}@Overridepublic Result login(LoginDTO dto) {// 查询用户User user = userMapper.selectByUsername(dto.getUsername());if (user == null || !passwordEncoder.matches(dto.getPassword(), user.getPassword())) {return Result.fail("用户名或密码错误");}// 检查用户状态if (user.getStatus() == 0) {return Result.fail("账号已被禁用");}// 生成tokenString token = jwtUtils.generateToken(user);// 记录登录日志logService.recordLogin(user.getId());return Result.success().data("token", token);}
}
1.2 角色权限控制
数据库设计
-- 角色表
CREATE TABLE sys_role (id BIGINT PRIMARY KEY AUTO_INCREMENT,role_code VARCHAR(50) NOT NULL COMMENT '角色编码',role_name VARCHAR(50) NOT NULL COMMENT '角色名称',description VARCHAR(200) COMMENT '描述',create_time DATETIME DEFAULT CURRENT_TIMESTAMP,UNIQUE KEY uk_role_code (role_code)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;-- 权限表
CREATE TABLE sys_permission (id BIGINT PRIMARY KEY AUTO_INCREMENT,permission_code VARCHAR(100) NOT NULL COMMENT '权限编码',permission_name VARCHAR(100) NOT NULL COMMENT '权限名称',menu_url VARCHAR(200) COMMENT '菜单URL',parent_id BIGINT COMMENT '父级ID',type TINYINT COMMENT '类型(1:菜单,2:按钮)',UNIQUE KEY uk_permission_code (permission_code)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
前端实现
<!-- src/components/Permission.vue -->
<template><component :is="tag" v-if="hasPermission"v-bind="$attrs"><slot></slot></component>
</template><script setup>
import { computed } from 'vue'
import { useUserStore } from '@/stores/user'const props = defineProps({code: {type: String,required: true},tag: {type: String,default: 'div'}
})const userStore = useUserStore()const hasPermission = computed(() => {return userStore.permissions.includes(props.code)
})
</script><!-- 使用示例 -->
<Permission code="announcement:create" tag="el-button">新建公告
</Permission>
后端实现
java">@Aspect
@Component
public class PermissionAspect {@Autowiredprivate UserService userService;@Around("@annotation(requiresPermission)")public Object checkPermission(ProceedingJoinPoint point, RequiresPermission requiresPermission) throws Throwable {// 获取当前用户权限Long userId = SecurityUtils.getCurrentUserId();Set<String> permissions = userService.getUserPermissions(userId);// 检查是否有权限String requiredPermission = requiresPermission.value();if (!permissions.contains(requiredPermission)) {throw new UnauthorizedException("无权限执行此操作");}return point.proceed();}
}@Service
public class UserServiceImpl implements UserService {@Autowiredprivate PermissionMapper permissionMapper;@Overridepublic Set<String> getUserPermissions(Long userId) {// 获取用户角色User user = userMapper.selectById(userId);if (user == null) {return Collections.emptySet();}// 获取角色权限List<Permission> permissions = permissionMapper.selectByRoleCode(user.getRole());return permissions.stream().map(Permission::getPermissionCode).collect(Collectors.toSet());}
}
1.3 个人信息管理
数据库设计
-- 用户信息扩展表
CREATE TABLE user_profile (user_id BIGINT PRIMARY KEY,avatar VARCHAR(200) COMMENT '头像URL',gender TINYINT COMMENT '性别(0:女,1:男)',birthday DATE COMMENT '生日',address VARCHAR(200) COMMENT '地址',signature VARCHAR(500) COMMENT '个性签名',update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,FOREIGN KEY (user_id) REFERENCES sys_user(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
前端实现
<!-- src/views/user/Profile.vue -->
<template><div class="profile-container"><el-card><template #header><div class="card-header"><span>个人信息</span><el-button type="primary" @click="handleEdit">编辑</el-button></div></template><el-form label-width="100px"><div class="avatar-container"><el-avatar :size="100" :src="profile.avatar" /><el-uploadv-if="isEditing"class="avatar-uploader":action="uploadUrl":headers="headers":show-file-list="false":on-success="handleAvatarSuccess"><el-button size="small">更换头像</el-button></el-upload></div><el-form-item label="用户名"><span v-if="!isEditing">{{ profile.username }}</span><el-input v-else v-model="form.username" disabled /></el-form-item><el-form-item label="真实姓名"><span v-if="!isEditing">{{ profile.realName }}</span><el-input v-else v-model="form.realName" /></el-form-item><el-form-item label="性别"><span v-if="!isEditing">{{ genderLabel }}</span><el-radio-group v-else v-model="form.gender"><el-radio :label="1">男</el-radio><el-radio :label="0">女</el-radio></el-radio-group></el-form-item><el-form-item label="邮箱"><span v-if="!isEditing">{{ profile.email }}</span><el-input v-else v-model="form.email" /></el-form-item><el-form-item label="个性签名"><span v-if="!isEditing">{{ profile.signature }}</span><el-input v-else v-model="form.signature" type="textarea" /></el-form-item></el-form><div v-if="isEditing" class="button-container"><el-button @click="cancelEdit">取消</el-button><el-button type="primary" @click="saveProfile">保存</el-button></div></el-card></div>
</template><script setup>
import { ref, reactive, computed } from 'vue'
import { getUserProfile, updateProfile } from '@/api/user'const profile = ref({})
const isEditing = ref(false)
const form = reactive({})const genderLabel = computed(() => {return profile.value.gender === 1 ? '男' : '女'
})const loadProfile = async () => {const { data } = await getUserProfile()profile.value = data
}const handleEdit = () => {isEditing.value = trueObject.assign(form, profile.value)
}const cancelEdit = () => {isEditing.value = false
}const saveProfile = async () => {try {await updateProfile(form)await loadProfile()isEditing.value = falseElMessage.success('保存成功')} catch (error) {console.error('保存失败:', error)}
}const handleAvatarSuccess = (response) => {profile.value.avatar = response.data.url
}onMounted(() => {loadProfile()
})
</script>
后端实现
java">@RestController
@RequestMapping("/api/user")
public class UserProfileController {@Autowiredprivate UserProfileService userProfileService;@GetMapping("/profile")public Result getUserProfile() {return userProfileService.getCurrentUserProfile();}@PutMapping("/profile")public Result updateProfile(@RequestBody @Valid UpdateProfileDTO dto) {return userProfileService.updateProfile(dto);}@PostMapping("/avatar")public Result uploadAvatar(@RequestParam("file") MultipartFile file) {return userProfileService.updateAvatar(file);}
}@Service
public class UserProfileServiceImpl implements UserProfileService {@Autowiredprivate UserMapper userMapper;@Autowiredprivate UserProfileMapper profileMapper;@Autowiredprivate FileService fileService;@Overridepublic Result getCurrentUserProfile() {Long userId = SecurityUtils.getCurrentUserId();// 获取用户基本信息User user = userMapper.selectById(userId);if (user == null) {return Result.fail("用户不存在");}// 获取用户详细信息UserProfile profile = profileMapper.selectByUserId(userId);// 组装返回数据UserProfileVO vo = new UserProfileVO();BeanUtils.copyProperties(user, vo);if (profile != null) {BeanUtils.copyProperties(profile, vo);}return Result.success().data("profile", vo);}@Override@Transactional(rollbackFor = Exception.class)public Result updateProfile(UpdateProfileDTO dto) {Long userId = SecurityUtils.getCurrentUserId();// 更新用户基本信息User user = new User();user.setId(userId);user.setRealName(dto.getRealName());user.setEmail(dto.getEmail());userMapper.updateById(user);// 更新用户详细信息UserProfile profile = new UserProfile();profile.setUserId(userId);profile.setGender(dto.getGender());profile.setSignature(dto.getSignature());profileMapper.insertOrUpdate(profile);return Result.success();}@Overridepublic Result updateAvatar(MultipartFile file) {// 上传文件String avatarUrl = fileService.uploadFile(file);// 更新头像URLUserProfile profile = new UserProfile();profile.setUserId(SecurityUtils.getCurrentUserId());profile.setAvatar(avatarUrl);profileMapper.updateById(profile);return Result.success().data("url", avatarUrl);}
}
二、公告管理模块
2.1 公告发布与编辑
数据库设计
-- 公告表
CREATE TABLE announcement (id BIGINT PRIMARY KEY AUTO_INCREMENT,title VARCHAR(200) NOT NULL COMMENT '标题',content TEXT NOT NULL COMMENT '内容',category_id BIGINT COMMENT '分类ID',author_id BIGINT NOT NULL COMMENT '作者ID',status TINYINT DEFAULT 0 COMMENT '状态(0:草稿,1:待审核,2:已发布,3:已驳回)',is_top TINYINT DEFAULT 0 COMMENT '是否置顶',view_count INT DEFAULT 0 COMMENT '查看次数',create_time DATETIME DEFAULT CURRENT_TIMESTAMP,update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,FOREIGN KEY (category_id) REFERENCES announcement_category(id),FOREIGN KEY (author_id) REFERENCES sys_user(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;-- 公告附件表
CREATE TABLE announcement_attachment (id BIGINT PRIMARY KEY AUTO_INCREMENT,announcement_id BIGINT NOT NULL,file_name VARCHAR(200) NOT NULL,file_url VARCHAR(500) NOT NULL,file_size BIGINT COMMENT '文件大小',create_time DATETIME DEFAULT CURRENT_TIMESTAMP,FOREIGN KEY (announcement_id) REFERENCES announcement(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
前端实现 - 公告发布与编辑
<!-- src/views/announcement/Edit.vue -->
<template><div class="announcement-edit"><el-form ref="formRef" :model="form" :rules="rules" label-width="100px"><el-form-item label="标题" prop="title"><el-input v-model="form.title" placeholder="请输入公告标题" /></el-form-item><el-form-item label="分类" prop="categoryId"><el-select v-model="form.categoryId" placeholder="请选择分类"><el-optionv-for="item in categories":key="item.id":label="item.name":value="item.id"/></el-select></el-form-item><el-form-item label="内容" prop="content"><editor v-model="form.content" /></el-form-item><el-form-item label="附件"><el-upload:action="uploadUrl":headers="headers":on-success="handleUploadSuccess":on-remove="handleRemove"multiple><el-button type="primary">上传附件</el-button></el-upload></el-form-item><el-form-item><el-button @click="saveDraft">保存草稿</el-button><el-button type="primary" @click="submitForReview">提交审核</el-button></el-form-item></el-form></div>
</template><script setup>
import { ref, reactive, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import Editor from '@/components/Editor.vue'
import { getCategories } from '@/api/category'
import { createAnnouncement, getAnnouncementDetail, updateAnnouncement } from '@/api/announcement'const route = useRoute()
const router = useRouter()
const formRef = ref(null)
const categories = ref([])const form = reactive({title: '',categoryId: '',content: '',attachments: []
})const rules = {title: [{ required: true, message: '请输入标题', trigger: 'blur' }],categoryId: [{ required: true, message: '请选择分类', trigger: 'change' }],content: [{ required: true, message: '请输入内容', trigger: 'blur' }]
}// 加载分类数据
const loadCategories = async () => {const { data } = await getCategories()categories.value = data
}// 如果是编辑模式,加载公告详情
const loadAnnouncementDetail = async (id) => {const { data } = await getAnnouncementDetail(id)Object.assign(form, data)
}// 保存草稿
const saveDraft = async () => {try {await formRef.value.validate()const params = { ...form, status: 0 }if (route.params.id) {await updateAnnouncement(route.params.id, params)} else {await createAnnouncement(params)}ElMessage.success('保存成功')router.push('/announcement/list')} catch (error) {console.error('保存失败:', error)}
}// 提交审核
const submitForReview = async () => {try {await formRef.value.validate()const params = { ...form, status: 1 }if (route.params.id) {await updateAnnouncement(route.params.id, params)} else {await createAnnouncement(params)}ElMessage.success('提交成功')router.push('/announcement/list')} catch (error) {console.error('提交失败:', error)}
}onMounted(() => {loadCategories()if (route.params.id) {loadAnnouncementDetail(route.params.id)}
})
</script>
后端实现 - 公告发布与编辑
java">@RestController
@RequestMapping("/api/announcement")
public class AnnouncementController {@Autowiredprivate AnnouncementService announcementService;@PostMapping@RequiresPermission("announcement:create")public Result createAnnouncement(@RequestBody @Valid CreateAnnouncementDTO dto) {return announcementService.createAnnouncement(dto);}@PutMapping("/{id}")@RequiresPermission("announcement:update")public Result updateAnnouncement(@PathVariable Long id, @RequestBody @Valid UpdateAnnouncementDTO dto) {return announcementService.updateAnnouncement(id, dto);}@GetMapping("/{id}")public Result getAnnouncementDetail(@PathVariable Long id) {return announcementService.getAnnouncementDetail(id);}@DeleteMapping("/{id}")@RequiresPermission("announcement:delete")public Result deleteAnnouncement(@PathVariable Long id) {return announcementService.deleteAnnouncement(id);}
}@Service
public class AnnouncementServiceImpl implements AnnouncementService {@Autowiredprivate AnnouncementMapper announcementMapper;@Autowiredprivate AttachmentMapper attachmentMapper;@Override@Transactional(rollbackFor = Exception.class)public Result createAnnouncement(CreateAnnouncementDTO dto) {// 创建公告Announcement announcement = new Announcement();BeanUtils.copyProperties(dto, announcement);announcement.setAuthorId(SecurityUtils.getCurrentUserId());announcementMapper.insert(announcement);// 保存附件if (!CollectionUtils.isEmpty(dto.getAttachments())) {List<AnnouncementAttachment> attachments = dto.getAttachments().stream().map(attachment -> {AnnouncementAttachment att = new AnnouncementAttachment();att.setAnnouncementId(announcement.getId());att.setFileName(attachment.getFileName());att.setFileUrl(attachment.getFileUrl());att.setFileSize(attachment.getFileSize());return att;}).collect(Collectors.toList());attachmentMapper.batchInsert(attachments);}return Result.success();}@Override@Transactional(rollbackFor = Exception.class)public Result updateAnnouncement(Long id, UpdateAnnouncementDTO dto) {// 检查权限Announcement announcement = announcementMapper.selectById(id);if (!SecurityUtils.getCurrentUserId().equals(announcement.getAuthorId())) {return Result.fail("无权修改此公告");}// 更新公告BeanUtils.copyProperties(dto, announcement);announcementMapper.updateById(announcement);// 更新附件attachmentMapper.deleteByAnnouncementId(id);if (!CollectionUtils.isEmpty(dto.getAttachments())) {List<AnnouncementAttachment> attachments = dto.getAttachments().stream().map(attachment -> {AnnouncementAttachment att = new AnnouncementAttachment();att.setAnnouncementId(id);att.setFileName(attachment.getFileName());att.setFileUrl(attachment.getFileUrl());att.setFileSize(attachment.getFileSize());return att;}).collect(Collectors.toList());attachmentMapper.batchInsert(attachments);}return Result.success();}
}
2.2 公告分类管理
数据库设计
-- 公告分类表
CREATE TABLE announcement_category (id BIGINT PRIMARY KEY AUTO_INCREMENT,name VARCHAR(50) NOT NULL COMMENT '分类名称',code VARCHAR(50) NOT NULL COMMENT '分类编码',parent_id BIGINT COMMENT '父级ID',sort INT DEFAULT 0 COMMENT '排序号',status TINYINT DEFAULT 1 COMMENT '状态(0:禁用,1:启用)',create_time DATETIME DEFAULT CURRENT_TIMESTAMP,update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,UNIQUE KEY uk_code (code)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
前端实现
<!-- src/views/announcement/category/Index.vue -->
<template><div class="category-manage"><div class="toolbar"><el-button type="primary" @click="handleAdd" v-permission="'category:create'">新增分类</el-button></div><el-table :data="categoryList" row-key="id" border><el-table-column prop="name" label="分类名称" /><el-table-column prop="code" label="分类编码" /><el-table-column prop="sort" label="排序" width="100" /><el-table-column prop="status" label="状态" width="100"><template #default="{ row }"><el-tag :type="row.status === 1 ? 'success' : 'danger'">{{ row.status === 1 ? '启用' : '禁用' }}</el-tag></template></el-table-column><el-table-column label="操作" width="200" fixed="right"><template #default="{ row }"><el-button link @click="handleEdit(row)">编辑</el-button><el-button link type="danger" @click="handleDelete(row)"v-permission="'category:delete'">删除</el-button></template></el-table-column></el-table><!-- 新增/编辑对话框 --><el-dialog:title="dialogTitle"v-model="dialogVisible"width="500px"><el-form ref="formRef" :model="form" :rules="rules" label-width="100px"><el-form-item label="分类名称" prop="name"><el-input v-model="form.name" /></el-form-item><el-form-item label="分类编码" prop="code"><el-input v-model="form.code" :disabled="!!form.id" /></el-form-item><el-form-item label="排序号" prop="sort"><el-input-number v-model="form.sort" :min="0" /></el-form-item><el-form-item label="状态"><el-switch v-model="form.status" :active-value="1" :inactive-value="0" /></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>
import { ref, reactive, computed } from 'vue'
import { getCategoryList, createCategory, updateCategory, deleteCategory
} from '@/api/category'const categoryList = ref([])
const dialogVisible = ref(false)
const formRef = ref(null)const form = reactive({id: null,name: '',code: '',sort: 0,status: 1
})const dialogTitle = computed(() => form.id ? '编辑分类' : '新增分类')const rules = {name: [{ required: true, message: '请输入分类名称', trigger: 'blur' }],code: [{ required: true, message: '请输入分类编码', trigger: 'blur' }]
}// 加载分类列表
const loadCategoryList = async () => {const { data } = await getCategoryList()categoryList.value = data
}// 新增分类
const handleAdd = () => {Object.assign(form, {id: null,name: '',code: '',sort: 0,status: 1})dialogVisible.value = true
}// 编辑分类
const handleEdit = (row) => {Object.assign(form, row)dialogVisible.value = true
}// 提交表单
const handleSubmit = async () => {try {await formRef.value.validate()if (form.id) {await updateCategory(form.id, form)} else {await createCategory(form)}ElMessage.success('保存成功')dialogVisible.value = falseloadCategoryList()} catch (error) {console.error('保存失败:', error)}
}// 删除分类
const handleDelete = async (row) => {try {await ElMessageBox.confirm('确定要删除该分类吗?')await deleteCategory(row.id)ElMessage.success('删除成功')loadCategoryList()} catch (error) {console.error('删除失败:', error)}
}onMounted(() => {loadCategoryList()
})
</script>
后端实现
java">@RestController
@RequestMapping("/api/category")
public class CategoryController {@Autowiredprivate CategoryService categoryService;@GetMappingpublic Result getCategoryList() {return categoryService.getCategoryList();}@PostMapping@RequiresPermission("category:create")public Result createCategory(@RequestBody @Valid CategoryDTO dto) {return categoryService.createCategory(dto);}@PutMapping("/{id}")@RequiresPermission("category:update")public Result updateCategory(@PathVariable Long id, @RequestBody @Valid CategoryDTO dto) {return categoryService.updateCategory(id, dto);}@DeleteMapping("/{id}")@RequiresPermission("category:delete")public Result deleteCategory(@PathVariable Long id) {return categoryService.deleteCategory(id);}
}@Service
public class CategoryServiceImpl implements CategoryService {@Autowiredprivate CategoryMapper categoryMapper;@Autowiredprivate AnnouncementMapper announcementMapper;@Overridepublic Result getCategoryList() {List<Category> categories = categoryMapper.selectList(new LambdaQueryWrapper<Category>().orderByAsc(Category::getSort));return Result.success().data("list", categories);}@Override@Transactional(rollbackFor = Exception.class)public Result createCategory(CategoryDTO dto) {// 检查编码是否重复if (categoryMapper.selectByCode(dto.getCode()) != null) {return Result.fail("分类编码已存在");}Category category = new Category();BeanUtils.copyProperties(dto, category);categoryMapper.insert(category);return Result.success();}@Override@Transactional(rollbackFor = Exception.class)public Result updateCategory(Long id, CategoryDTO dto) {Category category = categoryMapper.selectById(id);if (category == null) {return Result.fail("分类不存在");}BeanUtils.copyProperties(dto, category);categoryMapper.updateById(category);return Result.success();}@Override@Transactional(rollbackFor = Exception.class)public Result deleteCategory(Long id) {// 检查是否有公告使用该分类if (announcementMapper.countByCategoryId(id) > 0) {return Result.fail("该分类下存在公告,无法删除");}categoryMapper.deleteById(id);return Result.success();}
}
2.3 公告置顶与下架
数据库设计
-- 在公告表中添加置顶相关字段
ALTER TABLE announcement
ADD COLUMN is_top TINYINT DEFAULT 0 COMMENT '是否置顶(0:否,1:是)',
ADD COLUMN top_expire_time DATETIME COMMENT '置顶过期时间',
ADD COLUMN top_sort INT DEFAULT 0 COMMENT '置顶排序';-- 公告状态变更记录表
CREATE TABLE announcement_status_log (id BIGINT PRIMARY KEY AUTO_INCREMENT,announcement_id BIGINT NOT NULL COMMENT '公告ID',operator_id BIGINT NOT NULL COMMENT '操作人ID',old_status TINYINT COMMENT '原状态',new_status TINYINT COMMENT '新状态',reason VARCHAR(500) COMMENT '操作原因',create_time DATETIME DEFAULT CURRENT_TIMESTAMP,FOREIGN KEY (announcement_id) REFERENCES announcement(id),FOREIGN KEY (operator_id) REFERENCES sys_user(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
前端实现
<!-- src/views/announcement/List.vue -->
<template><div class="announcement-list"><el-table :data="announcementList" border><el-table-column type="selection" width="55" /><el-table-column prop="title" label="标题"><template #default="{ row }"><div class="title-cell"><el-tag v-if="row.isTop" type="warning" size="small">置顶</el-tag>{{ row.title }}</div></template></el-table-column><el-table-column prop="categoryName" label="分类" width="120" /><el-table-column prop="status" label="状态" width="100"><template #default="{ row }"><el-tag :type="getStatusType(row.status)">{{ getStatusText(row.status) }}</el-tag></template></el-table-column><el-table-column label="操作" width="250" fixed="right"><template #default="{ row }"><el-button v-if="!row.isTop" link @click="handleSetTop(row)"v-permission="'announcement:top'">置顶</el-button><el-button v-else link type="warning" @click="handleCancelTop(row)"v-permission="'announcement:top'">取消置顶</el-button><el-button v-if="row.status === 2"link type="danger" @click="handleTakeDown(row)"v-permission="'announcement:takedown'">下架</el-button></template></el-table-column></el-table><!-- 置顶设置对话框 --><el-dialogtitle="置顶设置"v-model="topDialogVisible"width="400px"><el-form ref="topFormRef" :model="topForm" label-width="100px"><el-form-item label="过期时间" prop="expireTime"><el-date-pickerv-model="topForm.expireTime"type="datetime"placeholder="选择过期时间"/></el-form-item><el-form-item label="置顶排序" prop="sort"><el-input-number v-model="topForm.sort" :min="0" /></el-form-item></el-form><template #footer><el-button @click="topDialogVisible = false">取消</el-button><el-button type="primary" @click="confirmSetTop">确定</el-button></template></el-dialog><!-- 下架原因对话框 --><el-dialogtitle="下架原因"v-model="takeDownDialogVisible"width="400px"><el-form ref="takeDownFormRef" :model="takeDownForm" label-width="80px"><el-form-item label="原因" prop="reason"><el-inputv-model="takeDownForm.reason"type="textarea":rows="3"placeholder="请输入下架原因"/></el-form-item></el-form><template #footer><el-button @click="takeDownDialogVisible = false">取消</el-button><el-button type="primary" @click="confirmTakeDown">确定</el-button></template></el-dialog></div>
</template><script setup>
import { ref, reactive } from 'vue'
import { getAnnouncementList, setAnnouncementTop, cancelAnnouncementTop,takeDownAnnouncement
} from '@/api/announcement'const announcementList = ref([])
const topDialogVisible = ref(false)
const takeDownDialogVisible = ref(false)
const currentAnnouncement = ref(null)const topForm = reactive({expireTime: null,sort: 0
})const takeDownForm = reactive({reason: ''
})// 设置置顶
const handleSetTop = (row) => {currentAnnouncement.value = rowtopDialogVisible.value = true
}const confirmSetTop = async () => {try {await setAnnouncementTop(currentAnnouncement.value.id, topForm)ElMessage.success('设置成功')topDialogVisible.value = falseloadAnnouncementList()} catch (error) {console.error('设置失败:', error)}
}// 取消置顶
const handleCancelTop = async (row) => {try {await ElMessageBox.confirm('确定要取消置顶吗?')await cancelAnnouncementTop(row.id)ElMessage.success('已取消置顶')loadAnnouncementList()} catch (error) {console.error('操作失败:', error)}
}// 下架公告
const handleTakeDown = (row) => {currentAnnouncement.value = rowtakeDownDialogVisible.value = true
}const confirmTakeDown = async () => {try {await takeDownAnnouncement(currentAnnouncement.value.id, takeDownForm)ElMessage.success('下架成功')takeDownDialogVisible.value = falseloadAnnouncementList()} catch (error) {console.error('下架失败:', error)}
}// 加载公告列表
const loadAnnouncementList = async () => {const { data } = await getAnnouncementList()announcementList.value = data
}onMounted(() => {loadAnnouncementList()
})
</script>
后端实现
java">@RestController
@RequestMapping("/api/announcement")
public class AnnouncementController {@PutMapping("/{id}/top")@RequiresPermission("announcement:top")public Result setTop(@PathVariable Long id, @RequestBody TopSettingDTO dto) {return announcementService.setTop(id, dto);}@DeleteMapping("/{id}/top")@RequiresPermission("announcement:top")public Result cancelTop(@PathVariable Long id) {return announcementService.cancelTop(id);}@PutMapping("/{id}/takedown")@RequiresPermission("announcement:takedown")public Result takeDown(@PathVariable Long id, @RequestBody TakeDownDTO dto) {return announcementService.takeDown(id, dto);}
}@Service
public class AnnouncementServiceImpl implements AnnouncementService {@Autowiredprivate AnnouncementMapper announcementMapper;@Autowiredprivate StatusLogMapper statusLogMapper;@Override@Transactional(rollbackFor = Exception.class)public Result setTop(Long id, TopSettingDTO dto) {Announcement announcement = announcementMapper.selectById(id);if (announcement == null) {return Result.fail("公告不存在");}// 更新置顶信息announcement.setIsTop(1);announcement.setTopExpireTime(dto.getExpireTime());announcement.setTopSort(dto.getSort());announcementMapper.updateById(announcement);return Result.success();}@Override@Transactional(rollbackFor = Exception.class)public Result cancelTop(Long id) {Announcement announcement = announcementMapper.selectById(id);if (announcement == null) {return Result.fail("公告不存在");}// 取消置顶announcement.setIsTop(0);announcement.setTopExpireTime(null);announcement.setTopSort(0);announcementMapper.updateById(announcement);return Result.success();}@Override@Transactional(rollbackFor = Exception.class)public Result takeDown(Long id, TakeDownDTO dto) {Announcement announcement = announcementMapper.selectById(id);if (announcement == null) {return Result.fail("公告不存在");}// 记录状态变更StatusLog log = new StatusLog();log.setAnnouncementId(id);log.setOperatorId(SecurityUtils.getCurrentUserId());log.setOldStatus(announcement.getStatus());log.setNewStatus(AnnouncementStatus.TAKEN_DOWN.getCode());log.setReason(dto.getReason());statusLogMapper.insert(log);// 更新公告状态announcement.setStatus(AnnouncementStatus.TAKEN_DOWN.getCode());announcementMapper.updateById(announcement);return Result.success();}
}
2.4 公告审核流程
数据库设计
-- 公告审核记录表
CREATE TABLE announcement_audit (id BIGINT PRIMARY KEY AUTO_INCREMENT,announcement_id BIGINT NOT NULL COMMENT '公告ID',auditor_id BIGINT NOT NULL COMMENT '审核人ID',audit_status TINYINT COMMENT '审核状态(1:通过,2:驳回)',audit_comment VARCHAR(500) COMMENT '审核意见',create_time DATETIME DEFAULT CURRENT_TIMESTAMP,FOREIGN KEY (announcement_id) REFERENCES announcement(id),FOREIGN KEY (auditor_id) REFERENCES sys_user(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;-- 审核流程配置表
CREATE TABLE audit_flow_config (id BIGINT PRIMARY KEY AUTO_INCREMENT,flow_name VARCHAR(100) NOT NULL COMMENT '流程名称',flow_type VARCHAR(50) NOT NULL COMMENT '流程类型',auditor_roles VARCHAR(200) COMMENT '审核角色列表(逗号分隔)',status TINYINT DEFAULT 1 COMMENT '状态(0:禁用,1:启用)',create_time DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
前端实现
<!-- src/views/announcement/audit/List.vue -->
<template><div class="audit-list"><el-tabs v-model="activeTab" @tab-click="handleTabClick"><el-tab-pane label="待审核" name="pending"><audit-table :data="tableData" :loading="loading"@audit="handleAudit"/></el-tab-pane><el-tab-pane label="已审核" name="processed"><audit-table :data="tableData" :loading="loading"type="history"/></el-tab-pane></el-tabs><!-- 审核对话框 --><el-dialogtitle="公告审核"v-model="auditDialogVisible"width="600px"><el-form ref="auditFormRef" :model="auditForm" :rules="auditRules"><el-form-item label="审核结果" prop="status"><el-radio-group v-model="auditForm.status"><el-radio :label="1">通过</el-radio><el-radio :label="2">驳回</el-radio></el-radio-group></el-form-item><el-form-item label="审核意见" prop="comment"><el-inputv-model="auditForm.comment"type="textarea":rows="4"placeholder="请输入审核意见"/></el-form-item></el-form><template #footer><el-button @click="auditDialogVisible = false">取消</el-button><el-button type="primary" @click="submitAudit">确定</el-button></template></el-dialog></div>
</template><script setup>
import { ref, reactive } from 'vue'
import AuditTable from './components/AuditTable.vue'
import { getAuditList, auditAnnouncement } from '@/api/announcement'const activeTab = ref('pending')
const tableData = ref([])
const loading = ref(false)
const auditDialogVisible = ref(false)
const currentAnnouncement = ref(null)const auditForm = reactive({status: 1,comment: ''
})const auditRules = {status: [{ required: true, message: '请选择审核结果', trigger: 'change' }],comment: [{ required: true, message: '请输入审核意见', trigger: 'blur' }]
}// 加载审核列表
const loadAuditList = async () => {loading.value = truetry {const { data } = await getAuditList({status: activeTab.value === 'pending' ? 1 : [2, 3]})tableData.value = data} catch (error) {console.error('加载失败:', error)} finally {loading.value = false}
}// 切换标签页
const handleTabClick = () => {loadAuditList()
}// 审核操作
const handleAudit = (row) => {currentAnnouncement.value = rowauditForm.status = 1auditForm.comment = ''auditDialogVisible.value = true
}// 提交审核
const submitAudit = async () => {try {await auditFormRef.value.validate()await auditAnnouncement(currentAnnouncement.value.id, auditForm)ElMessage.success('审核成功')auditDialogVisible.value = falseloadAuditList()} catch (error) {console.error('审核失败:', error)}
}onMounted(() => {loadAuditList()
})
</script>
后端实现
java">@RestController
@RequestMapping("/api/announcement/audit")
public class AuditController {@Autowiredprivate AuditService auditService;@GetMapping("/list")@RequiresPermission("announcement:audit")public Result getAuditList(AuditQuery query) {return auditService.getAuditList(query);}@PostMapping("/{id}")@RequiresPermission("announcement:audit")public Result audit(@PathVariable Long id, @RequestBody @Valid AuditDTO dto) {return auditService.audit(id, dto);}
}@Service
public class AuditServiceImpl implements AuditService {@Autowiredprivate AnnouncementMapper announcementMapper;@Autowiredprivate AuditMapper auditMapper;@Autowiredprivate AuditFlowConfigMapper flowConfigMapper;@Overridepublic Result getAuditList(AuditQuery query) {// 获取当前用户角色String userRole = SecurityUtils.getCurrentUserRole();// 获取可审核的流程配置List<AuditFlowConfig> flowConfigs = flowConfigMapper.selectList(new LambdaQueryWrapper<AuditFlowConfig>().eq(AuditFlowConfig::getStatus, 1).like(AuditFlowConfig::getAuditorRoles, userRole));if (CollectionUtils.isEmpty(flowConfigs)) {return Result.success().data("list", Collections.emptyList());}// 查询待审核公告List<Announcement> announcements = announcementMapper.selectForAudit(query);return Result.success().data("list", announcements);}@Override@Transactional(rollbackFor = Exception.class)public Result audit(Long id, AuditDTO dto) {// 获取公告Announcement announcement = announcementMapper.selectById(id);if (announcement == null) {return Result.fail("公告不存在");}// 检查是否可以审核if (announcement.getStatus() != AnnouncementStatus.PENDING.getCode()) {return Result.fail("该公告不在待审核状态");}// 记录审核信息AuditRecord audit = new AuditRecord();audit.setAnnouncementId(id);audit.setAuditorId(SecurityUtils.getCurrentUserId());audit.setAuditStatus(dto.getStatus());audit.setAuditComment(dto.getComment());auditMapper.insert(audit);// 更新公告状态announcement.setStatus(dto.getStatus() == 1 ? AnnouncementStatus.PUBLISHED.getCode() : AnnouncementStatus.REJECTED.getCode());announcementMapper.updateById(announcement);return Result.success();}
}
这个审核流程实现了以下功能:
- 支持配置不同角色的审核权限
- 提供待审核和已审核两个列表视图
- 审核时可以选择通过或驳回,并填写审核意见
- 记录完整的审核历史
- 审核通过后自动发布公告,驳回后需要修改重新提交
可以根据实际需求进一步扩展:
- 增加多级审核流程
- 添加审核时限提醒
- 实现审核意见模板
- 增加审核数据统计分析
- 添加邮件通知等功能
校园公告管理系统
三、评论互动模块
3.1 公告评论功能
数据库设计
-- 评论表
CREATE TABLE announcement_comment (id BIGINT PRIMARY KEY AUTO_INCREMENT,announcement_id BIGINT NOT NULL COMMENT '公告ID',user_id BIGINT NOT NULL COMMENT '评论用户ID',content TEXT NOT NULL COMMENT '评论内容',parent_id BIGINT COMMENT '父评论ID',reply_user_id BIGINT COMMENT '回复用户ID',status TINYINT DEFAULT 1 COMMENT '状态(0:已删除,1:正常)',create_time DATETIME DEFAULT CURRENT_TIMESTAMP,FOREIGN KEY (announcement_id) REFERENCES announcement(id),FOREIGN KEY (user_id) REFERENCES sys_user(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
前端实现
<!-- src/components/CommentList.vue -->
<template><div class="comment-list"><!-- 评论输入框 --><div class="comment-input"><el-inputv-model="commentContent"type="textarea":rows="3"placeholder="请输入评论内容"/><el-button type="primary" @click="submitComment">发表评论</el-button></div><!-- 评论列表 --><div class="comment-items"><div v-for="item in comments" :key="item.id" class="comment-item"><div class="comment-header"><el-avatar :src="item.userAvatar" /><span class="username">{{ item.username }}</span><span class="time">{{ formatTime(item.createTime) }}</span></div><div class="comment-content">{{ item.content }}</div><div class="comment-actions"><el-button link @click="handleReply(item)">回复</el-button><el-button v-if="canDelete(item)" link type="danger" @click="handleDelete(item)">删除</el-button></div><!-- 回复列表 --><div v-if="item.replies" class="reply-list"><div v-for="reply in item.replies" :key="reply.id" class="reply-item"><div class="reply-header"><el-avatar :src="reply.userAvatar" size="small" /><span class="username">{{ reply.username }}</span><span class="reply-to">回复</span><span class="username">{{ reply.replyUsername }}</span><span class="time">{{ formatTime(reply.createTime) }}</span></div><div class="reply-content">{{ reply.content }}</div></div></div></div></div></div>
</template><script setup>
import { ref, onMounted } from 'vue'
import { useUserStore } from '@/stores/user'
import { getComments, addComment, deleteComment } from '@/api/comment'const props = defineProps({announcementId: {type: Number,required: true}
})const userStore = useUserStore()
const comments = ref([])
const commentContent = ref('')
const replyTo = ref(null)// 加载评论列表
const loadComments = async () => {const { data } = await getComments(props.announcementId)comments.value = data
}// 提交评论
const submitComment = async () => {if (!commentContent.value.trim()) {ElMessage.warning('请输入评论内容')return}try {await addComment({announcementId: props.announcementId,content: commentContent.value,parentId: replyTo.value?.id,replyUserId: replyTo.value?.userId})ElMessage.success('评论成功')commentContent.value = ''replyTo.value = nullloadComments()} catch (error) {console.error('评论失败:', error)}
}// 删除评论
const handleDelete = async (comment) => {try {await ElMessageBox.confirm('确定要删除该评论吗?')await deleteComment(comment.id)ElMessage.success('删除成功')loadComments()} catch (error) {console.error('删除失败:', error)}
}onMounted(() => {loadComments()
})
</script>
后端实现
java">@RestController
@RequestMapping("/api/comment")
public class CommentController {@Autowiredprivate CommentService commentService;@GetMapping("/list")public Result getComments(@RequestParam Long announcementId) {return commentService.getCommentList(announcementId);}@PostMappingpublic Result addComment(@RequestBody @Valid CommentDTO dto) {return commentService.addComment(dto);}@DeleteMapping("/{id}")public Result deleteComment(@PathVariable Long id) {return commentService.deleteComment(id);}
}@Service
public class CommentServiceImpl implements CommentService {@Autowiredprivate CommentMapper commentMapper;@Overridepublic Result getCommentList(Long announcementId) {// 查询主评论List<Comment> comments = commentMapper.selectList(new LambdaQueryWrapper<Comment>().eq(Comment::getAnnouncementId, announcementId).isNull(Comment::getParentId).eq(Comment::getStatus, 1).orderByDesc(Comment::getCreateTime));// 查询回复for (Comment comment : comments) {List<Comment> replies = commentMapper.selectList(new LambdaQueryWrapper<Comment>().eq(Comment::getParentId, comment.getId()).eq(Comment::getStatus, 1).orderByAsc(Comment::getCreateTime));comment.setReplies(replies);}return Result.success().data("list", comments);}@Override@Transactional(rollbackFor = Exception.class)public Result addComment(CommentDTO dto) {Comment comment = new Comment();BeanUtils.copyProperties(dto, comment);comment.setUserId(SecurityUtils.getCurrentUserId());commentMapper.insert(comment);return Result.success();}@Override@Transactional(rollbackFor = Exception.class)public Result deleteComment(Long id) {Comment comment = commentMapper.selectById(id);if (comment == null) {return Result.fail("评论不存在");}// 检查权限Long currentUserId = SecurityUtils.getCurrentUserId();if (!currentUserId.equals(comment.getUserId()) && !SecurityUtils.hasRole("ADMIN")) {return Result.fail("无权删除此评论");}// 逻辑删除comment.setStatus(0);commentMapper.updateById(comment);return Result.success();}
}
3.2 评论管理
数据库设计
-- 评论举报表
CREATE TABLE comment_report (id BIGINT PRIMARY KEY AUTO_INCREMENT,comment_id BIGINT NOT NULL COMMENT '评论ID',reporter_id BIGINT NOT NULL COMMENT '举报人ID',reason VARCHAR(500) NOT NULL COMMENT '举报原因',status TINYINT DEFAULT 0 COMMENT '处理状态(0:待处理,1:已处理)',handle_time DATETIME COMMENT '处理时间',handle_result VARCHAR(500) COMMENT '处理结果',create_time DATETIME DEFAULT CURRENT_TIMESTAMP,FOREIGN KEY (comment_id) REFERENCES announcement_comment(id),FOREIGN KEY (reporter_id) REFERENCES sys_user(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;-- 评论敏感词表
CREATE TABLE sensitive_word (id BIGINT PRIMARY KEY AUTO_INCREMENT,word VARCHAR(50) NOT NULL COMMENT '敏感词',level TINYINT DEFAULT 1 COMMENT '级别(1:一般,2:严重)',create_time DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
前端实现
<!-- src/views/comment/Manage.vue -->
<template><div class="comment-manage"><el-tabs v-model="activeTab"><el-tab-pane label="评论列表" name="list"><el-table :data="commentList" border><el-table-column prop="content" label="评论内容" show-overflow-tooltip /><el-table-column prop="username" label="评论用户" width="120" /><el-table-column prop="announcementTitle" label="所属公告" width="200" /><el-table-column prop="createTime" label="评论时间" width="160" /><el-table-column prop="status" label="状态" width="100"><template #default="{ row }"><el-tag :type="row.status === 1 ? 'success' : 'danger'">{{ row.status === 1 ? '正常' : '已删除' }}</el-tag></template></el-table-column><el-table-column label="操作" width="150" fixed="right"><template #default="{ row }"><el-button v-if="row.status === 1"link type="danger" @click="handleDelete(row)">删除</el-button><el-button v-elselink type="success" @click="handleRestore(row)">恢复</el-button></template></el-table-column></el-table></el-tab-pane><el-tab-pane label="举报处理" name="report"><el-table :data="reportList" border><el-table-column prop="commentContent" label="评论内容" show-overflow-tooltip /><el-table-column prop="reason" label="举报原因" show-overflow-tooltip /><el-table-column prop="reporterName" label="举报人" width="120" /><el-table-column prop="createTime" label="举报时间" width="160" /><el-table-column prop="status" label="状态" width="100"><template #default="{ row }"><el-tag :type="row.status === 0 ? 'warning' : 'success'">{{ row.status === 0 ? '待处理' : '已处理' }}</el-tag></template></el-table-column><el-table-column label="操作" width="150" fixed="right"><template #default="{ row }"><el-button v-if="row.status === 0"link type="primary" @click="handleReport(row)">处理</el-button></template></el-table-column></el-table></el-tab-pane><el-tab-pane label="敏感词管理" name="sensitive"><div class="toolbar"><el-button type="primary" @click="handleAddWord">添加敏感词</el-button><el-button @click="handleBatchImport">批量导入</el-button></div><el-table :data="wordList" border><el-table-column prop="word" label="敏感词" /><el-table-column prop="level" label="级别" width="100"><template #default="{ row }"><el-tag :type="row.level === 2 ? 'danger' : ''">{{ row.level === 2 ? '严重' : '一般' }}</el-tag></template></el-table-column><el-table-column label="操作" width="150"><template #default="{ row }"><el-button link type="danger" @click="handleDeleteWord(row)">删除</el-button></template></el-table-column></el-table></el-tab-pane></el-tabs><!-- 处理举报对话框 --><el-dialogtitle="处理举报"v-model="reportDialogVisible"width="500px"><el-form ref="reportFormRef" :model="reportForm" label-width="80px"><el-form-item label="处理结果" prop="result"><el-inputv-model="reportForm.result"type="textarea":rows="3"placeholder="请输入处理结果"/></el-form-item></el-form><template #footer><el-button @click="reportDialogVisible = false">取消</el-button><el-button type="primary" @click="submitReport">确定</el-button></template></el-dialog></div>
</template><script setup>
import { ref, reactive } from 'vue'
import { getCommentList, deleteComment, restoreComment,getReportList,handleReport,getSensitiveWords,addSensitiveWord,deleteSensitiveWord
} from '@/api/comment'// ... 组件逻辑实现
</script>
后端实现
java">@RestController
@RequestMapping("/api/comment/manage")
public class CommentManageController {@Autowiredprivate CommentManageService commentManageService;@GetMapping("/list")@RequiresPermission("comment:manage")public Result getCommentList(CommentQuery query) {return commentManageService.getCommentList(query);}@PutMapping("/{id}/restore")@RequiresPermission("comment:manage")public Result restoreComment(@PathVariable Long id) {return commentManageService.restoreComment(id);}@GetMapping("/report/list")@RequiresPermission("comment:manage")public Result getReportList(ReportQuery query) {return commentManageService.getReportList(query);}@PutMapping("/report/{id}")@RequiresPermission("comment:manage")public Result handleReport(@PathVariable Long id, @RequestBody HandleReportDTO dto) {return commentManageService.handleReport(id, dto);}@GetMapping("/sensitive/list")@RequiresPermission("comment:manage")public Result getSensitiveWords() {return commentManageService.getSensitiveWords();}@PostMapping("/sensitive")@RequiresPermission("comment:manage")public Result addSensitiveWord(@RequestBody @Valid SensitiveWordDTO dto) {return commentManageService.addSensitiveWord(dto);}@DeleteMapping("/sensitive/{id}")@RequiresPermission("comment:manage")public Result deleteSensitiveWord(@PathVariable Long id) {return commentManageService.deleteSensitiveWord(id);}
}@Service
public class CommentManageServiceImpl implements CommentManageService {@Autowiredprivate CommentMapper commentMapper;@Autowiredprivate ReportMapper reportMapper;@Autowiredprivate SensitiveWordMapper sensitiveWordMapper;@Overridepublic Result getCommentList(CommentQuery query) {Page<Comment> page = commentMapper.selectPage(query);return Result.success().data("page", page);}@Override@Transactional(rollbackFor = Exception.class)public Result restoreComment(Long id) {Comment comment = commentMapper.selectById(id);if (comment == null) {return Result.fail("评论不存在");}comment.setStatus(1);commentMapper.updateById(comment);return Result.success();}@Overridepublic Result getReportList(ReportQuery query) {Page<CommentReport> page = reportMapper.selectPage(query);return Result.success().data("page", page);}@Override@Transactional(rollbackFor = Exception.class)public Result handleReport(Long id, HandleReportDTO dto) {CommentReport report = reportMapper.selectById(id);if (report == null) {return Result.fail("举报记录不存在");}report.setStatus(1);report.setHandleTime(new Date());report.setHandleResult(dto.getResult());reportMapper.updateById(report);return Result.success();}// ... 敏感词相关方法实现
}
3.3 点赞与收藏
数据库设计
-- 点赞记录表
CREATE TABLE announcement_like (id BIGINT PRIMARY KEY AUTO_INCREMENT,announcement_id BIGINT NOT NULL COMMENT '公告ID',user_id BIGINT NOT NULL COMMENT '用户ID',create_time DATETIME DEFAULT CURRENT_TIMESTAMP,UNIQUE KEY uk_announcement_user (announcement_id, user_id),FOREIGN KEY (announcement_id) REFERENCES announcement(id),FOREIGN KEY (user_id) REFERENCES sys_user(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;-- 收藏记录表
CREATE TABLE announcement_favorite (id BIGINT PRIMARY KEY AUTO_INCREMENT,announcement_id BIGINT NOT NULL COMMENT '公告ID',user_id BIGINT NOT NULL COMMENT '用户ID',create_time DATETIME DEFAULT CURRENT_TIMESTAMP,UNIQUE KEY uk_announcement_user (announcement_id, user_id),FOREIGN KEY (announcement_id) REFERENCES announcement(id),FOREIGN KEY (user_id) REFERENCES sys_user(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
前端实现
<!-- src/components/AnnouncementActions.vue -->
<template><div class="announcement-actions"><div class="action-item"><el-button :type="isLiked ? 'primary' : 'default'":icon="isLiked ? 'Thumb' : 'ThumbFilled'"circle@click="toggleLike"/><span class="count">{{ likeCount }}</span></div><div class="action-item"><el-button:type="isFavorited ? 'warning' : 'default'":icon="isFavorited ? 'Star' : 'StarFilled'"circle@click="toggleFavorite"/><span class="count">{{ favoriteCount }}</span></div></div>
</template><script setup>
import { ref, onMounted } from 'vue'
import { getLikeStatus, toggleLike, getFavoriteStatus, toggleFavorite
} from '@/api/interaction'const props = defineProps({announcementId: {type: Number,required: true}
})const isLiked = ref(false)
const isFavorited = ref(false)
const likeCount = ref(0)
const favoriteCount = ref(0)// 加载点赞和收藏状态
const loadStatus = async () => {const [likeRes, favoriteRes] = await Promise.all([getLikeStatus(props.announcementId),getFavoriteStatus(props.announcementId)])isLiked.value = likeRes.data.isLikedlikeCount.value = likeRes.data.countisFavorited.value = favoriteRes.data.isFavoritedfavoriteCount.value = favoriteRes.data.count
}// 切换点赞状态
const handleToggleLike = async () => {try {const { data } = await toggleLike(props.announcementId)isLiked.value = data.isLikedlikeCount.value = data.count} catch (error) {console.error('操作失败:', error)}
}// 切换收藏状态
const handleToggleFavorite = async () => {try {const { data } = await toggleFavorite(props.announcementId)isFavorited.value = data.isFavoritedfavoriteCount.value = data.count} catch (error) {console.error('操作失败:', error)}
}onMounted(() => {loadStatus()
})
</script><style scoped>
.announcement-actions {display: flex;gap: 20px;padding: 10px 0;
}.action-item {display: flex;align-items: center;gap: 5px;
}.count {font-size: 14px;color: #666;
}
</style><!-- src/views/user/Favorites.vue -->
<template><div class="favorites-page"><el-tabs v-model="activeTab"><el-tab-pane label="我的收藏" name="favorites"><announcement-list :data="favoriteList"@refresh="loadFavorites"/></el-tab-pane><el-tab-pane label="我的点赞" name="likes"><announcement-list :data="likeList"@refresh="loadLikes"/></el-tab-pane></el-tabs></div>
</template><script setup>
import { ref, onMounted } from 'vue'
import AnnouncementList from '@/components/AnnouncementList.vue'
import { getFavorites, getLikes } from '@/api/interaction'const activeTab = ref('favorites')
const favoriteList = ref([])
const likeList = ref([])// 加载收藏列表
const loadFavorites = async () => {const { data } = await getFavorites()favoriteList.value = data
}// 加载点赞列表
const loadLikes = async () => {const { data } = await getLikes()likeList.value = data
}onMounted(() => {loadFavorites()
})watch(() => activeTab.value, (tab) => {if (tab === 'likes') {loadLikes()} else {loadFavorites()}
})
</script>
后端实现
java">@RestController
@RequestMapping("/api/interaction")
public class InteractionController {@Autowiredprivate InteractionService interactionService;@GetMapping("/like/status")public Result getLikeStatus(@RequestParam Long announcementId) {return interactionService.getLikeStatus(announcementId);}@PostMapping("/like/{id}")public Result toggleLike(@PathVariable Long id) {return interactionService.toggleLike(id);}@GetMapping("/favorite/status")public Result getFavoriteStatus(@RequestParam Long announcementId) {return interactionService.getFavoriteStatus(announcementId);}@PostMapping("/favorite/{id}")public Result toggleFavorite(@PathVariable Long id) {return interactionService.toggleFavorite(id);}@GetMapping("/favorites")public Result getFavorites() {return interactionService.getUserFavorites();}@GetMapping("/likes")public Result getLikes() {return interactionService.getUserLikes();}
}@Service
public class InteractionServiceImpl implements InteractionService {@Autowiredprivate LikeMapper likeMapper;@Autowiredprivate FavoriteMapper favoriteMapper;@Autowiredprivate AnnouncementMapper announcementMapper;@Overridepublic Result getLikeStatus(Long announcementId) {Long userId = SecurityUtils.getCurrentUserId();// 查询点赞状态boolean isLiked = likeMapper.exists(new LambdaQueryWrapper<AnnouncementLike>().eq(AnnouncementLike::getAnnouncementId, announcementId).eq(AnnouncementLike::getUserId, userId));// 查询点赞数int count = likeMapper.selectCount(new LambdaQueryWrapper<AnnouncementLike>().eq(AnnouncementLike::getAnnouncementId, announcementId));return Result.success().data("isLiked", isLiked).data("count", count);}@Override@Transactional(rollbackFor = Exception.class)public Result toggleLike(Long announcementId) {Long userId = SecurityUtils.getCurrentUserId();// 查询是否已点赞LambdaQueryWrapper<AnnouncementLike> wrapper = new LambdaQueryWrapper<AnnouncementLike>().eq(AnnouncementLike::getAnnouncementId, announcementId).eq(AnnouncementLike::getUserId, userId);AnnouncementLike like = likeMapper.selectOne(wrapper);if (like == null) {// 添加点赞like = new AnnouncementLike();like.setAnnouncementId(announcementId);like.setUserId(userId);likeMapper.insert(like);} else {// 取消点赞likeMapper.delete(wrapper);}// 返回最新状态return getLikeStatus(announcementId);}// ... 收藏相关方法实现类似
}
四、系统管理模块
4.1 系统参数配置
数据库设计
-- 系统参数表
CREATE TABLE sys_config (id BIGINT PRIMARY KEY AUTO_INCREMENT,param_key VARCHAR(100) NOT NULL COMMENT '参数键',param_value TEXT COMMENT '参数值',param_desc VARCHAR(500) COMMENT '参数描述',type VARCHAR(20) DEFAULT 'text' COMMENT '参数类型(text/number/boolean/json)',is_system TINYINT DEFAULT 0 COMMENT '是否系统参数(0:否,1:是)',create_time DATETIME DEFAULT CURRENT_TIMESTAMP,update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,UNIQUE KEY uk_key (param_key)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;-- 参数修改记录表
CREATE TABLE sys_config_log (id BIGINT PRIMARY KEY AUTO_INCREMENT,param_key VARCHAR(100) NOT NULL COMMENT '参数键',old_value TEXT COMMENT '原值',new_value TEXT COMMENT '新值',operator_id BIGINT NOT NULL COMMENT '操作人ID',create_time DATETIME DEFAULT CURRENT_TIMESTAMP,FOREIGN KEY (operator_id) REFERENCES sys_user(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
前端实现
<!-- src/views/system/Config.vue -->
<template><div class="system-config"><div class="toolbar"><el-button type="primary" @click="handleAdd">新增参数</el-button><el-button @click="handleRefresh">刷新缓存</el-button></div><el-table :data="configList" border><el-table-column prop="paramKey" label="参数键" width="200" /><el-table-column prop="paramValue" label="参数值" show-overflow-tooltip><template #default="{ row }"><template v-if="row.type === 'boolean'"><el-tag :type="row.paramValue === 'true' ? 'success' : 'info'">{{ row.paramValue === 'true' ? '是' : '否' }}</el-tag></template><template v-else>{{ row.paramValue }}</template></template></el-table-column><el-table-column prop="paramDesc" label="描述" show-overflow-tooltip /><el-table-column prop="type" label="类型" width="100"><template #default="{ row }"><el-tag>{{ row.type }}</el-tag></template></el-table-column><el-table-column prop="isSystem" label="系统参数" width="100"><template #default="{ row }"><el-tag :type="row.isSystem ? 'danger' : ''">{{ row.isSystem ? '是' : '否' }}</el-tag></template></el-table-column><el-table-column label="操作" width="150" fixed="right"><template #default="{ row }"><el-button link @click="handleEdit(row)":disabled="row.isSystem">编辑</el-button><el-button link type="danger" @click="handleDelete(row)":disabled="row.isSystem">删除</el-button></template></el-table-column></el-table><!-- 新增/编辑对话框 --><el-dialog:title="dialogTitle"v-model="dialogVisible"width="500px"><el-form ref="formRef" :model="form" :rules="rules" label-width="100px"><el-form-item label="参数键" prop="paramKey"><el-input v-model="form.paramKey" :disabled="!!form.id"placeholder="请输入参数键"/></el-form-item><el-form-item label="参数值" prop="paramValue"><el-inputv-if="form.type === 'text'"v-model="form.paramValue"type="textarea":rows="3"placeholder="请输入参数值"/><el-input-numberv-else-if="form.type === 'number'"v-model="form.paramValue":controls="false"style="width: 100%"/><el-switchv-else-if="form.type === 'boolean'"v-model="form.paramValue":active-value="'true'":inactive-value="'false'"/><json-editorv-elsev-model="form.paramValue":height="200"/></el-form-item><el-form-item label="参数描述" prop="paramDesc"><el-inputv-model="form.paramDesc"type="textarea":rows="2"placeholder="请输入参数描述"/></el-form-item><el-form-item label="参数类型" prop="type"><el-select v-model="form.type" style="width: 100%"><el-option label="文本" value="text" /><el-option label="数字" value="number" /><el-option label="布尔" value="boolean" /><el-option label="JSON" value="json" /></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>
import { ref, reactive, computed } from 'vue'
import JsonEditor from '@/components/JsonEditor.vue'
import { getConfigList, addConfig, updateConfig, deleteConfig,refreshCache
} from '@/api/system'// ... 组件逻辑实现
</script>
后端实现
java">@RestController
@RequestMapping("/api/system/config")
public class ConfigController {@Autowiredprivate ConfigService configService;@GetMapping("/list")@RequiresPermission("system:config")public Result getConfigList() {return configService.getConfigList();}@PostMapping@RequiresPermission("system:config")public Result addConfig(@RequestBody @Valid ConfigDTO dto) {return configService.addConfig(dto);}@PutMapping("/{id}")@RequiresPermission("system:config")public Result updateConfig(@PathVariable Long id, @RequestBody @Valid ConfigDTO dto) {return configService.updateConfig(id, dto);}@DeleteMapping("/{id}")@RequiresPermission("system:config")public Result deleteConfig(@PathVariable Long id) {return configService.deleteConfig(id);}@PostMapping("/refresh")@RequiresPermission("system:config")public Result refreshCache() {return configService.refreshCache();}
}@Service
public class ConfigServiceImpl implements ConfigService {@Autowiredprivate ConfigMapper configMapper;@Autowiredprivate ConfigLogMapper configLogMapper;@Autowiredprivate RedisTemplate<String, String> redisTemplate;private static final String CONFIG_CACHE_KEY = "system:config";@Overridepublic Result getConfigList() {List<Config> configs = configMapper.selectList(null);return Result.success().data("list", configs);}@Override@Transactional(rollbackFor = Exception.class)public Result addConfig(ConfigDTO dto) {// 检查参数键是否重复if (configMapper.selectByKey(dto.getParamKey()) != null) {return Result.fail("参数键已存在");}Config config = new Config();BeanUtils.copyProperties(dto, config);configMapper.insert(config);// 更新缓存updateCache(config);return Result.success();}@Override@Transactional(rollbackFor = Exception.class)public Result updateConfig(Long id, ConfigDTO dto) {Config config = configMapper.selectById(id);if (config == null) {return Result.fail("参数不存在");}if (config.getIsSystem()) {return Result.fail("系统参数不允许修改");}// 记录修改日志ConfigLog log = new ConfigLog();log.setParamKey(config.getParamKey());log.setOldValue(config.getParamValue());log.setNewValue(dto.getParamValue());log.setOperatorId(SecurityUtils.getCurrentUserId());configLogMapper.insert(log);// 更新参数BeanUtils.copyProperties(dto, config);configMapper.updateById(config);// 更新缓存updateCache(config);return Result.success();}@Override@Transactional(rollbackFor = Exception.class)public Result deleteConfig(Long id) {Config config = configMapper.selectById(id);if (config == null) {return Result.fail("参数不存在");}if (config.getIsSystem()) {return Result.fail("系统参数不允许删除");}configMapper.deleteById(id);// 从缓存中删除redisTemplate.opsForHash().delete(CONFIG_CACHE_KEY, config.getParamKey());return Result.success();}@Overridepublic Result refreshCache() {// 清空缓存redisTemplate.delete(CONFIG_CACHE_KEY);// 重新加载所有配置到缓存List<Config> configs = configMapper.selectList(null);for (Config config : configs) {updateCache(config);}return Result.success();}private void updateCache(Config config) {redisTemplate.opsForHash().put(CONFIG_CACHE_KEY,config.getParamKey(),config.getParamValue());}
}
4.2 操作日志记录
数据库设计
-- 操作日志表
CREATE TABLE sys_operation_log (id BIGINT PRIMARY KEY AUTO_INCREMENT,user_id BIGINT COMMENT '操作人ID',username VARCHAR(50) COMMENT '操作人用户名',module VARCHAR(50) COMMENT '操作模块',operation VARCHAR(50) COMMENT '操作类型',method VARCHAR(200) COMMENT '请求方法',params TEXT COMMENT '请求参数',time BIGINT COMMENT '执行时长(毫秒)',ip VARCHAR(50) COMMENT '操作IP',location VARCHAR(100) COMMENT '操作地点',status TINYINT COMMENT '操作状态(0:失败,1:成功)',error_msg TEXT COMMENT '错误消息',create_time DATETIME DEFAULT CURRENT_TIMESTAMP,FOREIGN KEY (user_id) REFERENCES sys_user(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;-- 登录日志表
CREATE TABLE sys_login_log (id BIGINT PRIMARY KEY AUTO_INCREMENT,username VARCHAR(50) COMMENT '用户名',ip VARCHAR(50) COMMENT '登录IP',location VARCHAR(100) COMMENT '登录地点',browser VARCHAR(50) COMMENT '浏览器',os VARCHAR(50) COMMENT '操作系统',status TINYINT COMMENT '登录状态(0:失败,1:成功)',msg VARCHAR(200) COMMENT '提示消息',create_time DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
前端实现
<!-- src/views/system/OperationLog.vue -->
<template><div class="operation-log"><!-- 搜索表单 --><el-form :inline="true" :model="queryParams" class="search-form"><el-form-item label="操作人"><el-input v-model="queryParams.username" placeholder="请输入操作人" /></el-form-item><el-form-item label="操作模块"><el-select v-model="queryParams.module" placeholder="请选择模块"><el-optionv-for="item in moduleOptions":key="item.value":label="item.label":value="item.value"/></el-select></el-form-item><el-form-item label="操作类型"><el-select v-model="queryParams.operation" placeholder="请选择类型"><el-optionv-for="item in operationOptions":key="item.value":label="item.label":value="item.value"/></el-select></el-form-item><el-form-item label="状态"><el-select v-model="queryParams.status" placeholder="请选择状态"><el-option label="成功" :value="1" /><el-option label="失败" :value="0" /></el-select></el-form-item><el-form-item label="操作时间"><el-date-pickerv-model="queryParams.timeRange"type="daterange"range-separator="至"start-placeholder="开始日期"end-placeholder="结束日期"/></el-form-item><el-form-item><el-button type="primary" @click="handleQuery">查询</el-button><el-button @click="resetQuery">重置</el-button><el-button type="danger" @click="handleClear">清空日志</el-button></el-form-item></el-form><!-- 日志列表 --><el-table :data="logList" border><el-table-column prop="username" label="操作人" width="120" /><el-table-column prop="module" label="操作模块" width="120" /><el-table-column prop="operation" label="操作类型" width="120" /><el-table-column prop="method" label="请求方法" show-overflow-tooltip /><el-table-column prop="params" label="请求参数" show-overflow-tooltip><template #default="{ row }"><el-button link @click="showParams(row)">查看</el-button></template></el-table-column><el-table-column prop="time" label="执行时长" width="100"><template #default="{ row }">{{ row.time }}ms</template></el-table-column><el-table-column prop="ip" label="操作IP" width="120" /><el-table-column prop="location" label="操作地点" width="150" /><el-table-column prop="status" label="状态" width="80"><template #default="{ row }"><el-tag :type="row.status === 1 ? 'success' : 'danger'">{{ row.status === 1 ? '成功' : '失败' }}</el-tag></template></el-table-column><el-table-column prop="createTime" label="操作时间" width="160" /></el-table><!-- 分页 --><el-paginationv-model:current-page="queryParams.pageNum"v-model:page-size="queryParams.pageSize":total="total":page-sizes="[10, 20, 50, 100]"layout="total, sizes, prev, pager, next, jumper"@size-change="handleSizeChange"@current-change="handleCurrentChange"/><!-- 参数详情对话框 --><el-dialogtitle="请求参数"v-model="paramsDialogVisible"width="600px"><pre class="params-content">{{ currentParams }}</pre></el-dialog></div>
</template><script setup>
import { ref, reactive } from 'vue'
import { getOperationLogs, clearLogs } from '@/api/system'// ... 组件逻辑实现
</script><!-- src/views/system/LoginLog.vue -->
<template><div class="login-log"><!-- 类似操作日志的实现,但字段有所不同 --></div>
</template>
后端实现
java">@Aspect
@Component
public class LogAspect {@Autowiredprivate OperationLogService operationLogService;@Around("@annotation(operationLog)")public Object around(ProceedingJoinPoint point, OperationLog operationLog) throws Throwable {long startTime = System.currentTimeMillis();Object result = null;try {result = point.proceed();// 记录正常日志saveLog(point, operationLog, System.currentTimeMillis() - startTime, true, null);return result;} catch (Exception e) {// 记录异常日志saveLog(point, operationLog, System.currentTimeMillis() - startTime, false, e.getMessage());throw e;}}private void saveLog(ProceedingJoinPoint point, OperationLog log, long time, boolean status, String errorMsg) {OperationLogEntity entity = new OperationLogEntity();// 设置操作人信息LoginUser user = SecurityUtils.getLoginUser();if (user != null) {entity.setUserId(user.getId());entity.setUsername(user.getUsername());}// 设置请求信息ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();if (attributes != null) {HttpServletRequest request = attributes.getRequest();entity.setIp(IpUtils.getIpAddr(request));entity.setLocation(AddressUtils.getRealAddressByIP(entity.getIp()));entity.setMethod(request.getMethod() + " " + request.getRequestURI());}// 设置注解信息entity.setModule(log.module());entity.setOperation(log.operation());// 设置请求参数entity.setParams(JsonUtils.toJsonString(point.getArgs()));// 设置执行信息entity.setTime(time);entity.setStatus(status ? 1 : 0);entity.setErrorMsg(errorMsg);// 保存日志operationLogService.saveAsync(entity);}
}@Service
public class OperationLogServiceImpl implements OperationLogService {@Autowiredprivate OperationLogMapper operationLogMapper;@Async@Overridepublic void saveAsync(OperationLogEntity log) {operationLogMapper.insert(log);}@Overridepublic PageResult<OperationLogVO> getLogList(LogQuery query) {Page<OperationLogEntity> page = operationLogMapper.selectPage(query);List<OperationLogVO> records = page.getRecords().stream().map(this::convertToVO).collect(Collectors.toList());return new PageResult<>(records, page.getTotal());}@Override@Transactional(rollbackFor = Exception.class)public void clearLogs() {operationLogMapper.delete(null);}private OperationLogVO convertToVO(OperationLogEntity entity) {OperationLogVO vo = new OperationLogVO();BeanUtils.copyProperties(entity, vo);return vo;}
}@Service
public class LoginLogServiceImpl implements LoginLogService {@Autowiredprivate LoginLogMapper loginLogMapper;@Overridepublic void recordLoginInfo(String username, boolean status, String msg) {LoginLogEntity log = new LoginLogEntity();log.setUsername(username);ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();if (attributes != null) {HttpServletRequest request = attributes.getRequest();log.setIp(IpUtils.getIpAddr(request));log.setLocation(AddressUtils.getRealAddressByIP(log.getIp()));log.setBrowser(UserAgentUtils.getBrowser(request));log.setOs(UserAgentUtils.getOS(request));}log.setStatus(status ? 1 : 0);log.setMsg(msg);loginLogMapper.insert(log);}// ... 其他方法实现
}
这个日志记录模块实现了以下功能:
- 使用AOP自动记录操作日志
- 异步保存日志提高性能
- 记录详细的操作信息和请求参数
- 支持查询、导出和清空日志
- 分别记录操作日志和登录日志
- 记录IP地址和地理位置信息
可以根据需求扩展:
- 添加日志归档功能
- 实现日志分析统计
- 添加敏感数据脱敏
- 支持日志实时推送
- 增加日志告警功能