.orderBy(block: () -> Unit) {
+ orderBy()
+ block()
+}
+
+/**
+ * Execute as a delete query returning the number of rows deleted using the given transaction.
+ *
+ * Note that if the query includes joins then the generated delete statement may not be
+ * optimal depending on the database platform.
+ *
+ *
+ * @return the number of beans/rows that were deleted.
+ */
+fun TQRootBean.delete(tran: Transaction): Int {
+ return query().delete(tran)
+}
+
+inline fun ModelCompanion.transaction(block: (it: Transaction) -> R): R {
+ val transaction = database.createTransaction()
+ var err: Throwable? = null
+ try {
+ return block(transaction)
+ } catch (e: Throwable) {
+ err = e
+ throw e
+ } finally {
+ if (err == null) transaction.commit()
+ else transaction.rollback()
+ }
+}
+
+fun ObjectMapper.toModel(obj: Bundle, clazz: Class, markProperty: Boolean = false): T {
+ val model = readValue(writeValueAsString(obj), clazz)
+ if (markProperty) model.mark(obj)
+ return model
+}
+
+/**
+ * 将一个Bundle对象转为Model
+ */
+inline fun ObjectMapper.toModel(obj: Bundle, markProperty: Boolean = false): T {
+ return toModel(obj, T::class.java, markProperty)
+}
+
+private val modelPropCache = mutableMapOf, Set>()
+val Model.props: Set
+ get() {
+ val clazz = this::class.java
+ if (clazz in modelPropCache) return modelPropCache[clazz]!!
+ val list = (this as EntityBean)._ebean_getPropertyNames()
+ val set = Collections.unmodifiableSet(LinkedHashSet(list.toList()))
+ modelPropCache[clazz] = set
+
+ return set
+ }
+
+/**
+ * Mark the property as set or 'is loaded'.
+ *
+ * This would be used to specify a property that we did wish to include in a stateless update.
+ *
+ *
+ * ```kotlin
+ *
+ * // populate an entity bean from JSON or whatever
+ * val user: User = ...;
+ *
+ * // mark the email property as 'set' so that it is
+ * // included in a 'state update'
+ * user.markPropertySet("email");
+ *
+ * user.update();
+ * ```
+ * @param propertyName the name of the property on the bean to be marked as 'set'
+ */
+fun Model.markPropertySet(propertyName: String) {
+ (this as EntityBean)._ebean_getIntercept().setPropertyLoaded(propertyName, true)
+}
+
+/**
+ * 更新时排除一些属性
+ */
+fun Model.unmark(vararg prop: String) {
+ val prop0 = listOf(*prop, "createdAt", "updatedAt") intersect props
+ prop0.forEach(this::markPropertyUnset)
+}
+
+/**
+ * 更新时排除一些属性
+ */
+fun Model.unmark(vararg prop: KProperty<*>) {
+ val prop1 = prop.map { it.name }
+ val prop2 = (prop1 + listOf("createdAt", "updatedAt")) intersect props
+
+ prop2.forEach(this::markPropertyUnset)
+}
+
+/**
+ * 仅标记Bundle包含的属性,将不包含的属性排除
+ */
+fun Model.mark(bundle: Bundle) {
+ val props = props - ((bundle.keys - listOf("createdAt", "updatedAt")) intersect props)
+ unmark(*props.toTypedArray())
+}
\ No newline at end of file
diff --git a/src/main/kotlin/top/lolosia/web/util/json/package.kt b/src/main/kotlin/top/lolosia/web/util/json/package.kt
new file mode 100644
index 0000000..51d1177
--- /dev/null
+++ b/src/main/kotlin/top/lolosia/web/util/json/package.kt
@@ -0,0 +1,7 @@
+package top.lolosia.web.util.json
+
+import com.fasterxml.jackson.databind.node.ObjectNode
+
+operator fun ObjectNode.plusAssign(other: ObjectNode) {
+ this.setAll(other)
+}
\ No newline at end of file
diff --git a/src/main/kotlin/top/lolosia/web/util/kotlin/PromiseContinuation.kt b/src/main/kotlin/top/lolosia/web/util/kotlin/PromiseContinuation.kt
new file mode 100644
index 0000000..e432011
--- /dev/null
+++ b/src/main/kotlin/top/lolosia/web/util/kotlin/PromiseContinuation.kt
@@ -0,0 +1,37 @@
+package top.lolosia.web.util.kotlin
+
+import kotlinx.coroutines.Dispatchers
+import kotlin.coroutines.Continuation
+import kotlin.coroutines.CoroutineContext
+import kotlin.coroutines.suspendCoroutine
+
+class PromiseContinuation(override val context: CoroutineContext = Dispatchers.Default) : Continuation {
+ private val listener = mutableListOf>()
+ private var result: Result? = null
+ var isResumed = false
+ private set
+
+ override fun resumeWith(result: Result) {
+ if (isResumed) throw IllegalStateException("PromiseContinuation.resumeWith is called twice")
+ synchronized(listener) {
+ this.isResumed = true
+ this.result = result
+ listener.forEach {
+ it.resumeWith(result)
+ }
+ listener.clear()
+ }
+ }
+
+ suspend fun await(): T {
+ return suspendCoroutine {
+ synchronized(listener) {
+ if (isResumed) {
+ it.resumeWith(result!!)
+ } else {
+ listener += it
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/top/lolosia/web/util/kotlin/delegate.kt b/src/main/kotlin/top/lolosia/web/util/kotlin/delegate.kt
new file mode 100644
index 0000000..6ae98f8
--- /dev/null
+++ b/src/main/kotlin/top/lolosia/web/util/kotlin/delegate.kt
@@ -0,0 +1,152 @@
+package top.lolosia.web.util.kotlin
+
+import kotlin.reflect.KProperty
+
+open class Delegate(
+ protected var defaultValue: R? = null
+) {
+ companion object {
+ fun setter(
+ block: (target: T, property: KProperty<*>, value: R, setter: (R) -> Unit) -> Unit
+ ): IDelegate = LambdaSetterDelegate(block)
+
+
+ fun getter(
+ block: (target: T, property: KProperty<*>, getter: () -> R) -> R
+ ): IDelegate = LambdaGetterDelegate(block)
+ }
+
+ protected val chain = mutableListOf>()
+ protected val default = DefaultDelegate()
+
+ @Suppress("UNCHECKED_CAST")
+ operator fun getValue(target: T, property: KProperty<*>): R {
+ fun next(i: Int): R {
+ val delegate = chain.getOrNull(i) ?: default
+ delegate as IDelegate
+ return delegate.getValue(target, property) {
+ next(i + 1)
+ }
+ }
+ return next(0)
+ }
+
+ @Suppress("UNCHECKED_CAST")
+ operator fun setValue(target: T, property: KProperty<*>, value: R) {
+ fun next(i: Int, arg: R) {
+ val delegate = chain.getOrNull(i) ?: default
+ delegate as IDelegate
+ return delegate.setValue(target, property, arg) {
+ next(i + 1, it)
+ }
+ }
+ next(0, value)
+ }
+
+ /**
+ * 创建一个新的代理对象,并将两者拼合。
+ * @return 新的代理对象
+ */
+ operator fun plus(other: Delegate): Delegate {
+ val out = Delegate(other.defaultValue)
+ out.chain += chain
+ out.chain += other.chain
+ return out
+ }
+
+ /**
+ * 将另一个的所有代理元素添加到当前代理中。
+ */
+ operator fun plusAssign(other: Delegate) {
+ chain += other.chain
+ defaultValue = other.defaultValue
+ }
+
+ /**
+ * 将一个代理元素添加到当前代理中。
+ * @return 当前代理对象
+ */
+ operator fun plus(other: IDelegate): Delegate {
+ chain += other
+ return this
+ }
+
+ /**
+ * 将一个代理元素添加到当前代理中。
+ */
+ operator fun plusAssign(other: IDelegate) {
+ plus(other)
+ }
+
+ /**
+ * 设定委托默认值
+ */
+ infix fun default(value: R): Delegate {
+ defaultValue = value
+ return this
+ }
+
+ @Suppress("UNCHECKED_CAST")
+ protected inner class DefaultDelegate : IDelegate {
+ override fun getValue(target: T, property: KProperty<*>, getter: () -> R): R {
+ return defaultValue as R
+ }
+
+ override fun setValue(target: T, property: KProperty<*>, value: R, setter: (R) -> Unit) {
+ defaultValue = value
+ }
+ }
+}
+
+interface IDelegate {
+
+ operator fun getValue(target: T, property: KProperty<*>): R {
+ return getValue(target, property) {
+ throw IllegalArgumentException("未设置委托默认值")
+ }
+ }
+
+ fun getValue(target: T, property: KProperty<*>, getter: () -> R): R
+
+ operator fun setValue(target: T, property: KProperty<*>, value: R) {
+ setValue(target, property, value) {
+ throw IllegalArgumentException("未设置委托默认值")
+ }
+ }
+
+ fun setValue(target: T, property: KProperty<*>, value: R, setter: (R) -> Unit)
+
+ operator fun plus(other: IDelegate): Delegate {
+ return Delegate() + this + other
+ }
+
+ infix fun default(value: R): Delegate {
+ return Delegate(value) + this
+ }
+}
+
+private class LambdaSetterDelegate(
+ val block: (target: T, property: KProperty<*>, value: R, setter: (R) -> Unit) -> Unit
+) : IDelegate {
+
+ override fun getValue(target: T, property: KProperty<*>, getter: () -> R): R {
+ return getter()
+ }
+
+ override fun setValue(target: T, property: KProperty<*>, value: R, setter: (R) -> Unit) {
+ block(target, property, value, setter)
+ }
+}
+
+private class LambdaGetterDelegate(
+ val block: (target: T, property: KProperty<*>, getter: () -> R) -> R
+) : IDelegate {
+
+ override fun getValue(target: T, property: KProperty<*>, getter: () -> R): R {
+ return block(target, property, getter)
+ }
+
+ override fun setValue(target: T, property: KProperty<*>, value: R, setter: (R) -> Unit) {
+ setter(value)
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/top/lolosia/web/util/kotlin/package.kt b/src/main/kotlin/top/lolosia/web/util/kotlin/package.kt
new file mode 100644
index 0000000..05fa9de
--- /dev/null
+++ b/src/main/kotlin/top/lolosia/web/util/kotlin/package.kt
@@ -0,0 +1,62 @@
+package top.lolosia.web.util.kotlin
+
+import kotlinx.coroutines.Dispatchers
+import org.apache.commons.collections4.BidiMap
+import org.apache.commons.collections4.bidimap.DualLinkedHashBidiMap
+import org.slf4j.LoggerFactory
+import java.time.Instant
+import java.util.*
+import kotlin.coroutines.Continuation
+import kotlin.coroutines.CoroutineContext
+
+private val logger by lazy {
+ LoggerFactory.getLogger("top.lolosia.web.util.kotlin.PackageKt")
+}
+
+val pass = Unit
+
+fun Instant.toDate(): Date {
+ return Date(toEpochMilli())
+}
+
+/**
+ * 保留n位小数
+ * @param fractionDigits n
+ */
+fun Double.fixed(fractionDigits: Int): String {
+ return String.format("%.${fractionDigits}f", this)
+}
+
+/**
+ * 保留n位小数
+ * @param fractionDigits n
+ */
+fun Float.fixed(fractionDigits: Int): String {
+ return String.format("%.${fractionDigits}f", this)
+}
+
+
+fun createContinuation(): Continuation = createContinuation { _, e ->
+ if (e != null) {
+ logger.error("An exception occurs in Continuation", e)
+ }
+}
+
+fun createContinuation(block: (result: T?, error: Throwable?) -> Unit?): Continuation {
+ return object : Continuation {
+ override val context: CoroutineContext
+ get() = Dispatchers.Default
+
+ override fun resumeWith(result: Result) {
+ result.fold({
+ block(it, null)
+ }) {
+ block(null, it)
+ }
+ }
+ }
+}
+
+fun Map.toBidiMap(): BidiMap {
+ return DualLinkedHashBidiMap(this)
+}
\ No newline at end of file
diff --git a/src/main/kotlin/top/lolosia/web/util/package.kt b/src/main/kotlin/top/lolosia/web/util/package.kt
new file mode 100644
index 0000000..5b1c383
--- /dev/null
+++ b/src/main/kotlin/top/lolosia/web/util/package.kt
@@ -0,0 +1,17 @@
+package top.lolosia.web.util
+
+import com.fasterxml.jackson.databind.json.JsonMapper
+import org.springframework.http.HttpStatusCode
+import org.springframework.http.ProblemDetail
+import org.springframework.web.ErrorResponseException
+
+private val mapper = JsonMapper()
+
+fun success(msg: String = "success"): Any = mapper.writeValueAsString(msg)
+
+/**
+ * Constructor with a given message
+ */
+fun ErrorResponseException(status: HttpStatusCode, msg: String, e: Throwable? = null): ErrorResponseException {
+ return ErrorResponseException(status, ProblemDetail.forStatusAndDetail(status, msg), e)
+}
\ No newline at end of file
diff --git a/src/main/kotlin/top/lolosia/web/util/reactor/package.kt b/src/main/kotlin/top/lolosia/web/util/reactor/package.kt
new file mode 100644
index 0000000..24c65fe
--- /dev/null
+++ b/src/main/kotlin/top/lolosia/web/util/reactor/package.kt
@@ -0,0 +1,20 @@
+package top.lolosia.web.util.reactor
+
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import reactor.core.publisher.Flux
+import reactor.core.publisher.Mono
+
+
+fun Mono<*>.start(onError: ((Throwable) -> Unit) = { }) {
+ CoroutineScope(Dispatchers.IO).launch {
+ this@start.subscribe(null, onError)
+ }
+}
+
+fun Flux<*>.start(onError: ((Throwable) -> Unit) = { }) {
+ CoroutineScope(Dispatchers.IO).launch {
+ this@start.subscribe(null, onError)
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/top/lolosia/web/util/session/Context.kt b/src/main/kotlin/top/lolosia/web/util/session/Context.kt
new file mode 100644
index 0000000..c63f180
--- /dev/null
+++ b/src/main/kotlin/top/lolosia/web/util/session/Context.kt
@@ -0,0 +1,58 @@
+package top.lolosia.web.util.session
+
+import top.lolosia.web.util.ErrorResponseException
+import top.lolosia.web.util.bundle.Bundle
+import top.lolosia.web.util.bundle.invoke
+import top.lolosia.web.util.ebean.toUuid
+import org.springframework.context.ApplicationContext
+import org.springframework.http.HttpStatus
+import java.util.*
+import kotlin.reflect.KProperty
+
+abstract class Context {
+ abstract val applicationContext: ApplicationContext
+ abstract val session: Bundle
+ protected val proxy = proxy()
+ protected fun proxy(prop: String? = null) = SessionProxy(prop)
+
+ //
+ // 变量表
+ //
+
+ open val sessionId: UUID get() = session["sessionId"]!!.toString().toUuid()
+ val userId: String
+ get() = session("id") ?: throw ErrorResponseException(
+ HttpStatus.UNAUTHORIZED,
+ "身份认证失败,请重新登录"
+ )
+
+ val user = UserInfo(this)
+
+ /** 决策竞赛实例ID */
+ var decisionGameId: String? by proxy
+ var decisionTeamId: String? by proxy
+
+ override fun equals(other: Any?): Boolean {
+ return if (other is Context) other.sessionId == sessionId
+ else super.equals(other)
+ }
+
+ override fun hashCode(): Int {
+ return sessionId.hashCode()
+ }
+
+ //
+ // Session代理对象
+ //
+
+ protected class SessionProxy(private val propName: String? = null) {
+ @Suppress("UNCHECKED_CAST")
+ operator fun getValue(target: Context, prop: KProperty<*>): T {
+ return target.session[propName ?: prop.name] as T
+ }
+
+ operator fun setValue(target: Context, prop: KProperty<*>, value: T) {
+ target.session[propName ?: prop.name] = value
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/top/lolosia/web/util/session/SessionBasedContext.kt b/src/main/kotlin/top/lolosia/web/util/session/SessionBasedContext.kt
new file mode 100644
index 0000000..8cadff2
--- /dev/null
+++ b/src/main/kotlin/top/lolosia/web/util/session/SessionBasedContext.kt
@@ -0,0 +1,19 @@
+package top.lolosia.web.util.session
+
+import top.lolosia.web.manager.SessionManager
+import top.lolosia.web.util.bundle.Bundle
+import org.springframework.context.ApplicationContext
+import java.util.*
+
+open class SessionBasedContext(
+ override val applicationContext: ApplicationContext,
+ override val sessionId: UUID
+) : Context() {
+ private val sessionManager by lazy { applicationContext.getBean(SessionManager::class.java) }
+ private val mSession by lazy { sessionManager[this.sessionId] }
+ override val session: Bundle
+ get() {
+ mSession["session:lastAccess"] = Date().time
+ return mSession
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/top/lolosia/web/util/session/UserInfo.kt b/src/main/kotlin/top/lolosia/web/util/session/UserInfo.kt
new file mode 100644
index 0000000..dceed8c
--- /dev/null
+++ b/src/main/kotlin/top/lolosia/web/util/session/UserInfo.kt
@@ -0,0 +1,30 @@
+package top.lolosia.web.util.session
+
+import top.lolosia.web.model.system.query.QSysRoleEntity
+import top.lolosia.web.model.system.query.QSysUserEntity
+import top.lolosia.web.model.system.query.QSysUserRolesEntity
+import top.lolosia.web.util.ebean.toUuid
+import java.util.*
+
+class UserInfo(val context: Context) {
+ val id: UUID by lazy { context.userId.toUuid() }
+ private val userEntity by lazy {
+ QSysUserEntity().id.eq(id).findOne() ?: throw NoSuchElementException("未找到用户 $id")
+ }
+
+ private val roleEntity by lazy {
+ val ae = QSysUserRolesEntity().userId.eq(id).findOne() ?: throw NoSuchElementException("未找到用户角色 $id")
+ return@lazy QSysRoleEntity().id.eq(ae.roleId).findOne() ?: throw NoSuchElementException("找不到角色 ${ae.id}")
+ }
+
+ val userName get() = userEntity.userName
+ val realName get() = userEntity.realName
+ val phone get() = userEntity.phone
+ val isUse get() = userEntity.isUse
+ val roleId get() = roleEntity.id
+ val roleName get() = roleEntity.roleName
+ val roleType get() = roleEntity.type
+ val isAdmin get() = "admin" in roleType
+ val isTeacher get() = roleType == "teacher"
+ val isStudent get() = roleType == "student"
+}
\ No newline at end of file
diff --git a/src/main/kotlin/top/lolosia/web/util/session/WebExchangeContext.kt b/src/main/kotlin/top/lolosia/web/util/session/WebExchangeContext.kt
new file mode 100644
index 0000000..f025508
--- /dev/null
+++ b/src/main/kotlin/top/lolosia/web/util/session/WebExchangeContext.kt
@@ -0,0 +1,26 @@
+package top.lolosia.web.util.session
+
+import top.lolosia.web.manager.SessionManager
+import top.lolosia.web.util.bundle.Bundle
+import top.lolosia.web.util.ebean.toUuid
+import org.springframework.web.server.ServerWebExchange
+import java.util.*
+
+class WebExchangeContext(val exchange: ServerWebExchange) :
+ SessionBasedContext(exchange.applicationContext!!, UUID.randomUUID()) {
+ private val sessionManager by lazy { applicationContext.getBean(SessionManager::class.java) }
+
+ private val mSession: Bundle by lazy {
+ sessionManager.mySession(exchange) ?: throw IllegalStateException("请重新登录")
+ }
+
+ override val sessionId: UUID by lazy { session["sessionId"]!!.toString().toUuid() }
+
+ val isWebSocket by lazy { exchange.request.headers.upgrade == "websocket" }
+
+ override val session: Bundle
+ get() {
+ mSession["session:lastAccess"] = Date().time
+ return mSession
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/top/lolosia/web/util/spring/ContextArgumentResolver.kt b/src/main/kotlin/top/lolosia/web/util/spring/ContextArgumentResolver.kt
new file mode 100644
index 0000000..17e074a
--- /dev/null
+++ b/src/main/kotlin/top/lolosia/web/util/spring/ContextArgumentResolver.kt
@@ -0,0 +1,48 @@
+package top.lolosia.web.util.spring
+
+import top.lolosia.web.manager.EventSourceManager
+import top.lolosia.web.util.ebean.toUuid
+import top.lolosia.web.util.session.Context
+import top.lolosia.web.util.session.WebExchangeContext
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.core.MethodParameter
+import org.springframework.stereotype.Component
+import org.springframework.web.reactive.BindingContext
+import org.springframework.web.reactive.result.method.HandlerMethodArgumentResolver
+import org.springframework.web.server.ServerWebExchange
+import reactor.core.publisher.Mono
+import kotlin.reflect.KClass
+
+@Component
+class ContextArgumentResolver : HandlerMethodArgumentResolver {
+
+ @Autowired
+ lateinit var eventSourceManager: EventSourceManager
+
+ override fun supportsParameter(parameter: MethodParameter): Boolean {
+ if (Context::class assi parameter) return true
+ if (EventSource::class assi parameter) return true
+ return false
+ }
+
+ override fun resolveArgument(
+ parameter: MethodParameter,
+ bindingContext: BindingContext,
+ exchange: ServerWebExchange
+ ): Mono {
+ return when {
+ Context::class assi parameter -> Mono.just(WebExchangeContext(exchange))
+ EventSource::class assi parameter -> {
+ val id = exchange.request.headers.getFirst("event-source-id")!!.toUuid()
+ val eventSource = eventSourceManager.getEventSource(id)!!
+ Mono.just(eventSource)
+ }
+
+ else -> throw IllegalStateException("不支持")
+ }
+ }
+
+ private infix fun KClass<*>.assi(other: MethodParameter): Boolean {
+ return this.java.isAssignableFrom(other.parameterType)
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/top/lolosia/web/util/spring/EventSource.kt b/src/main/kotlin/top/lolosia/web/util/spring/EventSource.kt
new file mode 100644
index 0000000..6ba46de
--- /dev/null
+++ b/src/main/kotlin/top/lolosia/web/util/spring/EventSource.kt
@@ -0,0 +1,8 @@
+package top.lolosia.web.util.spring
+
+import java.io.Closeable
+
+interface EventSource : Closeable {
+ val isClosed: Boolean
+ fun send(event: String = "message", data: Any? = null)
+}
\ No newline at end of file
diff --git a/src/main/kotlin/top/lolosia/web/util/timer/DelayUpdater.kt b/src/main/kotlin/top/lolosia/web/util/timer/DelayUpdater.kt
new file mode 100644
index 0000000..4990fc7
--- /dev/null
+++ b/src/main/kotlin/top/lolosia/web/util/timer/DelayUpdater.kt
@@ -0,0 +1,36 @@
+package top.lolosia.web.util.timer
+
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import java.util.concurrent.atomic.AtomicInteger
+import kotlin.coroutines.CoroutineContext
+import kotlin.time.Duration
+
+/**
+ * 延时执行工具。
+ *
+ * 当在指定时间内连续执行操作时,除最后一次操作外皆被取消。
+ * 当最后一次操作的指定时间段内没有新的操作,则执行这一次操作。
+ */
+class DelayUpdater(
+ private val delay: Duration,
+ context: CoroutineContext = Dispatchers.Default
+) : CoroutineScope {
+ override val coroutineContext = context
+
+ private val lastAccess = AtomicInteger(0)
+ operator fun invoke(block: suspend () -> Unit) {
+ val acc = lastAccess.incrementAndGet()
+ launch {
+ delay(delay)
+ if (acc == lastAccess.get()) block()
+ }
+ }
+
+ fun now(block: () -> Unit) {
+ lastAccess.incrementAndGet()
+ block()
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/top/lolosia/web/util/timer/MaxTimeUpdater.kt b/src/main/kotlin/top/lolosia/web/util/timer/MaxTimeUpdater.kt
new file mode 100644
index 0000000..33d0de8
--- /dev/null
+++ b/src/main/kotlin/top/lolosia/web/util/timer/MaxTimeUpdater.kt
@@ -0,0 +1,48 @@
+package top.lolosia.web.util.timer
+
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import java.util.concurrent.atomic.AtomicInteger
+import kotlin.coroutines.CoroutineContext
+import kotlin.time.Duration
+
+/**
+ * 最大限时延迟执行工具。
+ *
+ * 当在指定时间内连续执行操作时,除最后一次操作外皆被取消。
+ * 当最后一次操作的指定时间段内没有新的操作,则执行这一次操作。
+ * 当指定操作是这一时间段内的最后一次操作时,该操作不会被取消。
+ */
+class MaxTimeUpdater(
+ private val delay: Duration,
+ context: CoroutineContext = Dispatchers.Default
+) : CoroutineScope {
+ override val coroutineContext = context
+
+ private val lastAccess = AtomicInteger(0)
+ private var lastAccessTime: Long = System.currentTimeMillis()
+ operator fun invoke(block: suspend () -> Unit) {
+ val acc = lastAccess.incrementAndGet()
+ launch {
+ delay(delay)
+ val now = System.currentTimeMillis()
+ if (now - lastAccessTime > delay.inWholeMilliseconds) {
+ lastAccessTime = now
+ block()
+ return@launch
+ }
+ if (acc == lastAccess.get()) {
+ block()
+ return@launch
+ }
+ }
+ }
+
+ fun now(block: () -> Unit) {
+ lastAccess.incrementAndGet()
+ lastAccessTime = System.currentTimeMillis()
+ block()
+ }
+}
\ No newline at end of file
diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml
new file mode 100644
index 0000000..bcdf3c9
--- /dev/null
+++ b/src/main/resources/application.yaml
@@ -0,0 +1,42 @@
+datasource:
+ db:
+ username: "root"
+ password: "123456"
+ driver: "org.sqlite.JDBC"
+ url: "jdbc:sqlite:work/database/db.sqlite"
+ decision:
+ username: "root"
+ password: "123456"
+ driver: "org.sqlite.JDBC"
+ url: "jdbc:sqlite:work/database/decision.sqlite"
+ session:
+ username: "root"
+ password: "123456"
+ driver: "org.sqlite.JDBC"
+ url: "jdbc:sqlite:work/database/session.sqlite"
+ system:
+ username: "root"
+ password: "123456"
+ driver: "org.sqlite.JDBC"
+ url: "jdbc:sqlite:work/database/system.sqlite"
+
+server:
+ port: 8002
+
+spring:
+ devtools:
+ restart:
+ enabled: true
+ jackson:
+ time-zone: GMT+8
+ application:
+ name: "lolosia-backend"
+
+logging:
+ charset:
+ console: GB18030
+ pattern:
+ console: "%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:%-5p}) %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:%wEx}"
+ level:
+ root: INFO
+ config: "classpath:logback-spring.xml"
\ No newline at end of file
diff --git a/src/test/kotlin/top/lolosia/web/LolosiaApplicationTests.kt b/src/test/kotlin/top/lolosia/web/LolosiaApplicationTests.kt
new file mode 100644
index 0000000..238a6cc
--- /dev/null
+++ b/src/test/kotlin/top/lolosia/web/LolosiaApplicationTests.kt
@@ -0,0 +1,13 @@
+package top.lolosia.web
+
+import org.junit.jupiter.api.Test
+import org.springframework.boot.test.context.SpringBootTest
+
+@SpringBootTest
+class LolosiaApplicationTests {
+
+ @Test
+ fun contextLoads() {
+ }
+
+}
diff --git a/static/build.gradle.kts b/static/build.gradle.kts
new file mode 100644
index 0000000..f3cc931
--- /dev/null
+++ b/static/build.gradle.kts
@@ -0,0 +1,100 @@
+import com.fasterxml.jackson.databind.json.JsonMapper
+import kotlinx.coroutines.*
+import kotlin.coroutines.resume
+import kotlin.coroutines.resumeWithException
+import kotlin.coroutines.suspendCoroutine
+import kotlin.io.path.Path
+import kotlin.io.path.absolute
+import kotlin.io.path.exists
+import kotlin.io.path.readText
+import kotlin.io.path.writeText
+
+buildscript {
+ dependencies {
+ classpath("com.fasterxml.jackson.core:jackson-databind:2.14.2")
+ }
+}
+
+plugins {
+ java
+}
+
+tasks.jar {
+ dependsOn("buildResources")
+ archiveFileName = "lolosia-static-web-${rootProject.version}.jar"
+
+ from(buildDir.resolve("tmp/webJar/resources/"))
+ manifest {
+ attributes["Implementation-Version"] = rootProject.version
+ attributes["Implementation-Title"] = rootProject.name
+ }
+}
+
+task("buildResources") {
+ group = "build"
+
+ val platformsPath = Path("${rootDir}/platforms.json")
+ if (!platformsPath.exists()) {
+ platformsPath.writeText(
+ """
+ |{
+ | "lolosiaWebPath": null
+ |}
+ """.trimMargin("|")
+ )
+ }
+
+ doLast {
+ val buildDir = project.buildDir
+ delete(buildDir.resolve("tmp/webJar"))
+
+ @Suppress("UNCHECKED_CAST")
+ val platforms = JsonMapper().readValue(
+ platformsPath.readText(),
+ LinkedHashMap::class.java
+ ) as Map
+
+ // 构建多个平台文件
+ runBlocking {
+ val jobs = mutableListOf()
+
+ // 智能决策平台
+ platforms["lolosiaWebPath"]?.let {
+ jobs += launch {
+ buildPlatform("home", it)
+ }
+ }
+ jobs.joinAll()
+ }
+ }
+}
+
+suspend fun buildPlatform(name: String, dirPath: String) {
+ val path = Path(dirPath).absolute()
+ val vite = path.resolve("node_modules/.bin/vite.CMD")
+ suspendCoroutine {
+ val process = ProcessBuilder(vite.toString(), "build", "--mode", "build").apply {
+ directory(path.toFile())
+ val env = environment()
+ env["VITE_APP_BASE_MODE"] = "local"
+ }.start()
+
+ CoroutineScope(Dispatchers.IO).launch {
+ process.inputStream.transferTo(System.out)
+ }
+ CoroutineScope(Dispatchers.IO).launch {
+ process.errorStream.transferTo(System.err)
+ }
+
+ process.onExit().thenAccept { p1 ->
+ if (p1.exitValue() != 0) {
+ it.resumeWithException(RuntimeException("Vite的退出值不为0"))
+ } else it.resume(0)
+ }
+ }
+
+ copy {
+ into(buildDir.resolve("tmp/webJar/resources/static/$name"))
+ from(path.resolve("dist").toString())
+ }
+}
\ No newline at end of file