feat: secret application.yaml #6

Merged
SOR2171 merged 4 commits from backend-dev into main 2026-08-06 17:52:59 +08:00
31 changed files with 512 additions and 718 deletions
+1
View File
@@ -5,6 +5,7 @@
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/src/beckend/.kotlin" />
<excludeFolder url="file://$MODULE_DIR$/src/beckend/build" />
<excludeFolder url="file://$MODULE_DIR$/.idea/dataSources" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
+3 -3
View File
@@ -7,11 +7,11 @@
<option name="jvmTarget" value="21" />
</component>
<component name="KotlinCommonCompilerArguments">
<option name="apiVersion" value="2.2" />
<option name="languageVersion" value="2.2" />
<option name="apiVersion" value="2.3" />
<option name="languageVersion" value="2.3" />
</component>
<component name="KotlinJpsPluginSettings">
<option name="externalSystemId" value="Gradle" />
<option name="version" value="2.2.21" />
<option name="version" value="2.3.21" />
</component>
</project>
+1
View File
@@ -18,6 +18,7 @@ class Response<T> {
- 除特别标注(公开)外,接口需在请求头携带 `Authorization: Bearer <token>`
- 下文路径均省略 `/api` 前缀,`data` 仅给出 `T` 的声明
- 部分 `/test` 接口的返回值不遵循上述格式
- 前端类示例使用 **TypeScript** 类定义
## 接口总览
+8 -74
View File
@@ -17,15 +17,10 @@
- /reset
- /departments
- /email
- /evaluation-records
- /finance
- /items
- /memos
- /personnel
- /project-groups
- /proof-images
- /usages
- /users
## 预计完成的 API 列表
@@ -51,24 +46,6 @@
---
### 2. **人员管理 (Personnel)**
| 端点 | 方法 | 功能说明 |
|---------------------------------|---------------------|----------------------------------|
| `personnel/` | GET/POST/PUT/DELETE | 人员信息 CRUD 操作 |
| `personnel/statistics/` | GET | 获取人员统计信息(按部门和职位) |
| `personnel/{id}/set_inactive/` | POST | 设置人员为已卸任状态 |
| `personnel/{id}/set_active/` | POST | 设置人员为在职状态 |
| `personnel/{id}/` | DELETE | 删除人员(发送邮件通知) |
| `personnel/check_expired/` | POST | 检查并更新已到期的人员状态 |
| `project-groups/` | GET/POST/PUT/DELETE | 项目组 CRUD 操作 |
| `project-groups/by_department/` | GET | 按部门获取项目组 |
| `project-groups/{id}/` | DELETE | 删除项目组(发送邮件通知) |
**搜索/过滤字段**: `name`, `student_id`, `phone`, `email`, `grade_major`
---
### 3. **财务管理 (Finance)**
| 端点 | 方法 | 功能说明 |
@@ -95,41 +72,7 @@
---
### 5. **备忘录 (Memo)**
| 端点 | 方法 | 功能说明 |
|----------------------------|---------------------|------------------|
| `memos/` | GET/POST/PUT/DELETE | 备忘录 CRUD 操作 |
| `memos/{id}/upload_image/` | POST | 上传备忘录图片 |
| `memos/{id}/delete_image/` | DELETE | 删除备忘录图片 |
**搜索字段**: `title`, `content`
**默认排序**: 按更新时间倒序
---
### 6. **考评系统 (Evaluation)**
| 端点 | 方法 | 功能说明 |
|-----------------------------------------|---------------------|--------------------------------------------|
| `evaluation-records/` | GET/POST/PUT/DELETE | 考评记录 CRUD 操作 |
| `evaluation-records/personnel-summary/` | GET | 获取人员考评汇总(按人员、部门、年级分组) |
| `evaluation-records/personnel-records/` | GET | 获取某个人员的所有考评记录 |
| `evaluation-records/delete-personnel/` | DELETE | 删除某个人员的所有考评记录 |
| `evaluation-records/export/` | GET | 导出考评记录为 Excel(每个人一个表格) |
| `evaluation-records/download-template/` | GET | 下载人员导入样表 |
| `evaluation-records/import/` | POST | 导入考评记录(支持 Excel/CSV) |
**功能**:
- 支持批量导入和导出
- 样表格式:只需部门、年级、姓名(初始分39分)
- 完整格式:包含详细的加分/扣分记录
- 所有操作发送邮件通知
---
### 7. **认证相关**
### 5. **认证相关**
这是原先的 Django REST Framework JWT 认证端点,主要用于获取和刷新 JWT 令牌。
现在改用 `/api/v1/auth/` 端点来处理登录、注册、重置密码等操作。
@@ -154,33 +97,24 @@ Authorization: Bearer <token>
[schema.sql](./schema.sql)
- users (系统用户/认证信息)
- users/account (系统用户/认证信息)
- departments (组织/部门)
- parent_id -> departments.id (自关联父级)
- personnel (人员信息)
- user_id -> users.id
- department_id -> departments.id
- categories (物品类别)
- parent_id -> categories.id (自关联父级)
- items (物品基本信息)
- category_id -> categories.id
- created_by -> personnel.id
- created_by -> account.id
- item_transactions (物品出入库流水)
- item_id -> items.id (ON DELETE CASCADE)
- operator_id -> personnel.id
- operator_id -> account.id
- borrow_records (借用/归还记录)
- item_id -> items.id (ON DELETE CASCADE)
- borrower_id -> personnel.id (ON DELETE CASCADE)
- approver_id -> personnel.id
- memos (备忘/备注)
- author_id -> personnel.id
- related_item_id -> items.id
- borrower (借入人,字符串)
- approver (借出/审批人,字符串)
- finance_records (财务相关记录)
- related_item_id -> items.id
- recorded_by -> personnel.id
- recorded_by -> account.id
- email_notices (邮件通知规则/历史)
- evaluations (物品/服务评价)
- related_item_id -> items.id
- evaluator_id -> personnel.id
- attachments (文件附件存储索引表)
- uploaded_by -> personnel.id
- uploaded_by -> account.id
+12 -66
View File
@@ -30,26 +30,6 @@ CREATE TABLE departments
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4 COMMENT ='组织/部门';
-- 人员表(可映射到 Django 的 profile)
DROP TABLE IF EXISTS personnel;
CREATE TABLE personnel
(
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT UNSIGNED, -- 关联 account 表(可为空)
employee_no VARCHAR(64) UNIQUE,
name VARCHAR(100) NOT NULL,
department_id BIGINT UNSIGNED,
phone VARCHAR(50),
email VARCHAR(254),
role VARCHAR(100),
is_active TINYINT(1) NOT NULL DEFAULT 1,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
CONSTRAINT fk_personnel_user FOREIGN KEY (user_id) REFERENCES account (id) ON DELETE SET NULL,
CONSTRAINT fk_personnel_department FOREIGN KEY (department_id) REFERENCES departments (id) ON DELETE SET NULL
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4 COMMENT ='人员信息';
-- 物品类别
DROP TABLE IF EXISTS categories;
CREATE TABLE categories
@@ -84,11 +64,11 @@ CREATE TABLE items
status VARCHAR(50) DEFAULT 'normal', -- 如 normal/disabled/lost/repair
description TEXT,
image_path VARCHAR(500),
created_by BIGINT UNSIGNED,
created_by BIGINT UNSIGNED, -- 关联 account.id
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
CONSTRAINT fk_items_category FOREIGN KEY (category_id) REFERENCES categories (id) ON DELETE SET NULL,
CONSTRAINT fk_items_creator FOREIGN KEY (created_by) REFERENCES personnel (id) ON DELETE SET NULL,
CONSTRAINT fk_items_creator FOREIGN KEY (created_by) REFERENCES account (id) ON DELETE SET NULL,
INDEX (name),
INDEX (code)
) ENGINE = InnoDB
@@ -105,11 +85,11 @@ CREATE TABLE item_transactions
from_location VARCHAR(200),
to_location VARCHAR(200),
related_id BIGINT UNSIGNED, -- 可关联采购单/借用记录/调拨单 id
operator_id BIGINT UNSIGNED, -- personnel 操作者
operator_id BIGINT UNSIGNED, -- 关联 account.id(操作者)
note TEXT,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_trans_item FOREIGN KEY (item_id) REFERENCES items (id) ON DELETE CASCADE,
CONSTRAINT fk_trans_operator FOREIGN KEY (operator_id) REFERENCES personnel (id) ON DELETE SET NULL,
CONSTRAINT fk_trans_operator FOREIGN KEY (operator_id) REFERENCES account (id) ON DELETE SET NULL,
INDEX (item_id),
INDEX (trans_type)
) ENGINE = InnoDB
@@ -121,41 +101,22 @@ CREATE TABLE borrow_records
(
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
item_id BIGINT UNSIGNED NOT NULL,
borrower_id BIGINT UNSIGNED NOT NULL, -- personnel id
borrower VARCHAR(100) NOT NULL, -- 借入人(姓名,字符串)
quantity INT NOT NULL DEFAULT 1,
borrow_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
due_date DATETIME DEFAULT NULL,
return_date DATETIME DEFAULT NULL,
status ENUM ('BORROWED','RETURNED','OVERDUE','CANCELED') NOT NULL DEFAULT 'BORROWED',
approver_id BIGINT UNSIGNED DEFAULT NULL,
approver VARCHAR(100) DEFAULT NULL, -- 借出/审批人(姓名,字符串)
note TEXT,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
CONSTRAINT fk_borrow_item FOREIGN KEY (item_id) REFERENCES items (id) ON DELETE CASCADE,
CONSTRAINT fk_borrow_borrower FOREIGN KEY (borrower_id) REFERENCES personnel (id) ON DELETE CASCADE,
CONSTRAINT fk_borrow_approver FOREIGN KEY (approver_id) REFERENCES personnel (id) ON DELETE SET NULL,
INDEX (borrower_id),
INDEX (borrower),
INDEX (status)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4 COMMENT ='借用/归还记录';
-- 备忘/便签
DROP TABLE IF EXISTS memos;
CREATE TABLE memos
(
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(200),
content TEXT,
author_id BIGINT UNSIGNED,
related_item_id BIGINT UNSIGNED,
is_public TINYINT(1) NOT NULL DEFAULT 0,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
CONSTRAINT fk_memo_author FOREIGN KEY (author_id) REFERENCES personnel (id) ON DELETE SET NULL,
CONSTRAINT fk_memo_item FOREIGN KEY (related_item_id) REFERENCES items (id) ON DELETE SET NULL
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4 COMMENT ='备忘/备注';
-- 财务记录(采购/报销等)
DROP TABLE IF EXISTS finance_records;
CREATE TABLE finance_records
@@ -168,11 +129,11 @@ CREATE TABLE finance_records
currency VARCHAR(10) DEFAULT 'CNY',
vendor VARCHAR(200),
occurred_at DATETIME,
recorded_by BIGINT UNSIGNED,
recorded_by BIGINT UNSIGNED, -- 关联 account.id(记录人)
note TEXT,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_finance_item FOREIGN KEY (related_item_id) REFERENCES items (id) ON DELETE SET NULL,
CONSTRAINT fk_finance_user FOREIGN KEY (recorded_by) REFERENCES personnel (id) ON DELETE SET NULL,
CONSTRAINT fk_finance_user FOREIGN KEY (recorded_by) REFERENCES account (id) ON DELETE SET NULL,
INDEX (record_no)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4 COMMENT ='财务相关记录';
@@ -194,35 +155,20 @@ CREATE TABLE email_notices
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4 COMMENT ='邮件通知规则/历史';
-- 评价/反馈(evaluation 模块对应)
DROP TABLE IF EXISTS evaluations;
CREATE TABLE evaluations
(
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
related_item_id BIGINT UNSIGNED,
evaluator_id BIGINT UNSIGNED,
score TINYINT UNSIGNED, -- 1-5
comment TEXT,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_evaluation_item FOREIGN KEY (related_item_id) REFERENCES items (id) ON DELETE SET NULL,
CONSTRAINT fk_evaluation_user FOREIGN KEY (evaluator_id) REFERENCES personnel (id) ON DELETE SET NULL
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4 COMMENT ='物品/服务评价';
-- 附件表(文件存储路径)
DROP TABLE IF EXISTS attachments;
CREATE TABLE attachments
(
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
related_table VARCHAR(100), -- 指示关联表,如 'items','memos'
related_table VARCHAR(100), -- 指示关联表,如 'items'
related_id BIGINT UNSIGNED, -- 关联记录 ID
file_name VARCHAR(300),
file_path VARCHAR(1000),
file_size BIGINT UNSIGNED,
content_type VARCHAR(200),
uploaded_by BIGINT UNSIGNED,
uploaded_by BIGINT UNSIGNED, -- 关联 account.id(上传者)
uploaded_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT fk_attachments_user FOREIGN KEY (uploaded_by) REFERENCES personnel (id) ON DELETE SET NULL,
CONSTRAINT fk_attachments_user FOREIGN KEY (uploaded_by) REFERENCES account (id) ON DELETE SET NULL,
INDEX (related_table, related_id)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4 COMMENT ='文件附件存储索引表';
+3
View File
@@ -5,6 +5,9 @@ build/
!**/src/main/**/build/
!**/src/test/**/build/
src/main/resources/
!src/main/resources/application.yaml
### STS ###
.apt_generated
.classpath
+3 -3
View File
@@ -1,7 +1,7 @@
plugins {
kotlin("jvm") version "2.2.21"
kotlin("plugin.spring") version "2.2.21"
id("org.springframework.boot") version "4.0.6"
kotlin("jvm") version "2.3.21"
kotlin("plugin.spring") version "2.3.21"
id("org.springframework.boot") version "4.1.0"
id("io.spring.dependency-management") version "1.1.7"
}
@@ -1,49 +0,0 @@
package club.itstudio.itmanager.controller.v1
import club.itstudio.itmanager.utils.Const
import org.springframework.validation.annotation.Validated
import org.springframework.web.bind.annotation.*
import reactor.core.publisher.Mono
@Validated
@RestController
@RequestMapping("/api/v1/evaluation-records")
class EvaluationRecordsController {
// evaluation-records/ GET/POST/PUT/DELETE 考评记录 CRUD 操作
@GetMapping("", "/")
fun getRecords(): Mono<String> = Const.INTERNAL_ERROR_MONO
@PostMapping("", "/")
fun createRecord(): Mono<String> = Const.INTERNAL_ERROR_MONO
@PutMapping("", "/")
fun updateRecord(): Mono<String> = Const.INTERNAL_ERROR_MONO
@DeleteMapping("", "/")
fun deleteRecord(@RequestParam(required = false) id: String?): Mono<String> = Const.INTERNAL_ERROR_MONO
// evaluation-records/personnel-summary/ GET 获取人员考评汇总
@GetMapping("/personnel-summary", "/personnel-summary/")
fun getPersonnelSummary(): Mono<String> = Const.INTERNAL_ERROR_MONO
// evaluation-records/personnel-records/ GET 获取某个人员的所有考评记录
@GetMapping("/personnel-records", "/personnel-records/")
fun getPersonnelRecords(@RequestParam(required = false) personId: String?): Mono<String> = Const.INTERNAL_ERROR_MONO
// evaluation-records/delete-personnel/ DELETE 删除某个人员的所有考评记录
@DeleteMapping("/delete-personnel", "/delete-personnel/")
fun deletePersonnelRecords(@RequestParam(required = false) personId: String?): Mono<String> = Const.INTERNAL_ERROR_MONO
// evaluation-records/export/ GET 导出考评记录为 Excel
@GetMapping("/export", "/export/")
fun exportRecords(): Mono<String> = Const.INTERNAL_ERROR_MONO
// evaluation-records/download-template/ GET 下载人员导入样表
@GetMapping("/download-template", "/download-template/")
fun downloadTemplate(): Mono<String> = Const.INTERNAL_ERROR_MONO
// evaluation-records/import/ POST 导入考评记录
@PostMapping("/import", "/import/")
fun importRecords(): Mono<String> = Const.INTERNAL_ERROR_MONO
}
@@ -1,75 +1,178 @@
package club.itstudio.itmanager.controller.v1
import club.itstudio.itmanager.entity.ApiResponse
import club.itstudio.itmanager.entity.dto.Attachment
import club.itstudio.itmanager.entity.dto.Category
import club.itstudio.itmanager.entity.dto.Item
import club.itstudio.itmanager.service.v1.ItemsApiService
import club.itstudio.itmanager.utils.Const
import com.baomidou.mybatisplus.extension.plugins.pagination.Page
import jakarta.validation.Valid
import org.slf4j.LoggerFactory
import org.springframework.http.codec.multipart.FilePart
import org.springframework.validation.annotation.Validated
import org.springframework.web.bind.annotation.*
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
@Validated
@RestController
@RequestMapping("/api/v1/items")
class ItemsController {
class ItemsController(
private val service: ItemsApiService
) {
private val logger = LoggerFactory.getLogger(this::class.java)
// items/ GET/POST/PUT/DELETE 物品 CRUD 操作
@GetMapping("", "/")
fun getItems(): Mono<String> = Const.INTERNAL_ERROR_MONO
fun getItems(
@RequestParam(defaultValue = "1") pageNum: Long,
@RequestParam(defaultValue = "10") pageSize: Long,
@RequestParam(required = false) keyword: String?,
@RequestParam(required = false) status: String?
): Mono<ApiResponse<Any?>> {
logger.info("Trying to list items")
val page = Page<Item>(pageNum, pageSize)
return service.getItems(page, keyword, status)
.map { ApiResponse.success(it) }
}
@PostMapping("", "/")
fun createItem(): Mono<String> = Const.INTERNAL_ERROR_MONO
fun createItem(@RequestBody @Valid item: Item): Mono<ApiResponse<Any?>> {
logger.info("Trying to create item")
return service.createItem(item)
.map(Const::messageHandler)
}
@PutMapping("", "/")
fun updateItem(): Mono<String> = Const.INTERNAL_ERROR_MONO
fun updateItem(@RequestBody @Valid item: Item): Mono<ApiResponse<Any?>> {
logger.info("Trying to update item")
return service.updateItem(item)
.map(Const::messageHandler)
}
@DeleteMapping("", "/")
fun deleteItem(@RequestParam(required = false) id: String?): Mono<String> = Const.INTERNAL_ERROR_MONO
fun deleteItem(@RequestParam(required = false) id: Long?): Mono<ApiResponse<Any?>> {
logger.info("Trying to delete item")
return service.deleteItem(id)
.map(Const::messageHandler)
}
// items/{id}/upload_images/ POST 上传物品图片
@PostMapping("/{id}/upload_images", "/{id}/upload_images/")
fun uploadImages(@PathVariable("id") id: String): Mono<String> = Const.INTERNAL_ERROR_MONO
fun uploadImages(
@PathVariable("id") id: Long,
@RequestPart("files") files: Flux<FilePart>
): Mono<ApiResponse<Any?>> {
logger.info("Trying to upload images for item {}", id)
return service.uploadImages(id, files)
.map(Const::messageHandler)
}
// items/{id}/set_primary_image/ POST 设置主图片
@PostMapping("/{id}/set_primary_image", "/{id}/set_primary_image/")
fun setPrimaryImage(@PathVariable("id") id: String): Mono<String> = Const.INTERNAL_ERROR_MONO
fun setPrimaryImage(
@PathVariable("id") id: Long,
@RequestParam imageId: Long
): Mono<ApiResponse<Any?>> {
logger.info("Trying to set primary image for item {}", id)
return service.setPrimaryImage(id, imageId)
.map(Const::messageHandler)
}
// items/{id}/borrow/ POST 借用物品
@PostMapping("/{id}/borrow", "/{id}/borrow/")
fun borrowItem(@PathVariable("id") id: String): Mono<String> = Const.INTERNAL_ERROR_MONO
fun borrowItem(
@PathVariable("id") id: Long,
@RequestParam(defaultValue = "1") quantity: Int
): Mono<ApiResponse<Any?>> {
logger.info("Trying to borrow item {}", id)
return service.borrowItem(id, quantity)
.map(Const::messageHandler)
}
// items/{id}/return_item/ POST 归还物品
@PostMapping("/{id}/return_item", "/{id}/return_item/")
fun returnItem(@PathVariable("id") id: String): Mono<String> = Const.INTERNAL_ERROR_MONO
fun returnItem(
@PathVariable("id") id: Long,
@RequestParam(defaultValue = "1") quantity: Int
): Mono<ApiResponse<Any?>> {
logger.info("Trying to return item {}", id)
return service.returnItem(id, quantity)
.map(Const::messageHandler)
}
// items/available/ GET 获取可用物品列表
@GetMapping("/available", "/available/")
fun getAvailableItems(): Mono<String> = Const.INTERNAL_ERROR_MONO
fun getAvailableItems(): Mono<ApiResponse<Any?>> {
logger.info("Trying to list available items")
return service.getAvailableItems()
.map { ApiResponse.success(it) }
}
// items/in_use/ GET 获取使用中的物品列表
@GetMapping("/in_use", "/in_use/")
fun getInUseItems(): Mono<String> = Const.INTERNAL_ERROR_MONO
fun getInUseItems(): Mono<ApiResponse<Any?>> {
logger.info("Trying to list in-use items")
return service.getInUseItems()
.map { ApiResponse.success(it) }
}
// items/categories/ GET/POST/PUT/DELETE 物品类别 CRUD 操作
@GetMapping("/categories", "/categories/")
fun getItemCategories(): Mono<String> = Const.INTERNAL_ERROR_MONO
fun getItemCategories(): Mono<ApiResponse<Any?>> {
logger.info("Trying to list item categories")
return service.getItemCategories()
.map { ApiResponse.success(it) }
}
@PostMapping("/categories", "/categories/")
fun createItemCategory(): Mono<String> = Const.INTERNAL_ERROR_MONO
fun createItemCategory(@RequestBody @Valid category: Category): Mono<ApiResponse<Any?>> {
logger.info("Trying to create item category")
return service.createItemCategory(category)
.map(Const::messageHandler)
}
@PutMapping("/categories", "/categories/")
fun updateItemCategory(): Mono<String> = Const.INTERNAL_ERROR_MONO
fun updateItemCategory(@RequestBody @Valid category: Category): Mono<ApiResponse<Any?>> {
logger.info("Trying to update item category")
return service.updateItemCategory(category)
.map(Const::messageHandler)
}
@DeleteMapping("/categories", "/categories/")
fun deleteItemCategory(@RequestParam(required = false) id: String?): Mono<String> = Const.INTERNAL_ERROR_MONO
fun deleteItemCategory(@RequestParam(required = false) id: Long?): Mono<ApiResponse<Any?>> {
logger.info("Trying to delete item category")
return service.deleteItemCategory(id)
.map(Const::messageHandler)
}
// items/images/ GET/POST/PUT/DELETE 物品图片 CRUD 操作
@GetMapping("/images", "/images/")
fun getItemImages(): Mono<String> = Const.INTERNAL_ERROR_MONO
fun getItemImages(): Mono<ApiResponse<Any?>> {
logger.info("Trying to list item images")
return service.getItemImages()
.map { ApiResponse.success(it) }
}
@PostMapping("/images", "/images/")
fun createItemImage(): Mono<String> = Const.INTERNAL_ERROR_MONO
fun createItemImage(@RequestBody @Valid attachment: Attachment): Mono<ApiResponse<Any?>> {
logger.info("Trying to create item image")
return service.createItemImage(attachment)
.map(Const::messageHandler)
}
@PutMapping("/images", "/images/")
fun updateItemImage(): Mono<String> = Const.INTERNAL_ERROR_MONO
fun updateItemImage(@RequestBody @Valid attachment: Attachment): Mono<ApiResponse<Any?>> {
logger.info("Trying to update item image")
return service.updateItemImage(attachment)
.map(Const::messageHandler)
}
@DeleteMapping("/images", "/images/")
fun deleteItemImage(@RequestParam(required = false) id: String?): Mono<String> = Const.INTERNAL_ERROR_MONO
fun deleteItemImage(@RequestParam(required = false) id: Long?): Mono<ApiResponse<Any?>> {
logger.info("Trying to delete item image")
return service.deleteItemImage(id)
.map(Const::messageHandler)
}
}
@@ -1,33 +0,0 @@
package club.itstudio.itmanager.controller.v1
import club.itstudio.itmanager.utils.Const
import org.springframework.validation.annotation.Validated
import org.springframework.web.bind.annotation.*
import reactor.core.publisher.Mono
@Validated
@RestController
@RequestMapping("/api/v1/memos")
class MemosController {
// memos/ GET/POST/PUT/DELETE 备忘录 CRUD 操作
@GetMapping("", "/")
fun getMemos(): Mono<String> = Const.INTERNAL_ERROR_MONO
@PostMapping("", "/")
fun createMemo(): Mono<String> = Const.INTERNAL_ERROR_MONO
@PutMapping("", "/")
fun updateMemo(): Mono<String> = Const.INTERNAL_ERROR_MONO
@DeleteMapping("", "/")
fun deleteMemo(@RequestParam(required = false) id: String?): Mono<String> = Const.INTERNAL_ERROR_MONO
// memos/{id}/upload_image/ POST 上传备忘录图片
@PostMapping("/{id}/upload_image", "/{id}/upload_image/")
fun uploadImage(@PathVariable("id") id: String): Mono<String> = Const.INTERNAL_ERROR_MONO
// memos/{id}/delete_image/ DELETE 删除备忘录图片
@DeleteMapping("/{id}/delete_image", "/{id}/delete_image/")
fun deleteImage(@PathVariable("id") id: String): Mono<String> = Const.INTERNAL_ERROR_MONO
}
@@ -1,45 +0,0 @@
package club.itstudio.itmanager.controller.v1
import club.itstudio.itmanager.utils.Const
import org.springframework.validation.annotation.Validated
import org.springframework.web.bind.annotation.*
import reactor.core.publisher.Mono
@Validated
@RestController
@RequestMapping("/api/v1/personnel")
class PersonnelController {
// personnel/ GET/POST/PUT/DELETE 人员信息 CRUD 操作
@GetMapping("", "/")
fun getPersonnel(): Mono<String> = Const.INTERNAL_ERROR_MONO
@PostMapping("", "/")
fun createPersonnel(): Mono<String> = Const.INTERNAL_ERROR_MONO
@PutMapping("", "/")
fun updatePersonnel(): Mono<String> = Const.INTERNAL_ERROR_MONO
@DeleteMapping("", "/")
fun deletePersonnel(@RequestParam(required = false) id: String?): Mono<String> = Const.INTERNAL_ERROR_MONO
// personnel/statistics/ GET 获取人员统计信息(按部门和职位)
@GetMapping("/statistics", "/statistics/")
fun getStatistics(): Mono<String> = Const.INTERNAL_ERROR_MONO
// personnel/{id}/set_inactive/ POST 设置人员为已卸任状态
@PostMapping("/{id}/set_inactive", "/{id}/set_inactive/")
fun setInactive(@PathVariable("id") id: String): Mono<String> = Const.INTERNAL_ERROR_MONO
// personnel/{id}/set_active/ POST 设置人员为在职状态
@PostMapping("/{id}/set_active", "/{id}/set_active/")
fun setActive(@PathVariable("id") id: String): Mono<String> = Const.INTERNAL_ERROR_MONO
// personnel/{id}/ DELETE 删除人员(发送邮件通知)
@DeleteMapping("/{id}", "/{id}/")
fun deleteById(@PathVariable("id") id: String): Mono<String> = Const.INTERNAL_ERROR_MONO
// personnel/check_expired/ POST 检查并更新已到期的人员状态
@PostMapping("/check_expired", "/check_expired/")
fun checkExpired(): Mono<String> = Const.INTERNAL_ERROR_MONO
}
@@ -1,33 +0,0 @@
package club.itstudio.itmanager.controller.v1
import club.itstudio.itmanager.utils.Const
import org.springframework.validation.annotation.Validated
import org.springframework.web.bind.annotation.*
import reactor.core.publisher.Mono
@Validated
@RestController
@RequestMapping("/api/v1/project-groups")
class ProjectGroupsController {
// project-groups/ GET/POST/PUT/DELETE 项目组 CRUD 操作
@GetMapping("", "/")
fun getProjectGroups(): Mono<String> = Const.INTERNAL_ERROR_MONO
@PostMapping("", "/")
fun createProjectGroup(): Mono<String> = Const.INTERNAL_ERROR_MONO
@PutMapping("", "/")
fun updateProjectGroup(): Mono<String> = Const.INTERNAL_ERROR_MONO
@DeleteMapping("", "/")
fun deleteProjectGroup(@RequestParam(required = false) id: String?): Mono<String> = Const.INTERNAL_ERROR_MONO
// project-groups/by_department/ GET 按部门获取项目组
@GetMapping("/by_department", "/by_department/")
fun getByDepartment(@RequestParam(required = false) departmentId: String?): Mono<String> = Const.INTERNAL_ERROR_MONO
// project-groups/{id}/ DELETE 删除项目组(发送邮件通知)
@DeleteMapping("/{id}", "/{id}/")
fun deleteById(@PathVariable("id") id: String): Mono<String> = Const.INTERNAL_ERROR_MONO
}
@@ -13,13 +13,13 @@ data class BorrowRecord(
@TableId(type = IdType.AUTO)
val id: Long? = null,
val itemId: Long = 0,
val borrowerId: Long = 0,
val borrower: String = "",
val quantity: Int = 1,
val borrowDate: LocalDateTime? = null,
val dueDate: LocalDateTime? = null,
val returnDate: LocalDateTime? = null,
val status: String = "BORROWED", // BORROWED / RETURNED / OVERDUE / CANCELED
val approverId: Long? = null,
val approver: String? = null,
val note: String? = null,
val createdAt: LocalDateTime? = null,
val updatedAt: LocalDateTime? = null
@@ -1,20 +0,0 @@
package club.itstudio.itmanager.entity.dto
import com.baomidou.mybatisplus.annotation.IdType
import com.baomidou.mybatisplus.annotation.TableId
import com.baomidou.mybatisplus.annotation.TableName
import club.itstudio.itmanager.entity.DataCopy
import kotlinx.datetime.LocalDateTime
import kotlinx.serialization.Serializable
@Serializable
@TableName("evaluations")
data class Evaluation(
@TableId(type = IdType.AUTO)
val id: Long? = null,
val relatedItemId: Long? = null,
val evaluatorId: Long? = null,
val score: Short? = null, // 1-5
val comment: String? = null,
val createdAt: LocalDateTime? = null
) : DataCopy
@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.annotation.IdType
import com.baomidou.mybatisplus.annotation.TableId
import com.baomidou.mybatisplus.annotation.TableName
import club.itstudio.itmanager.entity.DataCopy
import club.itstudio.itmanager.entity.enums.TransactionType
import kotlinx.datetime.LocalDateTime
import kotlinx.serialization.Serializable
@@ -13,7 +14,7 @@ data class ItemTransaction(
@TableId(type = IdType.AUTO)
val id: Long? = null,
val itemId: Long = 0,
val transType: String = "", // IN / OUT / ADJUST / TRANSFER
val transType: TransactionType = TransactionType.IN,
val quantity: Int = 0,
val fromLocation: String? = null,
val toLocation: String? = null,
@@ -1,22 +0,0 @@
package club.itstudio.itmanager.entity.dto
import com.baomidou.mybatisplus.annotation.IdType
import com.baomidou.mybatisplus.annotation.TableId
import com.baomidou.mybatisplus.annotation.TableName
import club.itstudio.itmanager.entity.DataCopy
import kotlinx.datetime.LocalDateTime
import kotlinx.serialization.Serializable
@Serializable
@TableName("memos")
data class Memo(
@TableId(type = IdType.AUTO)
val id: Long? = null,
val title: String? = null,
val content: String? = null,
val authorId: Long? = null,
val relatedItemId: Long? = null,
val isPublic: Boolean = false,
val createdAt: LocalDateTime? = null,
val updatedAt: LocalDateTime? = null
) : DataCopy
@@ -1,25 +0,0 @@
package club.itstudio.itmanager.entity.dto
import com.baomidou.mybatisplus.annotation.IdType
import com.baomidou.mybatisplus.annotation.TableId
import com.baomidou.mybatisplus.annotation.TableName
import club.itstudio.itmanager.entity.DataCopy
import kotlinx.datetime.LocalDateTime
import kotlinx.serialization.Serializable
@Serializable
@TableName("personnel")
data class Personnel(
@TableId(type = IdType.AUTO)
val id: Long? = null,
val userId: Long? = null,
val employeeNo: String? = null,
val name: String = "",
val departmentId: Long? = null,
val phone: String? = null,
val email: String? = null,
val role: String? = null,
val isActive: Boolean = true,
val createdAt: LocalDateTime? = null,
val updatedAt: LocalDateTime? = null
) : DataCopy
@@ -0,0 +1,19 @@
package club.itstudio.itmanager.entity.enums
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
enum class TransactionType {
@SerialName("IN")
IN,
@SerialName("OUT")
OUT,
@SerialName("ADJUST")
ADJUST,
@SerialName("TRANSFER")
TRANSFER,
}
@@ -1,8 +0,0 @@
package club.itstudio.itmanager.mapper
import com.baomidou.mybatisplus.core.mapper.BaseMapper
import club.itstudio.itmanager.entity.dto.Evaluation
import org.apache.ibatis.annotations.Mapper
@Mapper
interface EvaluationMapper : BaseMapper<Evaluation>
@@ -1,8 +0,0 @@
package club.itstudio.itmanager.mapper
import com.baomidou.mybatisplus.core.mapper.BaseMapper
import club.itstudio.itmanager.entity.dto.Memo
import org.apache.ibatis.annotations.Mapper
@Mapper
interface MemoMapper : BaseMapper<Memo>
@@ -1,8 +0,0 @@
package club.itstudio.itmanager.mapper
import com.baomidou.mybatisplus.core.mapper.BaseMapper
import club.itstudio.itmanager.entity.dto.Personnel
import org.apache.ibatis.annotations.Mapper
@Mapper
interface PersonnelMapper : BaseMapper<Personnel>
@@ -7,7 +7,7 @@ import reactor.core.publisher.Mono
interface BorrowRecordService : IService<BorrowRecord> {
fun getBorrowRecordById(id: Long): Mono<BorrowRecord>
fun listByItem(itemId: Long): Mono<List<BorrowRecord>>
fun listByBorrower(borrowerId: Long): Mono<List<BorrowRecord>>
fun listByBorrower(borrower: String): Mono<List<BorrowRecord>>
fun listByStatus(status: String): Mono<List<BorrowRecord>>
fun listOverdueRecords(): Mono<List<BorrowRecord>>
fun createBorrowRecord(record: BorrowRecord): Mono<Boolean>
@@ -1,17 +0,0 @@
package club.itstudio.itmanager.service
import com.baomidou.mybatisplus.extension.service.IService
import club.itstudio.itmanager.entity.dto.Evaluation
import reactor.core.publisher.Mono
interface EvaluationService : IService<Evaluation> {
fun getEvaluationById(id: Long): Mono<Evaluation>
fun listByEvaluator(evaluatorId: Long): Mono<List<Evaluation>>
fun listByRelatedItem(itemId: Long): Mono<List<Evaluation>>
fun listByScore(score: Short): Mono<List<Evaluation>>
fun createEvaluation(evaluation: Evaluation): Mono<Boolean>
fun updateEvaluation(evaluation: Evaluation): Mono<Boolean>
fun deleteEvaluation(id: Long): Mono<Boolean>
fun deleteByEvaluator(evaluatorId: Long): Mono<Boolean>
fun exportAllEvaluations(): Mono<List<Evaluation>>
}
@@ -1,17 +0,0 @@
package club.itstudio.itmanager.service
import com.baomidou.mybatisplus.extension.service.IService
import club.itstudio.itmanager.entity.dto.Memo
import reactor.core.publisher.Mono
interface MemoService : IService<Memo> {
fun getMemoById(id: Long): Mono<Memo>
fun searchMemos(keyword: String?): Mono<List<Memo>>
fun listByAuthor(authorId: Long): Mono<List<Memo>>
fun listByRelatedItem(itemId: Long): Mono<List<Memo>>
fun listPublicMemos(): Mono<List<Memo>>
fun createMemo(memo: Memo): Mono<Boolean>
fun updateMemo(memo: Memo): Mono<Boolean>
fun deleteMemo(id: Long): Mono<Boolean>
fun exportAllMemos(): Mono<List<Memo>>
}
@@ -1,17 +0,0 @@
package club.itstudio.itmanager.service
import com.baomidou.mybatisplus.extension.service.IService
import club.itstudio.itmanager.entity.dto.Personnel
import reactor.core.publisher.Mono
interface PersonnelService : IService<Personnel> {
fun getPersonnelById(id: Long): Mono<Personnel>
fun searchPersonnel(keyword: String?): Mono<List<Personnel>>
fun listByDepartment(departmentId: Long): Mono<List<Personnel>>
fun listByRole(role: String): Mono<List<Personnel>>
fun createPersonnel(personnel: Personnel): Mono<Boolean>
fun updatePersonnel(personnel: Personnel): Mono<Boolean>
fun deletePersonnel(id: Long): Mono<Boolean>
fun setActive(id: Long, active: Boolean): Mono<Boolean>
fun exportAllPersonnel(): Mono<List<Personnel>>
}
@@ -30,10 +30,10 @@ class BorrowRecordServiceImpl : ServiceImpl<BorrowRecordMapper, BorrowRecord>(),
ktQuery().eq(BorrowRecord::itemId, itemId).list()
}
override fun listByBorrower(borrowerId: Long): Mono<List<BorrowRecord>> =
override fun listByBorrower(borrower: String): Mono<List<BorrowRecord>> =
blockingCall {
logger.info("Searching borrow records by borrower id")
ktQuery().eq(BorrowRecord::borrowerId, borrowerId).list()
logger.info("Searching borrow records by borrower")
ktQuery().eq(BorrowRecord::borrower, borrower).list()
}
override fun listByStatus(status: String): Mono<List<BorrowRecord>> =
@@ -1,74 +0,0 @@
package club.itstudio.itmanager.service.impl
import club.itstudio.itmanager.entity.dto.Evaluation
import club.itstudio.itmanager.mapper.EvaluationMapper
import club.itstudio.itmanager.service.EvaluationService
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service
import reactor.core.publisher.Mono
import reactor.core.scheduler.Schedulers
import java.util.NoSuchElementException
@Service
class EvaluationServiceImpl : ServiceImpl<EvaluationMapper, Evaluation>(), EvaluationService {
private val logger = LoggerFactory.getLogger(this::class.java)
private fun <T : Any> blockingCall(block: () -> T?): Mono<T> =
Mono.fromCallable(block).subscribeOn(Schedulers.boundedElastic())
override fun getEvaluationById(id: Long): Mono<Evaluation> =
blockingCall {
logger.info("Searching evaluation by id")
ktQuery().eq(Evaluation::id, id).one()
}.switchIfEmpty(Mono.error(NoSuchElementException("Evaluation not found")))
override fun listByEvaluator(evaluatorId: Long): Mono<List<Evaluation>> =
blockingCall {
logger.info("Searching evaluations by evaluator id")
ktQuery().eq(Evaluation::evaluatorId, evaluatorId).list()
}
override fun listByRelatedItem(itemId: Long): Mono<List<Evaluation>> =
blockingCall {
logger.info("Searching evaluations by related item id")
ktQuery().eq(Evaluation::relatedItemId, itemId).list()
}
override fun listByScore(score: Short): Mono<List<Evaluation>> =
blockingCall {
logger.info("Searching evaluations by score")
ktQuery().eq(Evaluation::score, score).list()
}
override fun createEvaluation(evaluation: Evaluation): Mono<Boolean> =
blockingCall {
logger.info("Creating evaluation")
save(evaluation)
}
override fun updateEvaluation(evaluation: Evaluation): Mono<Boolean> =
blockingCall {
logger.info("Updating evaluation")
updateById(evaluation)
}
override fun deleteEvaluation(id: Long): Mono<Boolean> =
blockingCall {
logger.info("Deleting evaluation by id")
removeById(id)
}
override fun deleteByEvaluator(evaluatorId: Long): Mono<Boolean> =
blockingCall {
logger.info("Deleting evaluations by evaluator id")
remove(ktQuery().eq(Evaluation::evaluatorId, evaluatorId))
}
override fun exportAllEvaluations(): Mono<List<Evaluation>> =
blockingCall {
logger.info("Exporting all evaluations")
list()
}
}
@@ -1,80 +0,0 @@
package club.itstudio.itmanager.service.impl
import club.itstudio.itmanager.entity.dto.Memo
import club.itstudio.itmanager.mapper.MemoMapper
import club.itstudio.itmanager.service.MemoService
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service
import reactor.core.publisher.Mono
import reactor.core.scheduler.Schedulers
import java.util.NoSuchElementException
@Service
class MemoServiceImpl : ServiceImpl<MemoMapper, Memo>(), MemoService {
private val logger = LoggerFactory.getLogger(this::class.java)
private fun <T : Any> blockingCall(block: () -> T?): Mono<T> =
Mono.fromCallable(block).subscribeOn(Schedulers.boundedElastic())
override fun getMemoById(id: Long): Mono<Memo> =
blockingCall {
logger.info("Searching memo by id")
ktQuery().eq(Memo::id, id).one()
}.switchIfEmpty(Mono.error(NoSuchElementException("Memo not found")))
override fun searchMemos(keyword: String?): Mono<List<Memo>> =
blockingCall {
logger.info("Searching memos with keyword")
if (keyword.isNullOrBlank()) {
list()
} else {
ktQuery().and {
it.like(Memo::title, keyword).or().like(Memo::content, keyword)
}.list()
}
}
override fun listByAuthor(authorId: Long): Mono<List<Memo>> =
blockingCall {
logger.info("Searching memos by author id")
ktQuery().eq(Memo::authorId, authorId).list()
}
override fun listByRelatedItem(itemId: Long): Mono<List<Memo>> =
blockingCall {
logger.info("Searching memos by related item id")
ktQuery().eq(Memo::relatedItemId, itemId).list()
}
override fun listPublicMemos(): Mono<List<Memo>> =
blockingCall {
logger.info("Listing public memos")
ktQuery().eq(Memo::isPublic, true).list()
}
override fun createMemo(memo: Memo): Mono<Boolean> =
blockingCall {
logger.info("Creating memo")
save(memo)
}
override fun updateMemo(memo: Memo): Mono<Boolean> =
blockingCall {
logger.info("Updating memo")
updateById(memo)
}
override fun deleteMemo(id: Long): Mono<Boolean> =
blockingCall {
logger.info("Deleting memo by id")
removeById(id)
}
override fun exportAllMemos(): Mono<List<Memo>> =
blockingCall {
logger.info("Exporting all memos")
list()
}
}
@@ -1,83 +0,0 @@
package club.itstudio.itmanager.service.impl
import club.itstudio.itmanager.entity.dto.Personnel
import club.itstudio.itmanager.mapper.PersonnelMapper
import club.itstudio.itmanager.service.PersonnelService
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service
import reactor.core.publisher.Mono
import reactor.core.scheduler.Schedulers
import java.util.NoSuchElementException
@Service
class PersonnelServiceImpl : ServiceImpl<PersonnelMapper, Personnel>(), PersonnelService {
private val logger = LoggerFactory.getLogger(this::class.java)
private fun <T : Any> blockingCall(block: () -> T?): Mono<T> =
Mono.fromCallable(block).subscribeOn(Schedulers.boundedElastic())
override fun getPersonnelById(id: Long): Mono<Personnel> =
blockingCall {
logger.info("Searching personnel by id")
ktQuery().eq(Personnel::id, id).one()
}.switchIfEmpty(Mono.error(NoSuchElementException("Personnel not found")))
override fun searchPersonnel(keyword: String?): Mono<List<Personnel>> =
blockingCall {
logger.info("Searching personnel with keyword")
if (keyword.isNullOrBlank()) {
list()
} else {
ktQuery().and {
it.like(Personnel::name, keyword)
.or().like(Personnel::employeeNo, keyword)
.or().like(Personnel::phone, keyword)
.or().like(Personnel::email, keyword)
}.list()
}
}
override fun listByDepartment(departmentId: Long): Mono<List<Personnel>> =
blockingCall {
logger.info("Searching personnel by department id")
ktQuery().eq(Personnel::departmentId, departmentId).list()
}
override fun listByRole(role: String): Mono<List<Personnel>> =
blockingCall {
logger.info("Searching personnel by role")
ktQuery().eq(Personnel::role, role).list()
}
override fun createPersonnel(personnel: Personnel): Mono<Boolean> =
blockingCall {
logger.info("Creating personnel")
save(personnel)
}
override fun updatePersonnel(personnel: Personnel): Mono<Boolean> =
blockingCall {
logger.info("Updating personnel")
updateById(personnel)
}
override fun deletePersonnel(id: Long): Mono<Boolean> =
blockingCall {
logger.info("Deleting personnel by id")
removeById(id)
}
override fun setActive(id: Long, active: Boolean): Mono<Boolean> =
blockingCall {
logger.info("Setting personnel active status")
ktUpdate().eq(Personnel::id, id).set(Personnel::isActive, active).update()
}
override fun exportAllPersonnel(): Mono<List<Personnel>> =
blockingCall {
logger.info("Exporting all personnel")
list()
}
}
@@ -4,6 +4,7 @@ import club.itstudio.itmanager.entity.dto.Attachment
import club.itstudio.itmanager.entity.dto.Category
import club.itstudio.itmanager.entity.dto.Item
import club.itstudio.itmanager.entity.dto.ItemTransaction
import club.itstudio.itmanager.entity.enums.TransactionType
import club.itstudio.itmanager.service.AttachmentService
import club.itstudio.itmanager.service.CategoryService
import club.itstudio.itmanager.service.ItemService
@@ -15,12 +16,339 @@ import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Value
import org.springframework.http.codec.multipart.FilePart
import org.springframework.stereotype.Service
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
import reactor.core.scheduler.Schedulers
import java.io.IOException
import java.nio.file.Files
import java.nio.file.Paths
import java.util.NoSuchElementException
import java.util.UUID
@Service
class ItemsApiService
class ItemsApiService(
private val itemService: ItemService,
private val categoryService: CategoryService,
private val attachmentService: AttachmentService,
private val transactionService: ItemTransactionService,
@Value($$"${app.file.data-path}")
private val dataPath: String
) {
private val logger = LoggerFactory.getLogger(this::class.java)
fun getItems(page: Page<Item>, keyword: String?, status: String?): Mono<Page<Item>> {
logger.info("Listing items with keyword and status")
return itemService.pageItems(page, keyword, status)
}
fun createItem(item: Item): Mono<String> {
logger.info("Creating item")
val rightNow = getCurrentDateTime()
return itemService.createItem(
item.copy(
id = null,
createdAt = rightNow,
updatedAt = rightNow
)
).flatMap { success ->
if (success) Mono.just("")
else Mono.just("item create failed.")
}
}
fun updateItem(item: Item): Mono<String> {
logger.info("Updating item")
if (item.id == null) return Mono.just("item id is required.")
return itemService.updateItem(
item.copy(updatedAt = getCurrentDateTime())
).flatMap { success ->
if (success) Mono.just("")
else Mono.just("item update failed.")
}
}
fun deleteItem(id: Long?): Mono<String> {
logger.info("Deleting item with id {}", id)
if (id == null) return Mono.just("item id is required.")
return itemService.removeItemById(id).flatMap { success ->
if (success) {
cleanupItemAttachments(id)
} else {
Mono.just("item delete failed.")
}
}
}
fun uploadImages(id: Long, files: Flux<FilePart>): Mono<String> {
logger.info("Uploading images for item {}", id)
return itemService.getItemById(id)
.flatMap { item ->
files.flatMap { file -> saveItemImage(id, file) }
.collectList()
.flatMap { attachments ->
if (attachments.isEmpty()) {
Mono.just("no image files provided.")
} else {
Flux.fromIterable(attachments)
.flatMap { attachmentService.createAttachment(it) }
.collectList()
.flatMap { results ->
if (results.any { !it }) {
Mono.just("item image upload failed.")
} else {
setPrimaryImageIfBlank(item, attachments.first(), id)
}
}
}
}
}
.onErrorResume(NoSuchElementException::class.java) { Mono.just("item not found.") }
.onErrorResume { Mono.just(Const.INTERNAL_ERROR_STR) }
}
fun setPrimaryImage(id: Long, imageId: Long): Mono<String> {
logger.info("Setting primary image for item {}", id)
return itemService.getItemById(id)
.flatMap { _ ->
attachmentService.getAttachmentById(imageId)
.flatMap { attachment ->
if (attachment.relatedTable == "items" && attachment.relatedId == id) {
val path = attachment.filePath
if (path.isNullOrBlank()) {
Mono.just("image file path is empty.")
} else {
itemService.setPrimaryImage(id, path).flatMap { success ->
if (success) Mono.just("")
else Mono.just("set primary image failed.")
}
}
} else {
Mono.just("image does not belong to this item.")
}
}
.onErrorResume(NoSuchElementException::class.java) { _ -> Mono.just("image not found.") }
}
.onErrorResume(NoSuchElementException::class.java) { _ -> Mono.just("item not found.") }
.onErrorResume { _ -> Mono.just(Const.INTERNAL_ERROR_STR) }
}
fun borrowItem(id: Long, quantity: Int): Mono<String> {
logger.info("Borrowing item {} with quantity {}", id, quantity)
if (quantity <= 0) return Mono.just("quantity must be positive.")
return itemService.getItemById(id)
.flatMap { item ->
itemService.borrowItem(id, quantity)
.flatMap { success ->
if (success) {
transactionService.createTransaction(
ItemTransaction(
itemId = id,
transType = TransactionType.OUT,
quantity = quantity,
fromLocation = item.location,
createdAt = getCurrentDateTime()
)
).onErrorResume { error ->
logger.warn("Failed to record OUT transaction for item {}: {}", id, error.message)
Mono.just(false)
}.thenReturn("")
} else {
Mono.just("item available quantity is not enough.")
}
}
}
.onErrorResume(NoSuchElementException::class.java) { Mono.just("item not found.") }
.onErrorResume { Mono.just(Const.INTERNAL_ERROR_STR) }
}
fun returnItem(id: Long, quantity: Int): Mono<String> {
logger.info("Returning item {} with quantity {}", id, quantity)
if (quantity <= 0) return Mono.just("quantity must be positive.")
return itemService.getItemById(id)
.flatMap { item ->
itemService.returnItem(id, quantity)
.flatMap { success ->
if (success) {
transactionService.createTransaction(
ItemTransaction(
itemId = id,
transType = TransactionType.IN,
quantity = quantity,
toLocation = item.location,
createdAt = getCurrentDateTime()
)
).onErrorResume { error ->
logger.warn("Failed to record IN transaction for item {}: {}", id, error.message)
Mono.just(false)
}.thenReturn("")
} else {
Mono.just("item cannot be returned with the given quantity.")
}
}
}
.onErrorResume(NoSuchElementException::class.java) { Mono.just("item not found.") }
.onErrorResume { Mono.just(Const.INTERNAL_ERROR_STR) }
}
fun getAvailableItems(): Mono<List<Item>> {
logger.info("Listing available items")
return itemService.listAvailableItems()
}
fun getInUseItems(): Mono<List<Item>> {
logger.info("Listing in-use items")
return itemService.listInUseItems()
}
fun getItemCategories(): Mono<List<Category>> {
logger.info("Listing item categories")
return categoryService.listAllCategories()
}
fun createItemCategory(category: Category): Mono<String> {
logger.info("Creating item category")
return categoryService.createCategory(
category.copy(
id = null,
createdAt = getCurrentDateTime(),
updatedAt = getCurrentDateTime()
)
).flatMap { success ->
if (success) Mono.just("")
else Mono.just("category create failed.")
}
}
fun updateItemCategory(category: Category): Mono<String> {
logger.info("Updating item category")
if (category.id == null) return Mono.just("category id is required.")
return categoryService.updateCategory(
category.copy(updatedAt = getCurrentDateTime())
).flatMap { success ->
if (success) Mono.just("")
else Mono.just("category update failed.")
}
}
fun deleteItemCategory(id: Long?): Mono<String> {
logger.info("Deleting item category with id {}", id)
if (id == null) return Mono.just("category id is required.")
return categoryService.deleteCategory(id).flatMap { success ->
if (success) Mono.just("")
else Mono.just("category delete failed.")
}
}
fun getItemImages(): Mono<List<Attachment>> {
logger.info("Listing item images")
return attachmentService.exportAllAttachments()
.map { list -> list.filter { it.relatedTable == "items" } }
}
fun createItemImage(attachment: Attachment): Mono<String> {
logger.info("Creating item image")
return attachmentService.createAttachment(
attachment.copy(
id = null,
uploadedAt = getCurrentDateTime()
)
).flatMap { success ->
if (success) Mono.just("")
else Mono.just("item image create failed.")
}
}
fun updateItemImage(attachment: Attachment): Mono<String> {
logger.info("Updating item image")
if (attachment.id == null) return Mono.just("item image id is required.")
return Mono.fromCallable { attachmentService.updateById(attachment) }
.subscribeOn(Schedulers.boundedElastic())
.flatMap { success ->
if (success) Mono.just("")
else Mono.just("item image update failed.")
}
}
fun deleteItemImage(id: Long?): Mono<String> {
logger.info("Deleting item image with id {}", id)
if (id == null) return Mono.just("item image id is required.")
return attachmentService.getAttachmentById(id)
.flatMap { attachment ->
attachmentService.deleteAttachment(id).flatMap { success ->
if (success) {
deletePhysicalFile(attachment.filePath).thenReturn("")
} else {
Mono.just("item image delete failed.")
}
}
}
.onErrorResume(NoSuchElementException::class.java) { Mono.just("item image not found.") }
.onErrorResume { Mono.just(Const.INTERNAL_ERROR_STR) }
}
private fun setPrimaryImageIfBlank(item: Item, attachment: Attachment, id: Long): Mono<String> {
if (!item.imagePath.isNullOrBlank()) return Mono.just("")
val path = attachment.filePath ?: return Mono.just("")
return itemService.setPrimaryImage(id, path).flatMap { success ->
if (success) Mono.just("")
else Mono.just("set primary image failed.")
}
}
private fun saveItemImage(itemId: Long, file: FilePart): Mono<Attachment> {
val directory = Paths.get(dataPath, Const.FILE_DIR_IMAGE, "items", itemId.toString())
val savedName = generateSafeFileName(file.filename())
val target = directory.resolve(savedName)
return Mono.fromCallable {
Files.createDirectories(directory)
file.transferTo(target)
Attachment(
relatedTable = "items",
relatedId = itemId,
fileName = savedName,
filePath = target.toString(),
fileSize = Files.size(target),
contentType = file.headers().contentType?.toString(),
uploadedAt = getCurrentDateTime()
)
}.subscribeOn(Schedulers.boundedElastic())
}
private fun generateSafeFileName(original: String?): String {
val extension = original
?.substringAfterLast('.')
?.lowercase()
?.takeIf {
it.isNotBlank() &&
it.length <= 10 &&
it.all { ch -> ch.isLetterOrDigit() }
}
return if (extension != null) "${UUID.randomUUID()}.$extension" else UUID.randomUUID().toString()
}
private fun cleanupItemAttachments(itemId: Long): Mono<String> {
return attachmentService.listByRelated("items", itemId)
.flatMap { attachments ->
Flux.fromIterable(attachments)
.flatMap { deletePhysicalFile(it.filePath) }
.then(attachmentService.deleteByRelated("items", itemId))
.thenReturn("")
}
.onErrorResume { error ->
logger.warn("Failed to clean up attachments for item {}: {}", itemId, error.message)
Mono.just("")
}
}
private fun deletePhysicalFile(path: String?): Mono<Void> {
if (path.isNullOrBlank()) return Mono.empty()
return Mono.fromRunnable<Void> {
try {
Files.deleteIfExists(Paths.get(path))
} catch (e: IOException) {
logger.warn("Failed to delete file: {}", path, e)
}
}.subscribeOn(Schedulers.boundedElastic())
}
}
@@ -25,17 +25,14 @@ spring:
security:
jwt:
key: 6846513216
#key: 6846513216
expire-hours: 168
datasource:
url: jdbc:mariadb://localhost:3306/kotlin_springboot
driver-class-name: org.mariadb.jdbc.Driver
username: root
password: 123456
mybatis-plus:
type-handlers-package: com.github.sor2171.backend.config
#username: root
#password: 123456
app:
flow-limit: