Compare commits

...

10 Commits

18 changed files with 319 additions and 78 deletions

View File

@ -1,11 +1,48 @@
# Ktor 项目模板
# Ktor 项目
> Kotlin + Gradle + Ktor + Exposed + Docker
配置好啦
## api
- Database
- Logging
- Routing
- Session
- WebSocket
### bill
```bash
GET /api/v1/bill/{year}/{month?}/{day?}
POST /api/v1/bill/
Body List<{
type: Int,
date: String,
money: Float,
cls: String,
label: String,
options: String,
}>
DELETE /api/v1/bill/{id}
PUT /api/v1/bill/
Body {
id: Int?,
type: Int,
date: String,
money: Float,
cls: String,
label: String,
options: String,
}
```
### label
```bash
GET /api/v1/label/
POST /api/v1/label/
Body {
type: String,
name: String,
relativeId: Int?,
}
```

View File

@ -13,7 +13,7 @@ plugins {
group = "cn.fadinglight"
version = "0.0.1"
version = "1.0.1"
application {
mainClass.set("cn.fadinglight.ApplicationKt")

View File

@ -1,4 +1,13 @@
services:
# back:
# container_name: bill-ktor
# image: registry.cn-hangzhou.aliyuncs.com/fadinglight/bill-ktor:dev
# restart: always
# environment:
# JDBC_URL: mariadb://db/bill
# ports:
# - 80:4000
db:
container_name: bill-test-db
image: mariadb

View File

@ -0,0 +1,48 @@
import aiohttp
import asyncio
mongo_url = 'http://www.fadinglight.cn:8088/list'
mysql_url = 'http://localhost:8080/api/v1/bill/'
async def getMongoBills():
async with aiohttp.ClientSession() as session:
async with session.get(mongo_url) as response:
data = await response.json()
return data
async def postBillsToMysql(bills):
async with aiohttp.ClientSession() as session:
async with session.post(mysql_url, json=bills) as response:
data = await response.json()
return data
def dealOneBill(bill):
bill.setdefault('type', 0)
return dict(
money=bill['money'],
cls=bill['cls'],
label=bill['label'],
date=bill['date'],
options=bill['options'],
type="consume" if bill['type'] == 0 else "income",
)
def transType(bill):
bill['type'] = 0 if bill['type'].lower() == 'income' else 1
return bill
async def main():
data = await getMongoBills()
bills = data['data']
bills = list(map(dealOneBill, bills))
bills = list(map(transType, bills))
respData = await postBillsToMysql(bills)
print(respData)
asyncio.run(main())

View File

@ -0,0 +1,54 @@
import aiohttp
import asyncio
mongo_url = 'http://www.fadinglight.cn:8088/class'
mysql_url = 'http://localhost:8080/api/v1/label/'
async def getMongoLabels():
async with aiohttp.ClientSession() as session:
async with session.get(mongo_url) as response:
data = await response.json()
return data
async def postLabelsToMysql(label):
async with aiohttp.ClientSession() as session:
async with session.post(mysql_url, json=label) as response:
data = await response.json()
return data
def newLabel(name, type=0, relativeId=None):
return dict(
name=name,
type=type,
relativeId=relativeId,
)
async def main():
data = await getMongoLabels()
labels = data['data']
consumeLabels = labels['consume']
incomeLabels = labels['income']
print(consumeLabels)
for i in incomeLabels:
label = newLabel(i, type=2)
# print(label)
await postLabelsToMysql(label)
for i in consumeLabels:
subLabels = consumeLabels[i]
label = newLabel(i, type=0)
resp = await postLabelsToMysql(label)
id = resp['data']
print(id)
for j in subLabels:
sub_label = newLabel(j, type=1, relativeId=id)
await postLabelsToMysql(sub_label)
asyncio.run(main())

View File

@ -9,10 +9,10 @@ sealed class Resp<T> {
abstract fun json(): RespData<T>
class Ok<T>(private val data: T) : Resp<T>() {
override fun json() = RespData(code = 0, data = data, message = null)
override fun json() = RespData(code = 0, data = data, message = "")
}
class Error(private val message: String?, val code: Int) : Resp<Unit>() {
class Error(private val message: String? = "", val code: Int = -1) : Resp<Unit>() {
override fun json(): RespData<Unit> = RespData(code = code, data = null, message = message)
}

View File

@ -4,9 +4,18 @@ import kotlinx.serialization.Serializable
enum class BillType {
Consume,
Income,
CONSUME,
INCOME;
companion object {
fun of(n: Int): BillType = when (n) {
0 -> CONSUME
1 -> INCOME
else -> throw IllegalArgumentException("error code $n")
}
}
}
@Serializable
data class Bill(
var id: Int?,

View File

@ -2,9 +2,19 @@ package cn.fadinglight.models
enum class LabelType {
CLASS,
LABEL,
INCOME_CLASS,
CONSUME_CLASS,
CONSUME_LABEL,
INCOME_CLASS;
companion object {
fun of(n: Int) = when (n) {
0 -> CONSUME_CLASS
1 -> CONSUME_LABEL
2 -> INCOME_CLASS
else -> throw IllegalArgumentException("error code $n")
}
}
}
data class Label(
@ -12,5 +22,5 @@ data class Label(
val type: LabelType,
val name: String,
var count: Int,
val relativedId: Int?,
val relativeId: Int?,
)

View File

@ -14,6 +14,10 @@ const val HIKARI_CONFIG_KEY = "ktor.hikariConfig"
fun Application.configureDatabase() {
val configPath = environment.config.property(HIKARI_CONFIG_KEY).getString()
val dbConfig = HikariConfig(configPath)
val envs = System.getenv()
if (envs.containsKey("JDBC_URL")) {
dbConfig.jdbcUrl = envs["JDBC_URL"]
}
val dataSource = HikariDataSource(dbConfig)
Database.connect(dataSource)
createTables()

View File

@ -1,5 +1,6 @@
package cn.fadinglight.plugins
import cn.fadinglight.models.LabelType
import cn.fadinglight.routes.billRoute
import cn.fadinglight.routes.labelRoute
import io.ktor.server.application.*
@ -10,7 +11,7 @@ fun Application.configureRouting() {
routing {
route("/") {
get {
call.respondText("Welcome che's Bill App!")
call.respond("Hello world")
}
}
route("/api/v1") {

View File

@ -1,9 +1,11 @@
package cn.fadinglight.routes
import cn.fadinglight.libs.Resp
import cn.fadinglight.models.Bill
import cn.fadinglight.services.BillServiceImpl
import cn.fadinglight.vo.BillVO
import cn.fadinglight.vo.bill
import cn.fadinglight.vo.vo
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.request.*
@ -14,22 +16,25 @@ import io.ktor.server.util.*
fun Route.billRoute() {
val billService = BillServiceImpl()
route("/bill") {
get("/{year}/{month}") {
get("/{year}/{month?}/{day?}") {
runCatching {
val year = call.parameters["year"]!!
val month = call.parameters["month"]!!
billService.getManyBills(year, month)
val year = call.parameters.getOrFail("year")
val month = call.parameters["month"]
val day = call.parameters["day"]
if (month is String && day is String) {
billService.getManyBills(year, month, day).map(Bill::vo)
} else if (month is String) {
billService.getManyBills(year, month).map(Bill::vo)
} else {
billService.getManyBills(year).map(Bill::vo)
}
}.onSuccess {
call.respond(Resp.Ok(it).json())
}.onFailure {
call.respond(Resp.Error(it.message, code = -1).json())
call.respond(Resp.Error(it.message).json())
}
}
get("/") {
call.respond(status = HttpStatusCode.OK, Resp.Ok(billService.getManyBills()).json())
}
post("/") {
runCatching {
val bills = call.receive<List<BillVO>>().map(BillVO::bill)
@ -42,15 +47,25 @@ fun Route.billRoute() {
}
delete("{id}") {
val id = call.parameters.getOrFail("id").toInt()
val count = billService.deleteOneBill(id)
call.respond(status = HttpStatusCode.OK, Resp.Ok(count).json())
runCatching {
val id = call.parameters.getOrFail("id").toInt()
billService.deleteOneBill(id)
}.onSuccess {
call.respond(status = HttpStatusCode.OK, Resp.Ok(it).json())
}.onFailure {
call.respond(Resp.Error(it.message).json())
}
}
put("/") {
val bill = call.receive<BillVO>().bill()
val count = billService.updateOneBill(bill)
call.respond(status = HttpStatusCode.OK, Resp.Ok(count).json())
runCatching {
val bill = call.receive<BillVO>().bill()
billService.updateOneBill(bill)
}.onSuccess {
call.respond(status = HttpStatusCode.OK, Resp.Ok(it).json())
}.onFailure {
call.respond(Resp.Error(it.message).json())
}
}
}
}

View File

@ -2,8 +2,10 @@ package cn.fadinglight.routes
import cn.fadinglight.libs.Resp
import cn.fadinglight.services.LabelServiceImpl
import cn.fadinglight.vo.LabelVO
import cn.fadinglight.vo.LabelPost
import cn.fadinglight.vo.label
import io.ktor.server.application.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
@ -18,6 +20,15 @@ fun Route.labelRoute() {
).json()
)
}
post {}
post("/") {
runCatching {
call.receive<LabelPost>().label()
}.onSuccess {
val labelId = labelService.addLabel(it)
call.respond(Resp.Ok(labelId).json())
}.onFailure {
call.respond(Resp.Error(it.message).json())
}
}
}
}

View File

@ -3,6 +3,7 @@ package cn.fadinglight.services
import cn.fadinglight.models.Bill
interface BillService {
suspend fun getManyBills(year: String): List<Bill>
suspend fun getManyBills(year: String, month: String): List<Bill>
suspend fun getManyBills(year: String, month: String, day: String): List<Bill>
suspend fun getManyBills(): List<Bill>

View File

@ -4,13 +4,21 @@ import cn.fadinglight.mapers.Bills
import cn.fadinglight.mapers.Labels
import cn.fadinglight.models.Bill
import cn.fadinglight.models.BillType
import cn.fadinglight.models.LabelType
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.SqlExpressionBuilder.inList
import org.jetbrains.exposed.sql.SqlExpressionBuilder.like
import org.jetbrains.exposed.sql.transactions.transaction
class BillServiceImpl : BillService {
private fun formatSingletonNumber(n: String) = if (n.length == 1) {
"0$n"
} else {
n
}
private fun resultRowToBill(row: ResultRow) = Bill(
id = row[Bills.id].value,
type = BillType.valueOf(row[Bills.type]),
@ -21,16 +29,22 @@ class BillServiceImpl : BillService {
options = row[Bills.options]
)
override suspend fun getManyBills(year: String): List<Bill> = transaction {
Bills
.select(Bills.date like "${year}%")
.map(::resultRowToBill)
}
override suspend fun getManyBills(year: String, month: String): List<Bill> = transaction {
Bills
.select(Bills.date like "%${year}-${month}%")
.select(Bills.date like "%${year}-${formatSingletonNumber(month)}%")
.map(::resultRowToBill)
}
override suspend fun getManyBills(year: String, month: String, day: String): List<Bill> = transaction {
Bills
.select(Bills.date eq "${year}-${month}-${day}")
.select(Bills.date eq "${year}-${formatSingletonNumber(month)}-${formatSingletonNumber(day)}")
.map(::resultRowToBill)
}
@ -67,7 +81,7 @@ class BillServiceImpl : BillService {
}
override suspend fun addManyBills(bills: List<Bill>): Int = transaction {
Bills
val newBills = Bills
.batchInsert(bills) {
this[Bills.type] = it.type.name
this[Bills.date] = it.date
@ -76,7 +90,29 @@ class BillServiceImpl : BillService {
this[Bills.money] = it.money
this[Bills.options] = it.options
}
.count()
.map(::resultRowToBill)
newBills.forEach {
val classId = Labels
.select(
(Labels.name eq it.cls) and (Labels.type inList listOf(
LabelType.CONSUME_CLASS,
LabelType.INCOME_CLASS
).map(LabelType::toString))
)
.map { it2 -> it2[Labels.id].value }
.singleOrNull()
val labelId = if (it.type == BillType.CONSUME) Labels
.select((Labels.name eq it.label) and (Labels.type eq LabelType.CONSUME_LABEL.toString()))
.map { it2 -> it2[Labels.id].value }
.singleOrNull()
else null
Labels.update({ (Labels.id eq labelId) or (Labels.id eq classId) }) {
with(SqlExpressionBuilder) {
it[count] = count + 1
}
}
}
newBills.count()
}
override suspend fun updateManyBills(bills: List<Bill>): Int {

View File

@ -3,10 +3,11 @@ package cn.fadinglight.services
import cn.fadinglight.models.Label
import cn.fadinglight.models.LabelType
import cn.fadinglight.vo.LabelGroup
import cn.fadinglight.vo.LabelPost
interface LabelService {
suspend fun getLabels(): LabelGroup
suspend fun addLabel(labelType: LabelType, label: Label): Int
suspend fun addLabel(label: Label): Int
suspend fun deleteLabel(labelId: Int): Int
suspend fun addCount(labelId: Int): Int
}

View File

@ -1,6 +1,5 @@
package cn.fadinglight.services
import cn.fadinglight.vo.LabelVO
import cn.fadinglight.vo.vo
import cn.fadinglight.mapers.Labels
import cn.fadinglight.models.Label
@ -15,7 +14,7 @@ class LabelServiceImpl : LabelService {
type = LabelType.valueOf(row[Labels.type]),
name = row[Labels.name],
count = row[Labels.count],
relativedId = row[Labels.relativeId]
relativeId = row[Labels.relativeId]
)
override suspend fun getLabels(): LabelGroup {
@ -25,34 +24,33 @@ class LabelServiceImpl : LabelService {
.map(::resultRowToLabel)
.groupBy { it.type }
}
val consumeLabels = labelGroups[LabelType.CLASS]
val consumeLabels = labelGroups[LabelType.CONSUME_CLASS]
?.sortedByDescending { it.count }
?.map {
it.vo().apply {
this.labels = labelGroups[LabelType.LABEL]
?.filter { it2 -> it2.relativedId == it.id }
?.map(Label::vo)
?: emptyList()
this.labels = labelGroups[LabelType.CONSUME_LABEL]
?.filter { it2 -> it2.relativeId == it.id }
?.sortedByDescending { it2 -> it2.count }
?.map { it2 -> it2.name }
}
} ?: emptyList()
val incomeLabels = labelGroups[LabelType.CLASS]
?.map {
it.vo().apply {
this.labels = labelGroups[LabelType.INCOME_CLASS]
?.filter { it2 -> it2.relativedId == it.id }
?.map(Label::vo)
?: emptyList()
}
} ?: emptyList()
}
?: emptyList()
val incomeLabels = labelGroups[LabelType.INCOME_CLASS]
?.sortedByDescending { it.count }
?.map(Label::vo)
?: emptyList()
return LabelGroup(income = incomeLabels, consume = consumeLabels)
}
override suspend fun addLabel(labelType: LabelType, label: Label): Int = transaction {
Labels.insertAndGetId {
it[type] = label.type.name
it[name] = label.name
it[count] = label.count
it[relativeId] = label.relativedId
}.value
override suspend fun addLabel(label: Label): Int = transaction {
Labels
.insertAndGetId {
it[type] = label.type.name
it[name] = label.name
it[count] = 0
it[relativeId] = label.relativeId
}.value
}
override suspend fun deleteLabel(labelId: Int): Int {

View File

@ -3,11 +3,12 @@ package cn.fadinglight.vo
import cn.fadinglight.models.Bill
import cn.fadinglight.models.BillType
import kotlinx.serialization.Serializable
import java.util.*
@Serializable
data class BillVO(
val id: Int? = null,
val type: String,
val type: Int,
val date: String,
val money: Float,
val cls: String,
@ -17,7 +18,7 @@ data class BillVO(
fun BillVO.bill() = Bill(
id = id,
type = BillType.valueOf(type),
type = BillType.of(type),
date = date,
money = money,
cls = cls,
@ -27,7 +28,7 @@ fun BillVO.bill() = Bill(
fun Bill.vo() = BillVO(
id = id,
type = type.name,
type = type.ordinal,
date = date,
money = money,
cls = cls,

View File

@ -4,11 +4,26 @@ import cn.fadinglight.models.Label
import cn.fadinglight.models.LabelType
import kotlinx.serialization.Serializable
@Serializable
data class LabelPost(
val type: Int,
val name: String,
val relativeId: Int?,
)
fun LabelPost.label() = Label(
id = null,
type = LabelType.of(type),
name = name,
count = 0,
relativeId = relativeId
)
@Serializable
data class LabelVO(
val name: String,
val count: Int,
var labels: List<LabelVO>?,
var labels: List<String>?,
)
@Serializable
@ -18,16 +33,7 @@ data class LabelGroup(
)
fun LabelVO.label(type: LabelType) = Label(
id = null,
type = type,
name = name,
count = 0,
relativedId = null
)
fun Label.vo() = LabelVO(
name = name,
count = count,
labels = emptyList(),
labels = null,
)