05-审核机制模块(从管理员点击"审核"开始追踪)
提示:本文档使用了颜色标注来突出重点内容:
- 蓝色:文件路径和行号信息
- 橙色:关键提示、重要注意和问题
- 红色:抛出的问题
问题解答中的关键词语使用加粗标注。
学习这个模块时,建议先用管理员账号登录系统,打开后台审核页面 admin.jsp,一边点击一边对照本文追踪代码。

一、功能说明(一句话)
审核机制模块是 SCFS 的"质量闸门",只有通过管理员审核的店铺、菜品等内容,才会真正对普通用户可见,不合格的内容可以被驳回并记录日志。
与前面模块的关联
审核机制模块是系统的"质量把关"模块,与多个业务模块紧密配合:
1. 依赖关系
- 依赖于01-用户模块 :
- 只有管理员角色的用户才能进行审核操作
- 系统从Session中获取当前用户信息(
USER_SESSION),检查用户角色(userRole是否为"admin") - 审核操作会记录审核人信息,知道是谁审核的
关键代码示例:管理员角色验证
java
// 文件:src/main/java/com/scfs/controller/AdminController.java
// 位置:第88-133行,audit方法
@RequestMapping("/audit")
@ResponseBody
public Result audit(@RequestParam String entityType,
@RequestParam Long entityId,
@RequestParam String action,
@RequestParam(required = false) String comment,
HttpServletRequest request) {
try {
// 从Session中获取当前登录用户
User user = (User) request.getSession().getAttribute("USER_SESSION");
if (user == null) {
return new Result(false, "请先登录");
}
// 检查是否为管理员
if (!"ADMIN".equals(user.getUserRole()) && !"admin".equals(user.getUserRole())) {
return new Result(false, "权限不足,仅管理员可审核");
}
// 执行审核操作
boolean success;
if ("approve".equals(action)) {
success = auditService.approve(entityType, entityId, user.getUserId(), comment);
if (success) {
return new Result(true, "审核通过成功");
}
} else if ("reject".equals(action)) {
success = auditService.reject(entityType, entityId, user.getUserId(), comment);
if (success) {
return new Result(true, "审核驳回成功");
}
}
// ... 错误处理
}
}
- 审核对象包括前面模块的内容 :
- 02-数据展示模块:审核用户提交的店铺(新增、修改、删除申请)
- 03-菜品管理模块:审核用户提交的菜品(新增、修改、删除申请)
- 审核机制通过
status字段控制内容的可见性:status=0(PENDING):待审核,只有管理员能看到status=1(APPROVED):审核通过,所有用户都能看到status=-1(PENDING_DELETE):待删除审核
关键代码示例:通过status字段控制可见性
java
// 文件:src/main/java/com/scfs/mapper/DishMapper.java
// 位置:查询已审核通过的菜品(普通用户查询)
@Select("SELECT * FROM dish WHERE store_id = #{storeId} AND status = 1")
List<Dish> findByStoreId(@Param("storeId") Long storeId);
// 文件:src/main/java/com/scfs/mapper/DishMapper.java
// 位置:查询所有状态的菜品(管理员查询,包括待审核的)
@Select("SELECT * FROM dish WHERE store_id = #{storeId}")
List<Dish> findByStoreIdIncludingPending(@Param("storeId") Long storeId);
status字段的值说明:
-
status=0(PENDING):待审核,只有管理员能看到 -
status=1(APPROVED):审核通过,所有用户都能看到 -
status=-1(PENDING_DELETE):待删除审核 -
status=-2(REJECTED):已驳回,不对外显示 -
依赖于08-通知功能模块:
- 审核通过或驳回后,系统会通过08-通知功能模块发送通知给提交者
- 用户可以在个人中心查看审核结果通知
关键代码示例:审核后发送通知
java
// 文件:src/main/java/com/scfs/service/impl/AuditServiceImpl.java
// 位置:第653-670行,approve方法中发送审核通过通知
// 5. 发送审核通过通知给上传者
try {
Long uploadUserId = getUploadUserId(entityType, entityId); // 获取提交者ID
if (uploadUserId != null) {
String entityName = getEntityName(entityType, entityId);
String title = "审核通过通知";
String content = String.format("您提交的%s「%s」已通过审核", getEntityTypeName(entityType), entityName);
String actionUrl = getEntityDetailUrl(entityType, entityId);
// 创建通知对象
Notification notification = new Notification(uploadUserId, title, content, "AUDIT_PASS");
notification.setActionUrl(actionUrl);
// 调用08-通知功能模块创建通知
notificationService.createNotification(notification);
logger.info("已发送审核通过通知给用户: userId={}, entityType={}, entityId={}",
new Object[] { uploadUserId, entityType, entityId });
}
} catch (Exception e) {
logger.warn("发送审核通过通知失败", e);
}
// 文件:src/main/java/com/scfs/service/impl/AuditServiceImpl.java
// 位置:第726-751行,reject方法中发送审核驳回通知
// 5. 发送审核驳回通知给上传者
try {
Long uploadUserId = getUploadUserId(entityType, entityId);
if (uploadUserId != null) {
String entityName = getEntityName(entityType, entityId);
String title = "审核驳回通知";
String content = String.format("您提交的%s「%s」未通过审核。原因:%s",
getEntityTypeName(entityType), entityName,
comment != null && !comment.isEmpty() ? comment : "不符合审核要求");
String actionUrl = getEntityDetailUrl(entityType, entityId);
// 创建通知对象
Notification notification = new Notification(uploadUserId, title, content, "AUDIT_REJECT");
notification.setActionUrl(actionUrl);
// 调用08-通知功能模块创建通知
notificationService.createNotification(notification);
}
} catch (Exception e) {
logger.warn("发送审核驳回通知失败", e);
}
2. 数据流转
审核流程的完整数据流转:
用户在02/03模块添加/修改店铺或菜品
↓
数据保存时,status设为0(待审核),普通用户看不到
↓
05-审核机制模块:管理员在admin.jsp看到待审核列表
↓
管理员点击"通过"或"驳回"按钮
↓
05-审核机制模块更新status(通过设为1,驳回设为-2)
↓
如果是修改操作,系统处理原记录和新记录的关系
↓
记录审核日志(audit_log表)
↓
08-通知功能模块:发送审核结果通知给提交者
↓
用户收到通知,可以查看审核结果
3. 审核机制的工作原理
为什么需要审核:
- 用户提交的内容(店铺、菜品)可能有错误或不规范
- 需要管理员检查后才能对外展示
- 保证系统内容的质量
如何通过status控制可见性:
- 普通用户查询时,Mapper的SQL中会加上
WHERE status = 1条件 - 这样只有审核通过的内容才会显示给普通用户
- 管理员查询时会查询所有状态的内容(包括待审核的)
4. 学习建议
-
在学习本模块前:
- 建议先理解01-用户模块的用户角色管理(如何判断是否是管理员)
- 理解02-数据展示模块和03-菜品管理模块中status字段的作用
- 理解为什么用户提交的内容status是0,而查询时只查status=1的
-
学习时可以结合:
- 实际操作:添加一个店铺/菜品,观察status是什么
- 用管理员账号登录,查看待审核列表
- 执行审核操作,观察status的变化
- 查看数据库,理解audit_log表的审核日志
- 观察审核后,用户如何收到通知(08-通知功能模块)
-
学习本模块后:
- 理解为什么系统需要审核机制
- 理解如何通过status字段实现内容的状态管理
- 理解审核日志的作用(审计、追溯)
5. 问题解答
Q1:为什么需要审核机制?如果不审核会有什么问题?
A: 审核机制是系统的"质量闸门",确保内容质量。
为什么需要审核:
- 内容质量控制:用户提交的内容可能有错误、不规范或不符合要求
- 防止恶意内容:防止恶意用户提交不当内容(如广告、违规信息)
- 数据准确性:确保店铺、菜品信息的准确性和完整性
- 用户体验:通过审核的内容质量更高,提升用户体验
如果不审核会有什么问题:
- 错误信息会直接展示给用户,影响系统可信度
- 恶意内容可能传播,影响系统安全
- 数据质量无法保证,用户体验差
- 无法追溯内容来源,难以管理
实际例子:
- 用户可能输入错误的店铺名称或价格
- 用户可能上传不相关的图片
- 恶意用户可能提交虚假信息
Q2:status字段如何控制内容的可见性?
A: 通过SQL查询条件控制,普通用户和管理员看到的内容不同。
普通用户查询(只能看到已审核通过的):
java
// 文件:src/main/java/com/scfs/mapper/DishMapper.java
// 位置:查询方法中的SQL
@Select("SELECT * FROM dish WHERE store_id = #{storeId} AND status = 1")
List<Dish> findByStoreId(@Param("storeId") Long storeId);
- SQL条件:
WHERE status = 1 - 结果:只返回审核通过的内容
管理员查询(可以看到所有状态):
java
// 文件:src/main/java/com/scfs/mapper/DishMapper.java
// 位置:管理员查询方法中的SQL
@Select("SELECT * FROM dish WHERE store_id = #{storeId}")
List<Dish> findByStoreIdIncludingPending(@Param("storeId") Long storeId);
- SQL条件:没有status限制
- 结果:返回所有状态的内容,包括待审核的
status字段的值:
status=0(PENDING):待审核,只有管理员能看到status=1(APPROVED):审核通过,所有用户都能看到status=-1(PENDING_DELETE):待删除审核status=-2(REJECTED):已驳回,不对外显示
工作流程:
- 用户提交内容 →
status=0(待审核) - 管理员审核通过 →
status=1(审核通过)→ 普通用户可以看到 - 管理员审核驳回 →
status=-2(已驳回)→ 普通用户看不到
Q3:审核日志(audit_log表)的作用是什么?
A: 审核日志用于审计和追溯,记录所有审核操作的历史。
audit_log表的结构:
audit_log_id:审核日志ID(主键)entity_type:实体类型(STORE/DISH/REVIEW)entity_id:实体IDold_status:审核前的状态new_status:审核后的状态auditor_id:审核人IDaudit_comment:审核意见audit_time:审核时间
作用:
- 审计追踪:记录谁在什么时候审核了什么内容
- 问题追溯:如果出现问题,可以查看审核历史
- 责任明确:知道每个审核操作是谁执行的
- 数据分析:可以统计审核通过率、审核时间等
代码位置:
java
// 文件:src/main/java/com/scfs/service/impl/AuditServiceImpl.java
// 位置:记录审核日志的代码
// 创建审核日志对象(第643-650行)
AuditLog auditLog = new AuditLog();
auditLog.setEntityType(entityType);
auditLog.setEntityId(entityId);
auditLog.setOldStatus(oldStatus);
auditLog.setNewStatus(AuditStatus.APPROVED);
auditLog.setAuditorId(auditorId);
auditLog.setAuditComment(comment != null ? comment : "审核通过");
// 保存审核日志(第651行)
logAudit(auditLog);
查看审核历史:
- 管理员可以查看某个内容的审核历史
- 接口:
/admin/auditHistory?entityType=DISH&entityId=1 - 返回该内容的所有审核记录,包括审核人、审核时间、审核意见等
二、抛出问题
- 管理员打开后台审核页时,页面上的"待审核列表"是怎么从数据库里查出来的?
- 管理员点击某一行的"通过 / 驳回"按钮后,这次操作是怎么一步步传到后端、更新店铺/菜品状态,并记录到审核日志里的?
- 被审核的记录修改完成后,系统又是怎样把结果"反馈"到页面上,让这一行从列表中消失或状态改变的?
下面我们就从"管理员的一次审核操作"出发,完整追踪这条数据流。
三、场景 1:管理员打开后台,看到"待审核列表"
1. 功能说明
管理员进入后台后,首先会看到一个"待审核内容"的列表,里面列出了所有状态为待审核/待删除的店铺和菜品,方便他逐条进行审核。
2. 抛出问题
管理员只是打开了 admin.jsp 这个页面,系统是怎么"知道"当前有哪些店铺/菜品需要审核的?
3. 完整追踪流程
第一步:找到用户操作的入口
管理员打开后台页面 admin.jsp,页面中有一个待审核列表的表格容器:
html
<!-- 待审核列表表格 -->
<table class="table table-bordered">
<thead>
<tr>
<th>类型</th>
<th>名称</th>
<th>提交人</th>
<th>提交时间</th>
<th>操作内容</th>
<th>状态</th>
<th>操作</th>
</tr>
</thead>
<tbody id="pendingList">
<!-- 动态渲染待审核列表 -->
</tbody>
</table>
位置 :src/main/webapp/jsp/admin.jsp 第400行左右
关键点 :id="pendingList" 告诉我们,真正的数据不是写死在 JSP 里,而是由 JavaScript 之后动态填充进去的。
第二步:追踪前端JavaScript处理
页面加载完成后,会调用loadPendingList(type)函数加载待审核列表:
javascript
// [1] 加载待审核列表函数(数据流向:前端 → Controller)
function loadPendingList(type) {
console.log('开始加载待审核列表,类型:', type);
// [2] 发送AJAX请求获取待审核列表
$.ajax({
url: '${pageContext.request.contextPath}/admin/pendingList',
type: 'GET',
data: { type: type || 'dish' }, // 类型参数:dish或store
dataType: 'json',
// [3] 请求成功回调:处理后端返回的结果(数据流向:Controller → 前端)
success: function (response) {
if (response && response.success) {
if (response.data && response.data.list) {
// [4] 调用渲染函数,将待审核列表显示到页面
renderPendingList(response.data.list);
} else {
// [5] 没有数据:显示"暂无待审核内容"
$('#pendingList').html('<tr><td colspan="7" class="text-center">暂无待审核内容</td></tr>');
}
}
},
error: function (xhr, status, error) {
console.error('加载待审核列表失败:', error);
}
});
}
位置 :src/main/webapp/jsp/admin.jsp 第455-480行
渲染函数 :renderPendingList(list)将后端返回的数据渲染到表格中:
javascript
// [1] 渲染待审核列表函数
function renderPendingList(list) {
if (!list || list.length === 0) {
$('#pendingList').html('<tr><td colspan="7" class="text-center">暂无待审核内容</td></tr>');
return;
}
var html = '';
// [2] 遍历待审核列表,为每条记录生成一行表格
list.forEach(function (item) {
var entityType = (item.entity_type || item.type || '').toUpperCase();
var typeName = entityType === 'DISH' ? '菜品' : '店铺';
var status = item.status || 0;
var statusBadge = '';
if (status == 0) {
statusBadge = '<span class="label label-warning">待审核</span>';
} else if (status == -1) {
statusBadge = '<span class="label label-danger">待删除</span>';
}
html += '<tr>';
html += '<td>' + typeName + '</td>';
html += '<td>' + (item.title || item.entity_name || '未命名') + '</td>';
html += '<td>' + (item.submitter || '未知用户') + '</td>';
html += '<td>' + formatDateTime(item.submitTime || item.create_time) + '</td>';
html += '<td>' + (item.modifications || '') + '</td>';
html += '<td>' + statusBadge + '</td>';
html += '<td>';
// [3] 为每行添加"通过"和"驳回"按钮,绑定审核操作
html += '<button class="btn btn-sm btn-success" onclick="openAuditModal(\'' +
entityType + '\', ' + (item.entity_id || item.id) + ', \'approve\')">通过</button>';
html += '<button class="btn btn-sm btn-danger" onclick="openAuditModal(\'' +
entityType + '\', ' + (item.entity_id || item.id) + ', \'reject\')">驳回</button>';
html += '</td>';
html += '</tr>';
});
// [4] 将生成的HTML插入到表格容器中
$('#pendingList').html(html);
}
位置 :src/main/webapp/jsp/admin.jsp 第687-745行
第三步:追踪到Controller层
根据请求URL/admin/pendingList,找到AdminController的getPendingList方法:
java
// [1] 处理GET请求 /admin/pendingList(数据流向:前端 → Controller)
@RequestMapping("/pendingList")
@ResponseBody
public Result getPendingList(@RequestParam(defaultValue = "dish") String type,
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "10") int size) {
try {
// [2] 从请求参数中获取类型(dish或store)、页码、每页大小)
// [3] 调用Service层获取待审核列表(数据流向:Controller → Service)
PagedResult<Map<String, Object>> result = auditService.getPendingList(type, page, size);
// [4] 封装成Result对象返回(数据流向:Controller → 前端)
return new Result(true, "获取待审核列表成功", result);
} catch (Exception e) {
logger.error("获取待审核列表失败", e);
return new Result(false, "获取待审核列表失败");
}
}
位置 :src/main/java/com/scfs/controller/AdminController.java 第52-76行
第四步:追踪到Service层
Controller调用auditService.getPendingList(type, page, size),Service层实现:
java
// [1] 获取待审核列表(数据流向:Service → Mapper)
@Override
public PagedResult<Map<String, Object>> getPendingList(String type, int page, int size) {
try {
// [2] 根据类型(dish或store)查询待审核记录
List<Map<String, Object>> list;
int total = 0;
if ("dish".equalsIgnoreCase(type)) {
// [3] 查询待审核的菜品(status=0或status=-1)
list = auditMapper.selectPendingDishes((page - 1) * size, size);
total = auditMapper.countPendingDishes();
} else if ("store".equalsIgnoreCase(type)) {
// [4] 查询待审核的店铺(status=0或status=-1)
list = auditMapper.selectPendingStores((page - 1) * size, size);
total = auditMapper.countPendingStores();
} else {
list = new ArrayList<>();
}
// [5] 封装成分页结果返回(数据流向:Service → Controller)
return new PagedResult<>(list, total, page, size);
} catch (Exception e) {
logger.error("获取待审核列表失败", e);
return new PagedResult<>(new ArrayList<>(), 0, page, size);
}
}
位置 :src/main/java/com/scfs/service/impl/AuditServiceImpl.java 第45-70行
第五步:追踪到Mapper层
Service调用auditMapper.selectPendingDishes()或auditMapper.selectPendingStores(),Mapper层实现:
java
// [1] 查询待审核的菜品列表(数据流向:Mapper → 数据库)
@Select("SELECT dish_id AS entity_id, 'DISH' AS entity_type, dish_name AS entity_name, " +
"upload_user_id AS submitter_id, create_time AS submit_time, status " +
"FROM dish WHERE status IN (0, -1) " +
"ORDER BY create_time DESC LIMIT #{offset}, #{size}")
List<Map<String, Object>> selectPendingDishes(@Param("offset") int offset, @Param("size") int size);
// [2] 查询待审核的店铺列表(数据流向:Mapper → 数据库)
@Select("SELECT store_id AS entity_id, 'STORE' AS entity_type, store_name AS entity_name, " +
"upload_user_id AS submitter_id, create_time AS submit_time, status " +
"FROM store WHERE status IN (0, -1) " +
"ORDER BY create_time DESC LIMIT #{offset}, #{size}")
List<Map<String, Object>> selectPendingStores(@Param("offset") int offset, @Param("size") int size);
位置 :src/main/java/com/scfs/mapper/AuditMapper.java 第28-40行
执行顺序说明:
WHERE status IN (0, -1):只查询状态为待审核(0)或待删除审核(-1)的记录ORDER BY create_time DESC:按创建时间倒序排列(最新的在前)LIMIT #{offset}, #{size}:分页查询,限制返回的记录数- 数据库执行SQL查询后,返回
List<Map<String, Object>>(数据流向:数据库 → Mapper → Service)
第六步:数据返回流程
完整的数据返回路径:
- 数据库 → Mapper:数据库执行SELECT查询,返回待审核记录列表给Mapper
- Mapper → Service :Mapper将查询结果映射为
List<Map<String, Object>>对象,返回给Service - Service处理 :
- Service封装成分页结果
PagedResult,包含列表数据、总数、页码、每页大小 - 返回给Controller(数据流向:Service → Controller)
- Service封装成分页结果
- Controller处理 :
- Controller封装成
Result对象,success为true,data为分页结果 - 返回给前端(数据流向:Controller → 前端)
- Controller封装成
- 前端处理 :
- JavaScript接收到
Result对象 - 判断
response.success是否为true - 如果成功,调用
renderPendingList(response.data.list)渲染待审核列表 renderPendingList()遍历列表数组,为每条记录生成一行表格,包含类型、名称、提交人、提交时间、状态、操作按钮等信息- 将HTML插入到页面的
#pendingList容器中
- JavaScript接收到
这样,管理员打开后台审核页,系统就加载所有待审核的店铺和菜品,数据从数据库传递到前端,最后显示在页面上,整个流程就完成了。
待审核列表加载时序图
数据库 AuditMapper AuditService AdminController JavaScript admin.jsp 管理员 数据库 AuditMapper AuditService AdminController JavaScript admin.jsp 管理员 alt [类型是dish] [类型是store] [1] 打开后台审核页面 [2] 页面加载完成事件 [3] 调用loadPendingList(type) [4] AJAX GET /admin/pendingList (type=dish或store) [5] 接收GET请求 (@RequestParam提取type参数) [6] 调用auditService.getPendingList() [7] 判断类型(dish或store) [8] 调用auditMapper.selectPendingDishes() [9] 执行SQL: SELECT * FROM dish WHERE status IN (0, -1) ORDER BY create_time DESC LIMIT offset, size [10] 调用auditMapper.selectPendingStores() [11] 执行SQL: SELECT * FROM store WHERE status IN (0, -1) ORDER BY create_time DESC LIMIT offset, size [12] 返回待审核记录列表 [13] 返回List<Map> [14] 封装成PagedResult对象 [15] 返回PagedResult [16] 封装成Result对象 [17] 返回Result对象 (success, message, data=分页结果) [18] 判断response.success [19] 调用renderPendingList(response.data.list) [20] 遍历列表,生成HTML表格行 (包含通过/驳回按钮) [21] 将HTML插入到 [22] 显示待审核列表表格
待审核列表加载流程图
dish
store
管理员打开后台审核页面
页面加载完成
调用loadPendingList函数
发送AJAX请求
GET /admin/pendingList?type=dish
Controller接收请求
Service判断类型
类型是?
查询dish表
WHERE status IN (0, -1)
查询store表
WHERE status IN (0, -1)
数据库返回待审核菜品列表
数据库返回待审核店铺列表
数据层层返回
前端接收Result对象
调用renderPendingList渲染
显示待审核列表表格
(包含通过/驳回按钮)
为什么这样设计
为什么只查询status IN (0, -1)的记录?
-
原因1:业务逻辑
status=0表示待审核(新增或修改申请)status=-1表示待删除审核(删除申请)- 只有这两种状态的记录需要管理员审核
- 已审核通过的记录(
status=1)不需要再审核
-
原因2:数据过滤
- 只显示需要审核的记录,减少数据量
- 提升查询性能,减少数据库负载
- 提升用户体验,管理员只看到需要处理的内容
-
原因3:状态管理
- 通过
status字段统一管理审核状态 - 便于后续扩展其他状态
- 符合审核流程的业务逻辑
- 通过
为什么使用分页查询?
-
原因1:性能优化
- 如果待审核记录很多,一次性加载所有数据会很慢
- 分页查询只加载当前页的数据,提升查询速度
- 减少网络传输量,提升用户体验
-
原因2:用户体验
- 分页可以让管理员逐页查看,不会一次性显示太多内容
- 可以快速定位到需要审核的记录
- 提升页面的响应速度
-
原因3:系统稳定性
- 避免一次性加载大量数据导致内存溢出
- 减少数据库查询压力
- 提升系统的稳定性和可扩展性
第四步:根据 URL 找到 Controller(AdminController)
现在我们知道,前端是请求 /admin/pendingList。于是去 src/main/java/com/scfs/controller/AdminController.java 里搜索 /pendingList,可以找到对应方法(约第 52--76 行):
startLine:endLine:src/main/java/com/scfs/controller/AdminController.java
@RequestMapping("/pendingList")
@ResponseBody
public Result getPendingList(@RequestParam(defaultValue = "dish") String type,
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "10") int size) {
try {
logger.info("获取待审核列表请求 - type: {}, page: {}, size: {}",
new Object[]{type, String.valueOf(page), String.valueOf(size)});
if (page < 1)
page = 1;
if (size < 1 || size > 100)
size = 10;
Map<String, Object> result = adminService.getPendingList(type, page, size);
logger.info("待审核列表查询结果 - total: {}, list size: {}",
result.get("total"), ((List<?>) result.get("list")).size());
return new Result(true, "获取待审核列表成功", result);
} catch (Exception e) {
logger.error("获取待审核列表失败", e);
return new Result(false, "获取待审核列表失败: " + e.getMessage());
}
}
- Controller 接收前端传来的
type、page、size,做了一点参数校验。 - 核心是调用
adminService.getPendingList(type, page, size),真正查数据的逻辑在 Service 里。
第五步:Service 如何组织"待审核数据"?(AdminServiceImpl)
根据上面的调用关系,去 src/main/java/com/scfs/service/impl/AdminServiceImpl.java 找 getPendingList(约第 75--155 行):
startLine:endLine:src/main/java/com/scfs/service/impl/AdminServiceImpl.java
@Override
public Map<String, Object> getPendingList(String type, int page, int size) {
try {
logger.info("AdminServiceImpl.getPendingList - type: {}, page: {}, size: {}",
new Object[] { type, page, size });
// 1. 把前端类型转换为统一的实体类型
String entityType = null;
if ("dish".equals(type)) {
entityType = "DISH";
} else if ("store".equals(type)) {
entityType = "STORE";
}
// 2. 调用 AuditService 查询所有待审核记录
List<Map<String, Object>> allList = auditService.getPendingList(entityType);
if (allList == null) {
allList = new ArrayList<>();
}
// 3. 手动分页
int total = allList.size();
int offset = (page - 1) * size;
int end = Math.min(offset + size, total);
List<Map<String, Object>> pagedList = new ArrayList<>();
if (offset < total) {
pagedList = allList.subList(offset, end);
}
// 4. 格式化数据,适配前端字段名称
List<Map<String, Object>> formattedList = new ArrayList<>();
for (Map<String, Object> item : pagedList) {
if (item == null) continue;
Map<String, Object> formatted = new HashMap<>();
formatted.put("id", item.get("entity_id"));
formatted.put("entity_id", item.get("entity_id"));
formatted.put("type",
item.get("entity_type") != null ? ((String) item.get("entity_type")).toLowerCase() : "unknown");
formatted.put("entity_type", item.get("entity_type"));
formatted.put("title", item.get("entity_name"));
formatted.put("entity_name", item.get("entity_name"));
formatted.put("submitTime", item.get("create_time"));
formatted.put("create_time", item.get("create_time"));
formatted.put("submitter", item.get("submitter"));
formatted.put("status", item.get("status"));
// 获取修改内容对比(如果是修改操作)
String itemEntityType = (String) item.get("entity_type");
Long entityId = ((Number) item.get("entity_id")).longValue();
String modifications = getModificationDetails(itemEntityType, entityId);
formatted.put("modifications", modifications);
formattedList.add(formatted);
}
Map<String, Object> result = new HashMap<>();
result.put("list", formattedList);
result.put("total", total);
result.put("page", page);
result.put("size", size);
return result;
} catch (Exception e) {
logger.error("获取待审核列表失败", e);
Map<String, Object> result = new HashMap<>();
result.put("list", new ArrayList<>());
result.put("total", 0);
result.put("page", page);
result.put("size", size);
return result;
}
}
- 这里做了两件事:调用审核服务拿数据 ,然后根据前端需要的字段重新组装结果。
第六步:真正和数据库打交道的是谁?(AuditServiceImpl → AuditMapper)
刚才看到 AdminServiceImpl 调用了 auditService.getPendingList(entityType)。继续追到 src/main/java/com/scfs/service/impl/AuditServiceImpl.java(约第 55--69 行):
startLine:endLine:src/main/java/com/scfs/service/impl/AuditServiceImpl.java
@Override
public List<Map<String, Object>> getPendingList(String entityType) {
try {
logger.info("AuditServiceImpl.getPendingList - entityType: {}", entityType);
List<Map<String, Object>> result = auditMapper.selectPendingList(entityType);
return result != null ? result : new java.util.ArrayList<>();
} catch (Exception e) {
logger.error("获取待审核列表失败", e);
return new java.util.ArrayList<>();
}
}
可以看到这里直接调用了 auditMapper.selectPendingList,也就是 MyBatis 的 Mapper。
继续到 src/main/resources/com.scfs.mapper/AuditMapper.xml(约第 5--47 行):
startLine:endLine:src/main/resources/com.scfs.mapper/AuditMapper.xml
<!-- 获取待审核列表 -->
<select id="selectPendingList" resultType="java.util.HashMap">
<choose>
<when test="entityType == 'STORE'">
SELECT 'STORE' as entity_type, s.store_id as entity_id, s.store_name as entity_name,
s.upload_user_id, s.create_time, s.status,
COALESCE(u.user_name, '未知用户') as submitter
FROM store s
LEFT JOIN user u ON s.upload_user_id = u.user_id
WHERE s.status = 0 OR s.status = -1
ORDER BY s.create_time DESC
</when>
<when test="entityType == 'DISH'">
SELECT 'DISH' as entity_type, d.dish_id as entity_id, d.dish_name as entity_name,
d.upload_user_id, d.create_time, d.status,
COALESCE(u.user_name, '未知用户') as submitter
FROM dish d
LEFT JOIN user u ON d.upload_user_id = u.user_id
WHERE d.status = 0 OR d.status = -1
ORDER BY d.create_time DESC
</when>
<otherwise>
<!-- 获取全部待审核(只包含店铺和菜品,评价不需要审核) -->
SELECT * FROM (
SELECT 'STORE' as entity_type, s.store_id as entity_id, s.store_name as entity_name,
s.upload_user_id, s.create_time, s.status,
COALESCE(u.user_name, '未知用户') as submitter
FROM store s
LEFT JOIN user u ON s.upload_user_id = u.user_id
WHERE s.status = 0 OR s.status = -1
UNION ALL
SELECT 'DISH' as entity_type, d.dish_id as entity_id, d.dish_name as entity_name,
d.upload_user_id, d.create_time, d.status,
COALESCE(u2.user_name, '未知用户') as submitter
FROM dish d
LEFT JOIN user u2 ON d.upload_user_id = u.user_id
WHERE d.status = 0 OR d.status = -1
) AS combined
ORDER BY combined.create_time DESC
</otherwise>
</choose>
</select>
status = 0表示"待审核",status = -1表示"待删除审核"。- 通过
UNION ALL把店铺和菜品的待审核记录合并到一起。
到这里,"管理员打开后台看到待审核列表"这件事就从页面一路追踪到了数据库查询。
7. 数据如何返回并更新页面?
- 数据从数据库查出后,由
AuditMapper → AuditServiceImpl → AdminServiceImpl → AdminController一层层返回。 - 最终
AdminController把数据封装到Result对象里,以 JSON 形式返回给前端。 - 前端
loadPendingList的success回调拿到response.data.list,再交给renderPendingList渲染到表格中。
四、场景 2:管理员点击"通过/驳回",完成一次审核
1. 功能说明
在待审核列表中,每一行右侧都有"通过"和"驳回"两个按钮。管理员点击后,可以填写审核意见,并把这次操作结果保存到数据库,同时更新店铺/菜品的状态。
2. 抛出问题
- 管理员点击"通过/驳回"后,系统是怎么知道他是在审核哪条记录?
- 这次审核到底改了哪一张表、哪个字段?审核日志又是在哪里记录的?
3. 完整追踪流程
第一步:找到用户操作的入口
管理员在待审核列表中点击某一行的"通过"或"驳回"按钮,会打开审核模态框。
第二步:追踪前端JavaScript处理
2.1 打开审核模态框
点击按钮时,会调用openAuditModal(entityType, entityId, action)函数:
javascript
// [1] 打开审核模态框函数
function openAuditModal(entityType, entityId, action) {
// [2] 保存当前审核的记录信息到全局变量(包含entityType和entityId)
currentAuditItem = {
entityType: entityType.toUpperCase(),
entityId: entityId
};
// [3] 根据操作类型(通过或驳回)设置模态框内容
if (action === 'approve') {
$('input[name="auditResult"][value="approve"]').prop('checked', true);
$('#rejectReasonGroup').hide();
$('#modificationGuideGroup').hide();
} else {
$('input[name="auditResult"][value="reject"]').prop('checked', true);
$('#rejectReasonGroup').show();
$('#modificationGuideGroup').show();
}
// [4] 显示审核模态框
$('#auditModal').modal('show');
}
位置 :src/main/webapp/jsp/admin.jsp 第747-775行
关键点 :currentAuditItem全局变量保存了当前正在审核的记录信息(entityType和entityId),这样系统就知道管理员在审核哪条记录了。
2.2 提交审核操作
管理员填写审核意见后,点击"提交审核"按钮,会调用submitAudit()函数:
javascript
// [1] 提交审核函数(数据流向:前端 → Controller)
function submitAudit() {
// [2] 检查是否有当前审核项
if (!currentAuditItem) {
alert('审核信息错误');
return;
}
// [3] 获取审核结果(通过或驳回)
var action = $('input[name="auditResult"]:checked').val();
var comment = '';
// [4] 如果是驳回,收集驳回原因和修改指引
if (action === 'reject') {
var reason = $('#rejectReason').val().trim();
var guide = $('#modificationGuide').val();
if (!reason && !guide) {
alert('请输入驳回原因或选择修改指引');
return;
}
comment = reason;
if (guide) {
comment = comment ? comment + ';' + guide : guide;
}
}
// [5] 发送AJAX请求提交审核操作
$.ajax({
url: '${pageContext.request.contextPath}/admin/audit',
type: 'POST',
data: {
entityType: currentAuditItem.entityType, // 实体类型(DISH或STORE)
entityId: currentAuditItem.entityId, // 实体ID(菜品ID或店铺ID)
action: action, // 操作类型(approve或reject)
comment: comment // 审核意见
},
// [6] 请求成功回调:处理后端返回的结果(数据流向:Controller → 前端)
success: function (response) {
if (response.success) {
// [7] 审核成功:提示用户,关闭模态框,刷新列表
alert(response.message || '审核成功');
$('#auditModal').modal('hide');
// [8] 重新加载待审核列表(被审核的记录会从列表中消失)
var activeType = $('.box-primary .btn-group button.active').data('type');
var currentPage = parseInt($('#pendingPagination .active a').text()) || 1;
loadPendingList(currentPage, activeType || 'dish');
// [9] 清空表单
$('#rejectReason').val('');
$('#modificationGuide').val('');
$('input[name="auditResult"][value="approve"]').prop('checked', true);
$('#rejectReasonGroup').hide();
$('#modificationGuideGroup').hide();
currentAuditItem = null;
} else {
// [10] 审核失败:显示错误信息
alert('审核失败:' + (response.message || '未知错误'));
}
},
error: function (xhr, status, error) {
console.error('审核失败:', error);
alert('审核失败:' + error);
}
});
}
位置 :src/main/webapp/jsp/admin.jsp 第777-837行
关键点:
- 请求参数包含
entityType(DISH或STORE)和entityId(具体的实体ID),这样后端就知道管理员在审核哪条记录 - 请求参数包含
action(approve或reject),表示审核操作类型 - 请求参数包含
comment(审核意见),特别是驳回时会填写原因
第三步:追踪到Controller层
根据请求URL/admin/audit,找到AdminController的audit方法:
java
// [1] 处理POST请求 /admin/audit(数据流向:前端 → Controller)
@RequestMapping("/audit")
@ResponseBody
public Result audit(@RequestParam String entityType,
@RequestParam Long entityId,
@RequestParam String action,
@RequestParam(required = false) String comment,
HttpServletRequest request) {
try {
// [2] 从Session中获取当前登录用户(审核人)
User user = (User) request.getSession().getAttribute("USER_SESSION");
if (user == null) {
return new Result(false, "请先登录");
}
// [3] 权限校验:必须是管理员
if (!"ADMIN".equals(user.getUserRole()) && !"admin".equals(user.getUserRole())) {
return new Result(false, "权限不足,仅管理员可审核");
}
// [4] 根据操作类型调用Service层执行审核(数据流向:Controller → Service)
boolean success;
if ("approve".equals(action)) {
// [5] 审核通过:调用approve方法
success = auditService.approve(entityType, entityId, user.getUserId(), comment);
if (success) {
return new Result(true, "审核通过成功");
}
} else if ("reject".equals(action)) {
// [6] 审核驳回:调用reject方法
success = auditService.reject(entityType, entityId, user.getUserId(), comment);
if (success) {
return new Result(true, "审核驳回成功");
}
}
// [7] 审核失败:返回错误信息(数据流向:Controller → 前端)
return new Result(false, "审核操作失败");
} catch (Exception e) {
logger.error("审核操作失败", e);
return new Result(false, "审核操作失败: " + e.getMessage());
}
}
位置 :src/main/java/com/scfs/controller/AdminController.java 第88-133行
关键点:
@RequestParam接收前端传来的entityType、entityId、action、comment参数- 从Session获取当前登录用户,用于记录审核人信息
- 检查用户角色,确保只有管理员才能审核
- 根据
action参数调用不同的Service方法(approve或reject)
第四步:追踪到Service层
Controller调用auditService.approve()或auditService.reject(),Service层实现:
java
// [1] 审核通过方法(数据流向:Service → Mapper)
@Override
@Transactional // [2] 使用事务,确保数据一致性
public boolean approve(String entityType, Long entityId, Long auditorId, String comment) {
try {
// [3] 更新实体状态为已通过(status=1)
int updateResult = auditMapper.updateStatusByEntity(entityType, entityId, AuditStatus.APPROVED);
if (updateResult <= 0) {
return false;
}
// [4] 插入审核日志记录(数据流向:Service → Mapper)
AuditLog auditLog = new AuditLog();
auditLog.setEntityType(entityType);
auditLog.setEntityId(entityId);
auditLog.setOldStatus(AuditStatus.PENDING); // 旧状态:待审核
auditLog.setNewStatus(AuditStatus.APPROVED); // 新状态:已通过
auditLog.setAuditorId(auditorId);
auditLog.setAuditComment(comment);
auditLog.setCreateTime(new Date());
int logResult = auditMapper.insertAuditLog(auditLog);
return logResult > 0;
} catch (Exception e) {
logger.error("审核通过失败", e);
return false;
}
}
// [1] 审核驳回方法(数据流向:Service → Mapper)
@Override
@Transactional // [2] 使用事务,确保数据一致性
public boolean reject(String entityType, Long entityId, Long auditorId, String comment) {
try {
// [3] 更新实体状态为已驳回(status=-1)
int updateResult = auditMapper.updateStatusByEntity(entityType, entityId, AuditStatus.REJECTED);
if (updateResult <= 0) {
return false;
}
// [4] 插入审核日志记录(数据流向:Service → Mapper)
AuditLog auditLog = new AuditLog();
auditLog.setEntityType(entityType);
auditLog.setEntityId(entityId);
auditLog.setOldStatus(AuditStatus.PENDING); // 旧状态:待审核
auditLog.setNewStatus(AuditStatus.REJECTED); // 新状态:已驳回
auditLog.setAuditorId(auditorId);
auditLog.setAuditComment(comment);
auditLog.setCreateTime(new Date());
int logResult = auditMapper.insertAuditLog(auditLog);
return logResult > 0;
} catch (Exception e) {
logger.error("审核驳回失败", e);
return false;
}
}
位置 :src/main/java/com/scfs/service/impl/AuditServiceImpl.java 第120-180行
关键点:
- 使用
@Transactional确保更新状态和插入日志的原子性 - 先更新实体状态(
store或dish表的status字段) - 再插入审核日志(
audit_log表),记录审核历史
第五步:追踪到Mapper层
5.1 更新实体状态
Service调用auditMapper.updateStatusByEntity(),Mapper层实现:
java
// [1] 根据实体类型和ID更新状态(数据流向:Mapper → 数据库)
@Update("<script>" +
"<if test='entityType == \"STORE\"'>" +
"UPDATE store SET status = #{status} WHERE store_id = #{entityId}" +
"</if>" +
"<if test='entityType == \"DISH\"'>" +
"UPDATE dish SET status = #{status} WHERE dish_id = #{entityId}" +
"</if>" +
"</script>")
int updateStatusByEntity(@Param("entityType") String entityType,
@Param("entityId") Long entityId,
@Param("status") AuditStatus status);
位置 :src/main/java/com/scfs/mapper/AuditMapper.java 第45-52行
5.2 插入审核日志
Service调用auditMapper.insertAuditLog(),Mapper层实现:
java
// [1] 插入审核日志记录到数据库(数据流向:Mapper → 数据库)
@Insert("INSERT INTO audit_log (entity_type, entity_id, old_status, new_status, " +
"auditor_id, audit_comment, create_time) " +
"VALUES (#{entityType}, #{entityId}, #{oldStatus}, #{newStatus}, " +
"#{auditorId}, #{auditComment}, #{createTime})")
@Options(useGeneratedKeys = true, keyProperty = "auditId")
int insertAuditLog(AuditLog auditLog);
位置 :src/main/java/com/scfs/mapper/AuditMapper.java 第54-58行
执行顺序说明:
- 第一步:根据
entityType判断是更新store表还是dish表 - 第二步:执行UPDATE语句,更新
status字段(通过:status=1,驳回:status=-1) - 第三步:执行INSERT语句,插入审核日志到
audit_log表 - 数据库执行SQL后,返回受影响的行数(数据流向:数据库 → Mapper → Service)
第六步:数据返回流程
完整的数据返回路径:
- 数据库 → Mapper :
- 数据库执行UPDATE语句更新
store或dish表的status字段,返回受影响的行数(通常为1)给Mapper - 数据库执行INSERT语句插入
audit_log表,返回受影响的行数(通常为1)给Mapper
- 数据库执行UPDATE语句更新
- Mapper → Service :
- Mapper返回受影响的行数给Service
- Service判断行数大于0,返回
true给Controller
- Service → Controller :Service返回
true给Controller - Controller处理 :
- Controller封装成
Result对象,success为true,message为"审核通过成功"或"审核驳回成功" - 返回给前端(数据流向:Controller → 前端)
- Controller封装成
- 前端处理 :
- JavaScript接收到
Result对象 - 判断
response.success是否为true - 如果成功,弹出"审核成功"提示
- 关闭模态框,清空表单,重新加载待审核列表
- 被审核的记录会从列表中消失(因为它的
status已经不是0或-1了)
- JavaScript接收到
这样,管理员点击"通过/驳回"按钮后,系统就知道他在审核哪条记录(通过entityType和entityId),更新了对应表的status字段,并记录了审核日志,整个流程就完成了。
审核操作完整流程时序图
数据库 AuditMapper AuditService AdminController JavaScript admin.jsp 管理员 数据库 AuditMapper AuditService AdminController JavaScript admin.jsp 管理员 alt [action是approve] [action是reject] [1] 点击"通过"或"驳回"按钮 [2] 触发openAuditModal() [3] 保存currentAuditItem (entityType, entityId) [4] 显示审核模态框 [5] 填写审核意见 [6] 点击"提交审核"按钮 [7] 触发submitAudit() [8] 收集审核数据 (entityType, entityId, action, comment) [9] AJAX POST /admin/audit (entityType, entityId, action, comment) [10] 接收请求参数(@RequestParam) [11] 从Session获取用户信息 [12] 检查用户角色(必须是管理员) [13] 调用auditService.approve() [14] 开始事务 [15] 调用auditMapper.updateStatusByEntity() (status=1) [16] 执行SQL: UPDATE store/dish SET status = 1 WHERE id = ? [17] 返回受影响行数(1) [18] 返回受影响行数 [19] 调用auditMapper.insertAuditLog() [20] 执行SQL: INSERT INTO audit_log (entity_type, entity_id, old_status=0, new_status=1, ...) [21] 返回受影响行数(1) [22] 返回受影响行数 [23] 提交事务 [24] 返回true [25] 调用auditService.reject() [26] 开始事务 [27] 调用auditMapper.updateStatusByEntity() (status=-1) [28] 执行SQL: UPDATE store/dish SET status = -1 WHERE id = ? [29] 返回受影响行数(1) [30] 返回受影响行数 [31] 调用auditMapper.insertAuditLog() [32] 执行SQL: INSERT INTO audit_log (entity_type, entity_id, old_status=0, new_status=-1, ...) [33] 返回受影响行数(1) [34] 返回受影响行数 [35] 提交事务 [36] 返回true [37] 返回Result(true, "审核成功") [38] 提示"审核成功" [39] 关闭模态框,清空表单 [40] 重新加载待审核列表 [41] 被审核的记录从列表中消失 [42] 显示更新后的列表
审核操作流程图
否
是
approve
reject
否
是
管理员点击通过/驳回按钮
打开审核模态框
保存currentAuditItem
(entityType, entityId)
管理员填写审核意见
点击提交审核按钮
发送AJAX请求
POST /admin/audit
Controller接收请求
从Session获取用户信息
检查用户角色
是管理员?
返回权限不足错误
操作类型?
Service开始事务
Service开始事务
更新实体状态
UPDATE store/dish
SET status = 1
更新实体状态
UPDATE store/dish
SET status = -1
插入审核日志
INSERT INTO audit_log
操作成功?
回滚事务,返回失败
提交事务
返回成功提示
关闭模态框,刷新列表
被审核的记录从列表中消失
审核操作数据流转图
前端提交
entityType: DISH
entityId: 100
action: approve
comment: 审核通过
Controller接收
Service开始事务
更新dish表
UPDATE dish
SET status = 1
WHERE dish_id = 100
插入audit_log表
INSERT INTO audit_log
(entity_type='DISH',
entity_id=100,
old_status=0,
new_status=1,
auditor_id=1,
audit_comment='审核通过')
提交事务
返回成功
前端刷新列表
dish_id=100的记录
从待审核列表中消失
为什么这样设计
为什么使用@Transactional事务?
-
原因1:数据一致性
- 更新实体状态和插入审核日志必须同时成功或同时失败
- 如果更新状态成功但插入日志失败,会导致数据不一致
- 使用事务可以确保数据的一致性
-
原因2:原子性
- 整个操作要么全部成功,要么全部失败
- 如果中途出错,可以自动回滚,保证数据完整性
- 避免产生"半成品"数据
-
原因3:错误处理
- 如果插入日志失败,可以自动回滚状态更新
- 避免产生没有审核日志的状态变更
- 提升系统的可靠性
为什么要把审核信息保存到audit_log表?
-
原因1:审核历史追踪
- 可以查看某个实体被审核的历史记录
- 知道是谁审核的、什么时候审核的、审核结果是什么
- 便于问题排查和责任追溯
-
原因2:数据审计
- 记录所有审核操作,符合审计要求
- 可以分析审核流程的效率
- 便于后续的数据分析和优化
-
原因3:业务需求
- 管理员可能需要查看审核历史
- 用户可能需要知道为什么被驳回
- 符合实际业务场景
为什么通过entityType和entityId来标识审核对象?
-
原因1:通用性
- 可以统一处理不同类型的实体(店铺、菜品等)
- 不需要为每种实体类型写单独的审核方法
- 代码更简洁,易于维护
-
原因2:灵活性
- 可以方便地扩展新的实体类型
- 不需要修改核心审核逻辑
- 提升系统的可扩展性
-
原因3:数据一致性
- 使用统一的标识方式,便于管理
- 审核日志表可以统一存储不同类型的审核记录
- 便于后续的查询和统计
模态框的"提交"按钮会调用 submitAudit(),在 admin.jsp 大约第 777--837 行:
startLine:endLine:src/main/webapp/jsp/admin.jsp
// 提交审核
function submitAudit() {
if (!currentAuditItem) {
alert('审核信息错误');
return;
}
var action = $('input[name="auditResult"]:checked').val();
var comment = '';
if (action === 'reject') {
var reason = $('#rejectReason').val().trim();
var guide = $('#modificationGuide').val();
if (!reason && !guide) {
alert('请输入驳回原因或选择修改指引');
return;
}
comment = reason;
if (guide) {
comment = comment ? comment + ';' + guide : guide;
}
}
$.ajax({
url: '${pageContext.request.contextPath}/admin/audit',
type: 'POST',
data: {
entityType: currentAuditItem.entityType,
entityId: currentAuditItem.entityId,
action: action,
comment: comment
},
success: function (response) {
if (response.success) {
alert(response.message || '审核成功');
$('#auditModal').modal('hide');
// 重新加载当前页的列表
var activeType = $('.box-primary .btn-group button.active').data('type');
var currentPage = parseInt($('#pendingPagination .active a').text()) || 1;
loadPendingList(currentPage, activeType || 'dish');
// 清空表单
$('#rejectReason').val('');
$('#modificationGuide').val('');
$('input[name="auditResult"][value="approve"]').prop('checked', true);
$('#rejectReasonGroup').hide();
$('#modificationGuideGroup').hide();
currentAuditItem = null;
} else {
alert('审核失败:' + (response.message || '未知错误'));
}
},
error: function (xhr, status, error) {
console.error('审核失败:', error);
alert('审核失败:' + error);
}
});
}
这一步很关键:
- 请求 URL:
/admin/audit - 请求方式:
POST - 请求参数:
entityType:STORE / DISH / REVIEWentityId:具体的实体 ID,比如菜品 IDaction:approve或rejectcomment:审核意见(特别是驳回时会填写)
前端在收到成功响应后,会再次调用 loadPendingList(...) 刷新列表,这就是"被审核的那一行从列表中消失"的来源。
第三步:根据 URL 找到审核接口(AdminController.audit)
根据 /admin/audit 继续去 AdminController 中搜索,可以找到审核接口(约第 88--133 行):
startLine:endLine:src/main/java/com/scfs/controller/AdminController.java
@RequestMapping("/audit")
@ResponseBody
public Result audit(@RequestParam String entityType,
@RequestParam Long entityId,
@RequestParam String action,
@RequestParam(required = false) String comment,
HttpServletRequest request) {
try {
// 获取当前登录用户(审核人)
User user = (User) request.getSession().getAttribute("USER_SESSION");
if (user == null) {
return new Result(false, "请先登录");
}
// 权限校验:必须是管理员
if (!"ADMIN".equals(user.getUserRole()) && !"admin".equals(user.getUserRole())) {
return new Result(false, "权限不足,仅管理员可审核");
}
boolean success;
if ("approve".equals(action)) {
success = auditService.approve(entityType, entityId, user.getUserId(), comment);
if (success) {
return new Result(true, "审核通过成功");
}
} else if ("reject".equals(action)) {
success = auditService.reject(entityType, entityId, user.getUserId(), comment);
if (success) {
return new Result(true, "审核驳回成功");
}
} else {
return new Result(false, "无效的审核操作");
}
return new Result(false, "审核操作失败");
} catch (Exception e) {
logger.error("审核操作失败: entityType={}, entityId={}, action={}",
new Object[]{entityType, String.valueOf(entityId), action}, e);
return new Result(false, "审核操作失败: " + (e.getMessage() != null ? e.getMessage() : "未知错误"));
}
}
这里做了两件很重要的事:
- 从 Session 里拿当前登录用户,并确认他是管理员,防止普通用户伪造审核请求。
- 根据
action决定调用auditService.approve(...)还是auditService.reject(...)。
第四步:审核通过时,到底改了什么?(AuditServiceImpl.approve)
接着去 src/main/java/com/scfs/service/impl/AuditServiceImpl.java 找 approve 方法(约第 72--180 行的一部分):
startLine:endLine:src/main/java/com/scfs/service/impl/AuditServiceImpl.java
@Override
@Transactional
public boolean approve(String entityType, Long entityId, Long auditorId, String comment) {
try {
// 1. 查询当前实体的状态
Integer oldStatus = auditMapper.selectStatusByEntity(entityType, entityId);
if (oldStatus == null) {
logger.warn("审核通过失败,实体不存在: {}#{}", entityType, entityId);
return false;
}
// 2. 如果是待删除审核(-1),审核通过后真正删除
if (oldStatus != null && oldStatus == -1) {
int deleted = auditMapper.deleteEntity(entityType, entityId);
if (deleted == 0) {
Integer currentStatus = auditMapper.selectStatusByEntity(entityType, entityId);
if (currentStatus == null) {
logger.info("实体已不存在,视为删除成功: {}#{}", entityType, entityId);
} else {
logger.warn("删除实体失败: {}#{}", entityType, entityId);
return false;
}
}
// 记录审核日志
AuditLog auditLog = new AuditLog();
auditLog.setEntityType(entityType);
auditLog.setEntityId(entityId);
auditLog.setOldStatus(oldStatus);
auditLog.setNewStatus(AuditStatus.DELETED);
auditLog.setAuditorId(auditorId);
auditLog.setAuditComment(comment != null ? comment : "审核通过,已删除");
logAudit(auditLog);
// 这里通常还会发送删除成功的通知(代码略)
return true;
}
// 3. 普通待审核(0)的情况:最终会把状态更新为"已通过"
int updated = auditMapper.updateStatusByEntity(entityType, entityId, AuditStatus.APPROVED);
if (updated == 0) {
logger.warn("审核通过失败,更新状态失败: {}#{}", entityType, entityId);
return false;
}
// 4. 写入审核日志
AuditLog auditLog = new AuditLog();
auditLog.setEntityType(entityType);
auditLog.setEntityId(entityId);
auditLog.setOldStatus(oldStatus);
auditLog.setNewStatus(AuditStatus.APPROVED);
auditLog.setAuditorId(auditorId);
auditLog.setAuditComment(comment != null ? comment : "审核通过");
logAudit(auditLog);
// 5. 发送"审核通过"通知(代码略)
logger.info("审核通过成功: {}#{}, 审核人: {}", entityType, entityId, auditorId);
return true;
} catch (Exception e) {
logger.error("审核通过失败: {}#{}", entityType, entityId, e);
return false;
}
}
可以看到,审核通过的大致流程是:
- 查出原来的状态
oldStatus。 - 如果是"待删除审核"(
-1),则真正删除这条记录,并把日志状态记为DELETED。 - 否则就是正常的新增/修改审核,通过后把状态更新为
APPROVED。 - 同时写入一条
audit_log记录。
状态更新和日志写入具体要靠 Mapper 完成。
第五步:审核驳回又做了什么?(AuditServiceImpl.reject)
再看 reject 方法(约第 682--750 行):
startLine:endLine:src/main/java/com/scfs/service/impl/AuditServiceImpl.java
@Override
@Transactional
public boolean reject(String entityType, Long entityId, Long auditorId, String comment) {
try {
// 1. 查询当前状态
Integer oldStatus = auditMapper.selectStatusByEntity(entityType, entityId);
if (oldStatus == null) {
logger.warn("审核驳回失败,实体不存在: {}#{}", entityType, entityId);
return false;
}
// 2. 根据不同状态决定新的状态
int newStatus;
if (oldStatus != null && oldStatus == -1) {
// 删除申请被驳回,恢复为已通过
newStatus = AuditStatus.APPROVED;
} else {
// 新增/修改被驳回,标记为 REJECTED
newStatus = AuditStatus.REJECTED;
}
// 3. 更新状态
int updated = auditMapper.updateStatusByEntity(entityType, entityId, newStatus);
if (updated == 0) {
logger.warn("审核驳回失败,更新状态失败: {}#{}", entityType, entityId);
return false;
}
// 4. 写入审核日志
AuditLog auditLog = new AuditLog();
auditLog.setEntityType(entityType);
auditLog.setEntityId(entityId);
auditLog.setOldStatus(oldStatus);
auditLog.setNewStatus(newStatus);
auditLog.setAuditorId(auditorId);
auditLog.setAuditComment(comment != null ? comment : "审核驳回");
logAudit(auditLog);
// 5. 发送"审核驳回"通知(代码略)
logger.info("审核驳回成功: {}#{}, 审核人: {}", entityType, entityId, auditorId);
return true;
} catch (Exception e) {
logger.error("审核驳回失败: {}#{}", entityType, entityId, e);
return false;
}
}
这一步说明:
- 删除类的申请被驳回,相当于"不删了",就把状态恢复为
APPROVED。 - 其他申请被驳回,则把状态改为
REJECTED,同时写入日志并发送通知。
第六步:Mapper 真正更新了哪些表?(AuditMapper.xml)
回到 AuditMapper.xml,看状态更新和日志插入(约第 84--95 行、148--152 行):
startLine:endLine:src/main/resources/com.scfs.mapper/AuditMapper.xml
<!-- 更新实体状态 -->
<update id="updateStatusByEntity">
<choose>
<when test="entityType == 'STORE'">
UPDATE store SET status = #{status} WHERE store_id = #{entityId}
</when>
<when test="entityType == 'DISH'">
UPDATE dish SET status = #{status} WHERE dish_id = #{entityId}
</when>
</choose>
</update>
startLine:endLine:src/main/resources/com.scfs.mapper/AuditMapper.xml
<!-- 插入审核日志 -->
<insert id="insertAuditLog" parameterType="com.scfs.domain.AuditLog">
INSERT INTO audit_log (entity_type, entity_id, old_status, new_status, auditor_id, audit_comment, create_time)
VALUES (#{entityType}, #{entityId}, #{oldStatus}, #{newStatus}, #{auditorId}, #{auditComment}, NOW())
</insert>
- 可以看到,审核本质上就是修改
store或dish表的status字段,同时在audit_log表插入一条记录。
第七步:审核完成后,页面怎么刷新?
回到前端的 submitAudit(),在接口成功后会:
- 弹出"审核成功"的提示。
- 关闭审核模态框。
- 重新调用
loadPendingList(...),所以刚才审核的那条记录就不再出现在"待审核列表"里了。
五、场景 3:查看某条记录的审核历史
1. 功能说明
管理员在排查问题时,可能需要回顾某个店铺/菜品曾经被谁审核过、改过几次状态,这时就会查看"审核历史"。
2. 抛出问题
一条记录可能经历多次"通过/驳回",这些历史是怎样被查询出来并展示给管理员的?
3. 完整追踪流程
第一步:找到用户操作的入口
管理员在查看某个店铺或菜品的详情时,可以点击"审核历史"按钮,查看该实体的审核历史记录。
第二步:追踪前端JavaScript处理
管理员点击"审核历史"按钮,会调用loadAuditHistory(entityType, entityId)函数:
javascript
// [1] 加载审核历史函数(数据流向:前端 → Controller)
function loadAuditHistory(entityType, entityId) {
// [2] 发送AJAX请求获取审核历史
$.ajax({
url: '${pageContext.request.contextPath}/admin/auditHistory',
type: 'GET',
data: {
entityType: entityType, // 实体类型(DISH或STORE)
entityId: entityId // 实体ID(菜品ID或店铺ID)
},
dataType: 'json',
// [3] 请求成功回调:处理后端返回的结果(数据流向:Controller → 前端)
success: function (response) {
if (response && response.success) {
// [4] 调用渲染函数,将审核历史显示到页面
renderAuditHistory(response.data);
} else {
console.error('获取审核历史失败:', response.message);
}
},
error: function (xhr, status, error) {
console.error('获取审核历史失败:', error);
}
});
}
位置 :src/main/webapp/jsp/admin.jsp 第850-870行
渲染函数 :renderAuditHistory(history)将后端返回的审核历史渲染到页面:
javascript
// [1] 渲染审核历史函数
function renderAuditHistory(history) {
if (!history || history.length === 0) {
$('#auditHistoryList').html('<tr><td colspan="6" class="text-center">暂无审核历史</td></tr>');
return;
}
var html = '';
// [2] 遍历审核历史列表,为每条记录生成一行表格
history.forEach(function (item) {
var statusText = '';
if (item.newStatus == 1) {
statusText = '<span class="label label-success">已通过</span>';
} else if (item.newStatus == -1) {
statusText = '<span class="label label-danger">已驳回</span>';
}
html += '<tr>';
html += '<td>' + formatDateTime(item.createTime) + '</td>';
html += '<td>' + (item.auditorName || '未知管理员') + '</td>';
html += '<td>' + statusText + '</td>';
html += '<td>' + (item.auditComment || '无') + '</td>';
html += '</tr>';
});
// [3] 将生成的HTML插入到审核历史容器中
$('#auditHistoryList').html(html);
}
位置 :src/main/webapp/jsp/admin.jsp 第872-900行
第三步:追踪到Controller层
根据请求URL/admin/auditHistory,找到AdminController的getAuditHistory方法:
java
// [1] 处理GET请求 /admin/auditHistory(数据流向:前端 → Controller)
@RequestMapping("/auditHistory")
@ResponseBody
public Result getAuditHistory(@RequestParam String entityType, @RequestParam Long entityId) {
try {
// [2] 从请求参数中获取实体类型和ID(@RequestParam)
// [3] 调用Service层获取审核历史(数据流向:Controller → Service)
List<AuditLog> history = auditService.getAuditHistory(entityType, entityId);
// [4] 封装成Result对象返回(数据流向:Controller → 前端)
return new Result(true, "获取审核历史成功", history);
} catch (Exception e) {
logger.error("获取审核历史失败", e);
return new Result(false, "获取审核历史失败");
}
}
位置 :src/main/java/com/scfs/controller/AdminController.java 第142-152行
第四步:追踪到Service层
Controller调用auditService.getAuditHistory(entityType, entityId),Service层实现:
java
// [1] 获取审核历史(数据流向:Service → Mapper)
@Override
public List<AuditLog> getAuditHistory(String entityType, Long entityId) {
try {
// [2] 调用Mapper查询审核历史(数据流向:Service → Mapper)
List<AuditLog> history = auditMapper.selectAuditHistory(entityType, entityId);
// [3] 为每条审核记录加载审核人信息(可选,提升可读性)
for (AuditLog log : history) {
if (log.getAuditorId() != null) {
User auditor = userMapper.findById(log.getAuditorId());
if (auditor != null) {
log.setAuditorName(auditor.getUserName());
}
}
}
// [4] 返回审核历史列表(数据流向:Service → Controller)
return history != null ? history : new ArrayList<>();
} catch (Exception e) {
logger.error("获取审核历史失败: {}#{}", entityType, entityId, e);
return new ArrayList<>();
}
}
位置 :src/main/java/com/scfs/service/impl/AuditServiceImpl.java 第750-770行
第五步:追踪到Mapper层
Service调用auditMapper.selectAuditHistory(entityType, entityId),Mapper层实现:
java
// [1] 根据实体类型和ID查询审核历史(数据流向:Mapper → 数据库)
@Select("SELECT audit_id AS auditId, entity_type AS entityType, entity_id AS entityId, " +
"old_status AS oldStatus, new_status AS newStatus, " +
"auditor_id AS auditorId, audit_comment AS auditComment, create_time AS createTime " +
"FROM audit_log " +
"WHERE entity_type = #{entityType} AND entity_id = #{entityId} " +
"ORDER BY create_time DESC")
List<AuditLog> selectAuditHistory(@Param("entityType") String entityType,
@Param("entityId") Long entityId);
位置 :src/main/java/com/scfs/mapper/AuditMapper.java 第60-65行
执行顺序说明:
WHERE entity_type = #{entityType} AND entity_id = #{entityId}:只查询指定实体类型和ID的审核记录ORDER BY create_time DESC:按创建时间倒序排列(最新的在前)- 数据库执行SQL查询后,返回
List<AuditLog>(数据流向:数据库 → Mapper → Service)
第六步:数据返回流程
完整的数据返回路径:
- 数据库 → Mapper :数据库执行SELECT查询
audit_log表,返回审核历史记录列表给Mapper - Mapper → Service :Mapper将查询结果映射为
List<AuditLog>对象,返回给Service - Service处理 :
- Service为每条审核记录加载审核人信息(可选,提升可读性)
- 返回审核历史列表给Controller(数据流向:Service → Controller)
- Controller处理 :
- Controller封装成
Result对象,success为true,data为审核历史列表 - 返回给前端(数据流向:Controller → 前端)
- Controller封装成
- 前端处理 :
- JavaScript接收到
Result对象 - 判断
response.success是否为true - 如果成功,调用
renderAuditHistory(response.data)渲染审核历史 renderAuditHistory()遍历审核历史数组,为每条记录生成一行表格,包含审核时间、审核人、审核结果、审核意见等信息- 将HTML插入到页面的审核历史容器中
- JavaScript接收到
这样,管理员查看审核历史时,系统就查询该实体的所有审核记录,数据从数据库传递到前端,最后显示在页面上,整个流程就完成了。
审核历史查询时序图
数据库 UserMapper AuditMapper AuditService AdminController JavaScript admin.jsp 管理员 数据库 UserMapper AuditMapper AuditService AdminController JavaScript admin.jsp 管理员 loop [遍历每条审核记录] [1] 点击"审核历史"按钮 [2] 触发loadAuditHistory() [3] 获取entityType和entityId [4] AJAX GET /admin/auditHistory (entityType, entityId) [5] 接收GET请求 (@RequestParam提取参数) [6] 调用auditService.getAuditHistory() [7] 调用auditMapper.selectAuditHistory() [8] 执行SQL: SELECT * FROM audit_log WHERE entity_type = ? AND entity_id = ? ORDER BY create_time DESC [9] 返回审核历史记录列表 [10] 返回List<AuditLog> [11] 调用userMapper.findById() (获取审核人信息) [12] 执行SQL: SELECT * FROM user WHERE user_id = ? [13] 返回User对象 [14] 返回User对象 [15] 设置log.setAuditorName() [16] 返回List<AuditLog>(包含审核人信息) [17] 封装成Result对象 [18] 返回Result对象 (success, message, data=审核历史列表) [19] 判断response.success [20] 调用renderAuditHistory(response.data) [21] 遍历审核历史数组,生成HTML表格行 (包含审核时间、审核人、审核结果、审核意见) [22] 将HTML插入到审核历史容器 [23] 显示审核历史列表
审核历史查询流程图
管理员点击审核历史按钮
调用loadAuditHistory函数
发送AJAX请求
GET /admin/auditHistory
?entityType=DISH&entityId=100
Controller接收请求
Service查询审核历史
Mapper执行SQL查询
SELECT * FROM audit_log
WHERE entity_type = ? AND entity_id = ?
ORDER BY create_time DESC
数据库返回审核历史记录列表
Service加载审核人信息
数据层层返回
前端接收Result对象
调用renderAuditHistory渲染
显示审核历史列表
(包含多次审核记录)
审核历史数据流转图
前端请求
entityType: DISH
entityId: 100
查询audit_log表
WHERE entity_type='DISH'
AND entity_id=100
返回多条审核记录
记录1: 2024-01-01 通过
记录2: 2024-01-02 驳回
记录3: 2024-01-03 通过
加载审核人信息
返回完整审核历史
(包含审核时间、审核人、
审核结果、审核意见)
前端渲染列表
显示所有审核历史记录
为什么这样设计
为什么要把审核历史保存到单独的audit_log表?
-
原因1:历史记录完整性
- 一条记录可能经历多次审核(通过→驳回→通过)
- 如果只保存当前状态,无法查看历史记录
- 使用单独的审核日志表,可以完整记录所有审核历史
-
原因2:数据审计
- 记录所有审核操作,符合审计要求
- 可以追踪谁在什么时候做了什么审核操作
- 便于问题排查和责任追溯
-
原因3:业务需求
- 管理员可能需要查看审核历史,了解审核过程
- 用户可能需要知道为什么被驳回,查看审核意见
- 符合实际业务场景
为什么按时间倒序排列审核历史?
-
原因1:用户体验
- 最新的审核记录在最前面,管理员可以快速看到最新的审核结果
- 符合用户的阅读习惯,从新到旧查看
-
原因2:业务逻辑
- 最新的审核结果通常是最重要的
- 可以快速了解当前的状态和最近的审核操作
- 便于管理员快速定位问题
-
原因3:数据展示
- 倒序排列可以让用户先看到最新的信息
- 如果审核历史很多,可以只显示最新的几条
- 提升页面的可读性
六、审核状态一览表(辅助理解)
在 src/main/java/com/scfs/domain/AuditStatus.java(约第 11--42 行)里定义了几个关键状态值:
APPROVED (1):已通过,可以正常显示。PENDING (0):待审核,等待管理员处理。REJECTED (-1):已驳回(或部分场景下用于"待删除审核"的中间状态)。DELETED (-3):已删除,通常只在审核日志中出现,用于标记已经被物理删除的记录。
理解这些枚举值有助于看懂 store / dish 表里 status 字段的含义。
七、完整流程总结(从管理员操作到数据库,再回到页面)
下面用一段文字,把前面两个主要场景串起来:
- 管理员打开后台审核页
浏览器请求admin.jsp,页面加载完成后,前端 JS 调用loadPendingList(type),向/admin/pendingList发送 AJAX 请求。 - 后端查询待审核数据
AdminController.getPendingList接到请求后,调用AdminServiceImpl.getPendingList,再由AuditServiceImpl.getPendingList和AuditMapper.selectPendingList从store、dish表中查出status=0/-1的记录,组装成列表返回。 - 前端渲染列表
前端拿到数据后,调用renderPendingList把每条记录渲染到表格中,为每一行绑定"通过/驳回"按钮。 - 管理员进行审核操作
管理员点击按钮,前端先用openAuditModal记录当前审核对象,再在模态框中填写意见,最后由submitAudit函数向/admin/audit发送POST请求,携带实体类型、ID、操作类型和审核意见。 - 后端更新状态并记录日志
AdminController.audit做登录和权限校验后,调用AuditServiceImpl.approve或reject。Service 中通过AuditMapper.updateStatusByEntity修改store/dish的status字段,同时用insertAuditLog向audit_log表插入一条审核日志记录。 - 前端刷新结果
审核成功后,接口返回success=true,前端关闭模态框并重新调用loadPendingList,于是被审核的记录从"待审核列表"中消失或状态改变,管理员可以继续审核下一条。
通过这样"从管理员点击开始,一路找到数据库再返回页面"的追踪,你就可以清晰地知道:审核机制本质上就是围绕 status 字段和 audit_log 表的一套工作流。