diff --git a/src/main/kotlin/com/msksbr/bookmgr/config/ApiResultStatusAdvice.kt b/src/main/kotlin/com/msksbr/bookmgr/config/ApiResultStatusAdvice.kt new file mode 100644 index 0000000..ed41254 --- /dev/null +++ b/src/main/kotlin/com/msksbr/bookmgr/config/ApiResultStatusAdvice.kt @@ -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 { + + override fun supports(returnType: MethodParameter, converterType: Class>): Boolean = true + + override fun beforeBodyWrite( + body: Any?, + returnType: MethodParameter, + selectedContentType: MediaType, + selectedConverterType: Class>, + request: ServerHttpRequest, + response: ServerHttpResponse + ): Any? { + if (body is ApiResult<*>) { + (response as ServletServerHttpResponse).servletResponse.status = body.code + } + return body + } +} diff --git a/src/main/kotlin/com/msksbr/bookmgr/config/JwtPopulateFilter.kt b/src/main/kotlin/com/msksbr/bookmgr/config/JwtPopulateFilter.kt index 4d2dd51..ddc1a20 100644 --- a/src/main/kotlin/com/msksbr/bookmgr/config/JwtPopulateFilter.kt +++ b/src/main/kotlin/com/msksbr/bookmgr/config/JwtPopulateFilter.kt @@ -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) diff --git a/src/main/kotlin/com/msksbr/bookmgr/config/JwtUtils.kt b/src/main/kotlin/com/msksbr/bookmgr/config/JwtUtils.kt index e9879c4..1d6dbab 100644 --- a/src/main/kotlin/com/msksbr/bookmgr/config/JwtUtils.kt +++ b/src/main/kotlin/com/msksbr/bookmgr/config/JwtUtils.kt @@ -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 { diff --git a/src/main/kotlin/com/msksbr/bookmgr/config/OpenApiConfig.kt b/src/main/kotlin/com/msksbr/bookmgr/config/OpenApiConfig.kt new file mode 100644 index 0000000..4239572 --- /dev/null +++ b/src/main/kotlin/com/msksbr/bookmgr/config/OpenApiConfig.kt @@ -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().`$ref`("#/components/schemas/ApiResultString")))) +} diff --git a/src/main/kotlin/com/msksbr/bookmgr/config/PasswordConfig.kt b/src/main/kotlin/com/msksbr/bookmgr/config/PasswordConfig.kt index fe65bdd..0e83c53 100644 --- a/src/main/kotlin/com/msksbr/bookmgr/config/PasswordConfig.kt +++ b/src/main/kotlin/com/msksbr/bookmgr/config/PasswordConfig.kt @@ -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 diff --git a/src/main/kotlin/com/msksbr/bookmgr/controller/AdminBookController.kt b/src/main/kotlin/com/msksbr/bookmgr/controller/AdminBookController.kt index 1985ddc..66c5f8e 100644 --- a/src/main/kotlin/com/msksbr/bookmgr/controller/AdminBookController.kt +++ b/src/main/kotlin/com/msksbr/bookmgr/controller/AdminBookController.kt @@ -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( diff --git a/src/main/kotlin/com/msksbr/bookmgr/controller/AuthController.kt b/src/main/kotlin/com/msksbr/bookmgr/controller/AuthController.kt index 1c0fc5e..2118ca4 100644 --- a/src/main/kotlin/com/msksbr/bookmgr/controller/AuthController.kt +++ b/src/main/kotlin/com/msksbr/bookmgr/controller/AuthController.kt @@ -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") } } diff --git a/src/main/kotlin/com/msksbr/bookmgr/controller/BookController.kt b/src/main/kotlin/com/msksbr/bookmgr/controller/BookController.kt index a3f2c24..1bb3c3a 100644 --- a/src/main/kotlin/com/msksbr/bookmgr/controller/BookController.kt +++ b/src/main/kotlin/com/msksbr/bookmgr/controller/BookController.kt @@ -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?, diff --git a/src/main/kotlin/com/msksbr/bookmgr/service/impl/AdminBookServiceImpl.kt b/src/main/kotlin/com/msksbr/bookmgr/service/impl/AdminBookServiceImpl.kt index 02e5ec9..225d414 100644 --- a/src/main/kotlin/com/msksbr/bookmgr/service/impl/AdminBookServiceImpl.kt +++ b/src/main/kotlin/com/msksbr/bookmgr/service/impl/AdminBookServiceImpl.kt @@ -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 { 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 { 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 { 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 { 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() .eq("name", book.name) diff --git a/src/main/kotlin/com/msksbr/bookmgr/service/impl/AdminBorrowServiceImpl.kt b/src/main/kotlin/com/msksbr/bookmgr/service/impl/AdminBorrowServiceImpl.kt index 6639670..1fc8247 100644 --- a/src/main/kotlin/com/msksbr/bookmgr/service/impl/AdminBorrowServiceImpl.kt +++ b/src/main/kotlin/com/msksbr/bookmgr/service/impl/AdminBorrowServiceImpl.kt @@ -37,7 +37,7 @@ class AdminBorrowServiceImpl( override fun searchBorrows(query: String): ApiResult> { 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, diff --git a/src/main/kotlin/com/msksbr/bookmgr/service/impl/BookServiceImpl.kt b/src/main/kotlin/com/msksbr/bookmgr/service/impl/BookServiceImpl.kt index 5dfeb1e..68f82fd 100644 --- a/src/main/kotlin/com/msksbr/bookmgr/service/impl/BookServiceImpl.kt +++ b/src/main/kotlin/com/msksbr/bookmgr/service/impl/BookServiceImpl.kt @@ -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> { 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() @@ -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 { 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() @@ -50,6 +60,10 @@ class BookServiceImpl(private val bookMapper: BookMapper) : BookService { return ApiResult.success(result) } + /* + * 查询全部图书 + * @return 全部图书列表 + */ override fun getAllBooks(): ApiResult> { val books = bookMapper.selectList(null) log.info("[Book] getAll: found {} books", books.size) diff --git a/src/main/kotlin/com/msksbr/bookmgr/service/impl/BorrowServiceImpl.kt b/src/main/kotlin/com/msksbr/bookmgr/service/impl/BorrowServiceImpl.kt index 7327858..c0e7340 100644 --- a/src/main/kotlin/com/msksbr/bookmgr/service/impl/BorrowServiceImpl.kt +++ b/src/main/kotlin/com/msksbr/bookmgr/service/impl/BorrowServiceImpl.kt @@ -51,7 +51,7 @@ class BorrowServiceImpl( ): ApiResult> { 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() @@ -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, diff --git a/src/main/kotlin/com/msksbr/bookmgr/template/ApiResult.kt b/src/main/kotlin/com/msksbr/bookmgr/template/ApiResult.kt index 8e5eb4d..7019fb1 100644 --- a/src/main/kotlin/com/msksbr/bookmgr/template/ApiResult.kt +++ b/src/main/kotlin/com/msksbr/bookmgr/template/ApiResult.kt @@ -100,5 +100,19 @@ data class ApiResult( data = null ) } + + /* + * 请求参数错误响应 — 返回 400 + 自定义提示 + * @param message 提示信息,如 "Search query cannot be empty" + * + * JSON 输出示例:{ "code": 400, "message": "xxx" } + */ + fun badRequest(message: String): ApiResult { + return ApiResult( + code = 400, + message = message, + data = null + ) + } } } diff --git a/src/main/kotlin/com/msksbr/bookmgr/vo/LoginVo.kt b/src/main/kotlin/com/msksbr/bookmgr/vo/LoginVo.kt index 51705b8..fbf938e 100644 --- a/src/main/kotlin/com/msksbr/bookmgr/vo/LoginVo.kt +++ b/src/main/kotlin/com/msksbr/bookmgr/vo/LoginVo.kt @@ -1,5 +1,9 @@ package com.msksbr.bookmgr.vo +/* +* 登录响应 VO +* 登录成功后返回给客户端的令牌和用户基本信息 +*/ data class LoginVo( val token: String, val username: String, diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 973d8c6..63f6412 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -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} \ No newline at end of file + secret: ${JWT_SECRET:} # 密钥(不配置则每次启动随机生成,已签发 token 失效) + expiration: ${JWT_EXPIRATION_TIME:86400000} # token 有效期(毫秒),默认 24 小时 + +# ---- SpringDoc / Swagger ---- +springdoc: + api-docs: + enabled: false # 生产环境关闭,dev 配置中开启 + swagger-ui: + enabled: false # 生产环境关闭,dev 配置中开启 \ No newline at end of file