From 79510b3267988321b7ecf982d75402d7c57d90d2 Mon Sep 17 00:00:00 2001 From: msksbr515 Date: Fri, 22 May 2026 11:48:56 +0800 Subject: [PATCH] feat(admin): add admin dashboard service and role-based access control - rename DashBoardController to AdminDashBoardController - add AdminDashBoardService interface with getAllBooks and getAllBorrowRecords - add GlobalExceptionHandler for unified Result error responses - add RequireRole annotation and RequireRoleAspect for role-based auth - fix BorrowRecord entity table name from book_record to borrow_record - add Result.forbidden() factory method returning 403 responses --- .../msksbr/bookmgr/annotation/RequireRole.kt | 9 +++ .../bookmgr/config/GlobalExceptionHandler.kt | 21 +++++++ .../bookmgr/config/RequireRoleAspect.kt | 52 ++++++++++++++++ .../controller/AdminBorrowController.kt | 1 - .../controller/AdminDashBoardController.kt | 61 +++++++++++++++++++ .../bookmgr/controller/DashBoardController.kt | 14 ----- .../com/msksbr/bookmgr/entity/BorrowRecord.kt | 2 +- .../bookmgr/service/AdminDashBoardService.kt | 13 ++++ .../service/impl/AdminDashBoardServiceImpl.kt | 48 +++++++++++++++ .../com/msksbr/bookmgr/template/Result.kt | 16 +++++ 10 files changed, 221 insertions(+), 16 deletions(-) create mode 100644 src/main/kotlin/com/msksbr/bookmgr/annotation/RequireRole.kt create mode 100644 src/main/kotlin/com/msksbr/bookmgr/config/GlobalExceptionHandler.kt create mode 100644 src/main/kotlin/com/msksbr/bookmgr/config/RequireRoleAspect.kt create mode 100644 src/main/kotlin/com/msksbr/bookmgr/controller/AdminDashBoardController.kt delete mode 100644 src/main/kotlin/com/msksbr/bookmgr/controller/DashBoardController.kt create mode 100644 src/main/kotlin/com/msksbr/bookmgr/service/AdminDashBoardService.kt create mode 100644 src/main/kotlin/com/msksbr/bookmgr/service/impl/AdminDashBoardServiceImpl.kt diff --git a/src/main/kotlin/com/msksbr/bookmgr/annotation/RequireRole.kt b/src/main/kotlin/com/msksbr/bookmgr/annotation/RequireRole.kt new file mode 100644 index 0000000..51ab15c --- /dev/null +++ b/src/main/kotlin/com/msksbr/bookmgr/annotation/RequireRole.kt @@ -0,0 +1,9 @@ +package com.msksbr.bookmgr.annotation + +/* +* 角色权限注解,贴在 Controller 方法上由 RequireRoleAspect 校验 +* @param role 要求的角色名,如 "admin"、"user" +*/ +@Target(AnnotationTarget.FUNCTION) +@Retention(AnnotationRetention.RUNTIME) +annotation class RequireRole(val role: String) \ No newline at end of file diff --git a/src/main/kotlin/com/msksbr/bookmgr/config/GlobalExceptionHandler.kt b/src/main/kotlin/com/msksbr/bookmgr/config/GlobalExceptionHandler.kt new file mode 100644 index 0000000..1270a51 --- /dev/null +++ b/src/main/kotlin/com/msksbr/bookmgr/config/GlobalExceptionHandler.kt @@ -0,0 +1,21 @@ +package com.msksbr.bookmgr.config + +import com.msksbr.bookmgr.script.log +import org.springframework.web.bind.annotation.ExceptionHandler +import org.springframework.web.bind.annotation.RestControllerAdvice +import com.msksbr.bookmgr.template.Result + +/* +* 全局异常处理,将未捕获异常统一转为 Result 格式,避免泄露堆栈信息 +* +* 返回格式: +* {"code":500,"message":"Internal server error"} +*/ +@RestControllerAdvice +class GlobalExceptionHandler { + @ExceptionHandler(Exception::class) + fun handle(ex: Exception): Result{ + log.error("[Global] unhandled exception: {}", ex.message) + return Result.error("Internal server error") + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/msksbr/bookmgr/config/RequireRoleAspect.kt b/src/main/kotlin/com/msksbr/bookmgr/config/RequireRoleAspect.kt new file mode 100644 index 0000000..bc0f988 --- /dev/null +++ b/src/main/kotlin/com/msksbr/bookmgr/config/RequireRoleAspect.kt @@ -0,0 +1,52 @@ +package com.msksbr.bookmgr.config + +import com.msksbr.bookmgr.annotation.RequireRole +import com.msksbr.bookmgr.script.log +import com.msksbr.bookmgr.template.Result +import org.aspectj.lang.ProceedingJoinPoint +import org.aspectj.lang.annotation.Around +import org.aspectj.lang.annotation.Aspect +import org.springframework.stereotype.Component +import org.springframework.web.context.request.RequestContextHolder +import org.springframework.web.context.request.ServletRequestAttributes + +/* +* 角色权限校验切面 +* 拦截 @RequireRole 注解的方法,校验 request 中 role 是否匹配 +* +* 等保三级审计日志字段: +* - 用户身份:username +* - 来源 IP: +* - 请求路径:method + URI +* - 所需角色 / 实际角色 +* - 操作结果:denied / passed +*/ +@Aspect +@Component +class RequireRoleAspect( + private val ipExtractor: IpExtractor +) { + @Around("@annotation(requireRole)") + fun checkRole( + joinPoint: ProceedingJoinPoint, + requireRole: RequireRole + ): Any? { + val request = (RequestContextHolder + .currentRequestAttributes() as ServletRequestAttributes).request + val username = request.getAttribute("username") as? String ?: "unknown" + val role = request.getAttribute("role") as? String + val ip = ipExtractor.getRealIp(request) + val path = "${request.method} ${request.requestURI}" + + if (role != requireRole.role) { + log.warn( + "[AUDIT] access denied | user={} | ip={} | path={} | required={} | actual={}", + username, ip, path, requireRole.role, role + ) + return Result.forbidden("Access denied: insufficient permissions") + } + log.info("[AUDIT] access allowed | user={} | ip={} | path={} | role={}", + username, ip, path, role) + return joinPoint.proceed() + } +} diff --git a/src/main/kotlin/com/msksbr/bookmgr/controller/AdminBorrowController.kt b/src/main/kotlin/com/msksbr/bookmgr/controller/AdminBorrowController.kt index 5139936..549a38d 100644 --- a/src/main/kotlin/com/msksbr/bookmgr/controller/AdminBorrowController.kt +++ b/src/main/kotlin/com/msksbr/bookmgr/controller/AdminBorrowController.kt @@ -8,7 +8,6 @@ import org.springframework.web.bind.annotation.RestController * * 计划接口: * - 全量借阅记录查询 -* - 催还 / 标记逾期 * - 手动归还 */ @RestController diff --git a/src/main/kotlin/com/msksbr/bookmgr/controller/AdminDashBoardController.kt b/src/main/kotlin/com/msksbr/bookmgr/controller/AdminDashBoardController.kt new file mode 100644 index 0000000..751d9a2 --- /dev/null +++ b/src/main/kotlin/com/msksbr/bookmgr/controller/AdminDashBoardController.kt @@ -0,0 +1,61 @@ +package com.msksbr.bookmgr.controller + +import com.msksbr.bookmgr.annotation.RequireRole +import com.msksbr.bookmgr.config.IpExtractor +import com.msksbr.bookmgr.script.log +import com.msksbr.bookmgr.service.AdminDashBoardService +import com.msksbr.bookmgr.template.Result +import jakarta.servlet.http.HttpServletRequest +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RequestAttribute +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +/* +* 管理端仪表盘接口(仅限管理员访问) +* 路径前缀:/api/admin/dashboard +* +* 所有接口需要 @RequireRole("admin") + JWT 鉴权拦截器双重校验 +*/ +@RestController +@RequestMapping("/api/admin/dashboard") +class AdminDashBoardController( + private val adminDashBoardService: AdminDashBoardService, + private val ipExtractor: IpExtractor, +) { + /* + * GET /api/admin/dashboard/get-all-books + * 查询全部图书列表 + * + * 成功响应(code=200): + * { "code":200, "message":"success", "data":[{"id":1,"name":"三体",...}, ...] } + */ + @RequireRole("admin") + @GetMapping("/get-all-books") + fun getAllBooks( + @RequestAttribute username: String, + request: HttpServletRequest + ): Result { + log.info("[AdminDashBoard] getAllBooks: user={}, ip={}", username, ipExtractor.getRealIp(request)) + log.debug("[AdminDashBoard] user agent: {}", request.getHeader("User-Agent")) + return adminDashBoardService.getAllBooks() + } + + /* + * GET /api/admin/dashboard/get-all-borrow-records + * 查询全部借阅记录列表 + * + * 成功响应(code=200): + * { "code":200, "message":"success", "data":[{"id":1,"userId":1,"bookId":2,...}, ...] } + */ + @RequireRole("admin") + @GetMapping("/get-all-borrow-records") + fun getAllBorrowRecords( + @RequestAttribute username: String, + request: HttpServletRequest + ): Result { + log.info("[AdminDashBoard] getAllBorrowRecords: user={}, ip={}", username, ipExtractor.getRealIp(request)) + log.debug("[AdminDashBoard] user agent: {}", request.getHeader("User-Agent")) + return adminDashBoardService.getAllBorrowRecords() + } +} diff --git a/src/main/kotlin/com/msksbr/bookmgr/controller/DashBoardController.kt b/src/main/kotlin/com/msksbr/bookmgr/controller/DashBoardController.kt deleted file mode 100644 index 9e8039a..0000000 --- a/src/main/kotlin/com/msksbr/bookmgr/controller/DashBoardController.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.msksbr.bookmgr.controller - -import org.springframework.web.bind.annotation.RestController - -/* -* 仪表盘接口 -* 路径前缀(待定):/api/dashboard -* -* 计划接口: -* - 图书总数、借出数量、逾期数量等统计数据 -*/ -@RestController -class DashBoardController { -} diff --git a/src/main/kotlin/com/msksbr/bookmgr/entity/BorrowRecord.kt b/src/main/kotlin/com/msksbr/bookmgr/entity/BorrowRecord.kt index e3b1c67..cc99a3e 100644 --- a/src/main/kotlin/com/msksbr/bookmgr/entity/BorrowRecord.kt +++ b/src/main/kotlin/com/msksbr/bookmgr/entity/BorrowRecord.kt @@ -16,7 +16,7 @@ import java.util.Date * returnTime - 归还时间,null 表示尚未归还 * status - 记录状态:borrowed(借出中)/ returned(已归还)/ overdue(逾期) */ -@TableName("book_record") +@TableName("borrow_record") data class BorrowRecord( @TableId(type = IdType.AUTO) val id: Long? = null, diff --git a/src/main/kotlin/com/msksbr/bookmgr/service/AdminDashBoardService.kt b/src/main/kotlin/com/msksbr/bookmgr/service/AdminDashBoardService.kt new file mode 100644 index 0000000..3d184a7 --- /dev/null +++ b/src/main/kotlin/com/msksbr/bookmgr/service/AdminDashBoardService.kt @@ -0,0 +1,13 @@ +package com.msksbr.bookmgr.service + +import com.msksbr.bookmgr.template.Result + +/* +* 管理端仪表盘服务,聚合图书和借阅统计查询 +*/ +interface AdminDashBoardService { + // 查询全部图书列表 + fun getAllBooks(): Result + // 查询全部借阅记录列表 + fun getAllBorrowRecords(): Result +} \ No newline at end of file diff --git a/src/main/kotlin/com/msksbr/bookmgr/service/impl/AdminDashBoardServiceImpl.kt b/src/main/kotlin/com/msksbr/bookmgr/service/impl/AdminDashBoardServiceImpl.kt new file mode 100644 index 0000000..34d6b14 --- /dev/null +++ b/src/main/kotlin/com/msksbr/bookmgr/service/impl/AdminDashBoardServiceImpl.kt @@ -0,0 +1,48 @@ +package com.msksbr.bookmgr.service.impl + +import com.msksbr.bookmgr.mapper.BookMapper +import com.msksbr.bookmgr.mapper.BorrowRecordMapper +import com.msksbr.bookmgr.script.log +import com.msksbr.bookmgr.service.AdminDashBoardService +import com.msksbr.bookmgr.template.Result +import org.springframework.stereotype.Service + +/* +* 管理端仪表盘服务实现 +* 聚合 BookMapper 和 BorrowRecordMapper 提供全量数据查询 +*/ +@Service +class AdminDashBoardServiceImpl( + private val bookMapper: BookMapper, + private val borrowRecordMapper: BorrowRecordMapper +) : AdminDashBoardService { + override fun getAllBooks(): Result { + val bookList = bookMapper.selectList(null) + val bookJsonList = bookList.map { book -> + mapOf( + "id" to book.id, + "name" to book.name, + "author" to book.author, + "stock" to book.stock, + ) + } + log.debug("[AdminDashBoard] getAllBooks completed, count={}", bookJsonList.size) + return Result.success(bookJsonList) + } + + override fun getAllBorrowRecords(): Result { + val borrowRecordList = borrowRecordMapper.selectList(null) + val borrowRecordJsonList = borrowRecordList.map { borrowRecord -> + mapOf( + "id" to borrowRecord.id, + "userId" to borrowRecord.userId, + "bookId" to borrowRecord.bookId, + "borrowTime" to borrowRecord.borrowTime, + "returnTime" to borrowRecord.returnTime, + "status" to borrowRecord.status, + ) + } + log.debug("[AdminDashBoard] getAllBorrowRecords completed, count={}", borrowRecordJsonList.size) + return Result.success(borrowRecordJsonList) + } +} diff --git a/src/main/kotlin/com/msksbr/bookmgr/template/Result.kt b/src/main/kotlin/com/msksbr/bookmgr/template/Result.kt index e056224..5442e64 100644 --- a/src/main/kotlin/com/msksbr/bookmgr/template/Result.kt +++ b/src/main/kotlin/com/msksbr/bookmgr/template/Result.kt @@ -53,6 +53,10 @@ data class Result( * * JSON 输出示例:{ "code": 401, "message": "xxx" } */ + /* + * 未登录响应 — 返回 401 + * JSON 输出示例:{"code":401,"message":"Missing Authorization header"} + */ fun unauthorized(message: String): Result { return Result( code = 401, @@ -60,5 +64,17 @@ data class Result( data = null ) } + + /* + * 权限不足响应 — 返回 403 + * JSON 输出示例:{"code":403,"message":"Access denied: insufficient permissions"} + */ + fun forbidden(message: String): Result { + return Result( + code = 403, + message = message, + data = null + ) + } } }