支持不提供Discord令牌启动服务

重整代码内的Swagger排版
代码格式优化
This commit is contained in:
TheWhiteDog9487
2026-07-31 13:36:39 +08:00
parent 0d87eede70
commit 360f6686b6
11 changed files with 151 additions and 156 deletions
+8
View File
@@ -8,6 +8,14 @@
能用,但缺的东西非常多。 能用,但缺的东西非常多。
就像这个文档一样。 就像这个文档一样。
# 命令行参数
| 参数 | 说明 |
|---|------------------------------------------------------------|
| `install` / `--install` | 安装为 systemd 服务 |
| `uninstall` / `--uninstall` | 卸载 systemd 服务 |
| `--Discord_Bot_Token <令牌>` | 设置 Discord 机器人令牌,优先级高于同名环境变量 |
| `--Disable-Discord` | 安装systemd服务时禁用 Discord 相关功能,故不再要求提供令牌 |
# 注意事项 # 注意事项
**本项目不保证配置文件和环境变量的向后兼容!!!** **本项目不保证配置文件和环境变量的向后兼容!!!**
**也就是说,如果你也要用的话,你可能必须在更新之后重新配置环境变量和配置文件** **也就是说,如果你也要用的话,你可能必须在更新之后重新配置环境变量和配置文件**
+1 -1
View File
@@ -13,7 +13,7 @@ plugins {
} }
group = "xyz.thewhitedog9487" group = "xyz.thewhitedog9487"
version = "0.8.2" version = "0.8.3"
java { java {
toolchain { toolchain {
@@ -21,7 +21,7 @@ class OpenApiConfiguration {
.contact(Contact() .contact(Contact()
.name("TheWhiteDog9487") .name("TheWhiteDog9487")
.url("https://www.github.com/TheWhiteDog9487/WebAPI")) .url("https://www.github.com/TheWhiteDog9487/WebAPI"))
.version("0.8.2") .version("0.8.3")
.license(License() .license(License()
.name("WTFPL") .name("WTFPL")
.url("https://spdx.org/licenses/WTFPL")) ) .url("https://spdx.org/licenses/WTFPL")) )
@@ -16,7 +16,7 @@ import xyz.thewhitedog9487.WebAPI.Data.Entity.AccessLog
import xyz.thewhitedog9487.WebAPI.Miscellaneous.SpringContextHolder.SpringContext import xyz.thewhitedog9487.WebAPI.Miscellaneous.SpringContextHolder.SpringContext
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
class ApiKeyAuthenticationFilter(): OncePerRequestFilter() { class ApiKeyAuthenticationFilter: OncePerRequestFilter() {
val ApiKeyManagerInstance = SpringContext.getBean<ApiKeyManager>() val ApiKeyManagerInstance = SpringContext.getBean<ApiKeyManager>()
override fun doFilterInternal(request: HttpServletRequest, response: HttpServletResponse, filterChain: FilterChain) { override fun doFilterInternal(request: HttpServletRequest, response: HttpServletResponse, filterChain: FilterChain) {
@@ -41,22 +41,18 @@ class AccessLog(val AccessLogRepositoryInstance: AccessLogRepository) {
examples = [ examples = [
ExampleObject( ExampleObject(
name = "Limit小于0", name = "Limit小于0",
value = """ value = """{
{
"code": 400, "code": 400,
"message": "Limit参数不能小于0", "message": "Limit参数不能小于0",
"data": null "data": null
} }""" ),
""" ),
ExampleObject( ExampleObject(
name = "字段不存在", name = "字段不存在",
value = """ value = """{
{
"code": 400, "code": 400,
"message": "试图访问一个不存在的数据库字段", "message": "试图访问一个不存在的数据库字段",
"data": "AccessLog类中不存在名为 NotExistField 的字段" "data": "AccessLog类中不存在名为 NotExistField 的字段"
} }""" ) ] ) ] ),
""" ) ] ) ] ),
ApiResponse( ApiResponse(
responseCode = "200", responseCode = "200",
description = "成功获取到数据", description = "成功获取到数据",
@@ -68,8 +64,7 @@ class AccessLog(val AccessLogRepositoryInstance: AccessLogRepository) {
schema = Schema(implementation = AccessLog::class) ), schema = Schema(implementation = AccessLog::class) ),
examples = [ examples = [
ExampleObject( ExampleObject(
value = """ value = """[
[
{ {
"ID": 18, "ID": 18,
"RequestID": "3", "RequestID": "3",
@@ -105,27 +100,8 @@ class AccessLog(val AccessLogRepositoryInstance: AccessLogRepository) {
"Header": "host: dev.thewhitedog9487.xyz\nx-real-ip: 172.71.150.157\nx-forwarded-for: 2409:8962:77a:49c:a579:f5af:58a3:e2c, 172.71.150.157\nx-forwarded-proto: https\nconnection: close\nx-api-key: asdasdaFSEA452453sdcv\nuser-agent: IntelliJ HTTP Client/IntelliJ IDEA 2025.2.3\naccept: */*\ncf-ray: 98cc7d6aaea1c37a-SEA\naccept-encoding: gzip, br\ncdn-loop: cloudflare; loops=1\ncf-connecting-ip: 2409:8962:77a:49c:a579:f5af:58a3:e2c\ncf-ipcountry: CN\ncf-visitor: {\"scheme\":\"https\"}\ncookie: JSESSIONID=A49527B913170D86A5544A252BD31040", "Header": "host: dev.thewhitedog9487.xyz\nx-real-ip: 172.71.150.157\nx-forwarded-for: 2409:8962:77a:49c:a579:f5af:58a3:e2c, 172.71.150.157\nx-forwarded-proto: https\nconnection: close\nx-api-key: asdasdaFSEA452453sdcv\nuser-agent: IntelliJ HTTP Client/IntelliJ IDEA 2025.2.3\naccept: */*\ncf-ray: 98cc7d6aaea1c37a-SEA\naccept-encoding: gzip, br\ncdn-loop: cloudflare; loops=1\ncf-connecting-ip: 2409:8962:77a:49c:a579:f5af:58a3:e2c\ncf-ipcountry: CN\ncf-visitor: {\"scheme\":\"https\"}\ncookie: JSESSIONID=A49527B913170D86A5544A252BD31040",
"Timestamp": "2025-10-11T07:02:05.718Z", "Timestamp": "2025-10-11T07:02:05.718Z",
"ResponseStatusCode": 200 "ResponseStatusCode": 200
},
{
"ID": 263,
"RequestID": "35",
"CF_Connecting_IP": "2409:8962:77a:49c:a579:f5af:58a3:e2c",
"X_Forwarded_For": "2409:8962:77a:49c:a579:f5af:58a3:e2c, 108.162.246.209",
"RemoteAddress": "192.168.128.11",
"CF_IPCountry": "CN",
"ISO3166": "CHN",
"UserAgent": "IntelliJ HTTP Client/IntelliJ IDEA 2025.2.3",
"HttpMethod": "GET",
"Protocol": "http",
"ProtocolVersion": "HTTP/1.0",
"URL": "http://dev.thewhitedog9487.xyz/ip/ip",
"QueryString": null,
"Header": "host: dev.thewhitedog9487.xyz\nx-real-ip: 108.162.246.209\nx-forwarded-for: 2409:8962:77a:49c:a579:f5af:58a3:e2c, 108.162.246.209\nx-forwarded-proto: https\nconnection: close\naccept-encoding: gzip, br\nuser-agent: IntelliJ HTTP Client/IntelliJ IDEA 2025.2.3\naccept: */*\ncf-ray: 98cc9ec12c48a1a5-SEA\ncdn-loop: cloudflare; loops=1\ncf-connecting-ip: 2409:8962:77a:49c:a579:f5af:58a3:e2c\ncf-ipcountry: CN\ncf-visitor: {\"scheme\":\"https\"}\ncookie: JSESSIONID=A49527B913170D86A5544A252BD31040",
"Timestamp": "2025-10-11T07:24:51.504Z",
"ResponseStatusCode": 200
} }
] ]""" ) ] ) ] ),
""" ) ] ) ] ),
ApiResponse( ApiResponse(
responseCode = "401", responseCode = "401",
description = "未提供API密钥", description = "未提供API密钥",
@@ -135,13 +111,11 @@ class AccessLog(val AccessLogRepositoryInstance: AccessLogRepository) {
schema = Schema(implementation = ResponseData::class), schema = Schema(implementation = ResponseData::class),
examples = [ examples = [
ExampleObject( ExampleObject(
value = """ value = """{
{
"code": 401, "code": 401,
"message": "API密钥验证失败,未传递X-API-Key请求头", "message": "API密钥验证失败,未传递X-API-Key请求头",
"data": null "data": null
} }""" ) ] ) ] ),
""" ) ] ) ] ),
ApiResponse( ApiResponse(
responseCode = "403", responseCode = "403",
description = "API密钥无效", description = "API密钥无效",
@@ -151,13 +125,11 @@ class AccessLog(val AccessLogRepositoryInstance: AccessLogRepository) {
schema = Schema(implementation = ResponseData::class), schema = Schema(implementation = ResponseData::class),
examples = [ examples = [
ExampleObject( ExampleObject(
value = """ value = """{
{
"code": 403, "code": 403,
"message": "API密钥验证失败,API密钥无效", "message": "API密钥验证失败,API密钥无效",
"data": null "data": null
} }""" ) ] ) ] ) ] )
""" ) ] ) ] ) ] )
@GetMapping @GetMapping
fun GetLog(@Parameter(description = "用于身份验证的API密钥", `in` = ParameterIn.HEADER, required = true, example = "ds1858dscc8745sfwe" ) fun GetLog(@Parameter(description = "用于身份验证的API密钥", `in` = ParameterIn.HEADER, required = true, example = "ds1858dscc8745sfwe" )
@RequestHeader("X-API-Key") ApiKey: String, @RequestHeader("X-API-Key") ApiKey: String,
@@ -26,12 +26,13 @@ class IP {
1. CF-Connecting-IP 1. CF-Connecting-IP
2. X-Forwarded-For 2. X-Forwarded-For
3. 直接使用请求的远程地址""") 3. 直接使用请求的远程地址""")
@ApiResponse(responseCode = "200", @ApiResponse(
responseCode = "200",
description = "成功获取到IP地址,内容为纯文本格式的IP地址",
content = [Content( content = [Content(
mediaType = MediaType.TEXT_PLAIN_VALUE, mediaType = MediaType.TEXT_PLAIN_VALUE,
schema = Schema(implementation = String::class), schema = Schema(implementation = String::class),
examples = [ExampleObject(value = "78.141.226.247")])], examples = [ExampleObject(value = "78.141.226.247") ] ) ] )
description = "成功获取到IP地址,内容为纯文本格式的IP地址")
@GetMapping("") @GetMapping("")
fun GetIP(@RequestHeader("CF-Connecting-IP", required = false) Header_CFConnectingIP: String?, fun GetIP(@RequestHeader("CF-Connecting-IP", required = false) Header_CFConnectingIP: String?,
@RequestHeader("X-Forwarded-For", required = false) Header_XForwardedFor: String?, @RequestHeader("X-Forwarded-For", required = false) Header_XForwardedFor: String?,
@@ -54,18 +55,20 @@ class IP {
如果请求没有经过Cloudflare,则会返回"未找到CF-IPCountry头部" 如果请求没有经过Cloudflare,则会返回"未找到CF-IPCountry头部"
""") """)
@ApiResponses(value = [ @ApiResponses(value = [
ApiResponse(responseCode = "200", ApiResponse(
responseCode = "200",
description = "成功获取到ISO 3166-1 alpha-2国家代码,内容为纯文本格式的国家代码",
content = [Content( content = [Content(
mediaType = MediaType.TEXT_PLAIN_VALUE, mediaType = MediaType.TEXT_PLAIN_VALUE,
schema = Schema(implementation = String::class), schema = Schema(implementation = String::class),
examples = [ExampleObject(value = "HK")])], examples = [ExampleObject(value = "HK") ] ) ] ),
description = "成功获取到ISO 3166-1 alpha-2国家代码,内容为纯文本格式的国家代码"), ApiResponse(
ApiResponse(responseCode = "404", responseCode = "404",
description = "未找到CF-IPCountry头部,内容为纯文本格式的错误信息",
content = [Content( content = [Content(
mediaType = MediaType.TEXT_PLAIN_VALUE, mediaType = MediaType.TEXT_PLAIN_VALUE,
schema = Schema(implementation = String::class), schema = Schema(implementation = String::class),
examples = [ExampleObject(value = "未找到CF-IPCountry头部")] ) ], examples = [ExampleObject(value = "未找到CF-IPCountry头部") ] ) ] ) ] )
description = "未找到CF-IPCountry头部,内容为纯文本格式的错误信息") ] )
@GetMapping("iso3166") @GetMapping("iso3166")
fun GetIso3166(@RequestHeader("CF-IPCountry", required = false) Header_CFIPCountry: String?): ResponseEntity<String> { fun GetIso3166(@RequestHeader("CF-IPCountry", required = false) Header_CFIPCountry: String?): ResponseEntity<String> {
Header_CFIPCountry?.let { Header_CFIPCountry?.let {
@@ -20,8 +20,8 @@ import org.springframework.http.HttpStatus
import org.springframework.http.MediaType import org.springframework.http.MediaType
import org.springframework.http.ResponseEntity import org.springframework.http.ResponseEntity
import org.springframework.http.converter.HttpMessageNotReadableException import org.springframework.http.converter.HttpMessageNotReadableException
import org.springframework.web.bind.MissingRequestHeaderException
import org.springframework.web.bind.annotation.* import org.springframework.web.bind.annotation.*
import xyz.thewhitedog9487.WebAPI.Exception.DiscordBotTokenDoesNotConfiguredException
import xyz.thewhitedog9487.WebAPI.Exception.NoSuchDiscordChannelException import xyz.thewhitedog9487.WebAPI.Exception.NoSuchDiscordChannelException
import xyz.thewhitedog9487.WebAPI.Miscellaneous.VirtualThreadCoroutineDispatcher import xyz.thewhitedog9487.WebAPI.Miscellaneous.VirtualThreadCoroutineDispatcher
import xyz.thewhitedog9487.WebAPI.Miscellaneous.link import xyz.thewhitedog9487.WebAPI.Miscellaneous.link
@@ -30,7 +30,7 @@ import java.net.URI
@Tag(name="远程消息处理相关") @Tag(name="远程消息处理相关")
@RestController @RestController
@RequestMapping("/message") @RequestMapping("/message")
class Message(val DiscordBotClient: Kord) { class Message(val DiscordBotClient: Kord?) {
@JsonAutoDetect( @JsonAutoDetect(
fieldVisibility = JsonAutoDetect.Visibility.ANY, fieldVisibility = JsonAutoDetect.Visibility.ANY,
getterVisibility = JsonAutoDetect.Visibility.NONE, getterVisibility = JsonAutoDetect.Visibility.NONE,
@@ -51,13 +51,13 @@ class Message(val DiscordBotClient: Kord) {
需要在请求头中提供X-API-Key以进行身份验证 需要在请求头中提供X-API-Key以进行身份验证
需要在请求体中提供频道ID和消息内容""") 需要在请求体中提供频道ID和消息内容""")
@ApiResponses(value = [ @ApiResponses(value = [
ApiResponse(responseCode = "201", ApiResponse(
responseCode = "201",
description = "消息发送成功,响应体中包含新消息的ID、频道ID和内容",
content = [Content( content = [Content(
mediaType = MediaType.APPLICATION_JSON_VALUE, mediaType = MediaType.APPLICATION_JSON_VALUE,
schema = Schema(implementation = ResponseData::class), schema = Schema(implementation = ResponseData::class),
examples = [ExampleObject(value = examples = [ExampleObject(value = """{
"""
{
"code": 201, "code": 201,
"message": "消息发送成功", "message": "消息发送成功",
"data": { "data": {
@@ -65,78 +65,83 @@ class Message(val DiscordBotClient: Kord) {
"频道ID": "1398192763845214239", "频道ID": "1398192763845214239",
"内容": "写点什么好呢" "内容": "写点什么好呢"
} }
} }""") ] ) ] ),
""", name = "成功") ] ) ], ApiResponse(
description = "消息发送成功,响应体中包含新消息的ID、频道ID和内容"), responseCode = "400",
ApiResponse(responseCode = "400", description = "客户端发送的请求存在问题,请检查响应的data字段以获取更多信息",
content = [Content( content = [Content(
mediaType = MediaType.APPLICATION_JSON_VALUE, mediaType = MediaType.APPLICATION_JSON_VALUE,
schema = Schema(implementation = ResponseData::class), schema = Schema(implementation = ResponseData::class),
examples = [ExampleObject(value = examples = [ExampleObject(
""" name = "请求体无法解析",
{ value = """{
"code": 400, "code": 400,
"message": "请求体无法解析", "message": "请求体无法解析",
"data": { "data": {
"错误信息": "JSON parse error: Unexpected character ('}' (code 125)): was expecting double-quote to start field name" "错误信息": "JSON parse error: Unexpected character ('}' (code 125)): was expecting double-quote to start field name"
} }
} }"""),
""", name = "请求体无法解析"), ExampleObject(
ExampleObject(value = name = "频道不存在",
""" value =
{ """{
"code": 400, "code": 400,
"message": "消息发送失败", "message": "消息发送失败",
"data": { "data": {
"错误信息": "ID为1398192763845214239的频道不存在", "错误信息": "ID为1398192763845214239的频道不存在",
"频道ID": "1398192763845214239" "频道ID": "1398192763845214239"
} }
} }""") ] ) ] ),
""", name = "频道不存在")] ) ], ApiResponse(
description = "客户端发送的请求存在问题,请检查响应的data字段以获取更多信息"), responseCode = "401",
ApiResponse(responseCode = "401", description = "未提供API密钥",
content = [Content( content = [Content(
mediaType = MediaType.APPLICATION_JSON_VALUE, mediaType = MediaType.APPLICATION_JSON_VALUE,
schema = Schema(implementation = ResponseData::class), schema = Schema(implementation = ResponseData::class),
examples = [ExampleObject(value = examples = [ExampleObject(value = """{
"""
{
"code": 401, "code": 401,
"message": "API密钥验证失败,未传递X-API-Key请求头", "message": "API密钥验证失败,未传递X-API-Key请求头",
"data": null "data": null
} }""") ] ) ] ),
""") ] ) ], ApiResponse(
description = "未提供API密钥"), responseCode = "403",
ApiResponse(responseCode = "403", description = "API密钥无效",
content = [Content( content = [Content(
mediaType = MediaType.APPLICATION_JSON_VALUE, mediaType = MediaType.APPLICATION_JSON_VALUE,
schema = Schema(implementation = ResponseData::class), schema = Schema(implementation = ResponseData::class),
examples = [ExampleObject(value = examples = [ExampleObject(value = """{
"""
{
"code": 403, "code": 403,
"message": "API密钥验证失败,API密钥无效", "message": "API密钥验证失败,API密钥无效",
"data": null "data": null
} }""") ] ) ] ),
""") ] ) ], ApiResponse(
description = "API密钥无效"), responseCode = "503",
ApiResponse(responseCode = "500", description = "服务端未配置Discord令牌,无法使用Discord相关功能",
content = [Content( content = [Content(
mediaType = MediaType.APPLICATION_JSON_VALUE, mediaType = MediaType.APPLICATION_JSON_VALUE,
schema = Schema(implementation = ResponseData::class), schema = Schema(implementation = ResponseData::class),
examples = [ExampleObject(value = examples = [ExampleObject(value = """{
"""{ "code": 503,
"message": "服务端尚未配置Discord令牌,无法使用Discord相关功能!",
"data": null
}""") ] ) ] ),
ApiResponse(
responseCode = "500",
description =
"""
处理请求时发生未知错误,可能是由于Discord服务器问题或其他内部错误,请查看响应Body以确定原因
另外,所有未被针对性处理的异常都会触发此响应
""",
content = [Content(
mediaType = MediaType.APPLICATION_JSON_VALUE,
schema = Schema(implementation = ResponseData::class),
examples = [ExampleObject(value = """{
"code": 500, "code": 500,
"message": "处理请求时发生未知错误", "message": "处理请求时发生未知错误",
"data": { "data": {
"错误信息": "POST /channels/1398192763845214/messages returned 404 Not Found with response {code=10003, message=Unknown Channel}" "错误信息": "POST /channels/1398192763845214/messages returned 404 Not Found with response {code=10003, message=Unknown Channel}"
} }
}""", name = "未知错误") ] ) ], }""") ] ) ] ) ] )
description =
"""
处理请求时发生未知错误,可能是由于Discord服务器问题或其他内部错误,请查看响应Body以确定原因
另外,所有未被针对性处理的异常都会触发此响应
""") ] )
@PostMapping("/discord") @PostMapping("/discord")
suspend fun DiscordPush( suspend fun DiscordPush(
@Parameter(description = "用于身份验证的API密钥", `in` = ParameterIn.HEADER, required = true, example = "ds1858dscc8745sfwe") @Parameter(description = "用于身份验证的API密钥", `in` = ParameterIn.HEADER, required = true, example = "ds1858dscc8745sfwe")
@@ -148,9 +153,10 @@ class Message(val DiscordBotClient: Kord) {
mediaType = MediaType.APPLICATION_JSON_VALUE, mediaType = MediaType.APPLICATION_JSON_VALUE,
schema = Schema(implementation = PostMessageData::class) ) ] ) schema = Schema(implementation = PostMessageData::class) ) ] )
@RequestBody RequestBody: PostMessageData): ResponseEntity<ResponseData> { @RequestBody RequestBody: PostMessageData): ResponseEntity<ResponseData> {
DiscordBotClient ?: throw DiscordBotTokenDoesNotConfiguredException()
return withContext(VirtualThreadCoroutineDispatcher) { return withContext(VirtualThreadCoroutineDispatcher) {
val ChannelID = Snowflake(RequestBody.ChannelId) val ChannelID = Snowflake(RequestBody.ChannelId)
val MessageData = DiscordBotClient val MessageData = DiscordBotClient!!
.getChannelOf<TextChannel>(ChannelID) .getChannelOf<TextChannel>(ChannelID)
?.createMessage(RequestBody.Content) ?.createMessage(RequestBody.Content)
?: throw NoSuchDiscordChannelException(ChannelID) ?: throw NoSuchDiscordChannelException(ChannelID)
@@ -162,6 +168,13 @@ class Message(val DiscordBotClient: Kord) {
"新ID" to MessageData.id.toString(), "新ID" to MessageData.id.toString(),
"频道ID" to ChannelID.value, "频道ID" to ChannelID.value,
"内容" to RequestBody.Content ) ) ) } } "内容" to RequestBody.Content ) ) ) } }
@ExceptionHandler(DiscordBotTokenDoesNotConfiguredException::class)
fun TriggerWhenDiscordBotTokenDoesNotConfigured(Exception: DiscordBotTokenDoesNotConfiguredException, Request: HttpServletRequest): ResponseEntity<ResponseData> {
return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE)
.body(ResponseData(
HttpStatus.SERVICE_UNAVAILABLE.value(),
Exception.message ) ) }
@ExceptionHandler(NoSuchDiscordChannelException::class) @ExceptionHandler(NoSuchDiscordChannelException::class)
fun TriggerWhenSpecificChannelDoesNotExist(Exception: NoSuchDiscordChannelException, Request: HttpServletRequest): ResponseEntity<ResponseData> { fun TriggerWhenSpecificChannelDoesNotExist(Exception: NoSuchDiscordChannelException, Request: HttpServletRequest): ResponseEntity<ResponseData> {
return ResponseEntity.badRequest() return ResponseEntity.badRequest()
@@ -171,13 +184,6 @@ class Message(val DiscordBotClient: Kord) {
mapOf( mapOf(
"错误信息" to Exception.message, "错误信息" to Exception.message,
"频道ID" to Exception.TargetChannelId.value ) ) ) } "频道ID" to Exception.TargetChannelId.value ) ) ) }
@ExceptionHandler(MissingRequestHeaderException::class)
fun HandleMissingHeader(Exception: MissingRequestHeaderException, Request: HttpServletRequest): ResponseEntity<ResponseData> {
return ResponseEntity.badRequest()
.body(ResponseData(
HttpStatus.BAD_REQUEST.value(),
"请求缺少必要的头部信息",
mapOf("缺失的头部" to Exception.headerName ) ) ) }
@ExceptionHandler(HttpMessageNotReadableException::class) @ExceptionHandler(HttpMessageNotReadableException::class)
fun HandleMessageNotReadable(Exception: HttpMessageNotReadableException, Request: HttpServletRequest): ResponseEntity<ResponseData> { fun HandleMessageNotReadable(Exception: HttpMessageNotReadableException, Request: HttpServletRequest): ResponseEntity<ResponseData> {
return ResponseEntity.badRequest() return ResponseEntity.badRequest()
@@ -105,7 +105,7 @@ cookie: JSESSIONID=A49527B913170D86A5544A252BD31040
var Header: String = "", var Header: String = "",
@JsonProperty("Timestamp") @JsonProperty("Timestamp")
@Schema(description = "请求时间ISO-8601格式", example = "2025-09-29T03:49:42.766Z") @Schema(description = "请求时间 ISO-8601格式", example = "2025-09-29T03:49:42.766Z")
var Timestamp: Instant = Instant.now(), var Timestamp: Instant = Instant.now(),
@JsonProperty("ResponseStatusCode") @JsonProperty("ResponseStatusCode")
@@ -130,7 +130,7 @@ cookie: JSESSIONID=A49527B913170D86A5544A252BD31040
Instant.now(), Instant.now(),
0 ) 0 )
fun SaveIntoDatabase(CachedResponse: ContentCachingResponseWrapper? = null){ fun SaveIntoDatabase(CachedResponse: ContentCachingResponseWrapper? = null){
val SQLiteWriteLock = SpringContext.getBean("getSQLiteWriteLock", ReentrantLock::class.java) val SQLiteWriteLock = SpringContext.getBean<ReentrantLock>("SQLiteWriteLock")
val AccessLogRepository = SpringContext.getBean<AccessLogRepository>() val AccessLogRepository = SpringContext.getBean<AccessLogRepository>()
try { try {
SQLiteWriteLock.lock() SQLiteWriteLock.lock()
@@ -0,0 +1,3 @@
package xyz.thewhitedog9487.WebAPI.Exception
class DiscordBotTokenDoesNotConfiguredException: Exception("服务端尚未配置Discord令牌,无法使用Discord相关功能!")
@@ -20,27 +20,27 @@ class GlobalSharedBean {
val Logger = KotlinLogging.logger{} val Logger = KotlinLogging.logger{}
@Bean @Bean
fun DiscordClient(@Value("\${Discord_Bot_Token:}") DiscordBotToken: String): Kord { fun DiscordClient(@Value("\${Discord_Bot_Token:}") DiscordBotToken: String): Kord? {
if (DiscordBotToken.isEmpty()) { if (DiscordBotToken.isEmpty()) {
Logger.error { "未通过参数--Discord_Bot_Token或环境变量Discord_Bot_Token提供Discord登录令牌,请检查配置" } Logger.warn { "未通过参数--Discord_Bot_Token或环境变量Discord_Bot_Token提供Discord登录令牌,将无法使用Discord相关功能" }
throw IllegalArgumentException("未提供Discord登录令牌") } return null }
return runBlocking(VirtualThreadCoroutineDispatcher) { Kord(DiscordBotToken) } } return runBlocking(VirtualThreadCoroutineDispatcher) { Kord(DiscordBotToken) } }
@Bean @Bean
fun KordLifecycleManager(KordInstance: Kord) = object: SmartLifecycle { fun KordLifecycleManager(KordInstance: Kord?) = object: SmartLifecycle {
var Job: Job? = null var Job: Job? = null
override fun start() { override fun start() {
Job = CoroutineScope(VirtualThreadCoroutineDispatcher).launch { Job = CoroutineScope(VirtualThreadCoroutineDispatcher).launch {
KordInstance.login { } } } KordInstance?.login { } } }
override fun stop() { override fun stop() {
runBlocking(VirtualThreadCoroutineDispatcher) { runBlocking(VirtualThreadCoroutineDispatcher) {
try { try {
KordInstance.logout() KordInstance?.logout()
} catch (_: IllegalStateException) { } } catch (_: IllegalStateException) { }
Job?.cancelAndJoin() } } Job?.cancelAndJoin() } }
override fun isRunning() = Job?.isActive ?: false } override fun isRunning() = Job?.isActive ?: false }
@get:Bean @get:Bean(name = ["SQLiteWriteLock"])
val SQLiteWriteLock: Lock = ReentrantLock() } val SQLiteWriteLock: Lock = ReentrantLock() }
@@ -60,13 +60,16 @@ fun ProcessArguments(CommandLineArguments: Array<String>) {
在Native Image / Jar文件旁边生成指向自身的,名为Current的软连接 在Native Image / Jar文件旁边生成指向自身的,名为Current的软连接
然后在软连接旁生成systemd服务文件,执行目标指向软连接 然后在软连接旁生成systemd服务文件,执行目标指向软连接
*/ */
val DisableDiscord = "--Disable-Discord" in CommandLineArguments
val DiscordBotToken = if ("--Discord_Bot_Token" in CommandLineArguments && val DiscordBotToken = if ("--Discord_Bot_Token" in CommandLineArguments &&
CommandLineArguments.getOrNull(CommandLineArguments.indexOf("--Discord_Bot_Token") + 1) != null ){ CommandLineArguments.getOrNull(CommandLineArguments.indexOf("--Discord_Bot_Token") + 1) != null ){
CommandLineArguments[CommandLineArguments.indexOf("--Discord_Bot_Token") + 1] } CommandLineArguments[CommandLineArguments.indexOf("--Discord_Bot_Token") + 1] }
else System.getenv().getOrDefault("Discord_Bot_Token", "") else System.getenv().getOrDefault("Discord_Bot_Token", "")
if (DiscordBotToken.isEmpty()) { if (DiscordBotToken.isEmpty() && DisableDiscord == false) {
Logger.error { "必须通过--Discord_Bot_Token参数或Discord_Bot_Token环境变量提供Discord机器人的令牌以使本程序正常工作。" } Logger.error { "必须通过--Discord_Bot_Token参数或Discord_Bot_Token环境变量提供Discord机器人的令牌以使本程序正常工作。" }
throw IllegalArgumentException("缺少必须的--Discord_Bot_Token参数") } throw IllegalArgumentException("缺少必须的--Discord_Bot_Token参数") }
if (DisableDiscord) {
Logger.warn { "已通过--Disable-Discord参数禁用Discord功能,程序无法使用Discord相关功能。" } }
Logger.info { "开始安装systemd服务" } Logger.info { "开始安装systemd服务" }
Logger.debug { "SystemProperties.os.name: $CurrentOS" } Logger.debug { "SystemProperties.os.name: $CurrentOS" }
if (CurrentOS.startsWith("Linux")) { if (CurrentOS.startsWith("Linux")) {
@@ -100,7 +103,7 @@ fun ProcessArguments(CommandLineArguments: Array<String>) {
ExecStart=$ExecCommand ExecStart=$ExecCommand
Restart=always Restart=always
Type=simple Type=simple
Environment="Discord_Bot_Token=$DiscordBotToken" ${if (DisableDiscord == false) "Environment=\"Discord_Bot_Token=$DiscordBotToken\"" else ""}
[Install] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target