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
This commit is contained in:
@@ -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)
|
||||||
@@ -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<Any?>{
|
||||||
|
log.error("[Global] unhandled exception: {}", ex.message)
|
||||||
|
return Result.error("Internal server error")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,7 +8,6 @@ import org.springframework.web.bind.annotation.RestController
|
|||||||
*
|
*
|
||||||
* 计划接口:
|
* 计划接口:
|
||||||
* - 全量借阅记录查询
|
* - 全量借阅记录查询
|
||||||
* - 催还 / 标记逾期
|
|
||||||
* - 手动归还
|
* - 手动归还
|
||||||
*/
|
*/
|
||||||
@RestController
|
@RestController
|
||||||
|
|||||||
@@ -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<Any?> {
|
||||||
|
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<Any?> {
|
||||||
|
log.info("[AdminDashBoard] getAllBorrowRecords: user={}, ip={}", username, ipExtractor.getRealIp(request))
|
||||||
|
log.debug("[AdminDashBoard] user agent: {}", request.getHeader("User-Agent"))
|
||||||
|
return adminDashBoardService.getAllBorrowRecords()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
package com.msksbr.bookmgr.controller
|
|
||||||
|
|
||||||
import org.springframework.web.bind.annotation.RestController
|
|
||||||
|
|
||||||
/*
|
|
||||||
* 仪表盘接口
|
|
||||||
* 路径前缀(待定):/api/dashboard
|
|
||||||
*
|
|
||||||
* 计划接口:
|
|
||||||
* - 图书总数、借出数量、逾期数量等统计数据
|
|
||||||
*/
|
|
||||||
@RestController
|
|
||||||
class DashBoardController {
|
|
||||||
}
|
|
||||||
@@ -16,7 +16,7 @@ import java.util.Date
|
|||||||
* returnTime - 归还时间,null 表示尚未归还
|
* returnTime - 归还时间,null 表示尚未归还
|
||||||
* status - 记录状态:borrowed(借出中)/ returned(已归还)/ overdue(逾期)
|
* status - 记录状态:borrowed(借出中)/ returned(已归还)/ overdue(逾期)
|
||||||
*/
|
*/
|
||||||
@TableName("book_record")
|
@TableName("borrow_record")
|
||||||
data class BorrowRecord(
|
data class BorrowRecord(
|
||||||
@TableId(type = IdType.AUTO)
|
@TableId(type = IdType.AUTO)
|
||||||
val id: Long? = null,
|
val id: Long? = null,
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package com.msksbr.bookmgr.service
|
||||||
|
|
||||||
|
import com.msksbr.bookmgr.template.Result
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 管理端仪表盘服务,聚合图书和借阅统计查询
|
||||||
|
*/
|
||||||
|
interface AdminDashBoardService {
|
||||||
|
// 查询全部图书列表
|
||||||
|
fun getAllBooks(): Result<Any?>
|
||||||
|
// 查询全部借阅记录列表
|
||||||
|
fun getAllBorrowRecords(): Result<Any?>
|
||||||
|
}
|
||||||
@@ -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<Any?> {
|
||||||
|
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<Any?> {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -53,6 +53,10 @@ data class Result<T>(
|
|||||||
*
|
*
|
||||||
* JSON 输出示例:{ "code": 401, "message": "xxx" }
|
* JSON 输出示例:{ "code": 401, "message": "xxx" }
|
||||||
*/
|
*/
|
||||||
|
/*
|
||||||
|
* 未登录响应 — 返回 401
|
||||||
|
* JSON 输出示例:{"code":401,"message":"Missing Authorization header"}
|
||||||
|
*/
|
||||||
fun unauthorized(message: String): Result<Any?> {
|
fun unauthorized(message: String): Result<Any?> {
|
||||||
return Result(
|
return Result(
|
||||||
code = 401,
|
code = 401,
|
||||||
@@ -60,5 +64,17 @@ data class Result<T>(
|
|||||||
data = null
|
data = null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 权限不足响应 — 返回 403
|
||||||
|
* JSON 输出示例:{"code":403,"message":"Access denied: insufficient permissions"}
|
||||||
|
*/
|
||||||
|
fun forbidden(message: String): Result<Any?> {
|
||||||
|
return Result(
|
||||||
|
code = 403,
|
||||||
|
message = message,
|
||||||
|
data = null
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user