基于AI图像识别与智能推荐的校园食堂评价系统研究 05-审核机制模块

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: 审核机制是系统的"质量闸门",确保内容质量。

为什么需要审核

  1. 内容质量控制:用户提交的内容可能有错误、不规范或不符合要求
  2. 防止恶意内容:防止恶意用户提交不当内容(如广告、违规信息)
  3. 数据准确性:确保店铺、菜品信息的准确性和完整性
  4. 用户体验:通过审核的内容质量更高,提升用户体验

如果不审核会有什么问题

  • 错误信息会直接展示给用户,影响系统可信度
  • 恶意内容可能传播,影响系统安全
  • 数据质量无法保证,用户体验差
  • 无法追溯内容来源,难以管理

实际例子

  • 用户可能输入错误的店铺名称或价格
  • 用户可能上传不相关的图片
  • 恶意用户可能提交虚假信息
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):已驳回,不对外显示

工作流程

  1. 用户提交内容 → status=0(待审核)
  2. 管理员审核通过 → status=1(审核通过)→ 普通用户可以看到
  3. 管理员审核驳回 → status=-2(已驳回)→ 普通用户看不到
Q3:审核日志(audit_log表)的作用是什么?

A: 审核日志用于审计和追溯,记录所有审核操作的历史。

audit_log表的结构

  • audit_log_id:审核日志ID(主键)
  • entity_type:实体类型(STORE/DISH/REVIEW)
  • entity_id:实体ID
  • old_status:审核前的状态
  • new_status:审核后的状态
  • auditor_id:审核人ID
  • audit_comment:审核意见
  • audit_time:审核时间

作用

  1. 审计追踪:记录谁在什么时候审核了什么内容
  2. 问题追溯:如果出现问题,可以查看审核历史
  3. 责任明确:知道每个审核操作是谁执行的
  4. 数据分析:可以统计审核通过率、审核时间等

代码位置

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,找到AdminControllergetPendingList方法:

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)
第六步:数据返回流程

完整的数据返回路径

  1. 数据库 → Mapper:数据库执行SELECT查询,返回待审核记录列表给Mapper
  2. Mapper → Service :Mapper将查询结果映射为List<Map<String, Object>>对象,返回给Service
  3. Service处理
    • Service封装成分页结果PagedResult,包含列表数据、总数、页码、每页大小
    • 返回给Controller(数据流向:Service → Controller)
  4. Controller处理
    • Controller封装成Result对象,successtruedata为分页结果
    • 返回给前端(数据流向:Controller → 前端)
  5. 前端处理
    • JavaScript接收到Result对象
    • 判断response.success是否为true
    • 如果成功,调用renderPendingList(response.data.list)渲染待审核列表
    • renderPendingList()遍历列表数组,为每条记录生成一行表格,包含类型、名称、提交人、提交时间、状态、操作按钮等信息
    • 将HTML插入到页面的#pendingList容器中

这样,管理员打开后台审核页,系统就加载所有待审核的店铺和菜品,数据从数据库传递到前端,最后显示在页面上,整个流程就完成了。

待审核列表加载时序图

数据库 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 接收前端传来的 typepagesize,做了一点参数校验。
  • 核心是调用 adminService.getPendingList(type, page, size),真正查数据的逻辑在 Service 里。
第五步:Service 如何组织"待审核数据"?(AdminServiceImpl)

根据上面的调用关系,去 src/main/java/com/scfs/service/impl/AdminServiceImpl.javagetPendingList(约第 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 形式返回给前端。
  • 前端 loadPendingListsuccess 回调拿到 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全局变量保存了当前正在审核的记录信息(entityTypeentityId),这样系统就知道管理员在审核哪条记录了。

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,找到AdminControlleraudit方法:

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接收前端传来的entityTypeentityIdactioncomment参数
  • 从Session获取当前登录用户,用于记录审核人信息
  • 检查用户角色,确保只有管理员才能审核
  • 根据action参数调用不同的Service方法(approvereject
第四步:追踪到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确保更新状态和插入日志的原子性
  • 先更新实体状态(storedish表的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)
第六步:数据返回流程

完整的数据返回路径

  1. 数据库 → Mapper
    • 数据库执行UPDATE语句更新storedish表的status字段,返回受影响的行数(通常为1)给Mapper
    • 数据库执行INSERT语句插入audit_log表,返回受影响的行数(通常为1)给Mapper
  2. Mapper → Service
    • Mapper返回受影响的行数给Service
    • Service判断行数大于0,返回true给Controller
  3. Service → Controller :Service返回true给Controller
  4. Controller处理
    • Controller封装成Result对象,successtruemessage为"审核通过成功"或"审核驳回成功"
    • 返回给前端(数据流向:Controller → 前端)
  5. 前端处理
    • JavaScript接收到Result对象
    • 判断response.success是否为true
    • 如果成功,弹出"审核成功"提示
    • 关闭模态框,清空表单,重新加载待审核列表
    • 被审核的记录会从列表中消失(因为它的status已经不是0或-1了)

这样,管理员点击"通过/驳回"按钮后,系统就知道他在审核哪条记录(通过entityTypeentityId),更新了对应表的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 / REVIEW
    • entityId:具体的实体 ID,比如菜品 ID
    • actionapprovereject
    • comment:审核意见(特别是驳回时会填写)

前端在收到成功响应后,会再次调用 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.javaapprove 方法(约第 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;
    }
}

可以看到,审核通过的大致流程是:

  1. 查出原来的状态 oldStatus
  2. 如果是"待删除审核"(-1),则真正删除这条记录,并把日志状态记为 DELETED
  3. 否则就是正常的新增/修改审核,通过后把状态更新为 APPROVED
  4. 同时写入一条 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>
  • 可以看到,审核本质上就是修改 storedish 表的 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,找到AdminControllergetAuditHistory方法:

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)
第六步:数据返回流程

