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$"> <content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/src/beckend/.kotlin" /> <excludeFolder url="file://$MODULE_DIR$/src/beckend/.kotlin" />
<excludeFolder url="file://$MODULE_DIR$/src/beckend/build" /> <excludeFolder url="file://$MODULE_DIR$/src/beckend/build" />
<excludeFolder url="file://$MODULE_DIR$/.idea/dataSources" />
</content> </content>
<orderEntry type="inheritedJdk" /> <orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" /> <orderEntry type="sourceFolder" forTests="false" />
+3 -3
View File
@@ -7,11 +7,11 @@
<option name="jvmTarget" value="21" /> <option name="jvmTarget" value="21" />
</component> </component>
<component name="KotlinCommonCompilerArguments"> <component name="KotlinCommonCompilerArguments">
<option name="apiVersion" value="2.2" /> <option name="apiVersion" value="2.3" />
<option name="languageVersion" value="2.2" /> <option name="languageVersion" value="2.3" />
</component> </component>
<component name="KotlinJpsPluginSettings"> <component name="KotlinJpsPluginSettings">
<option name="externalSystemId" value="Gradle" /> <option name="externalSystemId" value="Gradle" />
<option name="version" value="2.2.21" /> <option name="version" value="2.3.21" />
</component> </component>
</project> </project>
+1
View File
@@ -18,6 +18,7 @@ class Response<T> {
- 除特别标注(公开)外,接口需在请求头携带 `Authorization: Bearer <token>` - 除特别标注(公开)外,接口需在请求头携带 `Authorization: Bearer <token>`
- 下文路径均省略 `/api` 前缀,`data` 仅给出 `T` 的声明 - 下文路径均省略 `/api` 前缀,`data` 仅给出 `T` 的声明
- 部分 `/test` 接口的返回值不遵循上述格式 - 部分 `/test` 接口的返回值不遵循上述格式
- 前端类示例使用 **TypeScript** 类定义
## 接口总览 ## 接口总览
+8 -74
View File
@@ -17,15 +17,10 @@
- /reset - /reset
- /departments - /departments
- /email - /email
- /evaluation-records
- /finance - /finance
- /items - /items
- /memos
- /personnel
- /project-groups
- /proof-images - /proof-images
- /usages - /usages
- /users
## 预计完成的 API 列表 ## 预计完成的 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)** ### 3. **财务管理 (Finance)**
| 端点 | 方法 | 功能说明 | | 端点 | 方法 | 功能说明 |
@@ -95,41 +72,7 @@
--- ---
### 5. **备忘录 (Memo)** ### 5. **认证相关**
| 端点 | 方法 | 功能说明 |
|----------------------------|---------------------|------------------|
| `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. **认证相关**
这是原先的 Django REST Framework JWT 认证端点,主要用于获取和刷新 JWT 令牌。 这是原先的 Django REST Framework JWT 认证端点,主要用于获取和刷新 JWT 令牌。
现在改用 `/api/v1/auth/` 端点来处理登录、注册、重置密码等操作。 现在改用 `/api/v1/auth/` 端点来处理登录、注册、重置密码等操作。
@@ -154,33 +97,24 @@ Authorization: Bearer <token>
[schema.sql](./schema.sql) [schema.sql](./schema.sql)
- users (系统用户/认证信息) - users/account (系统用户/认证信息)
- departments (组织/部门) - departments (组织/部门)
- parent_id -> departments.id (自关联父级) - parent_id -> departments.id (自关联父级)
- personnel (人员信息)
- user_id -> users.id
- department_id -> departments.id
- categories (物品类别) - categories (物品类别)
- parent_id -> categories.id (自关联父级) - parent_id -> categories.id (自关联父级)
- items (物品基本信息) - items (物品基本信息)
- category_id -> categories.id - category_id -> categories.id
- created_by -> personnel.id - created_by -> account.id
- item_transactions (物品出入库流水) - item_transactions (物品出入库流水)
- item_id -> items.id (ON DELETE CASCADE) - item_id -> items.id (ON DELETE CASCADE)
- operator_id -> personnel.id - operator_id -> account.id
- borrow_records (借用/归还记录) - borrow_records (借用/归还记录)
- item_id -> items.id (ON DELETE CASCADE) - item_id -> items.id (ON DELETE CASCADE)
- borrower_id -> personnel.id (ON DELETE CASCADE) - borrower (借入人,字符串)
- approver_id -> personnel.id - approver (借出/审批人,字符串)
- memos (备忘/备注)
- author_id -> personnel.id
- related_item_id -> items.id
- finance_records (财务相关记录) - finance_records (财务相关记录)
- related_item_id -> items.id - related_item_id -> items.id
- recorded_by -> personnel.id - recorded_by -> account.id
- email_notices (邮件通知规则/历史) - email_notices (邮件通知规则/历史)
- evaluations (物品/服务评价)
- related_item_id -> items.id
- evaluator_id -> personnel.id
- attachments (文件附件存储索引表) - attachments (文件附件存储索引表)
- uploaded_by -> personnel.id - uploaded_by -> account.id
+12 -66
View File
@@ -30,26 +30,6 @@ CREATE TABLE departments
) ENGINE = InnoDB ) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4 COMMENT ='组织/部门'; 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; DROP TABLE IF EXISTS categories;
CREATE TABLE categories CREATE TABLE categories
@@ -84,11 +64,11 @@ CREATE TABLE items
status VARCHAR(50) DEFAULT 'normal', -- 如 normal/disabled/lost/repair status VARCHAR(50) DEFAULT 'normal', -- 如 normal/disabled/lost/repair
description TEXT, description TEXT,
image_path VARCHAR(500), image_path VARCHAR(500),
created_by BIGINT UNSIGNED, created_by BIGINT UNSIGNED, -- 关联 account.id
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE 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_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 (name),
INDEX (code) INDEX (code)
) ENGINE = InnoDB ) ENGINE = InnoDB
@@ -105,11 +85,11 @@ CREATE TABLE item_transactions
from_location VARCHAR(200), from_location VARCHAR(200),
to_location VARCHAR(200), to_location VARCHAR(200),
related_id BIGINT UNSIGNED, -- 可关联采购单/借用记录/调拨单 id related_id BIGINT UNSIGNED, -- 可关联采购单/借用记录/调拨单 id
operator_id BIGINT UNSIGNED, -- personnel 操作者 operator_id BIGINT UNSIGNED, -- 关联 account.id(操作者)
note TEXT, note TEXT,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, 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_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 (item_id),
INDEX (trans_type) INDEX (trans_type)
) ENGINE = InnoDB ) ENGINE = InnoDB
@@ -121,41 +101,22 @@ CREATE TABLE borrow_records
( (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
item_id BIGINT UNSIGNED NOT NULL, 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, quantity INT NOT NULL DEFAULT 1,
borrow_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, borrow_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
due_date DATETIME DEFAULT NULL, due_date DATETIME DEFAULT NULL,
return_date DATETIME DEFAULT NULL, return_date DATETIME DEFAULT NULL,
status ENUM ('BORROWED','RETURNED','OVERDUE','CANCELED') NOT NULL DEFAULT 'BORROWED', status ENUM ('BORROWED','RETURNED','OVERDUE','CANCELED') NOT NULL DEFAULT 'BORROWED',
approver_id BIGINT UNSIGNED DEFAULT NULL, approver VARCHAR(100) DEFAULT NULL, -- 借出/审批人(姓名,字符串)
note TEXT, note TEXT,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE 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_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, INDEX (borrower),
CONSTRAINT fk_borrow_approver FOREIGN KEY (approver_id) REFERENCES personnel (id) ON DELETE SET NULL,
INDEX (borrower_id),
INDEX (status) INDEX (status)
) ENGINE = InnoDB ) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4 COMMENT ='借用/归还记录'; 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; DROP TABLE IF EXISTS finance_records;
CREATE TABLE finance_records CREATE TABLE finance_records
@@ -168,11 +129,11 @@ CREATE TABLE finance_records
currency VARCHAR(10) DEFAULT 'CNY', currency VARCHAR(10) DEFAULT 'CNY',
vendor VARCHAR(200), vendor VARCHAR(200),
occurred_at DATETIME, occurred_at DATETIME,
recorded_by BIGINT UNSIGNED, recorded_by BIGINT UNSIGNED, -- 关联 account.id(记录人)
note TEXT, note TEXT,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, 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_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) INDEX (record_no)
) ENGINE = InnoDB ) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4 COMMENT ='财务相关记录'; DEFAULT CHARSET = utf8mb4 COMMENT ='财务相关记录';
@@ -194,35 +155,20 @@ CREATE TABLE email_notices
) ENGINE = InnoDB ) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4 COMMENT ='邮件通知规则/历史'; 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; DROP TABLE IF EXISTS attachments;
CREATE TABLE attachments CREATE TABLE attachments
( (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, 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 related_id BIGINT UNSIGNED, -- 关联记录 ID
file_name VARCHAR(300), file_name VARCHAR(300),
file_path VARCHAR(1000), file_path VARCHAR(1000),
file_size BIGINT UNSIGNED, file_size BIGINT UNSIGNED,
content_type VARCHAR(200), content_type VARCHAR(200),
uploaded_by BIGINT UNSIGNED, uploaded_by BIGINT UNSIGNED, -- 关联 account.id(上传者)
uploaded_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, 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) INDEX (related_table, related_id)
) ENGINE = InnoDB ) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4 COMMENT ='文件附件存储索引表'; DEFAULT CHARSET = utf8mb4 COMMENT ='文件附件存储索引表';
+3
View File
@@ -5,6 +5,9 @@ build/
!**/src/main/**/build/ !**/src/main/**/build/
!**/src/test/**/build/ !**/src/test/**/build/
src/main/resources/
!src/main/resources/application.yaml
### STS ### ### STS ###
.apt_generated .apt_generated
.classpath .classpath
+3 -3
View File
@@ -1,7 +1,7 @@
plugins { plugins {
kotlin("jvm") version "2.2.21" kotlin("jvm") version "2.3.21"
kotlin("plugin.spring") version "2.2.21" kotlin("plugin.spring") version "2.3.21"
id("org.springframework.boot") version "4.0.6" id("org.springframework.boot") version "4.1.0"
id("io.spring.dependency-management") version "1.1.7" 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 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 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.validation.annotation.Validated
import org.springframework.web.bind.annotation.* import org.springframework.web.bind.annotation.*
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono import reactor.core.publisher.Mono
@Validated @Validated
@RestController @RestController
@RequestMapping("/api/v1/items") @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 操作 // items/ GET/POST/PUT/DELETE 物品 CRUD 操作
@GetMapping("", "/") @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("", "/") @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("", "/") @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("", "/") @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 上传物品图片 // items/{id}/upload_images/ POST 上传物品图片
@PostMapping("/{id}/upload_images", "/{id}/upload_images/") @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 设置主图片 // items/{id}/set_primary_image/ POST 设置主图片
@PostMapping("/{id}/set_primary_image", "/{id}/set_primary_image/") @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 借用物品 // items/{id}/borrow/ POST 借用物品
@PostMapping("/{id}/borrow", "/{id}/borrow/") @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 归还物品 // items/{id}/return_item/ POST 归还物品
@PostMapping("/{id}/return_item", "/{id}/return_item/") @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 获取可用物品列表 // items/available/ GET 获取可用物品列表
@GetMapping("/available", "/available/") @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 获取使用中的物品列表 // items/in_use/ GET 获取使用中的物品列表
@GetMapping("/in_use", "/in_use/") @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 操作 // items/categories/ GET/POST/PUT/DELETE 物品类别 CRUD 操作
@GetMapping("/categories", "/categories/") @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/") @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/") @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/") @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 操作 // items/images/ GET/POST/PUT/DELETE 物品图片 CRUD 操作
@GetMapping("/images", "/images/") @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/") @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/") @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/") @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) @TableId(type = IdType.AUTO)
val id: Long? = null, val id: Long? = null,
val itemId: Long = 0, val itemId: Long = 0,
val borrowerId: Long = 0, val borrower: String = "",
val quantity: Int = 1, val quantity: Int = 1,
val borrowDate: LocalDateTime? = null, val borrowDate: LocalDateTime? = null,
val dueDate: LocalDateTime? = null, val dueDate: LocalDateTime? = null,
val returnDate: LocalDateTime? = null, val returnDate: LocalDateTime? = null,
val status: String = "BORROWED", // BORROWED / RETURNED / OVERDUE / CANCELED val status: String = "BORROWED", // BORROWED / RETURNED / OVERDUE / CANCELED
val approverId: Long? = null, val approver: String? = null,
val note: String? = null, val note: String? = null,
val createdAt: LocalDateTime? = null, val createdAt: LocalDateTime? = null,
val updatedAt: 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.TableId
import com.baomidou.mybatisplus.annotation.TableName import com.baomidou.mybatisplus.annotation.TableName
import club.itstudio.itmanager.entity.DataCopy import club.itstudio.itmanager.entity.DataCopy
import club.itstudio.itmanager.entity.enums.TransactionType
import kotlinx.datetime.LocalDateTime import kotlinx.datetime.LocalDateTime
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@@ -13,7 +14,7 @@ data class ItemTransaction(
@TableId(type = IdType.AUTO) @TableId(type = IdType.AUTO)
val id: Long? = null, val id: Long? = null,
val itemId: Long = 0, val itemId: Long = 0,
val transType: String = "", // IN / OUT / ADJUST / TRANSFER val transType: TransactionType = TransactionType.IN,
val quantity: Int = 0, val quantity: Int = 0,
val fromLocation: String? = null, val fromLocation: String? = null,
val toLocation: 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> { interface BorrowRecordService : IService<BorrowRecord> {
fun getBorrowRecordById(id: Long): Mono<BorrowRecord> fun getBorrowRecordById(id: Long): Mono<BorrowRecord>
fun listByItem(itemId: Long): Mono<List<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 listByStatus(status: String): Mono<List<BorrowRecord>>
fun listOverdueRecords(): Mono<List<BorrowRecord>> fun listOverdueRecords(): Mono<List<BorrowRecord>>
fun createBorrowRecord(record: BorrowRecord): Mono<Boolean> 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() ktQuery().eq(BorrowRecord::itemId, itemId).list()
} }
override fun listByBorrower(borrowerId: Long): Mono<List<BorrowRecord>> = override fun listByBorrower(borrower: String): Mono<List<BorrowRecord>> =
blockingCall { blockingCall {
logger.info("Searching borrow records by borrower id") logger.info("Searching borrow records by borrower")
ktQuery().eq(BorrowRecord::borrowerId, borrowerId).list() ktQuery().eq(BorrowRecord::borrower, borrower).list()
} }
override fun listByStatus(status: String): Mono<List<BorrowRecord>> = 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.Category
import club.itstudio.itmanager.entity.dto.Item import club.itstudio.itmanager.entity.dto.Item
import club.itstudio.itmanager.entity.dto.ItemTransaction 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.AttachmentService
import club.itstudio.itmanager.service.CategoryService import club.itstudio.itmanager.service.CategoryService
import club.itstudio.itmanager.service.ItemService import club.itstudio.itmanager.service.ItemService
@@ -15,12 +16,339 @@ import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Value import org.springframework.beans.factory.annotation.Value
import org.springframework.http.codec.multipart.FilePart import org.springframework.http.codec.multipart.FilePart
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono import reactor.core.publisher.Mono
import reactor.core.scheduler.Schedulers import reactor.core.scheduler.Schedulers
import java.io.IOException import java.io.IOException
import java.nio.file.Files import java.nio.file.Files
import java.nio.file.Paths import java.nio.file.Paths
import java.util.NoSuchElementException
import java.util.UUID import java.util.UUID
@Service @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: security:
jwt: jwt:
key: 6846513216 #key: 6846513216
expire-hours: 168 expire-hours: 168
datasource: datasource:
url: jdbc:mariadb://localhost:3306/kotlin_springboot url: jdbc:mariadb://localhost:3306/kotlin_springboot
driver-class-name: org.mariadb.jdbc.Driver driver-class-name: org.mariadb.jdbc.Driver
username: root #username: root
password: 123456 #password: 123456
mybatis-plus:
type-handlers-package: com.github.sor2171.backend.config
app: app:
flow-limit: flow-limit: