备份代码

This commit is contained in:
Superdandan 2024-05-19 23:07:25 +08:00
parent 27e398ac73
commit 15ddb83d2d
15 changed files with 397 additions and 115 deletions

View File

@ -33,16 +33,28 @@ dependencies {
implementation("io.ktor:ktor-serialization-gson-jvm")
implementation("io.ktor:ktor-server-freemarker-jvm")
implementation("io.ktor:ktor-server-netty-jvm")
implementation("io.insert-koin:koin-ktor:3.2.0")
implementation("io.insert-koin:koin-core:3.2.0")
implementation("ch.qos.logback:logback-classic:$logback_version")
implementation("io.ktor:ktor-server-config-yaml:2.3.10")
// Exposed
implementation("org.jetbrains.exposed:exposed-core:$exposed_version")
implementation("org.jetbrains.exposed:exposed-dao:$exposed_version")
implementation("org.jetbrains.exposed:exposed-crypt:$exposed_version")
implementation("org.jetbrains.exposed:exposed-jdbc:$exposed_version")
implementation("org.jetbrains.exposed:exposed-java-time:$exposed_version")
implementation("org.jetbrains.exposed:exposed-json:$exposed_version")
implementation("org.jetbrains.exposed:exposed-money:$exposed_version")
implementation("com.h2database:h2:$h2_version")
//aliyunOss
implementation("com.aliyun.oss:aliyun-sdk-oss:3.17.4")
implementation("javax.xml.bind:jaxb-api:2.3.1")
implementation("javax.activation:activation:1.1.1")
implementation("org.glassfish.jaxb:jaxb-runtime:2.3.3")
//pool
implementation("com.zaxxer:HikariCP:$hikaricp_version")
implementation("org.ehcache:ehcache:$ehcache_version")

View File

@ -2,7 +2,7 @@ ktor_version=2.3.10
kotlin_version=1.9.23
logback_version=1.4.14
kotlin.code.style=official
exposed_version = 0.41.1
exposed_version = 0.50.1
h2_version = 2.2.224
hikaricp_version = 5.1.0
ehcache_version = 3.10.8

View File

@ -1,34 +1,34 @@
package echo.org
import com.zaxxer.hikari.HikariDataSource
import echo.org.application.route.DocumentModule
import echo.org.instructure.AliyunOss
import echo.org.instructure.DatabaseSingleton
import echo.org.plugins.configureRouting
import echo.org.plugins.configureSecurity
import echo.org.plugins.configureSerialization
import echo.org.plugins.configureTemplating
import io.ktor.server.application.*
import org.jetbrains.exposed.sql.Database
import org.koin.ktor.ext.inject
import org.koin.ktor.plugin.Koin
fun main(args: Array<String>) {
io.ktor.server.netty.EngineMain.main(args)
}
fun Application.module() {
// Initialize Koin
install(Koin) {
modules(DocumentModule)
}
AliyunOss.init()
configureSecurity()
configureSerialization()
configureTemplating()
configureRouting()
configureTemplating()
initDatabase()
}
fun Application.initDatabase() {
val config = environment.config.config("storage")
val dateSourceConfig = DatabaseSingleton.createHikariDataSource(
url = config.property("jdbcURL").getString(),
driver = config.property("driverClassName").getString(),
usernameInput = config.property("username").getString(),
passwordInput = config.property("password").getString()
)
val dataSource = HikariDataSource(dateSourceConfig)
Database.connect(dataSource)
DatabaseSingleton.init(environment.config)
}

View File

@ -0,0 +1,100 @@
package echo.org.application.route
import echo.org.domain.*
import echo.org.instructure.AliyunOssFileServiceImpl
import echo.org.instructure.DocumentRepositoryImpl
import io.ktor.http.*
import io.ktor.http.content.*
import io.ktor.server.application.*
import io.ktor.server.freemarker.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import org.koin.dsl.module
import org.koin.ktor.ext.inject
import java.io.File
import java.net.URLEncoder
import java.nio.charset.StandardCharsets
import java.time.LocalDateTime
val DocumentModule = module {
single<DocumentRepository> { DocumentRepositoryImpl() }
single<FileOSS> { AliyunOssFileServiceImpl() }
}
fun Route.handleDocuments() {
val documentRepository by inject<DocumentRepository>()
val fileOSS by inject<FileOSS>()
route("/documents") {
get("") {
val res = documentRepository.pageQuery(DocumentQueryParam())
call.respond(FreeMarkerContent("index.ftl", res.results, "e"))
}
post("/add") {
// 添加一个文件
val multipart = call.receiveMultipart()
var fileSize: String?
var fileType: String?
var uploadName: String?
multipart.forEachPart { part ->
when (part) {
is PartData.FileItem -> {
val fileName = part.originalFileName as String
fileType = fileName.split(".").last()
uploadName = System.currentTimeMillis().toString() + "." + fileType
val file = File(uploadName!!)
part.streamProvider()
.use { its ->
file.outputStream().buffered()
.use { its.copyTo(it) }
}
fileSize = file.length().toString()
if (fileOSS.upload(file)) {
file.delete()
documentRepository.create(
DocumentVO(
fileName, "someAuthorId",
LocalDateTime.now(), LocalDateTime.now(),
fileType ?: "", uploadName ?: "",
fileSize ?: "", ""
)
)
}
}
else -> {}
}
part.dispose()
}
call.respond(Result.success(null))
}
get("/download") {
// 获取下载地址
val documentQueryParam = DocumentQueryParam()
documentQueryParam.id = 1
documentQueryParam.id?.let {
documentRepository.findById(it)?.let { document ->
val file = fileOSS.download(document.uploadName)
if (file.exists()) {
val encodedFileName = URLEncoder.encode(document.title, StandardCharsets.UTF_8.toString())
call.response.header(
HttpHeaders.ContentDisposition,
"attachment; filename*=UTF-8''$encodedFileName"
)
call.response.header(HttpHeaders.ContentType, document.fileType)
call.respondFile(file)
} else {
call.respond(HttpStatusCode.NotFound, mapOf("error" to "File not found"))
}
} ?: call.respond(HttpStatusCode.NotFound, mapOf("error" to "Document not found"))
} ?: call.respond(HttpStatusCode.BadRequest, mapOf("error" to "Missing or invalid document ID"))
}
post("/query") {
val documentQueryParam = call.receive<DocumentQueryParam>()
val pageQuery = documentRepository.pageQuery(documentQueryParam)
call.respond(Result.success(pageQuery))
}
}
}

View File

@ -6,23 +6,40 @@ import org.jetbrains.exposed.dao.LongEntityClass
import org.jetbrains.exposed.dao.id.EntityID
import java.time.LocalDateTime
class Document(id: EntityID<Long>) : LongEntity(id) {
companion object : LongEntityClass<Document>(Documents)
var title: String by Documents.title
var content: String by Documents.content
var authorId: String by Documents.authorId
var createdAt: LocalDateTime by Documents.createdAt
var updatedAt: LocalDateTime by Documents.updatedAt
var fileSize: String by Documents.fileSize
}
data class Document(
val id: Long,
val title: String,
val uploadName: String,
val fileType: String,
val authorId: String,
val createdAt: LocalDateTime,
val updatedAt: LocalDateTime,
val fileSize: String,
val filePath: String
)
data class DocumentVO(
val title: String, val authorId: String,
val updatedAt: LocalDateTime,
val createdAt: LocalDateTime,
val fileType: String,
val uploadName: String,
val fileSize: String,
val filePath: String
)
interface DocumentRepository {
suspend fun save(document: Document): Document?
suspend fun create(documentVO: DocumentVO): Document?
suspend fun edit(document: Document): Document?
suspend fun findById(id: Long): Document?
suspend fun deleteById(id: Long): Boolean
suspend fun pageQuery(documentQueryParam: DocumentQueryParam): PageResult<Document>
}
data class DocumentQueryParam(var title: String? = null,
var authorId: String? = null) : BasePageQueryParm()
data class DocumentQueryParam(
var id: Long? = null,
var title: String? = null,
var authorId: String? = null
) : BasePageQueryParm()

View File

@ -0,0 +1,9 @@
package echo.org.domain
import java.io.File
interface FileOSS {
fun upload(file: File): Boolean
fun download(fileName: String): File
fun delete(fileName: String): Boolean
}

View File

@ -0,0 +1,89 @@
package echo.org.instructure
import com.aliyun.oss.OSS
import com.aliyun.oss.OSSClientBuilder
import com.aliyun.oss.common.auth.CredentialsProvider
import com.aliyun.oss.common.auth.CredentialsProviderFactory
import com.aliyun.oss.common.auth.DefaultCredentialProvider
import com.aliyun.oss.common.auth.EnvironmentVariableCredentialsProvider
import com.aliyun.oss.model.Bucket
import echo.org.domain.FileOSS
import java.io.File
import java.io.FileOutputStream
//获取本地环境的oss访问凭证信息
fun credentialsProvider(): EnvironmentVariableCredentialsProvider =
CredentialsProviderFactory.newEnvironmentVariableCredentialsProvider()
object AliyunOss {
private lateinit var ossClient: OSS
private const val ENDPOINT = "https://oss-cn-hangzhou.aliyuncs.com"
private const val BUCKET_NAME = "echolaw"
fun init(): OSS {
val accessKeyId = System.getenv("OSS_ACCESS_KEY_ID")
val accessKeySecret = System.getenv("OSS_ACCESS_KEY_SECRET")
val credentialsProvider: CredentialsProvider = DefaultCredentialProvider(accessKeyId, accessKeySecret)
ossClient = OSSClientBuilder().build(ENDPOINT, credentialsProvider)
return ossClient
}
fun shutdown() {
ossClient.shutdown()
}
fun client(): OSS {
return ossClient
}
fun bucket(): Bucket {
return ossClient.createBucket(BUCKET_NAME)
}
fun bucketName(): String {
return BUCKET_NAME
}
}
class AliyunOssFileServiceImpl : FileOSS {
override fun upload(file: File): Boolean {
try {
AliyunOss.client().putObject(
AliyunOss.bucketName(),
file.name,
file
)
} catch (e: Exception) {
e.printStackTrace()
return false
}
return true
}
override fun download(fileName: String): File {
val ossObject = AliyunOss.client().getObject(AliyunOss.bucketName(), fileName)
val file = File(fileName)
val outputStream = FileOutputStream(file)
ossObject.objectContent?.use { input ->
outputStream.use { output ->
input.copyTo(output)
}
}
return file
}
override fun delete(fileName: String): Boolean {
val client = AliyunOss.client()
try {
client.deleteObject(AliyunOss.bucketName(), fileName)
} catch (e: Exception) {
e.printStackTrace()
return false
}
return true
}
}

View File

@ -4,18 +4,32 @@ import com.zaxxer.hikari.HikariConfig
import com.zaxxer.hikari.HikariDataSource
import io.ktor.server.config.*
import kotlinx.coroutines.Dispatchers
import org.jetbrains.exposed.sql.Database
import org.jetbrains.exposed.sql.SchemaUtils
import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction
import org.jetbrains.exposed.sql.transactions.transaction
object DatabaseSingleton {
fun init(config: ApplicationConfig) {
val driverClassName = config.property("storage.driverClassName").getString()
val jdbcURL = config.property("storage.jdbcURL").getString() +
config.propertyOrNull("storage.database")?.getString()
lateinit var dataSource: HikariDataSource
fun init(config: ApplicationConfig) {
val jdbcUrl = config.property("storage.jdbcURL").getString()
val database = config.property("storage.database").getString()
val dateSourceConfig = createHikariDataSource(
url = "$jdbcUrl/$database",
driver = config.property("storage.driverClassName").getString(),
usernameInput = config.property("storage.username").getString(),
passwordInput = config.property("storage.password").getString()
)
dataSource = HikariDataSource(dateSourceConfig)
Database.connect(dataSource)
transaction {
SchemaUtils.create(Documents)
}
}
fun createHikariDataSource(
private fun createHikariDataSource(
url: String,
driver: String,
usernameInput: String,

View File

@ -1,66 +1,95 @@
package echo.org.instructure
import echo.org.domain.Document
import echo.org.domain.DocumentQueryParam
import echo.org.domain.DocumentRepository
import echo.org.domain.PageResult
import echo.org.domain.*
import echo.org.instructure.DatabaseSingleton.dbQuery
import org.jetbrains.exposed.dao.id.LongIdTable
import org.jetbrains.exposed.sql.Column
import echo.org.instructure.Documents.id
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.andWhere
import org.jetbrains.exposed.sql.deleteWhere
import org.jetbrains.exposed.sql.javatime.datetime
import org.jetbrains.exposed.sql.selectAll
import java.time.LocalDateTime
object Documents : LongIdTable() {
val title: Column<String> = varchar("title", 255)
val content: Column<String> = text("content")
val authorId: Column<String> = varchar("author_id", 50)
val createdAt: Column<LocalDateTime> = datetime("created_at")
val updatedAt: Column<LocalDateTime> = datetime("updated_at")
val fileSize: Column<String> = varchar("file_size", 255)
object Documents : Table() {
val id = long("id").autoIncrement().uniqueIndex()
val title = varchar("title", 255)
val uploadName = varchar("upload_name", 255)
val fileType = varchar("file_type", 50)
val authorId = varchar("author_id", 255)
val createdAt = datetime("created_at")
val updatedAt = datetime("updated_at")
val fileSize = varchar("file_size", 100)
val filePath = varchar("file_path", 255)
override val primaryKey = PrimaryKey(id)
// Convert a ResultRow to a Document
fun toDocument(row: ResultRow): Document {
return Document(
id = row[id],
title = row[title],
authorId = row[authorId],
createdAt = row[createdAt],
updatedAt = row[updatedAt],
fileType = row[fileType],
uploadName = row[uploadName],
fileSize = row[fileSize],
filePath = row[filePath]
)
}
}
class DocumentRepositoryImpl : DocumentRepository {
override suspend fun save(document: Document): Document = dbQuery {
override suspend fun edit(document: Document): Document? = dbQuery {
// 检查文档是否有有效的ID
val existingDocument = document.id.let {
Document.findById(it)
val existingDocument = Documents.selectAll().where { id eq document.id }.singleOrNull()
// 如果文档存在,则更新,否则返回 null
existingDocument?.let {
Documents.update({ id eq document.id }) {
it[title] = document.title
it[authorId] = document.authorId
it[createdAt] = document.createdAt
it[updatedAt] = LocalDateTime.now() // 更新时间应为当前时间
it[fileSize] = document.fileSize
}
// 如果文档存在,则更新,否则创建新文档
existingDocument?.apply {
title = document.title
content = document.content
authorId = document.authorId
createdAt = document.createdAt
updatedAt = LocalDateTime.now() // 更新时间应为当前时间
fileSize = document.fileSize
} ?: Document.new {
title = document.title
content = document.content
authorId = document.authorId
createdAt = document.createdAt
updatedAt = document.updatedAt
fileSize = document.fileSize
// 返回更新后的 Document 对象
Documents.selectAll().where { id eq document.id }
.map { Documents.toDocument(it) }
.singleOrNull()
}
}
override suspend fun create(
documentVO: DocumentVO
): Document = dbQuery {
Documents.insert {
it[title] = documentVO.title
it[authorId] = documentVO.authorId
it[updatedAt] = documentVO.updatedAt
it[createdAt] = documentVO.createdAt
it[fileType] = documentVO.fileType
it[uploadName] = documentVO.uploadName
it[fileSize] = documentVO.fileSize
it[filePath] = documentVO.filePath
}
Documents.selectAll().where { id eq id }
.map { Documents.toDocument(it) }
.single()
}
override suspend fun findById(id: Long): Document? = dbQuery {
Document.find { Documents.id eq id }.firstOrNull()
Documents.selectAll().where { Documents.id eq id }
.mapNotNull { Documents.toDocument(it) }
.singleOrNull()
}
override suspend fun deleteById(id: Long): Boolean {
Document.find { Documents.id eq id }
.firstOrNull()?.delete()
return Documents.deleteWhere { Documents.id eq id } > 0
override suspend fun deleteById(id: Long): Boolean = dbQuery {
Documents.deleteWhere { Documents.id eq id } > 0
}
override suspend fun pageQuery(documentQueryParam: DocumentQueryParam): PageResult<Document> {
override suspend fun pageQuery(documentQueryParam: DocumentQueryParam): PageResult<Document> = dbQuery {
// 构建查询条件
val query = Documents.selectAll().apply {
documentQueryParam.authorId?.let {
@ -70,12 +99,13 @@ class DocumentRepositoryImpl : DocumentRepository {
andWhere { Documents.title like "%${documentQueryParam.title}%" }
}
}
val pageSize = documentQueryParam.pageSize ?: 10 // 默认页面大小
val pageIndex = documentQueryParam.pageNum ?: 0 // 默认页码
val pageSize = documentQueryParam.pageSize // 默认页面大小
val pageIndex = documentQueryParam.pageNum - 1 // 默认页码
val offset = pageSize * pageIndex.toLong()
// 执行查询
val count = query.count()
val documentList = query.limit(pageSize, offset).map { Document.wrapRow(it) }
return PageResult(documentList, count)
val documents = query.limit(pageSize, offset)
.map { Documents.toDocument(it) }
PageResult(documents, count)
}
}

View File

@ -1,47 +1,15 @@
package echo.org.plugins
import echo.org.domainDocuments
import echo.org.domain.Result
import io.ktor.http.content.*
import echo.org.application.route.handleDocuments
import io.ktor.server.application.*
import io.ktor.server.freemarker.*
import io.ktor.server.http.content.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import java.io.File
fun Application.configureRouting() {
routing {
staticResources("/files", "files")
route("/documents") {
get("") {
call.respond(FreeMarkerContent("index.ftl", model, "e"))
}
post("/add") {
// 添加一个文件
val multipart = call.receiveMultipart()
multipart.forEachPart { part ->
when (part) {
is PartData.FileItem -> {
val fileName = part.originalFileName as String
val file = File("upload-directory/$fileName")
part.streamProvider()
.use { its -> file.outputStream().buffered()
.use { its.copyTo(it) } }
}
else -> {}
}
part.dispose()
}
call.respond(Result.success(""))
}
post("/download"){
// 获取下载第
}
}
handleDocuments()
}
}

View File

@ -1,14 +1,20 @@
package echo.org.plugins
import com.google.gson.TypeAdapter
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonWriter
import io.ktor.serialization.gson.*
import io.ktor.server.application.*
import io.ktor.server.plugins.contentnegotiation.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
fun Application.configureSerialization() {
install(ContentNegotiation) {
gson {
registerTypeAdapter(LocalDateTime::class.java, LocalDateTimeAdapter())
}
}
routing {
@ -17,3 +23,15 @@ fun Application.configureSerialization() {
}
}
}
class LocalDateTimeAdapter : TypeAdapter<LocalDateTime>() {
private val formatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME
override fun write(out: JsonWriter, value: LocalDateTime) {
out.value(value.format(formatter))
}
override fun read(input: JsonReader): LocalDateTime {
return LocalDateTime.parse(input.nextString(), formatter)
}
}

View File

@ -3,12 +3,12 @@ ktor:
modules:
- echo.org.ApplicationKt.module
deployment:
port: 8080
port: 8012
storage:
driverClassName: "com.mysql.cj.jdbc.Driver"
jdbcURL: "jdbc:mysql://47.97.21.20:3306/"
jdbcURL: "jdbc:mysql://47.97.21.20:3306"
database: "document"
username: "documentAdmin"
password: "123456"

View File

@ -4,7 +4,7 @@
<pattern>%d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="trace">
<root level="INFO">
<appender-ref ref="STDOUT"/>
</root>
<logger name="org.eclipse.jetty" level="INFO"/>

View File

@ -1,10 +1,17 @@
package echo.org
import echo.org.application.route.DocumentModule
import echo.org.instructure.Documents
import echo.org.plugins.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.testing.*
import org.jetbrains.exposed.sql.SchemaUtils
import org.jetbrains.exposed.sql.transactions.transaction
import org.koin.dsl.module
import org.koin.ktor.plugin.Koin
import kotlin.test.*
class ApplicationTest {

View File

@ -0,0 +1,18 @@
package echo.org
import echo.org.instructure.AliyunOss
import io.ktor.server.testing.*
import java.io.File
import kotlin.test.Test
class ApplicationOSSTest {
@Test
fun testRoot() = testApplication {
AliyunOss.init()
AliyunOss.client().putObject(AliyunOss.bucketName(),
"测试.txt",
File("D:\\code\\blog\\upload-directory\\新建 文本文档 (3).txt"))
}
}