完整的数据返回路径

  1. 数据库 → Mapper :数据库执行SELECT查询audit_log表,返回审核历史记录列表给Mapper
  2. Mapper → Service :Mapper将查询结果映射为List<AuditLog>对象,返回给Service
  3. Service处理
    • Service为每条审核记录加载审核人信息(可选,提升可读性)
    • 返回审核历史列表给Controller(数据流向:Service → Controller)
  4. Controller处理
    • Controller封装成Result对象,successtruedata为审核历史列表
    • 返回给前端(数据流向:Controller → 前端)
  5. 前端处理
    • JavaScript接收到Result对象
    • 判断response.success是否为true
    • 如果成功,调用renderAuditHistory(response.data)渲染审核历史
    • renderAuditHistory()遍历审核历史数组,为每条记录生成一行表格,包含审核时间、审核人、审核结果、审核意见等信息
    • 将HTML插入到页面的审核历史容器中

这样,管理员查看审核历史时,系统就查询该实体的所有审核记录,数据从数据库传递到前端,最后显示在页面上,整个流程就完成了。

审核历史查询时序图

数据库 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 字段的含义。


七、完整流程总结(从管理员操作到数据库,再回到页面)

下面用一段文字,把前面两个主要场景串起来:

  1. 管理员打开后台审核页
    浏览器请求 admin.jsp,页面加载完成后,前端 JS 调用 loadPendingList(type),向 /admin/pendingList 发送 AJAX 请求。
  2. 后端查询待审核数据
    AdminController.getPendingList 接到请求后,调用 AdminServiceImpl.getPendingList,再由 AuditServiceImpl.getPendingListAuditMapper.selectPendingListstoredish 表中查出 status=0/-1 的记录,组装成列表返回。
  3. 前端渲染列表
    前端拿到数据后,调用 renderPendingList 把每条记录渲染到表格中,为每一行绑定"通过/驳回"按钮。
  4. 管理员进行审核操作
    管理员点击按钮,前端先用 openAuditModal 记录当前审核对象,再在模态框中填写意见,最后由 submitAudit 函数向 /admin/audit 发送 POST 请求,携带实体类型、ID、操作类型和审核意见。
  5. 后端更新状态并记录日志
    AdminController.audit 做登录和权限校验后,调用 AuditServiceImpl.approvereject。Service 中通过 AuditMapper.updateStatusByEntity 修改 store / dishstatus 字段,同时用 insertAuditLogaudit_log 表插入一条审核日志记录。
  6. 前端刷新结果
    审核成功后,接口返回 success=true,前端关闭模态框并重新调用 loadPendingList,于是被审核的记录从"待审核列表"中消失或状态改变,管理员可以继续审核下一条。

通过这样"从管理员点击开始,一路找到数据库再返回页面"的追踪,你就可以清晰地知道:审核机制本质上就是围绕 status 字段和 audit_log 表的一套工作流。

相关推荐
开开心心就好2 小时前
OCR识别工具可加AI接口,快捷键截图翻译便捷
java·网络·windows·随机森林·电脑·excel·推荐算法
linghuocaishui2 小时前
京东用工平台实践:亲测案例复盘分享
人工智能·python
你怎么知道我是队长2 小时前
python---新年烟花
开发语言·python·pygame
智算菩萨2 小时前
【Python机器学习】主成分分析(PCA):高维数据的“瘦身术“
开发语言·python·机器学习
爬山算法2 小时前
Hibernate(15)Hibernate中如何定义一个实体的主键?
java·后端·hibernate
廋到被风吹走2 小时前
【Spring】Spring AMQP 详细介绍
java·spring·wpf
一起养小猫3 小时前
LeetCode100天Day6-回文数与加一
java·leetcode
闻道且行之3 小时前
Linux|CUDA与cuDNN下载安装全指南:默认/指定路径双方案+多CUDA环境一键切换
linux·运维·服务器
Ahtacca3 小时前
Linux环境下前后端分离项目(Spring Boot + Vue)手动部署全流程指南
linux·运维·服务器·vue.js·spring boot·笔记