refactor(api): replace generic error responses with specific HTTP status codes
- Add ApiResult.badRequest() and ApiResult.notFound() helper methods - Replace generic ApiResult.error() with appropriate status-specific calls - Add ApiResultStatusAdvice for consistent response status handling - Add KDoc comments to admin controller and service methods
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
package com.msksbr.bookmgr.config
|
||||
|
||||
import com.msksbr.bookmgr.template.ApiResult
|
||||
import org.springframework.core.MethodParameter
|
||||
import org.springframework.http.MediaType
|
||||
import org.springframework.http.converter.HttpMessageConverter
|
||||
import org.springframework.http.server.ServerHttpRequest
|
||||
import org.springframework.http.server.ServerHttpResponse
|
||||
import org.springframework.http.server.ServletServerHttpResponse
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice
|
||||
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice
|
||||
|
||||
/*
|
||||
* ApiResult 状态码处理
|
||||
* 将 ApiResult.code(业务状态码)映射为 HTTP 响应状态码
|
||||
* 确保前端能通过 HTTP 状态码判断请求是否成功
|
||||
*/
|
||||
@RestControllerAdvice
|
||||
class ApiResultStatusAdvice : ResponseBodyAdvice<Any> {
|
||||
|
||||
override fun supports(returnType: MethodParameter, converterType: Class<out HttpMessageConverter<*>>): Boolean = true
|
||||
|
||||
override fun beforeBodyWrite(
|
||||
body: Any?,
|
||||
returnType: MethodParameter,
|
||||
selectedContentType: MediaType,
|
||||
selectedConverterType: Class<out HttpMessageConverter<*>>,
|
||||
request: ServerHttpRequest,
|
||||
response: ServerHttpResponse
|
||||
): Any? {
|
||||
if (body is ApiResult<*>) {
|
||||
(response as ServletServerHttpResponse).servletResponse.status = body.code
|
||||
}
|
||||
return body
|
||||
}
|
||||
}
|
||||
@@ -29,8 +29,9 @@ class JwtPopulateFilter(
|
||||
if (claims != null) {
|
||||
request.setAttribute("username", claims.subject)
|
||||
request.setAttribute("role", claims.get("role", String::class.java))
|
||||
request.setAttribute("userId", claims.get("userId", Long::class.java))
|
||||
log.debug("[JWT] identified: user={}, role={}, userId={}", claims.subject, claims.get("role", String::class.java), claims.get("userId", Long::class.java))
|
||||
val userId = claims.get("userId", Number::class.java)?.toLong()
|
||||
request.setAttribute("userId", userId)
|
||||
log.debug("[JWT] identified: user={}, role={}, userId={}", claims.subject, claims.get("role", String::class.java), userId)
|
||||
}
|
||||
}
|
||||
filterChain.doFilter(request, response)
|
||||
|
||||
@@ -35,7 +35,7 @@ class JwtUtils(
|
||||
// 初始化密钥:有配置时用 SHA-256 哈希,无配置时随机生成并打印警告
|
||||
init {
|
||||
val keyBytes = if (configuredSecret.isNotBlank()) {
|
||||
// 把用户提供的secret,用SHA-256哈希成256字节固定长度
|
||||
// 把用户提供的secret,用SHA-256哈希成32字节固定长度
|
||||
val md = MessageDigest.getInstance("SHA-256")
|
||||
md.digest(configuredSecret.toByteArray())
|
||||
} else {
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
package com.msksbr.bookmgr.config
|
||||
|
||||
import com.msksbr.bookmgr.annotation.RequireRole
|
||||
import io.swagger.v3.oas.models.Components
|
||||
import io.swagger.v3.oas.models.OpenAPI
|
||||
import io.swagger.v3.oas.models.media.Content
|
||||
import io.swagger.v3.oas.models.media.MediaType
|
||||
import io.swagger.v3.oas.models.media.Schema
|
||||
import io.swagger.v3.oas.models.responses.ApiResponse
|
||||
import io.swagger.v3.oas.models.security.SecurityRequirement
|
||||
import io.swagger.v3.oas.models.security.SecurityScheme
|
||||
import org.springdoc.core.customizers.GlobalOperationCustomizer
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
|
||||
/*
|
||||
* OpenAPI 文档配置
|
||||
* 配置 Swagger/OpenAPI 文档信息、JWT Bearer 认证方案以及全局错误响应定义
|
||||
*
|
||||
* 提供的 Bean:
|
||||
* - openAPI: 文档基本信息与安全方案
|
||||
* - securityCustomizer: 为 @RequireRole 接口自动添加 BearerAuth 安全要求
|
||||
* - errorResponseCustomizer: 为所有接口添加 400/500 响应,为受保护接口添加 401/403 响应
|
||||
*/
|
||||
@Configuration
|
||||
class OpenApiConfig {
|
||||
|
||||
@Bean
|
||||
fun openAPI(): OpenAPI = OpenAPI()
|
||||
.components(
|
||||
Components()
|
||||
.addSecuritySchemes("BearerAuth",
|
||||
SecurityScheme()
|
||||
.name("Authorization")
|
||||
.type(SecurityScheme.Type.HTTP)
|
||||
.scheme("bearer")
|
||||
.bearerFormat("JWT")
|
||||
.description("JWT token returned by login endpoint")
|
||||
)
|
||||
)
|
||||
|
||||
@Bean
|
||||
fun securityCustomizer(): GlobalOperationCustomizer =
|
||||
GlobalOperationCustomizer { operation, handlerMethod ->
|
||||
if (handlerMethod.hasMethodAnnotation(RequireRole::class.java)) {
|
||||
operation.addSecurityItem(SecurityRequirement().addList("BearerAuth"))
|
||||
}
|
||||
operation
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun errorResponseCustomizer(): GlobalOperationCustomizer =
|
||||
GlobalOperationCustomizer { operation, handlerMethod ->
|
||||
val isProtected = handlerMethod.hasMethodAnnotation(RequireRole::class.java)
|
||||
operation.responses
|
||||
.addApiResponse("400", errorResponse("Bad request — invalid parameters"))
|
||||
.addApiResponse("500", errorResponse("Internal server error"))
|
||||
if (isProtected) {
|
||||
operation.responses
|
||||
.addApiResponse("401", errorResponse("Unauthorized — missing or invalid token"))
|
||||
.addApiResponse("403", errorResponse("Forbidden — insufficient permissions"))
|
||||
}
|
||||
operation
|
||||
}
|
||||
|
||||
private fun errorResponse(description: String): ApiResponse =
|
||||
ApiResponse().description(description)
|
||||
.content(Content().addMediaType("*/*",
|
||||
MediaType().schema(Schema<Any>().`$ref`("#/components/schemas/ApiResultString"))))
|
||||
}
|
||||
@@ -15,7 +15,7 @@ import org.springframework.security.crypto.password.PasswordEncoder
|
||||
class PasswordConfig {
|
||||
/*
|
||||
* 返回 Argon2PasswordEncoder 实例(Spring Security 5.8 默认参数)
|
||||
* 参数:salt 长度 16 字节、hash 长度 32 字节、parallelism=1、memory=1<<12(4MB)、iterations=3
|
||||
* 参数:salt 长度 16 字节、hash 长度 32 字节、parallelism=1、memory=1<<12(4 MiB)、iterations=3
|
||||
*/
|
||||
// 将 Argon2 的加盐哈希方法注册成 Bean
|
||||
@Bean
|
||||
|
||||
@@ -27,6 +27,10 @@ import org.springframework.web.bind.annotation.*
|
||||
@RequestMapping("/api/admin/books")
|
||||
class AdminBookController(private val adminBookService: AdminBookService, private val ipExtractor: IpExtractor) {
|
||||
|
||||
/*
|
||||
* POST /api/admin/books/add
|
||||
* 新增图书
|
||||
*/
|
||||
@RequireRole("admin")
|
||||
@PostMapping("/add")
|
||||
fun addBook(
|
||||
@@ -46,6 +50,10 @@ class AdminBookController(private val adminBookService: AdminBookService, privat
|
||||
return adminBookService.addBook(bookAddDto)
|
||||
}
|
||||
|
||||
/*
|
||||
* POST /api/admin/books/update?id=xxx
|
||||
* 修改图书信息
|
||||
*/
|
||||
@RequireRole("admin")
|
||||
@PostMapping("/update")
|
||||
fun updateBook(
|
||||
@@ -61,6 +69,10 @@ class AdminBookController(private val adminBookService: AdminBookService, privat
|
||||
return adminBookService.updateBook(id, bookUpdateDto)
|
||||
}
|
||||
|
||||
/*
|
||||
* POST /api/admin/books/delete?id=xxx
|
||||
* 删除图书
|
||||
*/
|
||||
@RequireRole("admin")
|
||||
@PostMapping("/delete")
|
||||
fun deleteBook(
|
||||
@@ -73,6 +85,10 @@ class AdminBookController(private val adminBookService: AdminBookService, privat
|
||||
return adminBookService.deleteBook(id)
|
||||
}
|
||||
|
||||
/*
|
||||
* POST /api/admin/books/update-stock?id=xxx&stock=xxx
|
||||
* 调整图书库存
|
||||
*/
|
||||
@RequireRole("admin")
|
||||
@PostMapping("/update-stock")
|
||||
fun updateStock(
|
||||
|
||||
@@ -34,8 +34,8 @@ class AuthController(
|
||||
* 成功响应(code=200, message="success"):
|
||||
* { "code": 200, "message": "success", "data": { "token": "...", "username": "admin", "role": "admin" } }
|
||||
*
|
||||
* 失败响应(code=500, message="Incorrect username or password"):
|
||||
* { "code": 500, "message": "Incorrect username or password", "data": null }
|
||||
* 失败响应(code=401, message="Incorrect username or password"):
|
||||
* { "code": 401, "message": "Incorrect username or password", "data": null }
|
||||
*/
|
||||
@PostMapping("/login")
|
||||
fun login(
|
||||
@@ -59,7 +59,7 @@ class AuthController(
|
||||
ApiResult.success(loginVo)
|
||||
} else {
|
||||
log.warn("[Auth] login failed: user={}", loginDTO.username)
|
||||
ApiResult.error("Incorrect username or password")
|
||||
ApiResult.unauthorized("Incorrect username or password")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,10 @@ import org.springframework.web.bind.annotation.RestController
|
||||
@RestController
|
||||
@RequestMapping("/api/books")
|
||||
class BookController(private val bookService: BookService, private val ipExtractor: IpExtractor) {
|
||||
/*
|
||||
* GET /api/books/search?query=xxx
|
||||
* 按书名或作者模糊搜索图书
|
||||
*/
|
||||
@GetMapping("/search")
|
||||
fun searchBook(
|
||||
@RequestAttribute(required = false) username: String?,
|
||||
@@ -33,6 +37,10 @@ class BookController(private val bookService: BookService, private val ipExtract
|
||||
return bookService.searchBook(query)
|
||||
}
|
||||
|
||||
/*
|
||||
* GET /api/books/getone?id=xxx
|
||||
* 查询单本图书详情
|
||||
*/
|
||||
@GetMapping("/getone")
|
||||
fun getOneBook(
|
||||
@RequestAttribute(required = false) username: String?,
|
||||
@@ -44,6 +52,10 @@ class BookController(private val bookService: BookService, private val ipExtract
|
||||
return bookService.getOneBook(id)
|
||||
}
|
||||
|
||||
/*
|
||||
* GET /api/books/getall
|
||||
* 查询所有图书
|
||||
*/
|
||||
@GetMapping("/getall")
|
||||
fun getAllBooks(
|
||||
@RequestAttribute(required = false) username: String?,
|
||||
|
||||
@@ -16,6 +16,11 @@ import org.springframework.stereotype.Service
|
||||
*/
|
||||
@Service
|
||||
class AdminBookServiceImpl(private val bookMapper: BookMapper) : AdminBookService {
|
||||
/*
|
||||
* 新增图书
|
||||
* @param bookAddDto 图书信息(书名、作者、库存)
|
||||
* @return 操作结果,重名时返回 409
|
||||
*/
|
||||
override fun addBook(bookAddDto: BookAddDto): ApiResult<String> {
|
||||
val book = Book(
|
||||
id = null,
|
||||
@@ -32,18 +37,24 @@ class AdminBookServiceImpl(private val bookMapper: BookMapper) : AdminBookServic
|
||||
return ApiResult.success("Book added successfully")
|
||||
}
|
||||
|
||||
/*
|
||||
* 修改图书信息
|
||||
* @param id 图书 ID
|
||||
* @param bookUpdateDto 待更新的字段(书名、作者至少提供一个)
|
||||
* @return 操作结果,不存在的书返回 404,重名时返回 409
|
||||
*/
|
||||
override fun updateBook(
|
||||
id: Long,
|
||||
bookUpdateDto: BookUpdateDto
|
||||
): ApiResult<String> {
|
||||
if (bookUpdateDto.name.isBlank() && bookUpdateDto.author.isBlank()) {
|
||||
log.warn("[AdminBook] update: both name and author are blank, id={}", id)
|
||||
return ApiResult.error("At least one of name or author must be provided")
|
||||
return ApiResult.badRequest("At least one of name or author must be provided")
|
||||
}
|
||||
val existing = bookMapper.selectById(id)
|
||||
?: run {
|
||||
log.warn("[AdminBook] update: book not found, id={}", id)
|
||||
return ApiResult.error("Book does not exist")
|
||||
return ApiResult.notFound("Book does not exist")
|
||||
}
|
||||
val book = Book(
|
||||
id = id,
|
||||
@@ -60,26 +71,37 @@ class AdminBookServiceImpl(private val bookMapper: BookMapper) : AdminBookServic
|
||||
return ApiResult.success("Book updated successfully")
|
||||
}
|
||||
|
||||
/*
|
||||
* 删除图书
|
||||
* @param id 图书 ID
|
||||
* @return 操作结果,不存在的书返回 404
|
||||
*/
|
||||
override fun deleteBook(id: Long): ApiResult<String> {
|
||||
val existing = bookMapper.selectById(id)
|
||||
?: run {
|
||||
log.warn("[AdminBook] delete: book not found, id={}", id)
|
||||
return ApiResult.error("Book does not exist")
|
||||
return ApiResult.notFound("Book does not exist")
|
||||
}
|
||||
bookMapper.deleteById(id)
|
||||
log.info("[AdminBook] delete: success, id={}, name={}", id, existing.name)
|
||||
return ApiResult.success("Book deleted successfully")
|
||||
}
|
||||
|
||||
/*
|
||||
* 调整图书库存
|
||||
* @param id 图书 ID
|
||||
* @param stock 新库存数量,必须大于 0
|
||||
* @return 操作结果,不存在的书返回 404
|
||||
*/
|
||||
override fun updateStock(id: Long, stock: Int): ApiResult<String> {
|
||||
if (stock <= 0) {
|
||||
log.warn("[AdminBook] updateStock: invalid stock={}, id={}", stock, id)
|
||||
return ApiResult.error("Stock must be greater than zero")
|
||||
return ApiResult.badRequest("Stock must be greater than zero")
|
||||
}
|
||||
val existing = bookMapper.selectById(id)
|
||||
?: run {
|
||||
log.warn("[AdminBook] updateStock: book not found, id={}", id)
|
||||
return ApiResult.error("Book does not exist")
|
||||
return ApiResult.notFound("Book does not exist")
|
||||
}
|
||||
val book = Book(
|
||||
id = id,
|
||||
@@ -92,8 +114,12 @@ class AdminBookServiceImpl(private val bookMapper: BookMapper) : AdminBookServic
|
||||
return ApiResult.success("Book updated successfully")
|
||||
}
|
||||
|
||||
// 查询是否有书名和作者完全相同的书
|
||||
// TODO: 后续尽量改成ISBN
|
||||
/*
|
||||
* 按书名+作者判重
|
||||
* @param book 待检查的图书
|
||||
* @param excludeId 排除的图书 ID(更新时用于排除自身)
|
||||
* @return true 表示已存在重复
|
||||
*/
|
||||
private fun isBookDuplicate(book: Book, excludeId: Long? = null): Boolean {
|
||||
val wrapper = QueryWrapper<Book>()
|
||||
.eq("name", book.name)
|
||||
|
||||
@@ -37,7 +37,7 @@ class AdminBorrowServiceImpl(
|
||||
override fun searchBorrows(query: String): ApiResult<List<BorrowInfoVo>> {
|
||||
if (query.isBlank()) {
|
||||
log.warn("[AdminBorrow] search: query is blank")
|
||||
return ApiResult.error("Search query cannot be empty")
|
||||
return ApiResult.badRequest("Search query cannot be empty")
|
||||
}
|
||||
val (matchedUserIds, matchedBookIds) = runBlocking {
|
||||
val userDeferred = async(Dispatchers.IO) {
|
||||
@@ -143,11 +143,11 @@ class AdminBorrowServiceImpl(
|
||||
}
|
||||
if (matchedUser == null) {
|
||||
log.warn("[AdminBorrow] borrowBook: user not found, userId={}", userId)
|
||||
return ApiResult.error("User does not exist")
|
||||
return ApiResult.notFound("User does not exist")
|
||||
}
|
||||
if (matchedBook == null) {
|
||||
log.warn("[AdminBorrow] borrowBook: book not found, bookId={}", bookId)
|
||||
return ApiResult.error("Book does not exist")
|
||||
return ApiResult.notFound("Book does not exist")
|
||||
}
|
||||
if (matchedBook.stock < 1) {
|
||||
log.warn("[AdminBorrow] borrowBook: book out of stock, bookId={}, stock={}", bookId, matchedBook.stock)
|
||||
@@ -180,7 +180,7 @@ class AdminBorrowServiceImpl(
|
||||
val matchedBorrow = borrowRecordMapper.selectById(recordId)
|
||||
if (matchedBorrow == null) {
|
||||
log.warn("[AdminBorrow] returnBook: record not found, recordId={}", recordId)
|
||||
return ApiResult.error("Borrow record does not exist")
|
||||
return ApiResult.notFound("Borrow record does not exist")
|
||||
}
|
||||
if (matchedBorrow.status == "RETURNED") {
|
||||
log.warn("[AdminBorrow] returnBook: already returned, recordId={}", recordId)
|
||||
@@ -197,7 +197,7 @@ class AdminBorrowServiceImpl(
|
||||
val matchedBook = bookMapper.selectById(matchedBorrow.bookId)
|
||||
if (matchedBook == null) {
|
||||
log.warn("[AdminBorrow] returnBook: book not found, bookId={}", matchedBorrow.bookId)
|
||||
return ApiResult.error("Book does not exist")
|
||||
return ApiResult.notFound("Book does not exist")
|
||||
}
|
||||
val book = Book(
|
||||
id = matchedBook.id,
|
||||
|
||||
@@ -14,10 +14,15 @@ import org.springframework.stereotype.Service
|
||||
*/
|
||||
@Service
|
||||
class BookServiceImpl(private val bookMapper: BookMapper) : BookService {
|
||||
/*
|
||||
* 按书名或作者模糊搜索图书
|
||||
* @param query 搜索关键词,不能为空
|
||||
* @return 匹配的图书列表,未匹配时返回 404
|
||||
*/
|
||||
override fun searchBook(query: String): ApiResult<List<Book>> {
|
||||
if (query.isBlank()) {
|
||||
log.warn("[Book] search: query is blank")
|
||||
return ApiResult.error("Search query cannot be empty")
|
||||
return ApiResult.badRequest("Search query cannot be empty")
|
||||
}
|
||||
val result = bookMapper.selectList(
|
||||
QueryWrapper<Book>()
|
||||
@@ -33,10 +38,15 @@ class BookServiceImpl(private val bookMapper: BookMapper) : BookService {
|
||||
return ApiResult.success(result)
|
||||
}
|
||||
|
||||
/*
|
||||
* 根据 ID 查询单本图书
|
||||
* @param id 图书 ID,必须大于 0
|
||||
* @return 图书信息,不存在时返回 404
|
||||
*/
|
||||
override fun getOneBook(id: Long): ApiResult<Book> {
|
||||
if (id < 1) {
|
||||
log.warn("[Book] getOne: invalid id={}", id)
|
||||
return ApiResult.error("Invalid book ID")
|
||||
return ApiResult.badRequest("Invalid book ID")
|
||||
}
|
||||
val result = bookMapper.selectOne(
|
||||
QueryWrapper<Book>()
|
||||
@@ -50,6 +60,10 @@ class BookServiceImpl(private val bookMapper: BookMapper) : BookService {
|
||||
return ApiResult.success(result)
|
||||
}
|
||||
|
||||
/*
|
||||
* 查询全部图书
|
||||
* @return 全部图书列表
|
||||
*/
|
||||
override fun getAllBooks(): ApiResult<List<Book>> {
|
||||
val books = bookMapper.selectList(null)
|
||||
log.info("[Book] getAll: found {} books", books.size)
|
||||
|
||||
@@ -51,7 +51,7 @@ class BorrowServiceImpl(
|
||||
): ApiResult<List<MyBorrowVo>> {
|
||||
if (query.isBlank()) {
|
||||
log.warn("[Borrow] search: query is blank")
|
||||
return ApiResult.error("Search query cannot be empty")
|
||||
return ApiResult.badRequest("Search query cannot be empty")
|
||||
}
|
||||
val matchedBookIds = bookMapper.selectList(
|
||||
QueryWrapper<Book>()
|
||||
@@ -122,7 +122,7 @@ class BorrowServiceImpl(
|
||||
val matchedBook = bookMapper.selectById(bookId)
|
||||
if (matchedBook == null) {
|
||||
log.warn("[Borrow] borrow: book not found, bookId={}", bookId)
|
||||
return ApiResult.error("Book does not exist")
|
||||
return ApiResult.notFound("Book does not exist")
|
||||
}
|
||||
if (matchedBook.stock < 1) {
|
||||
log.warn("[Borrow] borrow: book out of stock, bookId={}, stock={}", bookId, matchedBook.stock)
|
||||
@@ -158,14 +158,14 @@ class BorrowServiceImpl(
|
||||
val matchedBorrow = borrowRecordMapper.selectById(borrowId)
|
||||
if (matchedBorrow == null) {
|
||||
log.warn("[Borrow] return: record not found, borrowId={}", borrowId)
|
||||
return ApiResult.error("Borrow record does not exist")
|
||||
return ApiResult.notFound("Borrow record does not exist")
|
||||
}
|
||||
if (matchedBorrow.userId != userId) {
|
||||
log.warn(
|
||||
"[Borrow] return: OWNERSHIP MISMATCH — userId={} attempted to return borrowId={} belonging to userId={}, bookId={}, status={}",
|
||||
userId, borrowId, matchedBorrow.userId, matchedBorrow.bookId, matchedBorrow.status
|
||||
)
|
||||
return ApiResult.error("Borrow record does not exist")
|
||||
return ApiResult.notFound("Borrow record does not exist")
|
||||
}
|
||||
if (matchedBorrow.status == "RETURNED") {
|
||||
log.warn("[Borrow] return: already returned, borrowId={}", borrowId)
|
||||
@@ -182,7 +182,7 @@ class BorrowServiceImpl(
|
||||
val matchedBook = bookMapper.selectById(matchedBorrow.bookId)
|
||||
if (matchedBook == null) {
|
||||
log.warn("[Borrow] return: book not found, bookId={}", matchedBorrow.bookId)
|
||||
return ApiResult.error("Book does not exist")
|
||||
return ApiResult.notFound("Book does not exist")
|
||||
}
|
||||
val book = Book(
|
||||
id = matchedBook.id,
|
||||
|
||||
@@ -100,5 +100,19 @@ data class ApiResult<T>(
|
||||
data = null
|
||||
)
|
||||
}
|
||||
|
||||
/*
|
||||
* 请求参数错误响应 — 返回 400 + 自定义提示
|
||||
* @param message 提示信息,如 "Search query cannot be empty"
|
||||
*
|
||||
* JSON 输出示例:{ "code": 400, "message": "xxx" }
|
||||
*/
|
||||
fun <T> badRequest(message: String): ApiResult<T> {
|
||||
return ApiResult(
|
||||
code = 400,
|
||||
message = message,
|
||||
data = null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
package com.msksbr.bookmgr.vo
|
||||
|
||||
/*
|
||||
* 登录响应 VO
|
||||
* 登录成功后返回给客户端的令牌和用户基本信息
|
||||
*/
|
||||
data class LoginVo(
|
||||
val token: String,
|
||||
val username: String,
|
||||
|
||||
@@ -1,27 +1,50 @@
|
||||
# =============================================================================
|
||||
# bookMgr 主配置
|
||||
# 所有敏感/环境相关的配置均通过环境变量注入
|
||||
#
|
||||
# 开发方式:
|
||||
# 1. 复制 application-dev.yaml(已 gitignored)到同目录
|
||||
# 2. 修改 DB、JWT 等配置为本地值
|
||||
# 3. 启动时指定 profile: --spring.profiles.active=dev
|
||||
# =============================================================================
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: bookMgr
|
||||
|
||||
# ---- 数据源(需配置环境变量) ----
|
||||
datasource:
|
||||
driver-class-name: ${DB_DRIVER}
|
||||
url: jdbc:${DB_TYPE}://${DB_URL}:${DB_PORT}/${DB_NAME}
|
||||
username: ${DB_USER}
|
||||
password: ${DB_PASSWORD}
|
||||
jackson:
|
||||
default-property-inclusion: non_null
|
||||
property-naming-strategy: SNAKE_CASE
|
||||
date-format: ${JSON_DATE_FORMAT:}
|
||||
time-zone: ${JSON_TIME_ZONE:GMT}
|
||||
|
||||
# ---- Jackson JSON 序列化 ----
|
||||
jackson:
|
||||
default-property-inclusion: non_null # 序列化时忽略 null 字段
|
||||
property-naming-strategy: SNAKE_CASE # 实体驼峰 → JSON snake_case
|
||||
date-format: ${JSON_DATE_FORMAT:} # 日期格式,dev 配置中覆盖为 yyyy-MM-dd HH:mm:ss
|
||||
time-zone: ${JSON_TIME_ZONE:GMT} # 时区,dev 配置中覆盖为 GMT+8
|
||||
|
||||
# ---- MyBatis-Plus ----
|
||||
mybatis-plus:
|
||||
configuration:
|
||||
# 开启驼峰命名法
|
||||
map-underscore-to-camel-case: true
|
||||
# 开启日志输出sql语句
|
||||
# log-impl: org.apache.ibatis.logging.commons.JakartaCommonsLoggingImpl
|
||||
map-underscore-to-camel-case: true # 数据库下划线 → 实体驼峰映射
|
||||
# log-impl: org.apache.ibatis.logging.commons.JakartaCommonsLoggingImpl # 调试时取消注释以输出 SQL
|
||||
|
||||
# ---- 日志 ----
|
||||
logging:
|
||||
level:
|
||||
com.msksbr.bookmgr: ${LOG_LEVEL:INFO}
|
||||
com.msksbr.bookmgr: ${LOG_LEVEL:INFO} # 包级别日志级别,dev 配置中覆盖为 DEBUG
|
||||
|
||||
# ---- JWT ----
|
||||
jwt:
|
||||
secret: ${JWT_SECRET:}
|
||||
expiration: ${JWT_EXPIRATION_TIME:86400000}
|
||||
secret: ${JWT_SECRET:} # 密钥(不配置则每次启动随机生成,已签发 token 失效)
|
||||
expiration: ${JWT_EXPIRATION_TIME:86400000} # token 有效期(毫秒),默认 24 小时
|
||||
|
||||
# ---- SpringDoc / Swagger ----
|
||||
springdoc:
|
||||
api-docs:
|
||||
enabled: false # 生产环境关闭,dev 配置中开启
|
||||
swagger-ui:
|
||||
enabled: false # 生产环境关闭,dev 配置中开启
|
||||
Reference in New Issue
Block a user