与前端匹配的后端代码

This commit is contained in:
BBIT-Kai
2025-05-12 13:57:27 +08:00
parent cdaa958e75
commit 965141def2
66 changed files with 4012 additions and 42 deletions
@@ -0,0 +1,65 @@
package ink.snowflake.server
import com.google.gson.Gson
import ink.snowflake.server.plugins.*
import ink.snowflake.server.route.func.User
import ink.snowflake.server.route.func.chat
import ink.snowflake.server.route.configureSockets
import ink.snowflake.server.route.func.ImageAnalytics
import ink.snowflake.server.route.func.RemoteDebug
import ink.snowflake.server.route.func.VideoAnalytics
import ink.snowflake.server.route.func.VideoAnalyticsJetson
import ink.snowflake.server.route.mainFunc
import ink.snowflake.server.utils.AppConfig
import io.ktor.server.application.*
import io.ktor.server.tomcat.jakarta.*
const val VIDEO_INPUT_PATH = "/tmp/"
/**
* 服务器地址
*/
const val SERVER_PATH = "171.212.101.201"
//const val SERVER_PATH = "localhost"
val gson = Gson()
fun main(args: Array<String>): Unit = EngineMain.main(args)
fun Application.module() {
// 使用 appConfig 进行配置
val appConfig = AppConfig(environment.config)
// 序列化
configureSerialization()
// Thymeleaf
configureTemplating()
// 设置-身份验证
configureSecurity(appConfig)
// 路径
configureStaticPath()
// 跨域
configureCORS()
// 设置数据库
configureDatabases(appConfig)
// 状态拦截
configureStatusPages()
// 设置-WebSocket
configureSockets()
// 业务-首页导航
mainFunc()
// 业务-用户信息相关操作
User(appConfig)
// 业务-聊天
chat()
// 业务-远程控制
RemoteDebug()
// 业务-视频分析
VideoAnalytics()
// 业务-视频分析-Jetson本地
VideoAnalyticsJetson()
// 业务-图片分析
ImageAnalytics()
}
@@ -0,0 +1,24 @@
### 200 首页
GET http://127.0.0.1:80
### 200 正常返回
GET http://127.0.0.1:80/tasks
### 404 找不到
GET http://127.0.0.1:80/tasks/byPriority/Vital
### 400 格式错误
GET http://127.0.0.1:80/tasks/byPriority/Mediu
###
GET http://0.0.0.0:8080/tasks/byPriority/Medium
Accept: application/json
###
DELETE http://0.0.0.0:8080/tasks/delByName/gardening
@@ -0,0 +1,21 @@
package ink.snowflake.server.model.ai
import com.google.gson.annotations.SerializedName
data class ChatRequest(
@SerializedName("messages")
val messages: List<Message> = listOf(),
@SerializedName("model")
val model: String = "llama3.2",
@SerializedName("stream")
val stream: Boolean = false
) {
data class Message(
@SerializedName("content")
val content: String = "",
@SerializedName("role")
val role: String = "user"
)
}
@@ -0,0 +1,37 @@
package ink.snowflake.server.model.ai
import com.google.gson.annotations.SerializedName
data class ChatResponse(
@SerializedName("created_at")
val createdAt: String = "",
@SerializedName("done")
val done: Boolean = false,
@SerializedName("done_reason")
val doneReason: String = "",
@SerializedName("eval_count")
val evalCount: Int = 0,
@SerializedName("eval_duration")
val evalDuration: Int = 0,
@SerializedName("load_duration")
val loadDuration: Int = 0,
@SerializedName("message")
val message: Message = Message(),
@SerializedName("model")
val model: String = "",
@SerializedName("prompt_eval_count")
val promptEvalCount: Int = 0,
@SerializedName("prompt_eval_duration")
val promptEvalDuration: Int = 0,
@SerializedName("total_duration")
val totalDuration: Int = 0
) {
data class Message(
@SerializedName("content")
val content: String = "",
@SerializedName("role")
val role: String = ""
)
}
@@ -0,0 +1,44 @@
package ink.snowflake.server.model.database
import kotlinx.serialization.Serializable
import org.jetbrains.exposed.dao.id.IntIdTable
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.json.json
import org.jetbrains.exposed.sql.kotlin.datetime.timestamp
import com.google.gson.reflect.TypeToken
import ink.snowflake.server.gson
// 定义 AI 表
object AIProfilesTable : IntIdTable("ai_profiles") {
val name = varchar("name", 100) // AI 名称
val avatarUrl = varchar("avatar_url", 255).nullable() // AI 头像 URL
val aiPersonality = json("ai_personality", serialize = { gson.toJson(it) }, deserialize = {
gson.fromJson(it, object : TypeToken<Map<String, String>>() {}.type)
}) // 存储 AI 人设,使用 JSON 类型
val memoryEnabled = bool("memory_enabled").default(true) // 是否启用记忆功能
val creationDate = timestamp("creation_date")
val isActive = bool("is_active").default(true) // 是否激活
}
@Serializable
data class AIProfile(
val id: Int,
val name: String,
val avatarUrl: String?,
val aiPersonality: Map<String, String>,
val memoryEnabled: Boolean,
val creationDate: String,
val isActive: Boolean
)
fun ResultRow.toAIProfile(): AIProfile {
return AIProfile(
id = this[AIProfilesTable.id].value,
name = this[AIProfilesTable.name],
avatarUrl = this[AIProfilesTable.avatarUrl],
aiPersonality = this[AIProfilesTable.aiPersonality] as Map<String, String>,
memoryEnabled = this[AIProfilesTable.memoryEnabled],
creationDate = this[AIProfilesTable.creationDate].toString(),
isActive = this[AIProfilesTable.isActive]
)
}
@@ -0,0 +1,49 @@
package ink.snowflake.server.model.database
import kotlinx.datetime.LocalDateTime
import org.jetbrains.exposed.dao.id.IntIdTable
import org.jetbrains.exposed.sql.kotlin.datetime.timestamp
import org.jetbrains.exposed.sql.Column
import org.jetbrains.exposed.sql.ResultRow
object ChatRecordsTable : IntIdTable("chat_records") {
// 用户 ID(外键)
val userId: Column<Int> = integer("user_id").references(UserTable.id)
// 消息内容
val message: Column<String> = text("message")
// 消息类型
val messageType: Column<String> = varchar("message_type", 20)
// 发送时间
val sentAt = timestamp("sent_at")
// 消息来源
val from: Column<String> = varchar("from", 20)
// 情绪分析
val emotion: Column<String?> = varchar("emotion", 50).nullable()
}
data class ChatRecord(
val id: Int,
val userId: Int,
val message: String,
val messageType: String,
val sentAt: String,// eg. "2024-12-10T06:19:04.433006Z"
val from: String,
val emotion: String?
)
fun ResultRow.toChatRecord(): ChatRecord {
return ChatRecord(
id = this[ChatRecordsTable.id].value,
userId = this[ChatRecordsTable.userId],
message = this[ChatRecordsTable.message],
messageType = this[ChatRecordsTable.messageType],
sentAt = this[ChatRecordsTable.sentAt].toString(),
from = this[ChatRecordsTable.from],
emotion = this[ChatRecordsTable.emotion]
)
}
@@ -0,0 +1,18 @@
package ink.snowflake.server.model.database
import kotlinx.serialization.Serializable
@Serializable
data class ImageAnalyticsRequest(
val object_name: String, // Minio存储名
val upload_datetime: String, // 上传时间
val file_name: String, // 文件名
val resolution: String, // 图片分辨率
val size: Float, // 文件大小,单位MB
val cocoon_count: Float, // 识别出的茧数量
val max_confidence: Float, // 最大置信度
val min_confidence: Float, // 最小置信度
val average_confidence: Float, // 平均置信度
val other_info: Map<String, String>, // 额外信息
val processing_time: String // 处理时间
)
@@ -0,0 +1,24 @@
package ink.snowflake.server.model.database
import com.google.gson.reflect.TypeToken
import ink.snowflake.server.gson
import org.jetbrains.exposed.dao.id.EntityID
import org.jetbrains.exposed.dao.id.IntIdTable
import org.jetbrains.exposed.sql.json.json
import org.jetbrains.exposed.sql.kotlin.datetime.datetime
object ImageTable : IntIdTable("image") {
val object_name = varchar("object_name", 255)
val upload_datetime = datetime("upload_datetime")
val file_name = varchar("file_name", 255)
val resolution = varchar("resolution", 255)
val size = float("size")
val cocoon_count = float("cocoon_count")
val max_confidence = float("max_confidence")
val min_confidence = float("min_confidence")
val average_confidence = float("average_confidence")
val other_info = json("other_info", serialize = { gson.toJson(it) }, deserialize = {
gson.fromJson(it, object : TypeToken<Map<String, String>>() {}.type)
})
val processing_time = datetime("processing_time")
}
@@ -0,0 +1,29 @@
package ink.snowflake.server.model.database
import org.jetbrains.exposed.dao.id.IntIdTable
import org.jetbrains.exposed.sql.Column
import org.jetbrains.exposed.sql.kotlin.datetime.timestamp
import java.time.LocalDateTime
object UserTable : IntIdTable("users") {
// 用户名
val username = varchar("username", length = 50).nullable()
// 邮箱
val email = varchar("email", length = 32).nullable()
// 手机号
val phone = varchar("phone", length = 32).nullable()
// 密码哈希
val passwordHash= varchar("password_hash", length = 255)
// 用户是否激活
val isActive= bool("is_active").default(true)
// 创建时间
val createdAt = timestamp("created_at").nullable()
// 更新时间
val updatedAt = timestamp("updated_at").nullable()
}
@@ -0,0 +1,12 @@
package ink.snowflake.server.model.database
import org.jetbrains.exposed.dao.id.IntIdTable
object VideoAnalyticsDetailTable : IntIdTable("video_analytics_detail") {
val vId = integer("v_id").references(VideoTable.id)
val aTimeStamp = integer("a_time_stamp")
val aTotalPeople = integer("a_total_people")
val aTotalPeopleMasked = integer("a_total_people_masked")
val aAction = varchar("a_action", 50)
}
@@ -0,0 +1,25 @@
package ink.snowflake.server.model.database
import org.jetbrains.exposed.dao.id.EntityID
import org.jetbrains.exposed.dao.id.IntIdTable
import org.jetbrains.exposed.sql.kotlin.datetime.datetime
object VideoTable : IntIdTable("video") {
val vName = varchar("v_name", 255)
val vObjectName = varchar("v_object_name", 255)
val vFileName = varchar("v_file_name", 255)
val vStartDateTime = datetime("v_start_datetime").nullable()
val vDuration = float("v_duration")
val vSize = float("v_size")
val vVideoCodec = varchar("v_video_codec", 50)
val vAudioCodec = varchar("v_audio_codec", 50)
val vOverallBitRate = integer("v_overall_bit_rate")
val vResolution = varchar("v_resolution", 20)
val vATime = datetime("v_a_time")
val vATotalPeople = integer("v_a_total_people")
val vACountPeople = integer("v_a_count_people")
val vAMaxStayTime = float("v_a_max_stay_time")
val vAMaxAction = varchar("v_a_max_action", 50)
val vAAverageMaskedRatio = float("v_a_average_masked_ratio")
}
@@ -0,0 +1,8 @@
package ink.snowflake.server.model.request
import com.google.gson.annotations.SerializedName
import kotlinx.serialization.Serializable
@Serializable
data class CommonRequest(@SerializedName("str") val str: String)
@@ -0,0 +1,74 @@
package ink.snowflake.server.model.request
import com.google.gson.annotations.SerializedName
data class DevicesInfo(
@SerializedName("proxies")
val proxies: List<Proxy> = listOf()
) {
data class Proxy(
@SerializedName("clientVersion")
val clientVersion: String? = null,
@SerializedName("conf")
val conf: Conf? = null,
@SerializedName("curConns")
val curConns: Int = 0,
@SerializedName("lastCloseTime")
val lastCloseTime: String = "",
@SerializedName("lastStartTime")
val lastStartTime: String = "",
@SerializedName("name")
val name: String = "",
@SerializedName("status")
val status: String = "",
@SerializedName("todayTrafficIn")
val todayTrafficIn: Long = 0,
@SerializedName("todayTrafficOut")
val todayTrafficOut: Long = 0
) {
data class Conf(
@SerializedName("healthCheck")
val healthCheck: HealthCheck = HealthCheck(),
@SerializedName("loadBalancer")
val loadBalancer: LoadBalancer = LoadBalancer(),
@SerializedName("localIP")
val localIP: String = "",
@SerializedName("name")
val name: String = "",
@SerializedName("plugin")
val plugin: Plugin = Plugin(),
@SerializedName("remotePort")
val remotePort: Int = 0,
@SerializedName("transport")
val transport: Transport = Transport(),
@SerializedName("type")
val type: String = ""
) {
data class HealthCheck(
@SerializedName("intervalSeconds")
val intervalSeconds: Int = 0,
@SerializedName("type")
val type: String = ""
)
data class LoadBalancer(
@SerializedName("group")
val group: String = ""
)
data class Plugin(
@SerializedName("ClientPluginOptions")
val clientPluginOptions: Any? = null,
@SerializedName("type")
val type: String = ""
)
data class Transport(
@SerializedName("bandwidthLimit")
val bandwidthLimit: String = "",
@SerializedName("bandwidthLimitMode")
val bandwidthLimitMode: String = ""
)
}
}
}
@@ -0,0 +1,6 @@
package ink.snowflake.server.model.request
import kotlinx.serialization.Serializable
@Serializable
data class LoginRequest(val account: String, val password: String)
@@ -0,0 +1,7 @@
package ink.snowflake.server.model.request
import ink.snowflake.server.model.response.Token
import kotlinx.serialization.Serializable
@Serializable
data class RefreshTokenRequest(val refreshToken: String)
@@ -0,0 +1,6 @@
package ink.snowflake.server.model.request
import kotlinx.serialization.Serializable
@Serializable
data class RegisterRequest(var account: String, var code: String, var password: String)
@@ -0,0 +1,32 @@
package ink.snowflake.server.model.request
import kotlinx.serialization.Serializable
@Serializable
data class VideoAnalyticsRequest(
val v_name: String, // 项目名
val v_object_name: String, // Minio存储名
val v_start_datetime: String, // 开始时间
val v_file_name: String, // 文件名
val v_duration: Float, // 视频时长
val v_size: Float, // 文件大小,单位MB
val v_video_codec: String, // 视频编码格式
val v_audio_codec: String, // 音频编码格式
val v_overall_bit_rate: Int, // 总比特率
val v_resolution: String, // 视频分辨率
val v_a_time: String, // 分析时间
val v_a_total_people: Int, // 总人数
val v_a_count_people: Int, // 累计出现人数
val v_a_max_stay_time: Float, // 最大停留时间
val v_a_max_action: String, // 最常见的动作
val v_a_average_masked_ratio: Float, // 平均佩戴口罩比率
val v_a_details: List<VideoDetail> // 详细记录
)
@Serializable
data class VideoDetail(
val a_time_stamp: Int, // 时间戳
val a_total_people: Int, // 出现的总人数
val a_total_people_masked: Int, // 佩戴口罩的人数
val a_action: String // 动作类型
)
@@ -0,0 +1,10 @@
package ink.snowflake.server.model.response
import kotlinx.serialization.Serializable
@Serializable
data class AiListForUser(
val aiId :Int,
val aiName :String,
val aiAvatar : String
)
@@ -0,0 +1,11 @@
package ink.snowflake.server.model.response
import kotlinx.serialization.Serializable
@Serializable
open class BaseResponse<T>(
val status: Boolean = true,
val message: String = if(status) "操作成功" else "操作失败",
val data: T? = null,
)
@@ -0,0 +1,9 @@
package ink.snowflake.server.model.response
import kotlinx.serialization.Serializable
@Serializable
data class DeviceItem (
val deviceName:String,
val devicePort:Int,
)
@@ -0,0 +1,8 @@
package ink.snowflake.server.model.response
import kotlinx.serialization.Serializable
@Serializable
data class LoginResponse(val userId: String, val accessToken: Token, val refreshToken: Token)
@@ -0,0 +1,6 @@
package ink.snowflake.server.model.response
import kotlinx.serialization.Serializable
@Serializable
data class RefreshTokenResponse(val accessToken: Token, val refreshToken: Token)
@@ -0,0 +1,6 @@
package ink.snowflake.server.model.response
import kotlinx.serialization.Serializable
@Serializable
data class Token(val token: String, val expiresAt: Long = 0,val expiresStr:String)
@@ -0,0 +1,12 @@
package ink.snowflake.server.model.response
import kotlinx.datetime.LocalDateTime
import kotlinx.serialization.Serializable
@Serializable
data class UserInfo(
val username: String?,
val email: String?,
val phone: String?,
val roles: List<String> = emptyList()
)
@@ -0,0 +1,47 @@
package ink.snowflake.server.model.response
import kotlinx.serialization.Serializable
@Serializable
data class VideoAnalyticsDetail(
val v_name: String,
val v_video_play_path: String,
val v_file_name: String,
val v_duration: Float,
val v_size: Float,
val v_start_datetime: String,
val v_video_codec: String,
val v_audio_codec: String,
val v_overall_bit_rate: Int,
val v_resolution: String,
val v_a_time: String,
val v_a_total_people: Int,
val v_a_count_people: Int,
val v_a_max_stay_time: Float,
val v_a_max_action: String,
val v_a_average_masked_ratio: Float,
val v_a_details: VideoAnalyticsData,
val v_details_list: List<DetailItem>
)
@Serializable
data class VideoAnalyticsData(
val yTotalData: List<Pair<String, Int>>, // 总人数数据 (时间, 总人数)
val yMaskedData: List<Pair<String, Int>>, // 佩戴口罩人数数据 (时间, 佩戴人数)
val areaData: List<List<Area>> // 区域数据,使用 Area 对象
)
@Serializable
data class Area(
val xAxis: String,
val itemStyle: ItemStyle
)
@Serializable
data class DetailItem(
val action: String,
val time: String
)
@Serializable
data class ItemStyle(
val color: String
)
@@ -0,0 +1,10 @@
package ink.snowflake.server.model.response
import kotlinx.serialization.Serializable
@Serializable
data class VideoListResponse(
val v_id: Int,
val v_name: String,
val v_a_time: String?
)
@@ -0,0 +1,12 @@
package com.cyberecho.mdoel.database
data class WSChatRecords(
val id: Long = 0, // 消息的唯一标识符,自增
val aiId: Int, // 交流的对象AI_ID
val messageType: String,// 消息类型(文本、图片、语音等)
val content: String, // 消息内容
val timestamp: Long, // 发送时间(时间戳,单位为毫秒)
val status: Int = 0, // 消息状态 默认为未读0
val msgFrom: String // 消息来源(AI 或 用户)
)
@@ -0,0 +1,25 @@
package ink.snowflake.server.plugins
import ink.snowflake.server.utils.AppConfig
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.plugins.partialcontent.*
import io.ktor.server.plugins.autohead.AutoHeadResponse
import io.ktor.server.plugins.cors.routing.*
fun Application.configureCORS() {
install(CORS) {
anyHost()
allowHost("localhost:8089")
allowHost("127.0.0.1:8089")
allowHost("171.212.101.199:8089")
allowHost("debug.scaitcn.com:8089")
// 进一步配置 CORS
allowMethod(HttpMethod.Get)
allowMethod(HttpMethod.Post)
allowHeader(HttpHeaders.ContentType)
allowHeader(HttpHeaders.Authorization)
}
}
@@ -0,0 +1,13 @@
package ink.snowflake.server.plugins
import ink.snowflake.server.utils.AppConfig
import org.jetbrains.exposed.sql.*
fun configureDatabases(config: AppConfig) {
Database.connect(
url = config.dbUrl,
driver = config.dbDriver,
user =config.dbUser,
password = config.dbPassword,
)
}
@@ -0,0 +1,40 @@
package ink.snowflake.server.plugins
import com.auth0.jwt.JWT
import com.auth0.jwt.algorithms.Algorithm
import ink.snowflake.server.model.response.*
import ink.snowflake.server.utils.AppConfig
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.auth.*
import io.ktor.server.auth.jwt.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import java.text.DateFormat
import java.util.*
fun Application.configureSecurity(config: AppConfig) {
authentication {
jwt {
realm = config.jwtRealm
verifier(
JWT
.require(Algorithm.HMAC256(config.jwtSecret))
.withAudience(config.jwtAudience)
.withIssuer(config.jwtDomain)
.build()
)
validate { credential ->
if (credential.payload.audience.contains(config.jwtAudience)
&& credential.payload.getClaim("token_type").asString() == "access_token"
) {
JWTPrincipal(credential.payload)
} else {
null
}
}
}
}
}
@@ -0,0 +1,20 @@
package ink.snowflake.server.plugins
import io.ktor.http.*
import io.ktor.serialization.*
import io.ktor.serialization.gson.*
import io.ktor.serialization.kotlinx.json.*
import io.ktor.server.application.*
import io.ktor.server.plugins.contentnegotiation.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
fun Application.configureSerialization() {
install(ContentNegotiation) {
json()
gson{
setPrettyPrinting()
}
}
}
@@ -0,0 +1,16 @@
package ink.snowflake.server.plugins
import ink.snowflake.server.utils.AppConfig
import io.ktor.server.application.*
import io.ktor.server.auth.*
import io.ktor.server.http.content.*
import io.ktor.server.plugins.autohead.AutoHeadResponse
import io.ktor.server.plugins.partialcontent.PartialContent
import io.ktor.server.routing.*
import org.jetbrains.exposed.sql.*
import java.io.File
fun Application.configureStaticPath() {
install(PartialContent)
install(AutoHeadResponse)
}
@@ -0,0 +1,87 @@
package ink.snowflake.server.plugins
import ink.snowflake.server.model.response.BaseResponse
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.plugins.statuspages.*
import io.ktor.server.response.*
fun Application.configureStatusPages() {
install(StatusPages) {
// 处理特定的异常
exception<IllegalStateException> { call, cause ->
call.respondText("App in illegal state as ${cause.message}", status = HttpStatusCode.InternalServerError)
}
exception<Throwable> { call, cause ->
// 记录异常信息,方便后续分析
println("Unhandled exception: ${cause.localizedMessage}" + cause)
call.respond(HttpStatusCode.InternalServerError, "Server Error")
}
exception<Exception> { call, cause ->
call.respond(
HttpStatusCode.InternalServerError, BaseResponse<Nothing>(
status = false,
message = cause.localizedMessage
)
)
}
// 处理不同的 HTTP 状态码
status(HttpStatusCode.Unauthorized) { call, status ->
val response = BaseResponse<Nothing>(
status = false,
message = "${status.value} Unauthorized," + status.description
)
call.respond(status, response)
}
status(HttpStatusCode.BadRequest) { call, status ->
val response = BaseResponse<Nothing>(
status = false,
message = "${status.value} Bad Request," + status.description
)
call.respond(status, response)
}
status(HttpStatusCode.Forbidden) { call, status ->
val response = BaseResponse<Nothing>(
status = false,
message = "${status.value} Forbidden," + status.description
)
call.respond(status, response)
}
status(HttpStatusCode.NotFound) { call, status ->
val response = BaseResponse<Nothing>(
status = false,
message = "${status.value} Not Found," + status.description
)
call.respond(status, response)
}
status(HttpStatusCode.MethodNotAllowed) { call, status ->
val response = BaseResponse<Nothing>(
status = false,
message = "${status.value} Method Not Allowed," + status.description
)
call.respond(status, response)
}
status(HttpStatusCode.Conflict) { call, status ->
val response = BaseResponse<Nothing>(
status = false,
message = "${status.value} Conflict," + status.description
)
call.respond(status, response)
}
status(HttpStatusCode.InternalServerError) { call, status ->
val response = BaseResponse<Nothing>(
status = false,
message = "${status.value} Internal Server Error," + status.description
)
call.respond(status, response)
}
status(HttpStatusCode.ServiceUnavailable) { call, status ->
val response = BaseResponse<Nothing>(
status = false,
message = "${status.value} Service Unavailable," + status.description
)
call.respond(status, response)
}
}
}
@@ -0,0 +1,21 @@
package ink.snowflake.server.plugins
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import io.ktor.server.thymeleaf.Thymeleaf
import io.ktor.server.thymeleaf.ThymeleafContent
import kotlinx.css.*
import org.thymeleaf.templateresolver.ClassLoaderTemplateResolver
fun Application.configureTemplating() {
install(Thymeleaf) {
setTemplateResolver(ClassLoaderTemplateResolver().apply {
prefix = "templates/thymeleaf/"
suffix = ".html"
characterEncoding = "utf-8"
})
}
}
@@ -0,0 +1,32 @@
package ink.snowflake.server.repository
import ink.snowflake.server.model.database.ImageAnalyticsRequest
import ink.snowflake.server.model.database.VideoTable
import ink.snowflake.server.model.database.VideoTable.vAAverageMaskedRatio
import ink.snowflake.server.model.database.VideoTable.vACountPeople
import ink.snowflake.server.model.database.VideoTable.vAMaxAction
import ink.snowflake.server.model.database.VideoTable.vAMaxStayTime
import ink.snowflake.server.model.database.VideoTable.vATotalPeople
import ink.snowflake.server.model.database.VideoTable.vStartDateTime
import ink.snowflake.server.model.response.Area
import ink.snowflake.server.model.response.DetailItem
import ink.snowflake.server.model.response.ItemStyle
import ink.snowflake.server.model.response.VideoAnalyticsData
import ink.snowflake.server.model.response.VideoAnalyticsDetail
import ink.snowflake.server.model.response.VideoListResponse
import ink.snowflake.server.utils.database.ImageDao
import ink.snowflake.server.utils.database.VideoDao
import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.toJavaLocalDateTime
import java.time.format.DateTimeFormatter
class ImageDataBase {
fun saveImageAnalyticsData(request: ImageAnalyticsRequest) {
return ImageDao.insertImageAnalyticsData(request)
}
fun getImageList(): List<ImageAnalyticsRequest> {
return ImageDao.getVideoList()
}
}
@@ -0,0 +1,49 @@
package ink.snowflake.server.repository
import ink.snowflake.server.utils.database.UserDAO
import java.security.MessageDigest
import kotlin.text.Charsets.UTF_8
class UserDataBase {
fun login(email: String, password: String): Int {
// 查找用户
val userId = UserDAO.getUserIdByEmail(email)
return if(userId == null){
// 账号不存在
-1
}else{
val userPassword = UserDAO.getPasswordById(userId)
// 验证密码
if (password == userPassword) {
// 登录成功
userId
} else {
// 账号密码不匹配
-2
}
}
}
/**
* 正数:userId 负数:错误码
*/
fun registerByEmail(email: String, password: String): Int {
// 查找用户
val userId = UserDAO.getUserIdByEmail(email)
return if(userId != null){
// 如果用户已存在,返回 0
0
}else{
// 用户不存在,插入新用户
UserDAO.registerByEmailAndGetId(email, password)
}
}
fun hashPassword(password: String): String {
val bytes = MessageDigest.getInstance("SHA-256").digest(password.toByteArray(UTF_8))
return bytes.joinToString("") { "%02x".format(it) }
}
}
@@ -0,0 +1,158 @@
package ink.snowflake.server.repository
import ink.snowflake.server.SERVER_PATH
import ink.snowflake.server.model.database.VideoTable
import ink.snowflake.server.model.database.VideoTable.vAAverageMaskedRatio
import ink.snowflake.server.model.database.VideoTable.vACountPeople
import ink.snowflake.server.model.database.VideoTable.vAMaxAction
import ink.snowflake.server.model.database.VideoTable.vAMaxStayTime
import ink.snowflake.server.model.database.VideoTable.vATotalPeople
import ink.snowflake.server.model.database.VideoTable.vStartDateTime
import ink.snowflake.server.model.response.Area
import ink.snowflake.server.model.response.DetailItem
import ink.snowflake.server.model.response.ItemStyle
import ink.snowflake.server.model.response.VideoAnalyticsData
import ink.snowflake.server.model.response.VideoAnalyticsDetail
import ink.snowflake.server.model.request.VideoAnalyticsRequest
import ink.snowflake.server.model.response.VideoListResponse
import ink.snowflake.server.utils.database.VideoDao
import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.toJavaLocalDateTime
import java.time.format.DateTimeFormatter
class VideoDataBase {
fun saveVideoAnalyticsData(request: VideoAnalyticsRequest) {
return VideoDao.insertVideoAnalyticsData(request)
}
fun getVideoList(): List<VideoListResponse> {
return VideoDao.getVideoList()
}
fun getAnalyticsDetailByVideoId(vId: Int): VideoAnalyticsDetail? {
// 查询视频信息
val video = VideoDao.getAnalyticsDetailByVideoId(vId)
if (video == null) {
return null
}
// 查询相关的分析详情
val details = VideoDao.selectVideoDetailByVid(vId)
// 用于返回的数据
val yTotalData = mutableListOf<Pair<String, Int>>() // (时间, 总人数)
val yMaskedData = mutableListOf<Pair<String, Int>>() // (时间, 佩戴口罩人数)
val areaData = mutableListOf<List<Area>>()
val detailList = mutableListOf<DetailItem>()
// 颜色映射
val colors = mapOf(
"feed" to "rgba(0, 255, 0, 0.4)", // 淡绿色
"disinfection" to "rgba(0, 0, 255, 0.4)", // 淡蓝色
"other" to "rgba(255, 0, 0, 0.4)" // 淡红色
)
// 生成 yTotalData 和 yMaskedData,同时计算 areaData
var lastAction: String? = null
var areaStartTime: String? = null
var currentColor: String? = null
for (detail in details) {
// 获取视频开始时间
val vStartDatetime: LocalDateTime = video[vStartDateTime]!!
val timeStr = vStartDatetime.toJavaLocalDateTime()
.plusSeconds(detail.a_time_stamp.toLong())
.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))
// 添加总人数和口罩佩戴人数
yTotalData.add(timeStr to detail.a_total_people)
yMaskedData.add(timeStr to detail.a_total_people_masked)
// 处理 areaData,根据 a_action 判断是否需要创建新的区域
if (detail.a_action != "--") { // 只处理非 "--" 动作
if (lastAction == null || lastAction != detail.a_action) {
// 如果上一个动作和当前动作不同,并且 areaStartTime 已经存在,创建新的区域
if (areaStartTime != null) {
areaData.add(
listOf(
Area(areaStartTime, ItemStyle(currentColor ?: "#FF0000")),
Area(timeStr, ItemStyle(currentColor ?: "#FF0000"))
)
)
}
// 添加到 detailList,记录动作开始的时刻
detailList.add(
DetailItem(getFriendlyActionName(detail.a_action), time = timeStr)
)
// 更新为新的动作
lastAction = detail.a_action
areaStartTime = timeStr
currentColor = colors[detail.a_action] ?: "#FF0000" // 默认红色
}
} else {
// 如果当前动作为 "--",则结束当前区域,并重置状态
if (areaStartTime != null) {
// 添加到 detailList,记录动作开始的时刻
detailList.add(
DetailItem(getFriendlyActionName(detail.a_action), timeStr)
)
// 结束当前区域
areaData.add(
listOf(
Area(areaStartTime, ItemStyle(currentColor ?: "#FF0000")),
Area(timeStr, ItemStyle(currentColor ?: "#FF0000"))
)
)
// 重置状态
lastAction = null
areaStartTime = null
currentColor = null
}
}
}
// 处理最后一个区域的结束时间
if (areaStartTime != null && lastAction != null) {
areaData.add(
listOf(
Area(areaStartTime, ItemStyle(currentColor ?: "#FF0000")),
Area(yTotalData.last().first, ItemStyle(currentColor ?: "#FF0000"))
)
)
}
val analyticsData = VideoAnalyticsData(
yTotalData = yTotalData,
yMaskedData = yMaskedData,
areaData = areaData
)
// 返回数据
return VideoAnalyticsDetail(
v_name = video[VideoTable.vName],
v_video_play_path = "http://${SERVER_PATH}:9000/video/" + video[VideoTable.vObjectName],
v_file_name = video[VideoTable.vFileName],
v_duration = video[VideoTable.vDuration],
v_size = video[VideoTable.vSize],
v_start_datetime = video[VideoTable.vStartDateTime]?.toString() ?: "",
v_video_codec = video[VideoTable.vVideoCodec],
v_audio_codec = video[VideoTable.vAudioCodec],
v_overall_bit_rate = video[VideoTable.vOverallBitRate],
v_resolution = video[VideoTable.vResolution],
v_a_time = video[VideoTable.vATime].toString(),
v_a_total_people = video[vATotalPeople], // 总人数
v_a_count_people = video[vACountPeople], // 佩戴口罩人数
v_a_max_stay_time = video[vAMaxStayTime], // 最大停留时间
v_a_max_action = getFriendlyActionName(video[vAMaxAction]), // 最大动作
v_a_average_masked_ratio = video[vAAverageMaskedRatio], // 平均佩戴口罩比例
v_a_details = analyticsData, // 这里是计算得来的 VideoAnalyticsData
v_details_list = detailList
)
}
}
private fun getFriendlyActionName(name: String): String {
return if (name == "feed") {
"喂桑"
} else if (name == "disinfection") {
"消毒"
} else {
name
}
}
@@ -0,0 +1,30 @@
package ink.snowflake.server.route
import com.google.gson.Gson
import ink.snowflake.server.model.response.BaseResponse
import ink.snowflake.server.model.response.DeviceItem
import ink.snowflake.server.utils.runCommand
import io.ktor.client.*
import io.ktor.client.engine.cio.*
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import java.io.BufferedReader
import java.io.InputStreamReader
import io.ktor.client.plugins.auth.*
import io.ktor.client.plugins.auth.providers.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.server.auth.*
import io.ktor.server.http.content.*
import java.io.File
fun Application.mainFunc() {
routing {
get("/") {
// call.respondFile(File("src/main/resources/page/html/login.html"))
// call.respondRedirect("html/login.html")
}
}
}
@@ -0,0 +1,21 @@
package ink.snowflake.server.route
import io.ktor.serialization.gson.*
import io.ktor.server.application.*
import io.ktor.server.websocket.*
import io.ktor.server.websocket.WebSockets
import kotlin.time.Duration.Companion.seconds
fun Application.configureSockets() {
install(WebSockets) {
contentConverter = GsonWebsocketContentConverter()
pingPeriod = 15.seconds
timeout = 15.seconds
masking = false
}
// install(WebSockets) {
// pingPeriod = Duration.ofSeconds(15)
// timeout = Duration.ofSeconds(15)
// maxFrameSize = Long.MAX_VALUE
// }
}
@@ -0,0 +1,144 @@
package ink.snowflake.server.route.func
import com.cyberecho.mdoel.database.WSChatRecords
import ink.snowflake.server.gson
import ink.snowflake.server.model.ai.ChatRequest
import ink.snowflake.server.model.ai.ChatResponse
import ink.snowflake.server.model.response.BaseResponse
import ink.snowflake.server.utils.WebSocketManager
import ink.snowflake.server.utils.database.ChatRecordsDao
import ink.snowflake.server.utils.getUserIdByToken
import io.ktor.client.*
import io.ktor.client.engine.cio.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.auth.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import io.ktor.server.websocket.*
import io.ktor.websocket.*
import kotlinx.coroutines.channels.consumeEach
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.launch
fun Application.chat() {
val messageResponseFlow = MutableSharedFlow<String>()
val sharedFlow = messageResponseFlow.asSharedFlow()
val client = HttpClient(CIO) {
}
routing {
route("/ws") {
webSocket("/ws") {
send("You are connected to WebSocket!")
val job = launch {
sharedFlow.collect { message ->
send(message)
}
}
runCatching {
incoming.consumeEach { frame ->
if (frame is Frame.Text) {
val receivedText = frame.readText()
messageResponseFlow.emit("回复$receivedText")
println("回复$receivedText")
}
}
}.onFailure { exception ->
println("WebSocket exception: ${exception.localizedMessage}")
}.also {
job.cancel()
}
}
get("/getAliveUserSize") {
call.respond(BaseResponse(data = "当前活跃用户数:" + WebSocketManager.getAllConnectionsSize()))
}
authenticate {
webSocket("/echo") {
val userId = getUserIdByToken(call)
if (userId == null) {
// 如果 token 无效,关闭连接
close(CloseReason(CloseReason.Codes.VIOLATED_POLICY, "Invalid token"))
return@webSocket
}
// 连接成功
WebSocketManager.addConnection(userId, this)
try {
// 监听并接收来自客户端的消息
incoming.consumeEach { frame ->
when (frame) {
is Frame.Text -> {
// 处理文本消息
val text = frame.readText()
val userMessage = gson.fromJson(text, WSChatRecords::class.java)
println("收到消息: $userMessage")
launch {
ChatRecordsDao.insertChatRecord(
userId = userId,
message = userMessage.content,
messageType = userMessage.messageType,
sendAt = userMessage.timestamp,
from = userMessage.msgFrom,
emotion = "normal"
)
val memory = ChatRecordsDao.getRecentChatRecords(userId)
val memoryMsg = memory.map { ChatRequest.Message(it.message,it.from) }.toList().reversed()
val result = client.post("http://localhost:11434/api/chat") {
contentType(ContentType.Application.Json)
setBody(gson.toJson(ChatRequest(memoryMsg)))
}
// val result2 = client.post("http://localhost:11434/api/chat") {
// contentType(ContentType.Application.Json)
// setBody(gson.toJson(ChatRequest(listOf(ChatRequest.Message(userMessage.content)))))
// }
val res = gson.fromJson(result.bodyAsText(), ChatResponse::class.java)
println(result.bodyAsText())
val wsChatRecords = WSChatRecords(
aiId = userMessage.aiId,// 交流的对象AI_ID 从哪儿来回哪儿去
messageType = "text",
content = res.message.content,
timestamp = System.currentTimeMillis(),
msgFrom = "AI"
)
WebSocketManager.sendMessageToUser(userId, wsChatRecords)
ChatRecordsDao.insertChatRecord(
userId = wsChatRecords.aiId,
message = wsChatRecords.content,
messageType = wsChatRecords.messageType,
sendAt = wsChatRecords.timestamp,
from = wsChatRecords.msgFrom,
emotion = "normal"
)
}
}
// 处理二进制消息
is Frame.Binary -> {
val data = frame.readBytes()
}
// 处理 pong 帧
is Frame.Pong -> {}
// 处理关闭帧
is Frame.Close -> {
WebSocketManager.removeConnection(userId)
close()
return@consumeEach
}
else -> Unit
}
}
} catch (e: Exception) {
// 错误处理
println("WebSocket error: ${e.localizedMessage}")
} finally {
println("User $userId disconnected")
WebSocketManager.removeConnection(userId)
}
}
}
}
}
}
@@ -0,0 +1,42 @@
package ink.snowflake.server.route.func
import ink.snowflake.server.model.database.ImageAnalyticsRequest
import ink.snowflake.server.model.request.VideoAnalyticsRequest
import ink.snowflake.server.model.response.*
import ink.snowflake.server.repository.ImageDataBase
import io.ktor.server.application.*
import io.ktor.server.auth.*
import io.ktor.server.routing.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.websocket.*
fun Application.ImageAnalytics() {
val repository = ImageDataBase()
routing {
route("/api") {
// 上传分析结果
post("/saveImageAnalyticsData") {
val request = call.receive<ImageAnalyticsRequest>()
call.respond(BaseResponse(data = repository.saveImageAnalyticsData(request)))
}
authenticate {
// 拍照保存为图片 并且调用Python程序进行分析
get("/takePhoto") {
val camera = call.request.queryParameters["cameraId"]
if (camera.isNullOrEmpty()) {
call.respond(BaseResponse(status = false, message = "摄像头名称不能为空", data = null))
return@get
}
stopHLSStream(camera)
call.respond(BaseResponse(message = "摄像头流已停止", data = null))
}
// 获取已分析视频列表
get("/getImageList") {
val res = repository.getImageList()
call.respond(BaseResponse(data = res))
}
}
}
}
}
@@ -0,0 +1,165 @@
package ink.snowflake.server.route.func
import com.google.gson.Gson
import ink.snowflake.server.SERVER_PATH
import ink.snowflake.server.model.request.DevicesInfo
import ink.snowflake.server.model.response.*
import ink.snowflake.server.utils.runCommand
import io.ktor.client.HttpClient
import io.ktor.client.engine.cio.CIO
import io.ktor.client.plugins.auth.Auth
import io.ktor.client.plugins.auth.providers.BasicAuthCredentials
import io.ktor.client.plugins.auth.providers.basic
import io.ktor.client.request.get
import io.ktor.client.statement.HttpResponse
import io.ktor.client.statement.bodyAsText
import io.ktor.http.content.*
import io.ktor.server.application.*
import io.ktor.server.auth.authenticate
import io.ktor.server.routing.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.websocket.*
import io.ktor.utils.io.*
import io.ktor.websocket.*
import io.ktor.websocket.send
import kotlinx.coroutines.channels.consumeEach
import kotlinx.coroutines.launch
import java.io.BufferedReader
import java.io.InputStreamReader
//import sun.jvm.hotspot.HelloWorld.e
import java.util.*
fun Application.RemoteDebug() {
val adbClients = Collections.synchronizedList<WebSocketServerSession>(ArrayList()) // 线程安全的客户端列表
val client = HttpClient(CIO) {
install(Auth) {
basic {
credentials {
1
BasicAuthCredentials(username = "roto", password = "jjkj@2021.cn")
}
}
}
}
routing {
route("/api") {
authenticate {
route("/remote") {
get("/connect") {
val ip = call.parameters["ip"]
val port = call.parameters["port"]
if (ip != null && port != null) {
val result = runAdbCommand("connect $ip:$port")
call.respond(BaseResponse(data = result))
} else {
call.respond(BaseResponse(status = false, message = "IP或端口无效", data = null))
}
}
get("/connectLocalDevice") {
val port = call.parameters["port"]
if (port != null) {
val result = runAdbCommand("connect ${SERVER_PATH}:$port")
call.respond(BaseResponse(data = result))
} else {
call.respond(BaseResponse(status = false, message = "IP或端口无效", data = null))
}
}
get("/disConnectAll") {
val result = runAdbCommand("disconnect")
call.respond(BaseResponse(data = result))
}
get("/runLinuxCommand") {
val command = call.parameters["command"]
if (command != null) {
call.respond(BaseResponse(data = runCommand(command)))
} else {
call.respond(BaseResponse(status = false, message = "Linux命令不可为空", data = null))
}
}
get("/refreshDeviceList") {
try {
val name = call.parameters["name"]
val response: HttpResponse = client.get("http://171.212.101.201:65534/api/proxy/tcp")
val responseBody: String = response.bodyAsText()
val devicesInfo: DevicesInfo = Gson().fromJson(responseBody, DevicesInfo::class.java)
val onlineDevices = devicesInfo.proxies.stream()
.filter { it.status == "online" && it.conf != null && it.conf.remotePort >= 10000 && it.conf.remotePort <= 20000 }
val devices: MutableList<DeviceItem> = mutableListOf()
for (data in onlineDevices) {
if (name == "null" || name == null || data.name.contains(name) && data.conf != null) {
devices.add(DeviceItem(data.name, data.conf!!.remotePort))
}
}
call.respond(BaseResponse(data = devices))
} catch (e: Exception) {
call.respond(
BaseResponse(
status = false,
message = "端口信息请求失败:${e.message}",
data = null
)
)
}
}
}
}
}
webSocket("/logStream") {
send("日志系统连接成功")
var process: Process? = null // 保存 Process 对象,便于后续关闭
var filter = ""
try {
// 启动 adb logcat 或其他命令,并通过 WebSocket 发送日志
process = Runtime.getRuntime().exec("adb logcat")//ping 127.0.0.1 测试用
val reader = BufferedReader(InputStreamReader(process.inputStream))
// 异步读取日志并通过 WebSocket 推送
launch {
var line: String?
while (reader.readLine().also { line = it } != null) {
if (filter.isNotEmpty()) {
if (line != null && line.lowercase().contains(filter.lowercase())) {
send(line)
}
} else {
send(line ?: "")
}
}
}
// 持续监听 WebSocket 消息
incoming.consumeEach { frame ->
when (frame) {
is Frame.Close -> {
// 客户端断开连接时,终止命令
process?.destroy() // 终止命令
clients.remove(this) // 移除客户端
return@consumeEach
}
is Frame.Text -> {
filter = frame.readText()
}
// 处理其他帧类型
is Frame.Binary -> TODO()
is Frame.Ping -> TODO()
is Frame.Pong -> TODO()
}
}
} catch (e: Exception) {
// 处理异常情况
clients.remove(this) // 移除客户端
} finally {
// 确保在连接关闭时终止后台进程并移除客户端
process?.destroy() // 终止命令
clients.remove(this)
}
}
}
}
fun runAdbCommand(command: String): String = runCommand("adb $command")
@@ -0,0 +1,256 @@
package ink.snowflake.server.route.func
import com.auth0.jwt.JWT
import com.auth0.jwt.algorithms.Algorithm
import ink.snowflake.server.utils.database.AIDao
import ink.snowflake.server.model.request.CommonRequest
import ink.snowflake.server.model.request.LoginRequest
import ink.snowflake.server.model.request.RefreshTokenRequest
import ink.snowflake.server.model.request.RegisterRequest
import ink.snowflake.server.model.response.*
import ink.snowflake.server.repository.UserDataBase
import ink.snowflake.server.utils.AppConfig
import ink.snowflake.server.utils.database.UserDAO
import ink.snowflake.server.utils.getUserIdByToken
import io.ktor.server.application.*
import io.ktor.server.auth.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import kotlinx.coroutines.launch
import org.redisson.Redisson
import org.redisson.api.RBucket
import org.redisson.api.RedissonClient
import org.redisson.config.Config
import java.text.DateFormat
import java.time.Duration
import java.util.*
import javax.mail.*
import javax.mail.internet.InternetAddress
import javax.mail.internet.MimeMessage
// 配置和初始化 Redis 客户端
fun setupRedis(): RedissonClient {
val config = Config()
config.useSingleServer().setAddress("redis://localhost:6379")
return Redisson.create(config)
}
fun Application.User(config: AppConfig) {
val repository = UserDataBase()
// 初始化 Redis 连接
val redisClient: RedissonClient = setupRedis()
routing {
route("/api") {
route("/user") {
post("/login") {
val loginRequest = call.receive<LoginRequest>()
val email = loginRequest.account
val password = loginRequest.password
val userId = repository.login(email, password)
call.respond(
if (userId == -1) {
BaseResponse(status = false, message = "尚未注册", data = null)
} else if (userId == -2) {
BaseResponse(status = false, message = "账号密码不匹配,请重新登录", data = null)
} else {
BaseResponse(
status = true, data = LoginResponse(
userId.toString(),
generateAccessToken(config, userId),
generateRefreshToken(config, userId)
)
)
// match == -1
}
)
}
post("/sendCode") {
val account = call.receive<CommonRequest>()
// 将验证码存入 Redis,设置过期时间为 10 分钟
val bucket: RBucket<String> = redisClient.getBucket("verificationCode:${account.str}")
// 生成随机验证码(6位数字)
val verificationCode = generateVerificationCode()
println("验证码:$verificationCode")
// 10分钟的过期时间
bucket.set(verificationCode, Duration.ofMinutes(10))
launch {
sendVerificationEmail(config, account.str, verificationCode)
}
call.respond(BaseResponse(status = true, message = "已发送验证码", data = null))
}
post("/register") {
// 可能返回的情况:1. 注册成功 -1. 账户已存在 -2. 验证码错误 -3. 验证码过期
val register = call.receive<RegisterRequest>()
val account = register.account
// 从 Redis 获取保存的验证码
val bucket: RBucket<String> = redisClient.getBucket("verificationCode:$account")
val storedCode = bucket.get()
if (storedCode == null) {
call.respond(BaseResponse(status = false, message = "验证码过期", data = null))
} else if (storedCode != register.code) {
call.respond(BaseResponse(status = false, message = "验证码错误", data = null))
} else {
val userId = repository.registerByEmail(register.account, register.password)
if (userId > 0) {
call.respond(
BaseResponse(
status = true, message = "注册成功", data =
BaseResponse(
status = true, data = LoginResponse(
account,
generateAccessToken(config, userId),
generateRefreshToken(config, userId)
)
)
)
)
} else {
call.respond(BaseResponse(status = false, message = "账户已存在", data = null))
}
}
}
post("/refreshToken") {
try {
val token = call.receive<RefreshTokenRequest>()
val verifier = JWT.require(Algorithm.HMAC256(config.jwtSecret))
.withAudience(config.jwtAudience)
.withIssuer(config.jwtDomain)
.build()
val decodedJWT = verifier.verify(token.refreshToken)
val tokenType = decodedJWT.getClaim("token_type").asString()
if (tokenType != "refresh_token") {
call.respond(
BaseResponse(
status = false,
message = "拿什么乱七八糟的东西跟我换Access Token呢,???",
data = null
)
)
}
val userId = decodedJWT.getClaim("user_id").asInt()
// 生成新的access token和refresh token
call.respond(
BaseResponse(
status = true, data = RefreshTokenResponse(
generateAccessToken(config, userId),
generateRefreshToken(config, userId)
)
)
)
call.respond(BaseResponse(data = generateAccessToken(config, userId)))
} catch (ex: Exception) {
call.respond(BaseResponse(status = false, message = "token解析错误", data = null))
}
}
authenticate {
get("/getAiList") {
// val allAIProfile = AIDao.getAllAIProfiles().stream().map {
// AiListForUser(aiId = it.id, aiName = it.name, aiAvatar = it.avatarUrl ?: "")
// }.toList()
// call.respond(BaseResponse(data = allAIProfile))
}
get("/getUserInfo") {
val userId = getUserIdByToken(call)
if (userId != null) {
val userInfo = UserDAO.getUserInfoByUserId(userId)
if (userInfo != null) {
val response = BaseResponse(data = userInfo)
call.respond(response)
} else {
call.respond(BaseResponse(data = "查无此人"))
}
} else {
call.respond(BaseResponse(data = "Token出错"))
}
}
}
}
}
}
}
fun generateAccessToken(config: AppConfig, userId: Int): Token {
// return generateToken(config, userId, 120, "access_token")
return generateToken(config, userId, 2 * 60 * 60, "access_token")
}
fun generateRefreshToken(config: AppConfig, userId: Int): Token {
return generateToken(config, userId, 10 * 24 * 60 * 60, "refresh_token")
}
fun generateToken(config: AppConfig, userId: Int, second: Int, tokenType: String): Token {
val expiresAt = Date(System.currentTimeMillis() + second * 1000) //
val token = JWT.create()
.withAudience(config.jwtAudience)
.withIssuer(config.jwtDomain)
.withClaim("user_id", userId)
.withClaim("token_type", tokenType)
.withExpiresAt(expiresAt)
.sign(Algorithm.HMAC256(config.jwtSecret))
return Token(token, expiresAt.time, DateFormat.getDateTimeInstance().format(expiresAt))
}
// 生成4位随机验证码
fun generateVerificationCode(): String {
return String.format("%04d", Random().nextInt(10000))
}
fun sendVerificationEmail(config: AppConfig, recipientEmail: String, verificationCode: String) {
// 设置邮件会话的属性
val properties = Properties().apply {
put("mail.smtp.host", config.smtpHost)
put("mail.smtp.port", config.smtpPort)
put("mail.smtp.auth", "true") // 启用身份验证
put("mail.smtp.socketFactory.class", "javax.net.ssl.SSLSocketFactory") // 启用 SSL
put("mail.smtp.socketFactory.fallback", "false") // 禁用备用连接
}
// 创建会话
val session = Session.getInstance(properties, object : Authenticator() {
override fun getPasswordAuthentication(): PasswordAuthentication {
return PasswordAuthentication(config.smtpUser, config.smtpPassword)
}
})
try {
// Create email content with a more polished template
val message = MimeMessage(session).apply {
setFrom(InternetAddress(config.smtpUser))
setRecipient(Message.RecipientType.TO, InternetAddress(recipientEmail))
subject = "Welcome to CyberEcho! Verification Code:$verificationCode"
val htmlContent = """
<html>
<body style="font-family: Arial, sans-serif; color: #333;">
<div style="max-width: 600px; margin: 0 auto; padding: 20px; border: 1px solid #ddd; border-radius: 10px; background-color: #f9f9f9;">
<h2 style="color: #4CAF50;">Welcome to CyberEcho!</h2>
<p style="font-size: 16px;">Thank you for signing up with <strong>CyberEcho</strong>! We are excited to have you on board.</p>
<p style="font-size: 16px;">To complete your registration, please use the verification code below:</p>
<div style="background-color: #f1f1f1; padding: 15px; border-radius: 5px; font-size: 20px; text-align: center; color: #333;">
<strong style="color: #4CAF50;">$verificationCode</strong>
</div>
<p style="font-size: 16px; margin-top: 20px;">This code is valid for the next <strong>10 minutes</strong>. If you did not request this, please ignore this email.</p>
<p style="font-size: 16px; margin-top: 20px;">Please do not reply to this email. This inbox is not monitored.</p>
<hr style="border-top: 1px solid #ddd; margin-top: 30px;">
<p style="font-size: 14px; color: #888; text-align: center;">
Best regards<br>
<strong>The CyberEcho Account Team</strong>
</p>
</div>
</body>
</html>
"""
setContent(htmlContent, "text/html")
}
// 发送邮件
Transport.send(message)
println("Verification email sent successfully to $recipientEmail")
} catch (e: MessagingException) {
e.printStackTrace()
println("Failed to send verification email.")
}
}
@@ -0,0 +1,159 @@
package ink.snowflake.server.route.func
import ink.snowflake.server.VIDEO_INPUT_PATH
import ink.snowflake.server.model.request.VideoAnalyticsRequest
import ink.snowflake.server.model.response.*
import ink.snowflake.server.repository.VideoDataBase
import ink.snowflake.server.utils.WebSocketManager.broadcastMessage
import ink.snowflake.server.utils.runCommand
import io.ktor.http.content.*
import io.ktor.server.application.*
import io.ktor.server.auth.*
import io.ktor.server.routing.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.websocket.*
import io.ktor.websocket.*
import kotlinx.coroutines.channels.consumeEach
import java.io.File
import java.io.IOException
import java.util.*
val clients = Collections.synchronizedList<WebSocketServerSession>(ArrayList()) // 线程安全的客户端列表
var aiState = "等待分析任务中"
fun Application.VideoAnalytics() {
val repository = VideoDataBase()
routing {
// 实时发送AI状态
webSocket("/handleState") { // WebSocket 路由
clients.add(this) // 添加当前连接的客户端
send(aiState) // 向客户端发送连接成功消息
try {
incoming.consumeEach { frame -> // 持续接收消息
when (frame) {
is Frame.Text -> {
aiState = frame.readText() // 更新状态
broadcastMessage(aiState) // 使用封装的方法广播消息
}
is Frame.Close -> {
println("Closed")
clients.remove(this)
close() // 确保关闭 WebSocket 连接
return@consumeEach
}
// 其他消息类型的处理
is Frame.Binary -> TODO() // 处理二进制消息
is Frame.Ping -> TODO() // 处理 Ping 消息
is Frame.Pong -> TODO() // 处理 Pong 消息
}
}
} catch (e: Exception) {
// 处理接收消息时的异常
close(CloseReason(CloseReason.Codes.NORMAL, "Client disconnected"))
e.printStackTrace()
} finally {
clients.remove(this) // 确保在连接关闭时移除客户端
}
}
route("/api") {
// 上传分析结果
post("/saveVideoAnalyticsData") {
val request = call.receive<VideoAnalyticsRequest>()
// todo 上传这里未做测试
call.respond(BaseResponse(data = repository.saveVideoAnalyticsData(request)))
}
authenticate {
post("/upload") {
val multipart = call.receiveMultipart() //1G
// 确保 uploads 目录存在
val uploadDir = File(VIDEO_INPUT_PATH)
if (!uploadDir.exists()) {
if (!uploadDir.mkdirs()) {
println("无法创建目录")
throw IOException("Failed to create upload directory.")
}
}
var fileName = ""
var name = ""
var datetime = ""
broadcastMessage("正在上传数据")
multipart.forEachPart { part ->
when (part) {
is PartData.FileItem -> {
fileName = part.originalFileName ?: "unknown"
val file = File("$VIDEO_INPUT_PATH$fileName") // 保存路径
//ktor3
// file.outputStream().use { outputStream ->
// val writableChannel = Channels.newChannel(outputStream)
// part.provider().copyTo(writableChannel) // 复制到 WritableByteChannel
// }
//ktor2
part.streamProvider().use { inputStream ->
file.outputStream().buffered().use { outputStream ->
inputStream.copyTo(outputStream)
}
}
}
is PartData.FormItem -> {
when (part.name) {
"name" -> name = part.value
"datetime" -> datetime = part.value
}
}
else -> part.dispose()
}
}
call.respond(BaseResponse(message = "上传成功", data = null))
broadcastMessage("上传完成,开始启动AI引擎")
val command = listOf(
"/usr/bin/python3",
"/home/xhcp/mine/IntelligentVideoAnalytics/AI_Project/DeepStream_Action_Recognition/core/final.py",
"$VIDEO_INPUT_PATH$fileName",
datetime,
name
)
println("-----------------" + command.joinToString(" "))
runCommand(command) {
println(it)
}
}
// 获取已分析视频列表
get("/getVideoList") {
val res = repository.getVideoList()
call.respond(BaseResponse(data = res))
}
// 获取某视频分析详情
get("/getAnalyticsDetailByVideoId") {
// 获取 vId 参数
val vIdParam = call.parameters["vId"]
val vId = vIdParam?.toIntOrNull() // 将 vId 转换为 Int,确保安全
if (vId == null) {
call.respond(BaseResponse(status = true, message = "Invalid vId", data = null))
return@get
}
val result = repository.getAnalyticsDetailByVideoId(vId)
if (result != null) {
call.respond(BaseResponse(data = result))
}
}
}
}
}
}
suspend fun broadcastMessage(message: String) { // 封装的广播消息方法
println("发送消息:$message")
clients.forEach { client ->
try {
client.send(message) // 发送消息到每个客户端
} catch (e: Exception) {
println("发送消息给客户端时出错,移除客户端: ${e.message}")
clients.remove(client) // 移除已断开的客户端
}
}
}
@@ -0,0 +1,187 @@
package ink.snowflake.server.route.func
import ink.snowflake.server.model.response.*
import io.ktor.server.application.*
import io.ktor.server.auth.*
import io.ktor.server.http.content.files
import io.ktor.server.http.content.static
import io.ktor.server.routing.*
import io.ktor.server.response.*
import io.ktor.server.websocket.*
import java.io.BufferedReader
import java.io.File
import java.io.InputStreamReader
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.TimeUnit
// 存储 FFmpeg 进程,key 是摄像头名称
private val ffmpegProcesses = ConcurrentHashMap<String, Process>()
fun Application.VideoAnalyticsJetson() {
val streamFile = "camera/stream/"
routing {
// 实时发送 AI 状态
webSocket("/handleState1") {
}
route("/api") {
static("/camera/stream") {
// files("camera/stream") // 确保 FFmpeg 输出的 HLS 片段和 m3u8 文件存放在这里
files(File(streamFile))
}
authenticate {
// 获取摄像头列表
get("/getCameraDevices") {
call.respond(BaseResponse(data = getCameraDevices()))
}
// 启动 HLS 流
get("/startCamera") {
val camera = call.request.queryParameters["cameraId"]
if (camera.isNullOrEmpty()) {
call.respond(BaseResponse(status = false, message = "摄像头连接失败,摄像头名称错误", data = null))
return@get
}
try {
val camAddr = streamFile + "output.m3u8"
// val camAddr = "C:\\Users\\Duan\\Desktop\\camera\\stream\\output.m3u8"
startHLSStream(camera, camAddr)
call.respond(BaseResponse(message = "摄像头连接成功", data = camAddr))
} catch (e: Exception) {
call.respond(BaseResponse(status = false, message = "摄像头连接失败,${e.message}", data = null))
}
}
// 关闭摄像头流
get("/stopCamera") {
val camera = call.request.queryParameters["cameraId"]
if (camera.isNullOrEmpty()) {
call.respond(BaseResponse(status = false, message = "摄像头名称不能为空", data = null))
return@get
}
stopHLSStream(camera)
call.respond(BaseResponse(message = "摄像头流已停止", data = null))
}
}
}
}
// 确保应用退出时清理所有 FFmpeg 进程
Runtime.getRuntime().addShutdownHook(Thread {
ffmpegProcesses.forEach { (camera, process) ->
println("应用退出,关闭摄像头流: $camera")
process.destroy()
}
})
}
// 获取可用摄像头设备列表
fun getCameraDevices(): List<String> {
val command = if (System.getProperty("os.name").contains("Windows")) {
arrayOf("ffmpeg", "-f", "dshow", "-list_devices", "true", "-i", "dummy")
} else {
arrayOf("v4l2-ctl", "--list-devices")
}
val process = ProcessBuilder(*command)
.redirectErrorStream(true) // 合并标准输出和错误输出
.start()
val output = BufferedReader(InputStreamReader(process.inputStream)).readText()
println("命令输出:\n$output")
val devices = mutableListOf<String>()
val regex = if (System.getProperty("os.name").contains("Windows")) {
""""(.*?)"""".toRegex()
} else {
"""\s+(.+/video\d+)""".toRegex()
}
regex.findAll(output).forEach {
devices.add(it.groupValues[1])
}
return devices
}
// 启动 HLS 流
fun startHLSStream(cameraName: String, streamPath: String) {
if (ffmpegProcesses.containsKey(cameraName)) {
println("摄像头 $cameraName 已经在运行")
return
}
val command: Array<String> = if (System.getProperty("os.name").contains("Windows")) {
arrayOf(
"ffmpeg", "-f", "dshow", "-i", "video=$cameraName",
"-c:v", "libx264", "-preset", "fast", "-tune", "zerolatency",
"-f", "hls", "-hls_time", "3",
"-hls_list_size", "2",
"-hls_flags", "delete_segments",
streamPath
)
} else {
arrayOf(
"ffmpeg", "-f", "v4l2", "-i", cameraName,
"-c:v", "libx264", "-preset", "fast", "-tune", "zerolatency",
"-f", "hls", "-hls_time", "3",
"-hls_list_size", "2",
"-hls_flags", "delete_segments",
streamPath
)
}
println(command.joinToString(" "))
try {
val process = ProcessBuilder(*command).start()
ffmpegProcesses[cameraName] = process
Thread {
BufferedReader(InputStreamReader(process.inputStream)).use { reader ->
var line: String?
while (reader.readLine().also { line = it } != null) {
println(line)
}
}
}.start()
Thread {
BufferedReader(InputStreamReader(process.errorStream)).use { reader ->
var line: String?
while (reader.readLine().also { line = it } != null) {
System.err.println(line)
}
}
}.start()
} catch (e: Exception) {
e.printStackTrace()
}
}
// 停止 HLS 流
fun stopHLSStream(cameraName: String) {
val process = ffmpegProcesses.remove(cameraName)
if (process != null) {
println("正在关闭摄像头流: $cameraName")
process.destroy()
try {
// 等待最多 3 秒,看看进程是否能正常退出
if (!process.waitFor(3, TimeUnit.SECONDS)) {
println("摄像头流 $cameraName 进程未正常退出,尝试强制关闭")
process.destroyForcibly() // 强制关闭
}
} catch (e: InterruptedException) {
println("关闭摄像头流时发生错误: ${e.message}")
process.destroyForcibly()
}
println("摄像头流 $cameraName 已停止")
} else {
println("未找到正在运行的摄像头流: $cameraName")
}
}
@@ -0,0 +1,19 @@
package ink.snowflake.server.utils
import io.ktor.server.config.*
class AppConfig(config: ApplicationConfig) {
val jwtAudience: String = config.property("ktor.security.jwt.audience").getString()
val jwtDomain: String = config.property("ktor.security.jwt.domain").getString()
val jwtRealm: String = config.property("ktor.security.jwt.realm").getString()
val jwtSecret: String = config.property("ktor.security.jwt.secret").getString()
val dbUrl: String = config.property("ktor.database.url").getString()
val dbDriver: String = config.property("ktor.database.driver").getString()
val dbUser: String = config.property("ktor.database.user").getString()
val dbPassword: String = config.property("ktor.database.password").getString()
val smtpHost: String = config.property("ktor.mail.smtp.host").getString()
val smtpPort: Int = config.property("ktor.mail.smtp.port").getString().toInt()
val smtpUser: String = config.property("ktor.mail.smtp.user").getString()
val smtpPassword: String = config.property("ktor.mail.smtp.password").getString()
}
@@ -0,0 +1,90 @@
package ink.snowflake.server.utils
import ink.snowflake.server.model.response.UserInfo
import io.ktor.server.application.*
import io.ktor.server.auth.*
import io.ktor.server.auth.jwt.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.format
import kotlinx.datetime.toJavaLocalDateTime
import java.io.BufferedReader
import java.io.InputStreamReader
import java.text.SimpleDateFormat
import java.time.format.DateTimeFormatter
import java.util.*
fun getUserIdByToken(call: ApplicationCall) : Int?{
// 通过token获取user_id
return call.principal<JWTPrincipal>()?.payload?.getClaim("user_id")?.asInt()
}
fun formatDateToTargetString(date: Date, targetFormat: String): String {
val formatter = SimpleDateFormat(targetFormat) // 创建格式化器
return formatter.format(date) // 格式化日期并返回字符串
}
fun formatLocalDateTimeToString(localDateTime: LocalDateTime): String {
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss") // 创建格式化器
return localDateTime.toJavaLocalDateTime().format(formatter) // 转换并格式化
}
fun runCommand(command: String): String {
println("command--$command")
return try {
val process = Runtime.getRuntime().exec(command)
// 等待命令执行完毕
process.waitFor()
// 读取输出流
val reader = BufferedReader(InputStreamReader(process.inputStream))
val errorReader = BufferedReader(InputStreamReader(process.errorStream))
val output = StringBuilder()
// 读取标准输出
reader.use { r ->
var line: String? = r.readLine()
while (line != null) {
output.append(line).append("\n")
line = r.readLine()
}
}
// 读取错误输出
errorReader.use { er ->
var errorLine: String? = er.readLine()
while (errorLine != null) {
output.append("ERROR: ").append(errorLine).append("\n")
errorLine = er.readLine()
}
}
output.toString()
} catch (e: Exception) {
"Error running command: ${e.message}"
}
}
fun runCommand(command: List<String>, logCallback: (String) -> Unit): Process {
// 使用 ProcessBuilder 构建命令并启动
val process = ProcessBuilder(command)
.redirectErrorStream(true) // 合并标准输出和错误输出
.start()
// 创建一个 CoroutineScope 来管理协程
val scope = CoroutineScope(Dispatchers.IO)
// 使用协程读取标准输出和错误输出
scope.launch {
try {
val reader = BufferedReader(InputStreamReader(process.inputStream))
var line: String?
while (reader.readLine().also { line = it } != null) {
logCallback(line ?: "") // 将日志推送到回调
}
} catch (e: Exception) {
logCallback("Error reading output: ${e.message}")
}
}
return process
}
@@ -0,0 +1,55 @@
package ink.snowflake.server.utils
import com.cyberecho.mdoel.database.WSChatRecords
import com.google.gson.Gson
import ink.snowflake.server.gson
import io.ktor.websocket.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.util.concurrent.ConcurrentHashMap
object WebSocketManager {
// 创建一个 CoroutineScope 来管理协程
val scope = CoroutineScope(Dispatchers.IO)
// 存储所有 WebSocket 连接的 Mapkey 为 user_idvalue 为 WebSocketSession
private val connections = ConcurrentHashMap<Int, WebSocketSession>()
// 添加 WebSocket 连接
fun addConnection(userId: Int, session: WebSocketSession) {
connections[userId] = session
println("新连接:$userId")
}
// 移除 WebSocket 连接
fun removeConnection(userId: Int) {
connections.remove(userId)
println("断开连接:$userId")
}
// 广播消息给所有连接的用户
fun broadcastMessage(message: String) {
connections.forEach { userId, session ->
scope.launch {
session.send(message)
}
}
}
// 向特定用户发送消息
suspend fun sendMessageToUser(userId: Int, message: WSChatRecords) {
connections[userId]?.send(gson.toJson(message))
}
// 获取所有当前活跃的连接
fun getAllConnections(): Set<WebSocketSession> {
return connections.values.toSet() // 返回连接值的集合
}
// 获取所有当前活跃的连接
fun getAllConnectionsSize(): Int {
return connections.size
}
}
@@ -0,0 +1,19 @@
package ink.snowflake.server.utils.database
import ink.snowflake.server.model.database.AIProfile
import ink.snowflake.server.model.database.AIProfilesTable
import ink.snowflake.server.model.database.toAIProfile
import org.jetbrains.exposed.sql.selectAll
import org.jetbrains.exposed.sql.transactions.transaction
object AIDao {
/**
* 获取所有AI
*/
fun getAllAIProfiles(): List<AIProfile> {
return transaction {
AIProfilesTable.selectAll().map { it.toAIProfile() }
}
}
}
@@ -0,0 +1,49 @@
package ink.snowflake.server.utils.database
import ink.snowflake.server.model.database.*
import kotlinx.datetime.Clock
import org.jetbrains.exposed.sql.SortOrder
import org.jetbrains.exposed.sql.insertAndGetId
import org.jetbrains.exposed.sql.kotlin.datetime.timestampLiteral
import org.jetbrains.exposed.sql.selectAll
import org.jetbrains.exposed.sql.transactions.transaction
object ChatRecordsDao {
/**
* 插入聊天记录
*/
fun insertChatRecord(
userId: Int,
message: String,
messageType: String,
sendAt: Long,
from: String,
emotion: String?
): Boolean {
return transaction {
ChatRecordsTable
.insertAndGetId {
it[ChatRecordsTable.userId] = userId
it[ChatRecordsTable.message] = message
it[ChatRecordsTable.messageType] = messageType
it[ChatRecordsTable.sentAt] = timestampLiteral(Clock.System.now())
it[ChatRecordsTable.from] = from
it[ChatRecordsTable.emotion] = emotion
}.value > 0
}
}
/**
* 取最近五条聊天记录
*/
fun getRecentChatRecords(userId: Int): List<ChatRecord> {
return transaction {
ChatRecordsTable
.selectAll()
.where { ChatRecordsTable.userId eq userId }
.orderBy(ChatRecordsTable.sentAt to SortOrder.DESC)
.limit(5)
.toList().map { it.toChatRecord() }
}
}
}
@@ -0,0 +1,60 @@
package ink.snowflake.server.utils.database
import ink.snowflake.server.SERVER_PATH
import ink.snowflake.server.model.database.ImageAnalyticsRequest
import ink.snowflake.server.model.database.ImageTable
import ink.snowflake.server.model.response.VideoListResponse
import ink.snowflake.server.utils.formatLocalDateTimeToString
import kotlinx.datetime.toKotlinLocalDateTime
import org.jetbrains.exposed.sql.SortOrder
import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.selectAll
import org.jetbrains.exposed.sql.transactions.transaction
import java.sql.Timestamp
object ImageDao {
fun insertImageAnalyticsData(request: ImageAnalyticsRequest) {
return transaction {
ImageTable.insert {
it[object_name] = request.object_name
it[upload_datetime] = Timestamp.valueOf(request.upload_datetime)
.toLocalDateTime().toKotlinLocalDateTime()
it[file_name] = request.file_name
it[resolution] = request.resolution
it[size] = request.size
it[cocoon_count] = request.cocoon_count
it[max_confidence] = request.max_confidence
it[min_confidence] = request.min_confidence
it[average_confidence] = request.average_confidence
it[other_info] = request.other_info // 直接存储 JSON
it[processing_time] = Timestamp.valueOf(request.processing_time)
.toLocalDateTime().toKotlinLocalDateTime()
}
}
}
fun getVideoList(): List<ImageAnalyticsRequest> {
return transaction {
ImageTable.selectAll()
.orderBy(ImageTable.upload_datetime, SortOrder.DESC)
.map {
ImageAnalyticsRequest(
object_name = "http://${SERVER_PATH}:9000/image/" + it[ImageTable.object_name],
upload_datetime = formatLocalDateTimeToString(it[ImageTable.upload_datetime]),
file_name = it[ImageTable.file_name],
resolution = it[ImageTable.resolution],
size = it[ImageTable.size],
cocoon_count = it[ImageTable.cocoon_count],
max_confidence = it[ImageTable.max_confidence],
min_confidence = it[ImageTable.min_confidence],
average_confidence = it[ImageTable.average_confidence],
other_info = it[ImageTable.other_info] as Map<String, String>,
processing_time = formatLocalDateTimeToString(it[ImageTable.processing_time])
)
}
}
}
}
@@ -0,0 +1,92 @@
package ink.snowflake.server.utils.database
import ink.snowflake.server.model.database.ImageTable
import ink.snowflake.server.model.database.UserTable
import ink.snowflake.server.model.response.UserInfo
import ink.snowflake.server.utils.formatLocalDateTimeToString
import kotlinx.datetime.Clock
import org.jetbrains.exposed.sql.Op
import org.jetbrains.exposed.sql.SqlExpressionBuilder
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.insertAndGetId
import org.jetbrains.exposed.sql.kotlin.datetime.timestampLiteral
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.selectAll
import org.jetbrains.exposed.sql.transactions.transaction
object UserDAO {
/**
* 根据 email 获取密码
* @return 密码的 SHA-256 哈希值
*/
fun getPasswordById(id: Int): String? {
return transaction {
// 查询指定 email 的用户并返回 passwordHash
UserTable
.selectAll().where { UserTable.id eq id }
.map { it[UserTable.passwordHash] }
.singleOrNull() // 如果没有找到用户,返回 null
}
}
/**
* 根据 email 获取用户 ID
* @return 用户的 ID
*/
fun getUserIdByEmail(email: String): Int? {
return transaction {
// 查询指定 email 的用户并返回 id
UserTable
.selectAll().where { UserTable.email eq email }
.map { it[UserTable.id].value }
.singleOrNull() // 如果没有找到用户,返回 null
}
}
/**
* 添加用户
* @return 如果添加成功,返回 1;如果用户已存在,返回 0;如果添加失败,返回 -1
*/
fun registerByEmailAndGetId(account: String, hashPassword: String): Int {
return transaction {
try {
val newUserId = UserTable
.insertAndGetId {
it[email] = account
it[passwordHash] = hashPassword
it[createdAt] = timestampLiteral(Clock.System.now())
it[updatedAt] = timestampLiteral(Clock.System.now())
}
// 如果插入成功,返回 userId
if (newUserId.value > 0) {
newUserId.value
} else {
// 插入失败,返回 -1
-1
}
} catch (e: Exception) {
// 如果发生异常(例如数据库连接问题),返回 -1
e.printStackTrace() // 打印异常堆栈
-1
}
}
}
fun getUserInfoByUserId(userId: Int?): UserInfo? {
if (userId == null) return null
return transaction {
UserTable
.selectAll().where { UserTable.id eq userId }
.map {
UserInfo(
username = it[UserTable.username],
email = it[UserTable.email],
phone = it[UserTable.phone]
)
}
.singleOrNull()
}
}
}
@@ -0,0 +1,92 @@
package ink.snowflake.server.utils.database
import ink.snowflake.server.model.database.VideoAnalyticsDetailTable
import ink.snowflake.server.model.database.VideoTable
import ink.snowflake.server.model.database.VideoTable.vATime
import ink.snowflake.server.model.database.VideoTable.vName
import ink.snowflake.server.model.request.VideoAnalyticsRequest
import ink.snowflake.server.model.request.VideoDetail
import ink.snowflake.server.model.response.VideoListResponse
import ink.snowflake.server.utils.formatLocalDateTimeToString
import kotlinx.datetime.toKotlinLocalDateTime
import org.jetbrains.exposed.sql.ResultRow
import org.jetbrains.exposed.sql.SortOrder
import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.insertAndGetId
import org.jetbrains.exposed.sql.selectAll
import org.jetbrains.exposed.sql.transactions.transaction
import java.sql.Timestamp
object VideoDao {
fun insertVideoAnalyticsData(request: VideoAnalyticsRequest) {
return transaction {
// 插入视频信息
val videoId = VideoTable.insertAndGetId {
it[vName] = request.v_name
it[vObjectName] = request.v_object_name
it[vFileName] = request.v_file_name
it[vStartDateTime] =
Timestamp.valueOf(request.v_start_datetime).toLocalDateTime().toKotlinLocalDateTime()
it[vDuration] = request.v_duration
it[vSize] = request.v_size
it[vVideoCodec] = request.v_video_codec
it[vAudioCodec] = request.v_audio_codec
it[vOverallBitRate] = request.v_overall_bit_rate
it[vResolution] = request.v_resolution
it[vATime] = Timestamp.valueOf(request.v_a_time).toLocalDateTime().toKotlinLocalDateTime()
it[vATotalPeople] = request.v_a_total_people
it[vACountPeople] = request.v_a_count_people
it[vAMaxStayTime] = request.v_a_max_stay_time
it[vAMaxAction] = request.v_a_max_action
it[vAAverageMaskedRatio] = request.v_a_average_masked_ratio
}
// 插入视频分析详情
request.v_a_details.forEach { detail ->
VideoAnalyticsDetailTable.insert {
it[vId] = videoId.value
it[aTimeStamp] = detail.a_time_stamp
it[aTotalPeople] = detail.a_total_people
it[aTotalPeopleMasked] = detail.a_total_people_masked
it[aAction] = detail.a_action
}
}
}
}
fun getVideoList():List<VideoListResponse> {
return transaction {
VideoTable.selectAll()
.orderBy(vATime, SortOrder.DESC)
.map {
VideoListResponse(
v_id = it[VideoTable.id].value,
v_name = it[vName],
v_a_time = formatLocalDateTimeToString(it[vATime])
)
}
}
}
fun getAnalyticsDetailByVideoId(vId: Int): ResultRow? {
return transaction {
VideoTable.selectAll().where { VideoTable.id eq vId }.singleOrNull()
}
}
fun selectVideoDetailByVid(vId:Int): List<VideoDetail>{
return transaction {
VideoAnalyticsDetailTable
.selectAll()
.where { VideoAnalyticsDetailTable.vId eq vId }
.map {
VideoDetail(
a_time_stamp = it[VideoAnalyticsDetailTable.aTimeStamp],
a_total_people = it[VideoAnalyticsDetailTable.aTotalPeople],
a_total_people_masked = it[VideoAnalyticsDetailTable.aTotalPeopleMasked],
a_action = it[VideoAnalyticsDetailTable.aAction]
)
}
}
}
}