Compare commits
10 Commits
934ea1fc0d
...
3efcb394fb
| Author | SHA1 | Date | |
|---|---|---|---|
| 3efcb394fb | |||
| 46efa3b781 | |||
| 2d4a7e2cdd | |||
| 23470ebda3 | |||
| ff044e6aab | |||
| 5dc91a4e8e | |||
| 8dc208d77d | |||
| d917a6247c | |||
| 63738f108a | |||
| 4155d5b1ea |
+1
-1
@@ -14,7 +14,7 @@ project_doc_fallback_filenames = ["AGENTS.md"]
|
|||||||
# Without this flag, hooks.json is ignored and Trellis context won't
|
# Without this flag, hooks.json is ignored and Trellis context won't
|
||||||
# be injected into Codex sessions.
|
# be injected into Codex sessions.
|
||||||
|
|
||||||
sandbox_mode = "workspace-write"
|
sandbox_mode = "danger-full-access"
|
||||||
|
|
||||||
[sandbox_workspace_write]
|
[sandbox_workspace_write]
|
||||||
network_access = true
|
network_access = true
|
||||||
|
|||||||
@@ -1,2 +1,5 @@
|
|||||||
# Maven build output
|
# Maven build output
|
||||||
target/
|
target/
|
||||||
|
|
||||||
|
# Local database configuration
|
||||||
|
src/main/resources/db.properties
|
||||||
|
|||||||
@@ -190,6 +190,95 @@ books/form.jsp -> JDBC -> INSERT INTO books using request parameters
|
|||||||
books/form.jsp -> BookManagementServlet -> BookService -> BookDao -> books
|
books/form.jsp -> BookManagementServlet -> BookService -> BookDao -> books
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Scenario: Book Category Maintenance Slice
|
||||||
|
|
||||||
|
### 1. Scope / Trigger
|
||||||
|
|
||||||
|
- Trigger: category maintenance completes the book-management core requirement
|
||||||
|
by adding staff-managed CRUD for `book_categories`, while existing book forms
|
||||||
|
and catalog searches continue to consume the same category source.
|
||||||
|
- Schema path: `src/main/resources/db/schema.sql`.
|
||||||
|
- JSP paths: `WEB-INF/jsp/books/categories.jsp` and
|
||||||
|
`WEB-INF/jsp/books/category-form.jsp`.
|
||||||
|
|
||||||
|
### 2. Signatures
|
||||||
|
|
||||||
|
- DAO signatures: `BookDao.findAllCategories()`, `findCategoryById(long id)`,
|
||||||
|
`findCategoryByName(String name)`, `createCategory(BookCategory category)`,
|
||||||
|
`updateCategory(BookCategory category)`, `deleteCategory(long id)`, and
|
||||||
|
`countBooksByCategoryId(long categoryId)`.
|
||||||
|
- Entity signature: `BookCategory(id, name, description)`.
|
||||||
|
- Service signatures: `BookService.listCategories()`,
|
||||||
|
`findCategory(long id)`, `createCategory(AuthenticatedUser actor,
|
||||||
|
BookCategory category)`, `updateCategory(AuthenticatedUser actor,
|
||||||
|
BookCategory category)`, and `deleteCategory(AuthenticatedUser actor,
|
||||||
|
long id)`, all returning `ServiceResult<T>`.
|
||||||
|
- Routes: `GET /book-categories`, `GET /book-categories/new`,
|
||||||
|
`GET /book-categories/edit?id=...`, `POST /book-categories`,
|
||||||
|
`POST /book-categories/update`, and `POST /book-categories/delete`.
|
||||||
|
- Protected permission: `/book-categories*` requires `MANAGE_BOOKS`.
|
||||||
|
|
||||||
|
### 3. Contracts
|
||||||
|
|
||||||
|
- `book_categories.name` is unique and is the display value used in book forms,
|
||||||
|
catalog filters, and management filters.
|
||||||
|
- `book_categories.description` is optional and limited to the database column
|
||||||
|
size.
|
||||||
|
- Book category deletes must check `books.category_id` usage before deletion
|
||||||
|
and return a safe validation result when the category is in use.
|
||||||
|
- Servlet controllers set JSP attributes such as `categories`, `category`,
|
||||||
|
`formTitle`, `formAction`, `formValues`, `errors`, `errorMessage`, and
|
||||||
|
`successMessage`.
|
||||||
|
- JSP pages render JavaBean properties only; they must not call DAOs or embed
|
||||||
|
SQL.
|
||||||
|
|
||||||
|
### 4. Validation & Error Matrix
|
||||||
|
|
||||||
|
- Missing category name -> field error on `name`.
|
||||||
|
- Category name longer than 96 characters -> field error on `name`.
|
||||||
|
- Description longer than 255 characters -> field error on `description`.
|
||||||
|
- Duplicate category name -> field error on `name`.
|
||||||
|
- Missing or non-positive category id for edit/delete -> `Select a valid
|
||||||
|
category.`
|
||||||
|
- Delete category used by any `books` row -> `Category is used by existing
|
||||||
|
books and cannot be deleted.`
|
||||||
|
- Reader or unauthenticated actor attempts mutation -> permission denial through
|
||||||
|
filter/service.
|
||||||
|
- DAO failure during list/search/write -> log server-side details and return
|
||||||
|
`Book service is temporarily unavailable. Please try again later.`
|
||||||
|
|
||||||
|
### 5. Good/Base/Bad Cases
|
||||||
|
|
||||||
|
- Good: a librarian creates `Architecture`, selects it on a book form, and sees
|
||||||
|
it in catalog filters.
|
||||||
|
- Base: `/book-categories` lists seed categories ordered by name.
|
||||||
|
- Bad: deleting a category with existing books surfaces a MySQL foreign-key
|
||||||
|
stack trace or lets JSP code perform the delete.
|
||||||
|
|
||||||
|
### 6. Tests Required
|
||||||
|
|
||||||
|
- Run `BookServiceCheck` assertions for reader category-write denial, duplicate
|
||||||
|
category names, successful create/update/delete, and used-category delete
|
||||||
|
rejection.
|
||||||
|
- Run `PermissionPolicyCheck` to confirm `MANAGE_BOOKS` remains staff-only.
|
||||||
|
- Scan category JSPs for scriptlets and SQL/JDBC references.
|
||||||
|
- When Maven/Tomcat dependencies are installed, run `mvn clean package` to
|
||||||
|
compile Servlets and package JSP resources.
|
||||||
|
|
||||||
|
### 7. Wrong vs Correct
|
||||||
|
|
||||||
|
#### Wrong
|
||||||
|
|
||||||
|
```text
|
||||||
|
categories.jsp -> JDBC -> DELETE FROM book_categories WHERE id = request.id
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Correct
|
||||||
|
|
||||||
|
```text
|
||||||
|
categories.jsp -> BookManagementServlet -> BookService -> BookDao -> book_categories
|
||||||
|
```
|
||||||
|
|
||||||
## Scenario: Reader Information Management Slice
|
## Scenario: Reader Information Management Slice
|
||||||
|
|
||||||
### 1. Scope / Trigger
|
### 1. Scope / Trigger
|
||||||
|
|||||||
@@ -52,3 +52,10 @@ DAOs report database failures without leaking SQL details to JSP pages.
|
|||||||
|
|
||||||
Use concise messages suitable for JSP rendering. For protected operations,
|
Use concise messages suitable for JSP rendering. For protected operations,
|
||||||
prefer generic denial messages over exposing permission internals.
|
prefer generic denial messages over exposing permission internals.
|
||||||
|
|
||||||
|
For this application, messages rendered into JSP pages should be Simplified
|
||||||
|
Chinese. This includes `ServiceResult.message`, field-level validation errors,
|
||||||
|
flash messages set by Servlet controllers, and display names returned by entity
|
||||||
|
helpers. Keep log-only diagnostics, exception types, stored enum codes, request
|
||||||
|
parameter names, and database values unchanged unless a separate contract change
|
||||||
|
requires it.
|
||||||
|
|||||||
@@ -47,6 +47,18 @@ the chosen IDEA/Tomcat project structure. Until then, documentation-only
|
|||||||
changes should run Trellis validation, Python compile checks for Trellis
|
changes should run Trellis validation, Python compile checks for Trellis
|
||||||
scripts when relevant, and placeholder scans for scaffold markers.
|
scripts when relevant, and placeholder scans for scaffold markers.
|
||||||
|
|
||||||
|
For this workspace, Maven is available at:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
/home/sjy/.sdkman/candidates/maven/current/bin/mvn
|
||||||
|
```
|
||||||
|
|
||||||
|
Use the explicit path when `mvn` is not on `PATH`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
/home/sjy/.sdkman/candidates/maven/current/bin/mvn clean package
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Review Checklist
|
## Review Checklist
|
||||||
|
|||||||
@@ -23,6 +23,19 @@ the reusable UI units.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Interface Copy
|
||||||
|
|
||||||
|
- Render user-visible JSP copy in Simplified Chinese, including navigation,
|
||||||
|
headings, form labels, buttons, table headers, empty states, and accessible
|
||||||
|
labels.
|
||||||
|
- Keep machine-readable values unchanged: URLs, request parameter names, CSS
|
||||||
|
classes, Java identifiers, enum codes, database values, and servlet names stay
|
||||||
|
in their existing code form.
|
||||||
|
- Translate display helper output and controller/service messages when they are
|
||||||
|
rendered into JSP pages.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Forms
|
## Forms
|
||||||
|
|
||||||
- Forms should post to Servlet controller endpoints, not directly to DAOs or
|
- Forms should post to Servlet controller endpoints, not directly to DAOs or
|
||||||
|
|||||||
+2
-2
@@ -3,7 +3,7 @@
|
|||||||
"name": "continue-development",
|
"name": "continue-development",
|
||||||
"title": "brainstorm: 继续开发程序",
|
"title": "brainstorm: 继续开发程序",
|
||||||
"description": "",
|
"description": "",
|
||||||
"status": "in_progress",
|
"status": "completed",
|
||||||
"dev_type": null,
|
"dev_type": null,
|
||||||
"scope": null,
|
"scope": null,
|
||||||
"package": null,
|
"package": null,
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
"creator": "Zzzz",
|
"creator": "Zzzz",
|
||||||
"assignee": "Zzzz",
|
"assignee": "Zzzz",
|
||||||
"createdAt": "2026-04-27",
|
"createdAt": "2026-04-27",
|
||||||
"completedAt": null,
|
"completedAt": "2026-04-27",
|
||||||
"branch": null,
|
"branch": null,
|
||||||
"base_branch": "master",
|
"base_branch": "master",
|
||||||
"worktree_path": null,
|
"worktree_path": null,
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{"file": ".trellis/spec/backend/index.md", "reason": "Verify category maintenance against backend core module expectations."}
|
||||||
|
{"file": ".trellis/spec/backend/database-guidelines.md", "reason": "Verify category DAO/service contracts and book-category integrity behavior."}
|
||||||
|
{"file": ".trellis/spec/backend/error-handling.md", "reason": "Verify validation and safe fallback messages."}
|
||||||
|
{"file": ".trellis/spec/backend/quality-guidelines.md", "reason": "Verify layer boundaries and test expectations."}
|
||||||
|
{"file": ".trellis/spec/frontend/index.md", "reason": "Verify JSP/CSS work stays in the approved frontend stack."}
|
||||||
|
{"file": ".trellis/spec/frontend/component-guidelines.md", "reason": "Verify page composition uses existing forms/tables/navigation patterns."}
|
||||||
|
{"file": ".trellis/spec/frontend/quality-guidelines.md", "reason": "Verify JSP safety, empty states, errors, and permission-specific navigation."}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{"file": ".trellis/spec/backend/index.md", "reason": "Category maintenance must follow backend layer and core module expectations."}
|
||||||
|
{"file": ".trellis/spec/backend/database-guidelines.md", "reason": "Defines book/category data contracts, DAO responsibilities, validation, and DB integrity rules."}
|
||||||
|
{"file": ".trellis/spec/backend/error-handling.md", "reason": "Guides safe service errors, field validation, and controller behavior."}
|
||||||
|
{"file": ".trellis/spec/backend/logging-guidelines.md", "reason": "Category maintenance is a key book operation and should preserve logging expectations."}
|
||||||
|
{"file": ".trellis/spec/backend/quality-guidelines.md", "reason": "Implementation must preserve Servlet-Service-DAO separation and validation checks."}
|
||||||
|
{"file": ".trellis/spec/frontend/index.md", "reason": "JSP/CSS changes must remain within the server-rendered frontend conventions."}
|
||||||
|
{"file": ".trellis/spec/frontend/component-guidelines.md", "reason": "Category pages should reuse existing form, table, empty-state, and navigation patterns."}
|
||||||
|
{"file": ".trellis/spec/frontend/quality-guidelines.md", "reason": "Check JSP safety, forms, tables, permissions, and accessibility basics."}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
# Core Function Gap Check
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Check the current MZH Library Management implementation against the documented
|
||||||
|
core modules and complete the highest-confidence missing core feature slice
|
||||||
|
without broad redesign.
|
||||||
|
|
||||||
|
## What I Already Know
|
||||||
|
|
||||||
|
* The user asked to check whether core functionality is still missing and to
|
||||||
|
complete it.
|
||||||
|
* The app is a Java 11 Maven WAR using JSP + Servlet + MySQL and JDBC DAOs.
|
||||||
|
* Existing implemented slices cover login/permissions, dashboard navigation,
|
||||||
|
book catalog/search, book CRUD, reader management, borrowing circulation,
|
||||||
|
reader loan history, reports, administrator user management, and system-log
|
||||||
|
viewing.
|
||||||
|
* Existing lightweight checks pass with `javac -Xlint:all` for non-Servlet
|
||||||
|
layers and all service check mains.
|
||||||
|
* Maven is available in this workspace at
|
||||||
|
`/home/sjy/.sdkman/candidates/maven/current/bin/mvn`.
|
||||||
|
* The clearest missing core requirement is book category maintenance. The
|
||||||
|
schema and selectors already have `book_categories`, but there is no route,
|
||||||
|
controller, JSP, DAO/service mutation API, or test coverage for maintaining
|
||||||
|
categories.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
* Preserve the existing JSP -> Servlet -> Service -> DAO -> MySQL layering.
|
||||||
|
* Keep category maintenance under the existing `MANAGE_BOOKS` permission.
|
||||||
|
* Add a staff-only category management flow for listing, creating, editing, and
|
||||||
|
deleting book categories.
|
||||||
|
* Validate required category name, name length, description length, duplicate
|
||||||
|
names, and invalid IDs with field-level service errors.
|
||||||
|
* Prevent deleting categories that still have book records, returning a safe
|
||||||
|
validation message instead of surfacing a database constraint failure.
|
||||||
|
* Reuse the existing book management visual patterns, flash messages, and
|
||||||
|
table/form conventions.
|
||||||
|
* Link category maintenance from the book management surface and staff
|
||||||
|
navigation where appropriate.
|
||||||
|
* Update focused service checks and fallback validation commands.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
* [x] A user with `MANAGE_BOOKS` can open a category management page.
|
||||||
|
* [x] Staff can create and update category names/descriptions.
|
||||||
|
* [x] Duplicate category names are rejected with a field error.
|
||||||
|
* [x] Categories used by books cannot be deleted.
|
||||||
|
* [x] Readers or unauthenticated users cannot mutate categories.
|
||||||
|
* [x] Book forms/search continue to load categories from the shared DAO/service
|
||||||
|
path.
|
||||||
|
* [x] JSPs do not contain SQL/JDBC/scriptlet logic.
|
||||||
|
* [x] Existing lightweight checks pass and Maven package succeeds through the
|
||||||
|
workspace Maven path.
|
||||||
|
|
||||||
|
## Definition of Done
|
||||||
|
|
||||||
|
* Tests/checks updated where practical.
|
||||||
|
* Lint/type-check/compile equivalent checks pass in this environment.
|
||||||
|
* Docs/notes updated if behavior changes.
|
||||||
|
* No broad framework or visual redesign.
|
||||||
|
|
||||||
|
## Out of Scope
|
||||||
|
|
||||||
|
* Role/permission editor UI.
|
||||||
|
* Full database dump/restore execution from the web app.
|
||||||
|
* Audit logging expansion for every non-user operation.
|
||||||
|
* Automatic reader-account/profile linking changes.
|
||||||
|
|
||||||
|
## Technical Notes
|
||||||
|
|
||||||
|
* Relevant specs:
|
||||||
|
`.trellis/spec/backend/index.md`,
|
||||||
|
`.trellis/spec/backend/database-guidelines.md`,
|
||||||
|
`.trellis/spec/backend/error-handling.md`,
|
||||||
|
`.trellis/spec/backend/logging-guidelines.md`,
|
||||||
|
`.trellis/spec/backend/quality-guidelines.md`,
|
||||||
|
`.trellis/spec/frontend/index.md`,
|
||||||
|
`.trellis/spec/frontend/component-guidelines.md`,
|
||||||
|
`.trellis/spec/frontend/quality-guidelines.md`.
|
||||||
|
* Current files most likely affected:
|
||||||
|
`BookDao`, `JdbcBookDao`, `BookService`, `BookServiceImpl`,
|
||||||
|
`BookManagementServlet`, `web.xml`, book JSPs, shared CSS, and
|
||||||
|
`BookServiceCheck`.
|
||||||
|
* Initial verification before implementation:
|
||||||
|
`javac -Xlint:all` over non-Servlet app layers and tests passed; all eight
|
||||||
|
service check mains passed.
|
||||||
|
* Final verification after implementation:
|
||||||
|
`javac -Xlint:all` over non-Servlet app layers and tests passed;
|
||||||
|
`PermissionPolicyCheck`, `AuthServiceCheck`, `BookServiceCheck`,
|
||||||
|
`ReaderServiceCheck`, `BorrowingServiceCheck`, `ReportServiceCheck`,
|
||||||
|
`UserAccountServiceCheck`, and `SystemLogServiceCheck` passed;
|
||||||
|
JSP/static scriptlet and SQL/JDBC scan returned no matches;
|
||||||
|
`git diff --check` passed.
|
||||||
|
* Maven verification on 2026-04-27:
|
||||||
|
`/home/sjy/.sdkman/candidates/maven/current/bin/mvn clean package` passed and
|
||||||
|
produced `target/library-management.war`.
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"id": "core-function-gap-check",
|
||||||
|
"name": "core-function-gap-check",
|
||||||
|
"title": "检查并补全核心功能",
|
||||||
|
"description": "",
|
||||||
|
"status": "completed",
|
||||||
|
"dev_type": null,
|
||||||
|
"scope": null,
|
||||||
|
"package": null,
|
||||||
|
"priority": "P2",
|
||||||
|
"creator": "Zzzz",
|
||||||
|
"assignee": "Zzzz",
|
||||||
|
"createdAt": "2026-04-27",
|
||||||
|
"completedAt": "2026-04-27",
|
||||||
|
"branch": null,
|
||||||
|
"base_branch": "master",
|
||||||
|
"worktree_path": null,
|
||||||
|
"commit": null,
|
||||||
|
"pr_url": null,
|
||||||
|
"subtasks": [],
|
||||||
|
"children": [],
|
||||||
|
"parent": null,
|
||||||
|
"relatedFiles": [],
|
||||||
|
"notes": "",
|
||||||
|
"meta": {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{"file": ".trellis/spec/backend/index.md", "reason": "Verify README architecture, modules, and backend descriptions against project guidelines."}
|
||||||
|
{"file": ".trellis/spec/frontend/index.md", "reason": "Verify README frontend/JSP descriptions against project guidelines."}
|
||||||
|
{"file": ".trellis/tasks/archive/2026-04/00-bootstrap-guidelines/research/project-requirements.md", "reason": "Verify README content against developer-provided project facts."}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{"file": ".trellis/spec/backend/index.md", "reason": "Project architecture, core modules, and backend stack facts needed for accurate README content."}
|
||||||
|
{"file": ".trellis/spec/frontend/index.md", "reason": "JSP/CSS presentation-layer context and frontend conventions relevant to README descriptions."}
|
||||||
|
{"file": ".trellis/tasks/archive/2026-04/00-bootstrap-guidelines/research/project-requirements.md", "reason": "Developer-provided stack, layered architecture, data model, and module requirements to summarize in README."}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
# brainstorm: 中文详细 README
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
将现有简短英文 README 扩展为一份面向开发、部署和验收的简体中文项目说明文档,让读者能快速理解 MZH 图书馆管理系统的用途、技术栈、功能模块、目录结构、本地运行、数据库初始化、构建部署和开发约定。
|
||||||
|
|
||||||
|
## What I already know
|
||||||
|
|
||||||
|
* 用户要求:“用中文写一个详细的readme”。
|
||||||
|
* 当前 `README.md` 只有项目标题、技术栈、基础本地部署步骤和一句已实现功能概述。
|
||||||
|
* 项目是 Java 11 Maven WAR 应用,使用 JSP + Servlet + MySQL + JDBC DAO,目标部署到 Tomcat。
|
||||||
|
* `pom.xml` 声明依赖:Servlet API 4.0.1、JSTL 1.2、MySQL Connector/J 8.0.33。
|
||||||
|
* Web 入口和路由集中在 `src/main/webapp/WEB-INF/web.xml`,包含登录、仪表盘、角色首页、图书目录、图书管理、分类管理、读者管理、借还续借、读者借阅历史、报表中心、用户管理和系统日志。
|
||||||
|
* 数据库脚本位于 `src/main/resources/db/schema.sql`,会创建 `mzh_library`,并包含角色、权限、用户、系统日志、读者、图书分类、图书、借阅记录等表和本地演示数据。
|
||||||
|
* 本地数据库配置模板位于 `src/main/resources/db.properties.example`,实际配置文件应复制为 `src/main/resources/db.properties` 且不提交真实凭据。
|
||||||
|
* 项目使用 Servlet -> Service -> DAO 的分层边界,认证会话只保存安全的 `AuthenticatedUser` 快照。
|
||||||
|
* 当前前端界面和服务端反馈消息已改为简体中文。
|
||||||
|
|
||||||
|
## Assumptions
|
||||||
|
|
||||||
|
* README 应替换为中文主文档,而不是额外新增第二份中文文档。
|
||||||
|
* README 可以保留英文项目名、Maven/Tomcat/MySQL/Servlet/JSP 等技术名词。
|
||||||
|
* 用户显式要求中文,因此本任务的 README 文档语言覆盖现有 spec 中“文档用英文”的默认约定。
|
||||||
|
* README 不应写入无法从仓库确认的真实生产账号、真实部署域名或私密数据库信息。
|
||||||
|
|
||||||
|
## Open Questions
|
||||||
|
|
||||||
|
* None for this MVP.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
* 用简体中文重写 `README.md`。
|
||||||
|
* README 至少包含:
|
||||||
|
* 项目概述和适用场景。
|
||||||
|
* 核心功能模块。
|
||||||
|
* 技术栈和运行环境。
|
||||||
|
* 项目目录结构。
|
||||||
|
* 数据库初始化和本地配置步骤。
|
||||||
|
* Maven 构建与 Tomcat 部署步骤。
|
||||||
|
* 主要访问入口和角色权限说明。
|
||||||
|
* 开发约定、测试/检查说明、常见问题。
|
||||||
|
* README 内容必须与当前仓库实际文件、脚本、路由、依赖和数据库结构一致。
|
||||||
|
* 不修改业务代码、数据库脚本或配置模板。
|
||||||
|
* 不写入真实密码、个人凭据或未经确认的默认登录明文密码。
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
* [ ] `README.md` 是完整中文说明,明显比当前版本更详细。
|
||||||
|
* [ ] README 覆盖本地初始化、构建、部署和关键功能模块。
|
||||||
|
* [ ] README 中的路径、命令、技术版本和路由与仓库当前状态一致。
|
||||||
|
* [ ] README 没有引入不可验证的账号密码或生产配置。
|
||||||
|
* [ ] 文档更新不要求 Java 业务测试通过,但应至少做 Markdown/内容自查。
|
||||||
|
|
||||||
|
## Definition of Done
|
||||||
|
|
||||||
|
* README 更新完成。
|
||||||
|
* 任务上下文已配置给 implement/check agent。
|
||||||
|
* Quality check agent 已复核文档与 PRD/仓库事实的一致性。
|
||||||
|
* 最终说明列出修改文件和验证结果。
|
||||||
|
|
||||||
|
## Out of Scope
|
||||||
|
|
||||||
|
* 新增业务功能或调整页面。
|
||||||
|
* 修改数据库表结构或种子数据。
|
||||||
|
* 创建英文版 README。
|
||||||
|
* 添加截图、架构图或部署脚本。
|
||||||
|
* 真实生产环境部署配置。
|
||||||
|
|
||||||
|
## Technical Notes
|
||||||
|
|
||||||
|
* Relevant current README: `README.md`.
|
||||||
|
* Relevant project metadata: `pom.xml`, `src/main/webapp/WEB-INF/web.xml`.
|
||||||
|
* Relevant database files: `src/main/resources/db/schema.sql`, `src/main/resources/db.properties.example`.
|
||||||
|
* Relevant spec files for context: `.trellis/spec/backend/index.md`, `.trellis/spec/frontend/index.md`, `.trellis/tasks/archive/2026-04/00-bootstrap-guidelines/research/project-requirements.md`.
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"id": "chinese-detailed-readme",
|
||||||
|
"name": "chinese-detailed-readme",
|
||||||
|
"title": "brainstorm: 中文详细 README",
|
||||||
|
"description": "",
|
||||||
|
"status": "completed",
|
||||||
|
"dev_type": null,
|
||||||
|
"scope": null,
|
||||||
|
"package": null,
|
||||||
|
"priority": "P2",
|
||||||
|
"creator": "Zzzz",
|
||||||
|
"assignee": "Zzzz",
|
||||||
|
"createdAt": "2026-04-28",
|
||||||
|
"completedAt": "2026-04-28",
|
||||||
|
"branch": null,
|
||||||
|
"base_branch": "master",
|
||||||
|
"worktree_path": null,
|
||||||
|
"commit": null,
|
||||||
|
"pr_url": null,
|
||||||
|
"subtasks": [],
|
||||||
|
"children": [],
|
||||||
|
"parent": null,
|
||||||
|
"relatedFiles": [],
|
||||||
|
"notes": "",
|
||||||
|
"meta": {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{"file": ".trellis/spec/frontend/index.md", "reason": "Check translated UI against frontend conventions."}
|
||||||
|
{"file": ".trellis/spec/frontend/component-guidelines.md", "reason": "Verify forms, tables, fragments, and UI copy remain presentation-focused."}
|
||||||
|
{"file": ".trellis/spec/frontend/type-safety.md", "reason": "Verify JSP/Servlet display contracts and escaping were preserved."}
|
||||||
|
{"file": ".trellis/spec/frontend/quality-guidelines.md", "reason": "Verify accessibility labels, layout preservation, and JSP/CSS quality."}
|
||||||
|
{"file": ".trellis/spec/backend/error-handling.md", "reason": "Review translated backend messages displayed in the UI."}
|
||||||
|
{"file": ".trellis/spec/backend/quality-guidelines.md", "reason": "Review backend layer boundaries for message-only changes."}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{"file": ".trellis/spec/frontend/index.md", "reason": "Frontend JSP/CSS conventions and pre-development checklist for UI changes."}
|
||||||
|
{"file": ".trellis/spec/frontend/directory-structure.md", "reason": "Confirms JSP/static asset layout and forbidden SPA conventions."}
|
||||||
|
{"file": ".trellis/spec/frontend/component-guidelines.md", "reason": "JSP fragments, forms, tables, and reusable UI conventions affected by translation."}
|
||||||
|
{"file": ".trellis/spec/frontend/type-safety.md", "reason": "JSP/Servlet display contracts and safe rendering guidance while changing visible text."}
|
||||||
|
{"file": ".trellis/spec/frontend/quality-guidelines.md", "reason": "Frontend quality bar for preserving JSP/CSS behavior and accessibility basics."}
|
||||||
|
{"file": ".trellis/spec/backend/error-handling.md", "reason": "Server-generated UI messages must remain safe and user-facing."}
|
||||||
|
{"file": ".trellis/spec/backend/quality-guidelines.md", "reason": "Layer boundary constraints for any controller/service message changes."}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
# brainstorm: Frontend Chinese UI
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Make the existing JSP/Servlet frontend interface display in Simplified Chinese so users see Chinese page titles, navigation, labels, action buttons, empty states, accessibility labels, and server-rendered feedback messages.
|
||||||
|
|
||||||
|
## What I already know
|
||||||
|
|
||||||
|
* The user requested: "前端界面需要中文".
|
||||||
|
* The project uses JSP/CSS rendered by Servlet/Tomcat, not a SPA framework.
|
||||||
|
* Visible English text is concentrated in `src/main/webapp/WEB-INF/jsp/**`.
|
||||||
|
* Some user-facing messages are set by Java controllers/services and rendered by JSP pages.
|
||||||
|
* The application already uses UTF-8 JSP page encoding and a UTF-8 character encoding filter.
|
||||||
|
|
||||||
|
## Assumptions
|
||||||
|
|
||||||
|
* Use Simplified Chinese for all user-visible frontend copy.
|
||||||
|
* Keep URLs, form field names, Java identifiers, enum codes, database values, CSS class names, and servlet names unchanged.
|
||||||
|
* Translate dynamic display names exposed by Java enums/role helpers where they are shown in the UI.
|
||||||
|
* Do not add a full i18n framework in this task.
|
||||||
|
|
||||||
|
## Open Questions
|
||||||
|
|
||||||
|
* None blocking; proceed with the Simplified Chinese assumption.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
* Translate all hardcoded visible English text in JSP pages to Simplified Chinese.
|
||||||
|
* Change HTML language attributes from English to Simplified Chinese where applicable.
|
||||||
|
* Translate visible document titles and the web app display name.
|
||||||
|
* Translate server-generated messages that are displayed to users on the frontend.
|
||||||
|
* Preserve existing page structure, routing, permissions, form submission behavior, JSTL/EL bindings, and backend workflows.
|
||||||
|
* Leave stored data and machine-readable codes unchanged.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
* [x] Navigation, page headings, labels, buttons, table headers, empty states, option labels, and accessible labels appear in Simplified Chinese.
|
||||||
|
* [x] Login, unauthorized, dashboard, role home, catalog, management, reader, borrowing, report, system log, and admin user pages no longer show English UI copy except product/brand names and technical/user data.
|
||||||
|
* [x] Server-rendered success/error messages shown in the UI are Simplified Chinese.
|
||||||
|
* [x] Maven build succeeds.
|
||||||
|
|
||||||
|
## Definition of Done
|
||||||
|
|
||||||
|
* Tests or build verification run where practical.
|
||||||
|
* Lint/typecheck/build green for the Java webapp.
|
||||||
|
* Spec update considered after implementation.
|
||||||
|
|
||||||
|
## Out of Scope
|
||||||
|
|
||||||
|
* Runtime language switching.
|
||||||
|
* Browser locale detection.
|
||||||
|
* New translation resource bundles.
|
||||||
|
* Database data migration.
|
||||||
|
* Visual redesign beyond any minor layout-preserving text fit adjustments.
|
||||||
|
|
||||||
|
## Technical Notes
|
||||||
|
|
||||||
|
* Relevant spec index: `.trellis/spec/frontend/index.md`.
|
||||||
|
* Backend messages may require `.trellis/spec/backend/error-handling.md` and `.trellis/spec/backend/quality-guidelines.md`.
|
||||||
|
* Likely affected frontend files include `src/main/webapp/WEB-INF/jsp/**`, `src/main/webapp/WEB-INF/web.xml`, and possibly `src/main/webapp/static/images/library-login.svg` for accessible text.
|
||||||
|
* Likely affected Java files include controllers/services/entities that expose user-facing display names or request messages.
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"id": "frontend-chinese-ui",
|
||||||
|
"name": "frontend-chinese-ui",
|
||||||
|
"title": "brainstorm: 前端界面中文化",
|
||||||
|
"description": "",
|
||||||
|
"status": "completed",
|
||||||
|
"dev_type": null,
|
||||||
|
"scope": null,
|
||||||
|
"package": null,
|
||||||
|
"priority": "P2",
|
||||||
|
"creator": "Zzzz",
|
||||||
|
"assignee": "Zzzz",
|
||||||
|
"createdAt": "2026-04-28",
|
||||||
|
"completedAt": "2026-04-28",
|
||||||
|
"branch": null,
|
||||||
|
"base_branch": "master",
|
||||||
|
"worktree_path": null,
|
||||||
|
"commit": null,
|
||||||
|
"pr_url": null,
|
||||||
|
"subtasks": [],
|
||||||
|
"children": [],
|
||||||
|
"parent": null,
|
||||||
|
"relatedFiles": [],
|
||||||
|
"notes": "",
|
||||||
|
"meta": {}
|
||||||
|
}
|
||||||
@@ -8,8 +8,8 @@
|
|||||||
|
|
||||||
<!-- @@@auto:current-status -->
|
<!-- @@@auto:current-status -->
|
||||||
- **Active File**: `journal-1.md`
|
- **Active File**: `journal-1.md`
|
||||||
- **Total Sessions**: 6
|
- **Total Sessions**: 10
|
||||||
- **Last Active**: 2026-04-27
|
- **Last Active**: 2026-04-28
|
||||||
<!-- @@@/auto:current-status -->
|
<!-- @@@/auto:current-status -->
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
<!-- @@@auto:active-documents -->
|
<!-- @@@auto:active-documents -->
|
||||||
| File | Lines | Status |
|
| File | Lines | Status |
|
||||||
|------|-------|--------|
|
|------|-------|--------|
|
||||||
| `journal-1.md` | ~275 | Active |
|
| `journal-1.md` | ~408 | Active |
|
||||||
<!-- @@@/auto:active-documents -->
|
<!-- @@@/auto:active-documents -->
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -29,6 +29,10 @@
|
|||||||
<!-- @@@auto:session-history -->
|
<!-- @@@auto:session-history -->
|
||||||
| # | Date | Title | Commits | Branch |
|
| # | Date | Title | Commits | Branch |
|
||||||
|---|------|-------|---------|--------|
|
|---|------|-------|---------|--------|
|
||||||
|
| 10 | 2026-04-28 | 中文详细 README | `2d4a7e2` | `master` |
|
||||||
|
| 9 | 2026-04-28 | Frontend Chinese UI | `ff044e6` | `master` |
|
||||||
|
| 8 | 2026-04-27 | Core Function Gap Check | `d917a62` | `master` |
|
||||||
|
| 7 | 2026-04-27 | Admin user management and system logs | `934ea1f`, `f99002e` | `master` |
|
||||||
| 6 | 2026-04-27 | 完成报表中心 | `f9a9c630c29e1aebd623a640411c0124c7c0b0db` | `master` |
|
| 6 | 2026-04-27 | 完成报表中心 | `f9a9c630c29e1aebd623a640411c0124c7c0b0db` | `master` |
|
||||||
| 5 | 2026-04-27 | Borrowing circulation management | `7502890` | `master` |
|
| 5 | 2026-04-27 | Borrowing circulation management | `7502890` | `master` |
|
||||||
| 4 | 2026-04-27 | Reader information management slice | `eff118e` | `master` |
|
| 4 | 2026-04-27 | Reader information management slice | `eff118e` | `master` |
|
||||||
|
|||||||
@@ -273,3 +273,136 @@ Implemented the staff report center with inventory, borrowing, overdue, and popu
|
|||||||
### Next Steps
|
### Next Steps
|
||||||
|
|
||||||
- None - task complete
|
- None - task complete
|
||||||
|
|
||||||
|
|
||||||
|
## Session 7: Admin user management and system logs
|
||||||
|
|
||||||
|
**Date**: 2026-04-27
|
||||||
|
**Task**: Admin user management and system logs
|
||||||
|
**Branch**: `master`
|
||||||
|
|
||||||
|
### Summary
|
||||||
|
|
||||||
|
Implemented administrator user/account management, read-only system-log viewing, Maven packaging verification, and Maven target gitignore coverage.
|
||||||
|
|
||||||
|
### Main Changes
|
||||||
|
|
||||||
|
(Add details)
|
||||||
|
|
||||||
|
### Git Commits
|
||||||
|
|
||||||
|
| Hash | Message |
|
||||||
|
|------|---------|
|
||||||
|
| `934ea1f` | (see git log) |
|
||||||
|
| `f99002e` | (see git log) |
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
- [OK] (Add test results)
|
||||||
|
|
||||||
|
### Status
|
||||||
|
|
||||||
|
[OK] **Completed**
|
||||||
|
|
||||||
|
### Next Steps
|
||||||
|
|
||||||
|
- None - task complete
|
||||||
|
|
||||||
|
|
||||||
|
## Session 8: Core Function Gap Check
|
||||||
|
|
||||||
|
**Date**: 2026-04-27
|
||||||
|
**Task**: Core Function Gap Check
|
||||||
|
**Branch**: `master`
|
||||||
|
|
||||||
|
### Summary
|
||||||
|
|
||||||
|
Checked core library-management functionality, completed book category maintenance, documented Maven workspace path, and verified package build.
|
||||||
|
|
||||||
|
### Main Changes
|
||||||
|
|
||||||
|
(Add details)
|
||||||
|
|
||||||
|
### Git Commits
|
||||||
|
|
||||||
|
| Hash | Message |
|
||||||
|
|------|---------|
|
||||||
|
| `d917a62` | (see git log) |
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
- [OK] (Add test results)
|
||||||
|
|
||||||
|
### Status
|
||||||
|
|
||||||
|
[OK] **Completed**
|
||||||
|
|
||||||
|
### Next Steps
|
||||||
|
|
||||||
|
- None - task complete
|
||||||
|
|
||||||
|
|
||||||
|
## Session 9: Frontend Chinese UI
|
||||||
|
|
||||||
|
**Date**: 2026-04-28
|
||||||
|
**Task**: Frontend Chinese UI
|
||||||
|
**Branch**: `master`
|
||||||
|
|
||||||
|
### Summary
|
||||||
|
|
||||||
|
Localized JSP frontend UI and displayed backend messages to Simplified Chinese, updated display helpers/tests, recorded the Chinese UI copy convention in Trellis specs, and verified the Maven build plus service checks.
|
||||||
|
|
||||||
|
### Main Changes
|
||||||
|
|
||||||
|
(Add details)
|
||||||
|
|
||||||
|
### Git Commits
|
||||||
|
|
||||||
|
| Hash | Message |
|
||||||
|
|------|---------|
|
||||||
|
| `ff044e6` | (see git log) |
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
- [OK] (Add test results)
|
||||||
|
|
||||||
|
### Status
|
||||||
|
|
||||||
|
[OK] **Completed**
|
||||||
|
|
||||||
|
### Next Steps
|
||||||
|
|
||||||
|
- None - task complete
|
||||||
|
|
||||||
|
|
||||||
|
## Session 10: 中文详细 README
|
||||||
|
|
||||||
|
**Date**: 2026-04-28
|
||||||
|
**Task**: 中文详细 README
|
||||||
|
**Branch**: `master`
|
||||||
|
|
||||||
|
### Summary
|
||||||
|
|
||||||
|
将 README 重写为简体中文详细文档,覆盖项目概述、功能模块、技术栈、目录结构、数据库初始化、本地配置、构建部署、路由、角色权限、测试检查和常见问题;trellis-check 已通过 Maven 测试、自检类和打包验证。
|
||||||
|
|
||||||
|
### Main Changes
|
||||||
|
|
||||||
|
(Add details)
|
||||||
|
|
||||||
|
### Git Commits
|
||||||
|
|
||||||
|
| Hash | Message |
|
||||||
|
|------|---------|
|
||||||
|
| `2d4a7e2` | (see git log) |
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
- [OK] (Add test results)
|
||||||
|
|
||||||
|
### Status
|
||||||
|
|
||||||
|
[OK] **Completed**
|
||||||
|
|
||||||
|
### Next Steps
|
||||||
|
|
||||||
|
- None - task complete
|
||||||
|
|||||||
@@ -1,25 +1,336 @@
|
|||||||
# MZH Library Management
|
# MZH 图书馆管理系统
|
||||||
|
|
||||||
Initial JSP + Servlet + MySQL scaffold for the library-management system.
|
MZH Library Management 是一个基于 Java 11、Maven WAR、JSP、Servlet、JDBC DAO 和 MySQL 的 B/S 图书馆管理系统。项目以 Tomcat 部署为目标,采用传统 Java Web 分层结构,适合用于课程设计、Java Web 实训、图书馆业务原型验证,以及学习 Servlet -> Service -> DAO -> MySQL 的完整数据流。
|
||||||
|
|
||||||
## Stack
|
当前仓库已经包含登录认证、角色权限、馆藏检索、图书管理、分类管理、读者档案、借阅流通、读者借阅历史、报表中心、管理员用户管理和系统日志查看等功能切片。
|
||||||
|
|
||||||
- Java 11
|
## 核心功能
|
||||||
- Maven WAR project layout
|
|
||||||
- JSP + Servlet on Tomcat
|
|
||||||
- MySQL through JDBC DAO classes
|
|
||||||
|
|
||||||
## Local Setup
|
- 登录与会话管理:通过 `/login` 登录,认证成功后进入 `/dashboard`;会话中只保存安全的 `AuthenticatedUser` 快照、角色代码和权限代码集合。
|
||||||
|
- 角色工作台:管理员、馆员、读者分别通过 `/admin/home`、`/librarian/home`、`/reader/home` 进入对应区域。
|
||||||
|
- 馆藏检索:通过 `/catalog` 按图书编号、书名、作者和分类检索图书。
|
||||||
|
- 图书管理:通过 `/books` 维护图书信息,支持新增、编辑、删除和库存状态管理。
|
||||||
|
- 图书分类管理:通过 `/book-categories` 维护图书分类,并防止删除仍被图书引用的分类。
|
||||||
|
- 读者管理:通过 `/readers` 维护读者档案、联系方式、状态和最大借阅数量。
|
||||||
|
- 借阅流通:通过 `/borrowing` 完成借书、还书、续借、逾期筛选和库存联动更新。
|
||||||
|
- 读者借阅历史:读者通过 `/reader/loans` 查看自己的借阅记录。
|
||||||
|
- 报表中心:通过 `/reports` 查看馆藏汇总、借阅汇总、逾期列表和热门图书排行。
|
||||||
|
- 用户管理:管理员通过 `/admin/users` 维护管理员、馆员和读者登录账户。
|
||||||
|
- 系统日志:管理员通过 `/admin/system-logs` 查询关键操作、审计和异常日志。
|
||||||
|
|
||||||
1. Create a MySQL database and run `src/main/resources/db/schema.sql`.
|
## 技术栈
|
||||||
2. Copy `src/main/resources/db.properties.example` to `src/main/resources/db.properties`.
|
|
||||||
3. Fill in the MySQL URL, username, and password.
|
| 类别 | 当前配置 |
|
||||||
4. Build with Maven when available:
|
| --- | --- |
|
||||||
|
| Java | Java 11,`maven.compiler.release=11` |
|
||||||
|
| 构建工具 | Maven,WAR 项目 |
|
||||||
|
| Web 技术 | Servlet 4.0、JSP、JSTL |
|
||||||
|
| Servlet API | `javax.servlet:javax.servlet-api:4.0.1`,`provided` |
|
||||||
|
| JSP 标签库 | `javax.servlet:jstl:1.2` |
|
||||||
|
| 数据库 | MySQL,JDBC DAO 访问 |
|
||||||
|
| MySQL 驱动 | `com.mysql:mysql-connector-j:8.0.33`,`runtime` |
|
||||||
|
| 部署产物 | `target/library-management.war` |
|
||||||
|
| Web 配置 | `src/main/webapp/WEB-INF/web.xml`,`web-app` 版本 4.0 |
|
||||||
|
| 编码 | Maven 源码编码 UTF-8;Web 请求通过 `CharacterEncodingFilter` 使用 UTF-8 |
|
||||||
|
|
||||||
|
建议使用兼容 Servlet 4.0 的 Tomcat 9.x 运行该 WAR。数据库脚本使用 InnoDB、`utf8mb4` 字符集和检查约束,建议使用 MySQL 8.x。
|
||||||
|
|
||||||
|
## 项目结构
|
||||||
|
|
||||||
|
```text
|
||||||
|
.
|
||||||
|
├── pom.xml
|
||||||
|
├── README.md
|
||||||
|
├── src
|
||||||
|
│ ├── main
|
||||||
|
│ │ ├── java/com/mzh/library
|
||||||
|
│ │ │ ├── controller/ Servlet 控制器,负责路由、参数读取和 JSP 转发/重定向
|
||||||
|
│ │ │ ├── dao/ DAO 接口
|
||||||
|
│ │ │ ├── dao/impl/ JDBC DAO 实现
|
||||||
|
│ │ │ ├── entity/ JavaBean、枚举、查询条件和报表对象
|
||||||
|
│ │ │ ├── exception/ DAO 异常
|
||||||
|
│ │ │ ├── filter/ 编码、登录认证、权限过滤器
|
||||||
|
│ │ │ ├── service/ Service 接口、权限策略和通用结果对象
|
||||||
|
│ │ │ ├── service/impl/ 业务服务实现
|
||||||
|
│ │ │ └── util/ JDBC、密码哈希、Session 常量等工具
|
||||||
|
│ │ ├── resources
|
||||||
|
│ │ │ ├── db/schema.sql
|
||||||
|
│ │ │ └── db.properties.example
|
||||||
|
│ │ └── webapp
|
||||||
|
│ │ ├── WEB-INF/web.xml
|
||||||
|
│ │ ├── WEB-INF/jsp/ 受保护的 JSP 页面和 JSP 片段
|
||||||
|
│ │ ├── static/css/app.css
|
||||||
|
│ │ ├── static/images/library-login.svg
|
||||||
|
│ │ └── index.jsp
|
||||||
|
│ └── test/java/com/mzh/library/service
|
||||||
|
│ └── *Check.java 服务层和权限策略自检类
|
||||||
|
└── target/ Maven 构建输出,已被 .gitignore 忽略
|
||||||
|
```
|
||||||
|
|
||||||
|
本地数据库配置文件 `src/main/resources/db.properties` 也已被 `.gitignore` 忽略。不要提交真实数据库地址、账号或密码。
|
||||||
|
|
||||||
|
## 分层设计
|
||||||
|
|
||||||
|
项目遵循以下边界:
|
||||||
|
|
||||||
|
```text
|
||||||
|
JSP/CSS 页面 -> Servlet 控制器 -> Service 业务层 -> DAO 数据访问层 -> MySQL
|
||||||
|
```
|
||||||
|
|
||||||
|
- JSP 只负责展示、表单和用户交互,不直接访问数据库,不编写业务流程。
|
||||||
|
- Servlet 负责读取请求参数、做基础格式校验、调用 Service,并决定转发 JSP 或重定向。
|
||||||
|
- Service 负责权限检查、业务规则、事务边界和跨 DAO 工作流,例如借书、还书、续借和库存更新。
|
||||||
|
- DAO 负责 SQL、JDBC 资源访问和 MySQL CRUD,不返回 HTML 或 Servlet 对象。
|
||||||
|
- 过滤器统一处理 UTF-8 编码、登录拦截和基于权限的访问控制。
|
||||||
|
|
||||||
|
## 数据库初始化
|
||||||
|
|
||||||
|
数据库脚本位于:
|
||||||
|
|
||||||
|
```text
|
||||||
|
src/main/resources/db/schema.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
脚本会创建并使用数据库 `mzh_library`,包含以下核心表:
|
||||||
|
|
||||||
|
- `roles`:角色定义。
|
||||||
|
- `permissions`:权限定义。
|
||||||
|
- `role_permissions`:角色与权限的关联。
|
||||||
|
- `users`:登录账户,密码字段保存 PBKDF2 哈希。
|
||||||
|
- `system_logs`:系统操作、审计和异常日志。
|
||||||
|
- `readers`:读者档案、联系方式、状态和借阅上限。
|
||||||
|
- `book_categories`:图书分类。
|
||||||
|
- `books`:图书基础信息、分类、总册数、可借册数和状态。
|
||||||
|
- `borrow_records`:借阅、归还、续借和逾期判断所需记录。
|
||||||
|
|
||||||
|
初始化示例:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mysql -u root -p < src/main/resources/db/schema.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
脚本内包含本地验证用的演示角色、权限、用户、读者、分类和图书数据。演示账户只用于本地脚手架验证;在非本地数据库中使用前应更换或删除这些数据。本文档不提供任何登录明文密码。
|
||||||
|
|
||||||
|
## 本地配置
|
||||||
|
|
||||||
|
复制配置模板:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp src/main/resources/db.properties.example src/main/resources/db.properties
|
||||||
|
```
|
||||||
|
|
||||||
|
模板内容使用以下键:
|
||||||
|
|
||||||
|
```properties
|
||||||
|
db.driver=com.mysql.cj.jdbc.Driver
|
||||||
|
db.url=jdbc:mysql://localhost:3306/mzh_library?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
|
||||||
|
db.username=library_user
|
||||||
|
db.password=change_me
|
||||||
|
```
|
||||||
|
|
||||||
|
按本机 MySQL 环境调整 `db.url`、`db.username` 和 `db.password`。实际配置文件不应提交到版本库。
|
||||||
|
|
||||||
|
如果你希望单独创建应用数据库用户,可以在 MySQL 中按需授权,例如:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE USER 'library_user'@'localhost' IDENTIFIED BY '<your-local-password>';
|
||||||
|
GRANT ALL PRIVILEGES ON mzh_library.* TO 'library_user'@'localhost';
|
||||||
|
FLUSH PRIVILEGES;
|
||||||
|
```
|
||||||
|
|
||||||
|
请将示例中的占位密码替换为本地安全密码,不要把真实密码写入 README、提交记录或共享截图。
|
||||||
|
|
||||||
|
## 构建与部署
|
||||||
|
|
||||||
|
在项目根目录执行:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
mvn clean package
|
mvn clean package
|
||||||
```
|
```
|
||||||
|
|
||||||
5. Deploy `target/library-management.war` to Tomcat.
|
如果当前 shell 找不到 `mvn`,本工作区规范记录的 Maven 路径为:
|
||||||
|
|
||||||
The implemented scaffold slices now cover login/permission checks, catalog and book management, reader profile management, borrowing circulation, reader loan history, and the staff report center. Authentication stores only a safe authenticated-user snapshot in the HTTP session, and business workflows stay in Servlet -> Service -> DAO boundaries.
|
```bash
|
||||||
|
/home/sjy/.sdkman/candidates/maven/current/bin/mvn clean package
|
||||||
|
```
|
||||||
|
|
||||||
|
构建成功后会生成:
|
||||||
|
|
||||||
|
```text
|
||||||
|
target/library-management.war
|
||||||
|
```
|
||||||
|
|
||||||
|
部署到 Tomcat 的常见方式:
|
||||||
|
|
||||||
|
1. 确认 MySQL 已启动,`mzh_library` 已初始化。
|
||||||
|
2. 确认 `src/main/resources/db.properties` 已写入本地数据库连接信息。
|
||||||
|
3. 执行 Maven 打包。
|
||||||
|
4. 将 `target/library-management.war` 放入 Tomcat 的 `webapps/` 目录,或通过 Tomcat Manager 上传。
|
||||||
|
5. 启动或重启 Tomcat。
|
||||||
|
6. 默认情况下,WAR 文件名会形成访问上下文 `/library-management`,实际路径以 Tomcat 配置为准。
|
||||||
|
|
||||||
|
示例访问地址:
|
||||||
|
|
||||||
|
```text
|
||||||
|
http://localhost:8080/library-management/login
|
||||||
|
```
|
||||||
|
|
||||||
|
## 主要路由
|
||||||
|
|
||||||
|
以下路由来自 `src/main/webapp/WEB-INF/web.xml`、welcome-file 配置和对应 Servlet:
|
||||||
|
|
||||||
|
| 路由 | 处理者 | 用途 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `/`(welcome-file: `index.jsp`) | `index.jsp` | 欢迎入口,页面会重定向到 `/login` |
|
||||||
|
| `/login` | `LoginServlet` | 登录页和登录提交 |
|
||||||
|
| `/logout` | `LogoutServlet` | 退出登录 |
|
||||||
|
| `/dashboard` | `DashboardServlet` | 登录后的总览页 |
|
||||||
|
| `/admin/home` | `RoleAreaServlet` | 管理员区域首页 |
|
||||||
|
| `/librarian/home` | `RoleAreaServlet` | 馆员工作台 |
|
||||||
|
| `/reader/home` | `RoleAreaServlet` | 读者中心 |
|
||||||
|
| `/catalog` | `BookCatalogServlet` | 馆藏检索 |
|
||||||
|
| `/books`、`/books/new`、`/books/edit`、`/books/update`、`/books/delete` | `BookManagementServlet` | 图书管理 |
|
||||||
|
| `/book-categories`、`/book-categories/new`、`/book-categories/edit`、`/book-categories/update`、`/book-categories/delete` | `BookManagementServlet` | 图书分类管理 |
|
||||||
|
| `/readers`、`/readers/new`、`/readers/edit`、`/readers/update`、`/readers/delete` | `ReaderManagementServlet` | 读者档案管理 |
|
||||||
|
| `/borrowing`、`/borrowing/new`、`/borrowing/create`、`/borrowing/return`、`/borrowing/renew` | `BorrowingManagementServlet` | 借阅、归还、续借 |
|
||||||
|
| `/reader/loans` | `ReaderLoanHistoryServlet` | 当前读者借阅历史 |
|
||||||
|
| `/reports` | `ReportServlet` | 报表中心 |
|
||||||
|
| `/admin/users`、`/admin/users/new`、`/admin/users/edit`、`/admin/users/update`、`/admin/users/deactivate` | `UserManagementServlet` | 管理员用户管理 |
|
||||||
|
| `/admin/system-logs` | `SystemLogServlet` | 系统日志查询 |
|
||||||
|
| `/unauthorized` | `UnauthorizedServlet` | 无权限提示 |
|
||||||
|
|
||||||
|
静态资源路径 `/static/` 不要求登录。除 `/`、`/login`、`/unauthorized`、`/favicon.ico` 和静态资源外,其他页面会经过 `AuthenticationFilter` 和 `AuthorizationFilter`。
|
||||||
|
|
||||||
|
## 角色与权限
|
||||||
|
|
||||||
|
系统当前包含三类角色。运行时权限由 `PermissionPolicy` 根据 `Role` 枚举授予;`schema.sql` 同时保留 `roles`、`permissions`、`role_permissions` 表和本地种子数据,但当前登录流程不会从 `role_permissions` 动态加载权限。
|
||||||
|
|
||||||
|
| 角色代码 | 显示名称 | 权限概览 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `administrator` | 管理员 | 运行时策略授予全部 `Permission` 枚举权限;具体入口仍受路由规则约束 |
|
||||||
|
| `librarian` | 馆员 | 可管理图书、读者、借阅流通,查看报表和馆藏 |
|
||||||
|
| `reader` | 读者 | 可查看馆藏,并访问读者自己的借阅历史 |
|
||||||
|
|
||||||
|
权限代码包括:
|
||||||
|
|
||||||
|
- `manage_users`
|
||||||
|
- `manage_books`
|
||||||
|
- `manage_readers`
|
||||||
|
- `manage_borrowing`
|
||||||
|
- `view_reports`
|
||||||
|
- `view_system_logs`
|
||||||
|
- `view_catalog`
|
||||||
|
- `borrow_books`
|
||||||
|
|
||||||
|
访问控制重点:
|
||||||
|
|
||||||
|
- `/admin/system-logs` 需要 `view_system_logs`。
|
||||||
|
- `/admin/**` 用户管理入口需要 `manage_users`。
|
||||||
|
- `/books/**` 和 `/book-categories/**` 需要 `manage_books`。
|
||||||
|
- `/readers/**` 需要 `manage_readers`。
|
||||||
|
- `/borrowing/**` 需要 `manage_borrowing`。
|
||||||
|
- `/reports` 需要 `view_reports`。
|
||||||
|
- `/catalog` 需要 `view_catalog`。
|
||||||
|
- `/reader/loans` 需要 `borrow_books`,并且必须是 `reader` 角色。
|
||||||
|
|
||||||
|
当前代码中的读者自助入口是 `/reader/loans` 借阅历史查看;借书、还书和续借操作由管理员或馆员通过 `/borrowing` 工作台处理。
|
||||||
|
|
||||||
|
## 业务规则摘要
|
||||||
|
|
||||||
|
- 图书以 `book_identifier` 作为面向用户的唯一编号。
|
||||||
|
- 图书分类名称唯一,已被图书引用的分类不能直接删除。
|
||||||
|
- 图书状态使用 `available`、`unavailable`、`archived`。
|
||||||
|
- 读者以 `reader_identifier` 作为面向用户的唯一编号。
|
||||||
|
- 读者状态使用 `active`、`suspended`、`inactive`,最大借阅数量范围为 1 到 50。
|
||||||
|
- 借阅记录状态使用 `active`、`returned`;逾期不是单独状态,而是由未归还且 `due_at` 早于当前时间的记录推导。
|
||||||
|
- 借书会在同一事务中创建借阅记录并减少图书可借册数。
|
||||||
|
- 还书会在同一事务中标记归还并恢复可借册数。
|
||||||
|
- 续借会延长应还时间并增加续借次数,当前 MVP 对单笔借阅限制一次续借。
|
||||||
|
- 用户管理变更会写入系统日志,审计信息不应记录密码、明文凭据或密码哈希。
|
||||||
|
|
||||||
|
## 开发约定
|
||||||
|
|
||||||
|
- 保持 Servlet -> Service -> DAO -> MySQL 的层次边界。
|
||||||
|
- JSP 页面只渲染 Servlet 设置的 request/session 属性,避免 JSP scriptlet、SQL、JDBC 或业务流程。
|
||||||
|
- 受保护操作必须经过服务层权限校验,不能只依赖页面按钮是否显示。
|
||||||
|
- 新增数据库访问时使用参数化 SQL 或 PreparedStatement 风格,避免拼接用户输入。
|
||||||
|
- 多表一致性操作放在服务层事务边界中,DAO 只处理具体 SQL。
|
||||||
|
- 用户可见的页面文案、表单标签、按钮、空状态和服务反馈消息使用简体中文。
|
||||||
|
- URL、请求参数名、Java 标识符、数据库枚举值和权限代码保持现有代码形式。
|
||||||
|
- 本地私密配置只放在 `src/main/resources/db.properties`,不要提交真实凭据。
|
||||||
|
|
||||||
|
## 测试与检查
|
||||||
|
|
||||||
|
当前仓库在 `src/test/java/com/mzh/library/service/` 下包含多个自检类:
|
||||||
|
|
||||||
|
- `AuthServiceCheck`
|
||||||
|
- `BookServiceCheck`
|
||||||
|
- `BorrowingServiceCheck`
|
||||||
|
- `PermissionPolicyCheck`
|
||||||
|
- `ReaderServiceCheck`
|
||||||
|
- `ReportServiceCheck`
|
||||||
|
- `SystemLogServiceCheck`
|
||||||
|
- `UserAccountServiceCheck`
|
||||||
|
|
||||||
|
这些自检类使用 `public static void main` 和内部断言。当前 `pom.xml` 没有引入 JUnit/TestNG,也没有配置 Surefire 执行 `*Check`,因此 `mvn test` 会编译测试源码,但不会自动运行这些自检类。
|
||||||
|
|
||||||
|
常用编译和打包检查命令:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mvn test
|
||||||
|
mvn clean package
|
||||||
|
```
|
||||||
|
|
||||||
|
如需手动运行自检类,可在 `mvn test` 编译完成后执行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
for check in AuthServiceCheck BookServiceCheck BorrowingServiceCheck PermissionPolicyCheck ReaderServiceCheck ReportServiceCheck SystemLogServiceCheck UserAccountServiceCheck; do
|
||||||
|
java -cp target/classes:target/test-classes "com.mzh.library.service.${check}"
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
如果 Maven 不在 `PATH` 中,可使用:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
/home/sjy/.sdkman/candidates/maven/current/bin/mvn test
|
||||||
|
/home/sjy/.sdkman/candidates/maven/current/bin/mvn clean package
|
||||||
|
```
|
||||||
|
|
||||||
|
文档或页面调整后的人工自查建议:
|
||||||
|
|
||||||
|
- README 中的路径、路由和依赖版本是否与 `pom.xml`、`web.xml`、`schema.sql` 一致。
|
||||||
|
- 是否意外写入真实数据库账号、密码、连接串或生产部署信息。
|
||||||
|
- JSP 中是否出现 SQL、JDBC、密码字段展示或业务逻辑 scriptlet。
|
||||||
|
- 管理入口是否仍与 `AuthorizationFilter` 的权限规则一致。
|
||||||
|
|
||||||
|
## 常见问题
|
||||||
|
|
||||||
|
### 访问页面时跳回登录页
|
||||||
|
|
||||||
|
除 `/`、`/login`、`/unauthorized`、`/favicon.ico` 和 `/static/` 外,系统默认要求登录。请确认已经登录,并检查 Tomcat 会话是否过期。当前登录会话超时时间为 30 分钟。
|
||||||
|
|
||||||
|
### 登录或数据库操作提示服务不可用
|
||||||
|
|
||||||
|
优先检查:
|
||||||
|
|
||||||
|
- MySQL 是否启动。
|
||||||
|
- `mzh_library` 是否已经通过 `schema.sql` 初始化。
|
||||||
|
- `src/main/resources/db.properties` 是否存在。
|
||||||
|
- `db.url`、`db.username`、`db.password` 是否与本地 MySQL 一致。
|
||||||
|
- MySQL Connector/J 依赖是否已被 Maven 正确下载。
|
||||||
|
|
||||||
|
### 打包后访问路径不是 `/library-management`
|
||||||
|
|
||||||
|
Maven 当前将 WAR 产物命名为 `library-management.war`。Tomcat 通常会用 WAR 文件名作为上下文路径,但如果你在 Tomcat 中手动配置了 Context,最终访问路径以 Tomcat 配置为准。
|
||||||
|
|
||||||
|
### 可以把 `db.properties` 提交吗?
|
||||||
|
|
||||||
|
不可以。`src/main/resources/db.properties` 是本地私密配置,已经被 `.gitignore` 忽略。只应提交 `src/main/resources/db.properties.example`。
|
||||||
|
|
||||||
|
### README 为什么不列出演示账号密码?
|
||||||
|
|
||||||
|
数据库脚本包含本地验证用演示数据,但项目要求 README 不写入未经确认的默认登录明文密码,也不扩散任何凭据。需要本地调试时,请由维护者按当前数据库脚本和安全要求单独确认或重置账号。
|
||||||
|
|
||||||
|
## 维护提示
|
||||||
|
|
||||||
|
当 `pom.xml`、`web.xml`、数据库表结构、角色权限、主要 JSP 路径或测试入口变化时,应同步更新本 README,避免部署步骤和功能说明与实际代码脱节。
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ import javax.servlet.http.HttpSession;
|
|||||||
public class BookManagementServlet extends HttpServlet {
|
public class BookManagementServlet extends HttpServlet {
|
||||||
private static final String MANAGE_JSP = "/WEB-INF/jsp/books/manage.jsp";
|
private static final String MANAGE_JSP = "/WEB-INF/jsp/books/manage.jsp";
|
||||||
private static final String FORM_JSP = "/WEB-INF/jsp/books/form.jsp";
|
private static final String FORM_JSP = "/WEB-INF/jsp/books/form.jsp";
|
||||||
|
private static final String CATEGORY_MANAGE_JSP = "/WEB-INF/jsp/books/categories.jsp";
|
||||||
|
private static final String CATEGORY_FORM_JSP = "/WEB-INF/jsp/books/category-form.jsp";
|
||||||
private static final String UNAUTHORIZED_JSP = "/WEB-INF/jsp/auth/unauthorized.jsp";
|
private static final String UNAUTHORIZED_JSP = "/WEB-INF/jsp/auth/unauthorized.jsp";
|
||||||
private static final String FLASH_SUCCESS = "flashSuccess";
|
private static final String FLASH_SUCCESS = "flashSuccess";
|
||||||
private static final String FLASH_ERROR = "flashError";
|
private static final String FLASH_ERROR = "flashError";
|
||||||
@@ -42,7 +44,7 @@ public class BookManagementServlet extends HttpServlet {
|
|||||||
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
|
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
|
||||||
String path = request.getServletPath();
|
String path = request.getServletPath();
|
||||||
if ("/books/new".equals(path)) {
|
if ("/books/new".equals(path)) {
|
||||||
renderForm(request, response, "Create book", "/books", new Book(), Collections.emptyMap(),
|
renderForm(request, response, "创建图书", "/books", new Book(), Collections.emptyMap(),
|
||||||
Collections.emptyMap(), null);
|
Collections.emptyMap(), null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -50,6 +52,19 @@ public class BookManagementServlet extends HttpServlet {
|
|||||||
showEditForm(request, response);
|
showEditForm(request, response);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if ("/book-categories".equals(path)) {
|
||||||
|
showCategoryList(request, response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ("/book-categories/new".equals(path)) {
|
||||||
|
renderCategoryForm(request, response, "创建分类", "/book-categories", new BookCategory(),
|
||||||
|
Collections.emptyMap(), Collections.emptyMap(), null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ("/book-categories/edit".equals(path)) {
|
||||||
|
showEditCategoryForm(request, response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!"/books".equals(path)) {
|
if (!"/books".equals(path)) {
|
||||||
response.sendError(HttpServletResponse.SC_NOT_FOUND);
|
response.sendError(HttpServletResponse.SC_NOT_FOUND);
|
||||||
return;
|
return;
|
||||||
@@ -73,6 +88,18 @@ public class BookManagementServlet extends HttpServlet {
|
|||||||
deleteBook(request, response);
|
deleteBook(request, response);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if ("/book-categories".equals(path)) {
|
||||||
|
createCategory(request, response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ("/book-categories/update".equals(path)) {
|
||||||
|
updateCategory(request, response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ("/book-categories/delete".equals(path)) {
|
||||||
|
deleteCategory(request, response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
response.sendError(HttpServletResponse.SC_NOT_FOUND);
|
response.sendError(HttpServletResponse.SC_NOT_FOUND);
|
||||||
}
|
}
|
||||||
@@ -107,26 +134,52 @@ public class BookManagementServlet extends HttpServlet {
|
|||||||
long id = requiredLong(request.getParameter("id"), -1L);
|
long id = requiredLong(request.getParameter("id"), -1L);
|
||||||
ServiceResult<Optional<Book>> result = bookService.findBook(id);
|
ServiceResult<Optional<Book>> result = bookService.findBook(id);
|
||||||
if (!result.isSuccessful() || !result.getData().isPresent()) {
|
if (!result.isSuccessful() || !result.getData().isPresent()) {
|
||||||
flashError(request, result.isSuccessful() ? "Book was not found." : result.getMessage());
|
flashError(request, result.isSuccessful() ? "未找到图书。" : result.getMessage());
|
||||||
response.sendRedirect(request.getContextPath() + "/books");
|
response.sendRedirect(request.getContextPath() + "/books");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
renderForm(request, response, "Edit book", "/books/update", result.getData().get(),
|
renderForm(request, response, "编辑图书", "/books/update", result.getData().get(),
|
||||||
|
Collections.emptyMap(), Collections.emptyMap(), null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showCategoryList(HttpServletRequest request, HttpServletResponse response)
|
||||||
|
throws ServletException, IOException {
|
||||||
|
applyFlash(request);
|
||||||
|
|
||||||
|
ServiceResult<List<BookCategory>> result = bookService.listCategories();
|
||||||
|
request.setAttribute("categories", result.isSuccessful() ? result.getData() : Collections.emptyList());
|
||||||
|
if (!result.isSuccessful()) {
|
||||||
|
request.setAttribute("errorMessage", result.getMessage());
|
||||||
|
}
|
||||||
|
request.getRequestDispatcher(CATEGORY_MANAGE_JSP).forward(request, response);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showEditCategoryForm(HttpServletRequest request, HttpServletResponse response)
|
||||||
|
throws ServletException, IOException {
|
||||||
|
long id = requiredLong(request.getParameter("id"), -1L);
|
||||||
|
ServiceResult<Optional<BookCategory>> result = bookService.findCategory(id);
|
||||||
|
if (!result.isSuccessful() || !result.getData().isPresent()) {
|
||||||
|
flashError(request, result.isSuccessful() ? "未找到分类。" : result.getMessage());
|
||||||
|
response.sendRedirect(request.getContextPath() + "/book-categories");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderCategoryForm(request, response, "编辑分类", "/book-categories/update", result.getData().get(),
|
||||||
Collections.emptyMap(), Collections.emptyMap(), null);
|
Collections.emptyMap(), Collections.emptyMap(), null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void createBook(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
|
private void createBook(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
|
||||||
BookForm form = readBookForm(request, false);
|
BookForm form = readBookForm(request, false);
|
||||||
if (!form.getErrors().isEmpty()) {
|
if (!form.getErrors().isEmpty()) {
|
||||||
renderForm(request, response, "Create book", "/books", form.getBook(), form.getValues(),
|
renderForm(request, response, "创建图书", "/books", form.getBook(), form.getValues(),
|
||||||
form.getErrors(), "Please correct the highlighted book fields.");
|
form.getErrors(), "请修正高亮的图书字段。");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ServiceResult<Long> result = bookService.createBook(currentUser(request), form.getBook());
|
ServiceResult<Long> result = bookService.createBook(currentUser(request), form.getBook());
|
||||||
if (!result.isSuccessful()) {
|
if (!result.isSuccessful()) {
|
||||||
handleFormFailure(request, response, "Create book", "/books", form, result);
|
handleFormFailure(request, response, "创建图书", "/books", form, result);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,14 +190,14 @@ public class BookManagementServlet extends HttpServlet {
|
|||||||
private void updateBook(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
|
private void updateBook(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
|
||||||
BookForm form = readBookForm(request, true);
|
BookForm form = readBookForm(request, true);
|
||||||
if (!form.getErrors().isEmpty()) {
|
if (!form.getErrors().isEmpty()) {
|
||||||
renderForm(request, response, "Edit book", "/books/update", form.getBook(), form.getValues(),
|
renderForm(request, response, "编辑图书", "/books/update", form.getBook(), form.getValues(),
|
||||||
form.getErrors(), "Please correct the highlighted book fields.");
|
form.getErrors(), "请修正高亮的图书字段。");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ServiceResult<Void> result = bookService.updateBook(currentUser(request), form.getBook());
|
ServiceResult<Void> result = bookService.updateBook(currentUser(request), form.getBook());
|
||||||
if (!result.isSuccessful()) {
|
if (!result.isSuccessful()) {
|
||||||
handleFormFailure(request, response, "Edit book", "/books/update", form, result);
|
handleFormFailure(request, response, "编辑图书", "/books/update", form, result);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,6 +220,60 @@ public class BookManagementServlet extends HttpServlet {
|
|||||||
response.sendRedirect(request.getContextPath() + "/books");
|
response.sendRedirect(request.getContextPath() + "/books");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void createCategory(HttpServletRequest request, HttpServletResponse response)
|
||||||
|
throws ServletException, IOException {
|
||||||
|
CategoryForm form = readCategoryForm(request, false);
|
||||||
|
if (!form.getErrors().isEmpty()) {
|
||||||
|
renderCategoryForm(request, response, "创建分类", "/book-categories", form.getCategory(),
|
||||||
|
form.getValues(), form.getErrors(), "请修正高亮的分类字段。");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ServiceResult<Long> result = bookService.createCategory(currentUser(request), form.getCategory());
|
||||||
|
if (!result.isSuccessful()) {
|
||||||
|
handleCategoryFormFailure(request, response, "创建分类", "/book-categories", form, result);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
flashSuccess(request, result.getMessage());
|
||||||
|
response.sendRedirect(request.getContextPath() + "/book-categories");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateCategory(HttpServletRequest request, HttpServletResponse response)
|
||||||
|
throws ServletException, IOException {
|
||||||
|
CategoryForm form = readCategoryForm(request, true);
|
||||||
|
if (!form.getErrors().isEmpty()) {
|
||||||
|
renderCategoryForm(request, response, "编辑分类", "/book-categories/update", form.getCategory(),
|
||||||
|
form.getValues(), form.getErrors(), "请修正高亮的分类字段。");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ServiceResult<Void> result = bookService.updateCategory(currentUser(request), form.getCategory());
|
||||||
|
if (!result.isSuccessful()) {
|
||||||
|
handleCategoryFormFailure(request, response, "编辑分类", "/book-categories/update", form, result);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
flashSuccess(request, result.getMessage());
|
||||||
|
response.sendRedirect(request.getContextPath() + "/book-categories");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void deleteCategory(HttpServletRequest request, HttpServletResponse response)
|
||||||
|
throws IOException, ServletException {
|
||||||
|
long id = requiredLong(request.getParameter("id"), -1L);
|
||||||
|
ServiceResult<Void> result = bookService.deleteCategory(currentUser(request), id);
|
||||||
|
if (isPermissionDenied(result)) {
|
||||||
|
forwardDenied(request, response, result.getMessage());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (result.isSuccessful()) {
|
||||||
|
flashSuccess(request, result.getMessage());
|
||||||
|
} else {
|
||||||
|
flashError(request, result.getMessage());
|
||||||
|
}
|
||||||
|
response.sendRedirect(request.getContextPath() + "/book-categories");
|
||||||
|
}
|
||||||
|
|
||||||
private void handleFormFailure(HttpServletRequest request, HttpServletResponse response, String title,
|
private void handleFormFailure(HttpServletRequest request, HttpServletResponse response, String title,
|
||||||
String action, BookForm form, ServiceResult<?> result)
|
String action, BookForm form, ServiceResult<?> result)
|
||||||
throws ServletException, IOException {
|
throws ServletException, IOException {
|
||||||
@@ -178,6 +285,17 @@ public class BookManagementServlet extends HttpServlet {
|
|||||||
result.getMessage());
|
result.getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void handleCategoryFormFailure(HttpServletRequest request, HttpServletResponse response, String title,
|
||||||
|
String action, CategoryForm form, ServiceResult<?> result)
|
||||||
|
throws ServletException, IOException {
|
||||||
|
if (isPermissionDenied(result)) {
|
||||||
|
forwardDenied(request, response, result.getMessage());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
renderCategoryForm(request, response, title, action, form.getCategory(), form.getValues(),
|
||||||
|
result.getErrors(), result.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
private void renderForm(HttpServletRequest request, HttpServletResponse response, String title, String action,
|
private void renderForm(HttpServletRequest request, HttpServletResponse response, String title, String action,
|
||||||
Book book, Map<String, String> formValues, Map<String, String> errors, String errorMessage)
|
Book book, Map<String, String> formValues, Map<String, String> errors, String errorMessage)
|
||||||
throws ServletException, IOException {
|
throws ServletException, IOException {
|
||||||
@@ -199,26 +317,41 @@ public class BookManagementServlet extends HttpServlet {
|
|||||||
request.getRequestDispatcher(FORM_JSP).forward(request, response);
|
request.getRequestDispatcher(FORM_JSP).forward(request, response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void renderCategoryForm(HttpServletRequest request, HttpServletResponse response, String title,
|
||||||
|
String action, BookCategory category, Map<String, String> formValues,
|
||||||
|
Map<String, String> errors, String errorMessage)
|
||||||
|
throws ServletException, IOException {
|
||||||
|
request.setAttribute("formTitle", title);
|
||||||
|
request.setAttribute("formAction", action);
|
||||||
|
request.setAttribute("category", category);
|
||||||
|
request.setAttribute("formValues", formValues);
|
||||||
|
request.setAttribute("errors", errors);
|
||||||
|
if (errorMessage != null && !errorMessage.isEmpty()) {
|
||||||
|
request.setAttribute("errorMessage", errorMessage);
|
||||||
|
}
|
||||||
|
request.getRequestDispatcher(CATEGORY_FORM_JSP).forward(request, response);
|
||||||
|
}
|
||||||
|
|
||||||
private BookForm readBookForm(HttpServletRequest request, boolean requireId) {
|
private BookForm readBookForm(HttpServletRequest request, boolean requireId) {
|
||||||
Map<String, String> values = formValues(request);
|
Map<String, String> values = formValues(request);
|
||||||
Map<String, String> errors = new LinkedHashMap<>();
|
Map<String, String> errors = new LinkedHashMap<>();
|
||||||
Book book = new Book();
|
Book book = new Book();
|
||||||
|
|
||||||
if (requireId) {
|
if (requireId) {
|
||||||
book.setId(parseLong(values.get("id"), "id", "Select a valid book.", errors));
|
book.setId(parseLong(values.get("id"), "id", "请选择有效的图书。", errors));
|
||||||
}
|
}
|
||||||
book.setIdentifier(values.get("identifier"));
|
book.setIdentifier(values.get("identifier"));
|
||||||
book.setTitle(values.get("title"));
|
book.setTitle(values.get("title"));
|
||||||
book.setAuthor(values.get("author"));
|
book.setAuthor(values.get("author"));
|
||||||
book.setCategoryId(parseLong(values.get("categoryId"), "categoryId", "Select a category.", errors));
|
book.setCategoryId(parseLong(values.get("categoryId"), "categoryId", "请选择分类。", errors));
|
||||||
book.setTotalCopies(parseInt(values.get("totalCopies"), "totalCopies", "Enter a valid total copy count.", errors));
|
book.setTotalCopies(parseInt(values.get("totalCopies"), "totalCopies", "请输入有效的馆藏总数。", errors));
|
||||||
book.setAvailableCopies(parseInt(values.get("availableCopies"), "availableCopies",
|
book.setAvailableCopies(parseInt(values.get("availableCopies"), "availableCopies",
|
||||||
"Enter a valid available copy count.", errors));
|
"请输入有效的可借数量。", errors));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
book.setStatus(BookStatus.fromCode(values.get("status")));
|
book.setStatus(BookStatus.fromCode(values.get("status")));
|
||||||
} catch (IllegalArgumentException ex) {
|
} catch (IllegalArgumentException ex) {
|
||||||
errors.put("status", "Select a status.");
|
errors.put("status", "请选择状态。");
|
||||||
}
|
}
|
||||||
|
|
||||||
return new BookForm(book, values, errors);
|
return new BookForm(book, values, errors);
|
||||||
@@ -237,6 +370,27 @@ public class BookManagementServlet extends HttpServlet {
|
|||||||
return values;
|
return values;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private CategoryForm readCategoryForm(HttpServletRequest request, boolean requireId) {
|
||||||
|
Map<String, String> values = categoryFormValues(request);
|
||||||
|
Map<String, String> errors = new LinkedHashMap<>();
|
||||||
|
BookCategory category = new BookCategory();
|
||||||
|
|
||||||
|
if (requireId) {
|
||||||
|
category.setId(parseLong(values.get("id"), "id", "请选择有效的分类。", errors));
|
||||||
|
}
|
||||||
|
category.setName(values.get("name"));
|
||||||
|
category.setDescription(values.get("description"));
|
||||||
|
return new CategoryForm(category, values, errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, String> categoryFormValues(HttpServletRequest request) {
|
||||||
|
Map<String, String> values = new LinkedHashMap<>();
|
||||||
|
values.put("id", trim(request.getParameter("id")));
|
||||||
|
values.put("name", trim(request.getParameter("name")));
|
||||||
|
values.put("description", trim(request.getParameter("description")));
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
private BookSearchCriteria searchCriteria(HttpServletRequest request) {
|
private BookSearchCriteria searchCriteria(HttpServletRequest request) {
|
||||||
return new BookSearchCriteria(
|
return new BookSearchCriteria(
|
||||||
request.getParameter("identifier"),
|
request.getParameter("identifier"),
|
||||||
@@ -300,7 +454,7 @@ public class BookManagementServlet extends HttpServlet {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private boolean isPermissionDenied(ServiceResult<?> result) {
|
private boolean isPermissionDenied(ServiceResult<?> result) {
|
||||||
return !result.isSuccessful() && "You do not have permission to manage books.".equals(result.getMessage());
|
return !result.isSuccessful() && "您无权管理图书。".equals(result.getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void forwardDenied(HttpServletRequest request, HttpServletResponse response, String message)
|
private void forwardDenied(HttpServletRequest request, HttpServletResponse response, String message)
|
||||||
@@ -368,4 +522,28 @@ public class BookManagementServlet extends HttpServlet {
|
|||||||
return errors;
|
return errors;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static final class CategoryForm {
|
||||||
|
private final BookCategory category;
|
||||||
|
private final Map<String, String> values;
|
||||||
|
private final Map<String, String> errors;
|
||||||
|
|
||||||
|
private CategoryForm(BookCategory category, Map<String, String> values, Map<String, String> errors) {
|
||||||
|
this.category = category;
|
||||||
|
this.values = values;
|
||||||
|
this.errors = errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
private BookCategory getCategory() {
|
||||||
|
return category;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, String> getValues() {
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, String> getErrors() {
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ public class BorrowingManagementServlet extends HttpServlet {
|
|||||||
throws IOException, ServletException {
|
throws IOException, ServletException {
|
||||||
long id = requiredLong(request.getParameter("id"), -1L);
|
long id = requiredLong(request.getParameter("id"), -1L);
|
||||||
ServiceResult<Void> result = id <= 0
|
ServiceResult<Void> result = id <= 0
|
||||||
? ServiceResult.failure("Select a valid borrowing record.")
|
? ServiceResult.failure("请选择有效的借阅记录。")
|
||||||
: borrowingService.returnBook(currentUser(request), id);
|
: borrowingService.returnBook(currentUser(request), id);
|
||||||
redirectWithResult(request, response, result);
|
redirectWithResult(request, response, result);
|
||||||
}
|
}
|
||||||
@@ -124,7 +124,7 @@ public class BorrowingManagementServlet extends HttpServlet {
|
|||||||
throws IOException, ServletException {
|
throws IOException, ServletException {
|
||||||
long id = requiredLong(request.getParameter("id"), -1L);
|
long id = requiredLong(request.getParameter("id"), -1L);
|
||||||
ServiceResult<Void> result = id <= 0
|
ServiceResult<Void> result = id <= 0
|
||||||
? ServiceResult.failure("Select a valid borrowing record.")
|
? ServiceResult.failure("请选择有效的借阅记录。")
|
||||||
: borrowingService.renewLoan(currentUser(request), id);
|
: borrowingService.renewLoan(currentUser(request), id);
|
||||||
redirectWithResult(request, response, result);
|
redirectWithResult(request, response, result);
|
||||||
}
|
}
|
||||||
@@ -185,12 +185,12 @@ public class BorrowingManagementServlet extends HttpServlet {
|
|||||||
if (result.hasErrors()) {
|
if (result.hasErrors()) {
|
||||||
return result.getErrors().values().iterator().next();
|
return result.getErrors().values().iterator().next();
|
||||||
}
|
}
|
||||||
return "Borrowing action failed.";
|
return "借阅操作失败。";
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isPermissionDenied(ServiceResult<?> result) {
|
private boolean isPermissionDenied(ServiceResult<?> result) {
|
||||||
return !result.isSuccessful()
|
return !result.isSuccessful()
|
||||||
&& "You do not have permission to manage borrowing.".equals(result.getMessage());
|
&& "您无权管理借阅。".equals(result.getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void forwardDenied(HttpServletRequest request, HttpServletResponse response, String message)
|
private void forwardDenied(HttpServletRequest request, HttpServletResponse response, String message)
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import javax.servlet.http.HttpSession;
|
|||||||
public class ReaderLoanHistoryServlet extends HttpServlet {
|
public class ReaderLoanHistoryServlet extends HttpServlet {
|
||||||
private static final String HISTORY_JSP = "/WEB-INF/jsp/reader/loans.jsp";
|
private static final String HISTORY_JSP = "/WEB-INF/jsp/reader/loans.jsp";
|
||||||
private static final String UNAUTHORIZED_JSP = "/WEB-INF/jsp/auth/unauthorized.jsp";
|
private static final String UNAUTHORIZED_JSP = "/WEB-INF/jsp/auth/unauthorized.jsp";
|
||||||
private static final String HISTORY_DENIED_MESSAGE = "You do not have permission to view loan history.";
|
private static final String HISTORY_DENIED_MESSAGE = "您无权查看借阅历史。";
|
||||||
|
|
||||||
private BorrowingServiceImpl borrowingService;
|
private BorrowingServiceImpl borrowingService;
|
||||||
|
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ public class ReaderManagementServlet extends HttpServlet {
|
|||||||
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
|
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
|
||||||
String path = request.getServletPath();
|
String path = request.getServletPath();
|
||||||
if ("/readers/new".equals(path)) {
|
if ("/readers/new".equals(path)) {
|
||||||
renderForm(request, response, "Create reader", "/readers", defaultReader(), Collections.emptyMap(),
|
renderForm(request, response, "创建读者", "/readers", defaultReader(), Collections.emptyMap(),
|
||||||
Collections.emptyMap(), null);
|
Collections.emptyMap(), null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -99,12 +99,12 @@ public class ReaderManagementServlet extends HttpServlet {
|
|||||||
long id = requiredLong(request.getParameter("id"), -1L);
|
long id = requiredLong(request.getParameter("id"), -1L);
|
||||||
ServiceResult<Optional<Reader>> result = readerService.findReader(id);
|
ServiceResult<Optional<Reader>> result = readerService.findReader(id);
|
||||||
if (!result.isSuccessful() || !result.getData().isPresent()) {
|
if (!result.isSuccessful() || !result.getData().isPresent()) {
|
||||||
flashError(request, result.isSuccessful() ? "Reader profile was not found." : result.getMessage());
|
flashError(request, result.isSuccessful() ? "未找到读者档案。" : result.getMessage());
|
||||||
response.sendRedirect(request.getContextPath() + "/readers");
|
response.sendRedirect(request.getContextPath() + "/readers");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
renderForm(request, response, "Edit reader", "/readers/update", result.getData().get(),
|
renderForm(request, response, "编辑读者", "/readers/update", result.getData().get(),
|
||||||
Collections.emptyMap(), Collections.emptyMap(), null);
|
Collections.emptyMap(), Collections.emptyMap(), null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,14 +112,14 @@ public class ReaderManagementServlet extends HttpServlet {
|
|||||||
throws ServletException, IOException {
|
throws ServletException, IOException {
|
||||||
ReaderForm form = readReaderForm(request, false);
|
ReaderForm form = readReaderForm(request, false);
|
||||||
if (!form.getErrors().isEmpty()) {
|
if (!form.getErrors().isEmpty()) {
|
||||||
renderForm(request, response, "Create reader", "/readers", form.getReader(), form.getValues(),
|
renderForm(request, response, "创建读者", "/readers", form.getReader(), form.getValues(),
|
||||||
form.getErrors(), "Please correct the highlighted reader fields.");
|
form.getErrors(), "请修正高亮的读者字段。");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ServiceResult<Long> result = readerService.createReader(currentUser(request), form.getReader());
|
ServiceResult<Long> result = readerService.createReader(currentUser(request), form.getReader());
|
||||||
if (!result.isSuccessful()) {
|
if (!result.isSuccessful()) {
|
||||||
handleFormFailure(request, response, "Create reader", "/readers", form, result);
|
handleFormFailure(request, response, "创建读者", "/readers", form, result);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,14 +131,14 @@ public class ReaderManagementServlet extends HttpServlet {
|
|||||||
throws ServletException, IOException {
|
throws ServletException, IOException {
|
||||||
ReaderForm form = readReaderForm(request, true);
|
ReaderForm form = readReaderForm(request, true);
|
||||||
if (!form.getErrors().isEmpty()) {
|
if (!form.getErrors().isEmpty()) {
|
||||||
renderForm(request, response, "Edit reader", "/readers/update", form.getReader(), form.getValues(),
|
renderForm(request, response, "编辑读者", "/readers/update", form.getReader(), form.getValues(),
|
||||||
form.getErrors(), "Please correct the highlighted reader fields.");
|
form.getErrors(), "请修正高亮的读者字段。");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ServiceResult<Void> result = readerService.updateReader(currentUser(request), form.getReader());
|
ServiceResult<Void> result = readerService.updateReader(currentUser(request), form.getReader());
|
||||||
if (!result.isSuccessful()) {
|
if (!result.isSuccessful()) {
|
||||||
handleFormFailure(request, response, "Edit reader", "/readers/update", form, result);
|
handleFormFailure(request, response, "编辑读者", "/readers/update", form, result);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,21 +195,21 @@ public class ReaderManagementServlet extends HttpServlet {
|
|||||||
Reader reader = new Reader();
|
Reader reader = new Reader();
|
||||||
|
|
||||||
if (requireId) {
|
if (requireId) {
|
||||||
reader.setId(parseLong(values.get("id"), "id", "Select a valid reader.", errors));
|
reader.setId(parseLong(values.get("id"), "id", "请选择有效的读者。", errors));
|
||||||
}
|
}
|
||||||
reader.setIdentifier(values.get("identifier"));
|
reader.setIdentifier(values.get("identifier"));
|
||||||
reader.setUserId(optionalPositiveLong(values.get("userId"), "userId",
|
reader.setUserId(optionalPositiveLong(values.get("userId"), "userId",
|
||||||
"Enter a valid linked account ID.", errors));
|
"请输入有效的关联账户 ID。", errors));
|
||||||
reader.setFullName(values.get("fullName"));
|
reader.setFullName(values.get("fullName"));
|
||||||
reader.setPhone(values.get("phone"));
|
reader.setPhone(values.get("phone"));
|
||||||
reader.setEmail(values.get("email"));
|
reader.setEmail(values.get("email"));
|
||||||
reader.setMaxBorrowCount(parseInt(values.get("maxBorrowCount"), "maxBorrowCount",
|
reader.setMaxBorrowCount(parseInt(values.get("maxBorrowCount"), "maxBorrowCount",
|
||||||
"Enter a valid max borrow count.", errors));
|
"请输入有效的最大借阅数量。", errors));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
reader.setStatus(ReaderStatus.fromCode(values.get("status")));
|
reader.setStatus(ReaderStatus.fromCode(values.get("status")));
|
||||||
} catch (IllegalArgumentException ex) {
|
} catch (IllegalArgumentException ex) {
|
||||||
errors.put("status", "Select a status.");
|
errors.put("status", "请选择状态。");
|
||||||
}
|
}
|
||||||
|
|
||||||
return new ReaderForm(reader, values, errors);
|
return new ReaderForm(reader, values, errors);
|
||||||
@@ -304,7 +304,7 @@ public class ReaderManagementServlet extends HttpServlet {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private boolean isPermissionDenied(ServiceResult<?> result) {
|
private boolean isPermissionDenied(ServiceResult<?> result) {
|
||||||
return !result.isSuccessful() && "You do not have permission to manage readers.".equals(result.getMessage());
|
return !result.isSuccessful() && "您无权管理读者。".equals(result.getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void forwardDenied(HttpServletRequest request, HttpServletResponse response, String message)
|
private void forwardDenied(HttpServletRequest request, HttpServletResponse response, String message)
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ public class ReportServlet extends HttpServlet {
|
|||||||
|
|
||||||
private boolean isPermissionDenied(ServiceResult<?> result) {
|
private boolean isPermissionDenied(ServiceResult<?> result) {
|
||||||
return !result.isSuccessful()
|
return !result.isSuccessful()
|
||||||
&& "You do not have permission to view reports.".equals(result.getMessage());
|
&& "您无权查看报表。".equals(result.getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void forwardDenied(HttpServletRequest request, HttpServletResponse response, String message)
|
private void forwardDenied(HttpServletRequest request, HttpServletResponse response, String message)
|
||||||
|
|||||||
@@ -14,14 +14,14 @@ public class RoleAreaServlet extends HttpServlet {
|
|||||||
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
|
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
|
||||||
String servletPath = request.getServletPath();
|
String servletPath = request.getServletPath();
|
||||||
if (servletPath.startsWith("/admin")) {
|
if (servletPath.startsWith("/admin")) {
|
||||||
request.setAttribute("areaName", "Administration");
|
request.setAttribute("areaName", "系统管理");
|
||||||
request.setAttribute("areaSummary", "Account, role, permission, and system-maintenance entry point.");
|
request.setAttribute("areaSummary", "账户、角色、权限和系统维护入口。");
|
||||||
} else if (servletPath.startsWith("/librarian")) {
|
} else if (servletPath.startsWith("/librarian")) {
|
||||||
request.setAttribute("areaName", "Librarian Workspace");
|
request.setAttribute("areaName", "馆员工作台");
|
||||||
request.setAttribute("areaSummary", "Book, reader, borrowing, return, renewal, and overdue entry point.");
|
request.setAttribute("areaSummary", "图书、读者、借阅、归还、续借和逾期处理入口。");
|
||||||
} else {
|
} else {
|
||||||
request.setAttribute("areaName", "Reader Center");
|
request.setAttribute("areaName", "读者中心");
|
||||||
request.setAttribute("areaSummary", "Catalog search and reader self-service entry point.");
|
request.setAttribute("areaSummary", "馆藏检索和读者自助服务入口。");
|
||||||
}
|
}
|
||||||
|
|
||||||
request.getRequestDispatcher(ROLE_HOME_JSP).forward(request, response);
|
request.getRequestDispatcher(ROLE_HOME_JSP).forward(request, response);
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import javax.servlet.http.HttpSession;
|
|||||||
public class SystemLogServlet extends HttpServlet {
|
public class SystemLogServlet extends HttpServlet {
|
||||||
private static final String LOGS_JSP = "/WEB-INF/jsp/maintenance/system-logs.jsp";
|
private static final String LOGS_JSP = "/WEB-INF/jsp/maintenance/system-logs.jsp";
|
||||||
private static final String UNAUTHORIZED_JSP = "/WEB-INF/jsp/auth/unauthorized.jsp";
|
private static final String UNAUTHORIZED_JSP = "/WEB-INF/jsp/auth/unauthorized.jsp";
|
||||||
private static final String DENIED_MESSAGE = "You do not have permission to view system logs.";
|
private static final String DENIED_MESSAGE = "您无权查看系统日志。";
|
||||||
|
|
||||||
private SystemLogService systemLogService;
|
private SystemLogService systemLogService;
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ public class UserManagementServlet extends HttpServlet {
|
|||||||
private static final String UNAUTHORIZED_JSP = "/WEB-INF/jsp/auth/unauthorized.jsp";
|
private static final String UNAUTHORIZED_JSP = "/WEB-INF/jsp/auth/unauthorized.jsp";
|
||||||
private static final String FLASH_SUCCESS = "flashSuccess";
|
private static final String FLASH_SUCCESS = "flashSuccess";
|
||||||
private static final String FLASH_ERROR = "flashError";
|
private static final String FLASH_ERROR = "flashError";
|
||||||
private static final String DENIED_MESSAGE = "You do not have permission to manage users.";
|
private static final String DENIED_MESSAGE = "您无权管理用户。";
|
||||||
|
|
||||||
private UserAccountService userAccountService;
|
private UserAccountService userAccountService;
|
||||||
|
|
||||||
@@ -44,7 +44,7 @@ public class UserManagementServlet extends HttpServlet {
|
|||||||
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
|
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
|
||||||
String path = request.getServletPath();
|
String path = request.getServletPath();
|
||||||
if ("/admin/users/new".equals(path)) {
|
if ("/admin/users/new".equals(path)) {
|
||||||
renderForm(request, response, "Create user account", "/admin/users", defaultUser(),
|
renderForm(request, response, "创建用户账户", "/admin/users", defaultUser(),
|
||||||
Collections.emptyMap(), Collections.emptyMap(), null);
|
Collections.emptyMap(), Collections.emptyMap(), null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -108,12 +108,12 @@ public class UserManagementServlet extends HttpServlet {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!result.isSuccessful() || !result.getData().isPresent()) {
|
if (!result.isSuccessful() || !result.getData().isPresent()) {
|
||||||
flashError(request, result.isSuccessful() ? "User account was not found." : result.getMessage());
|
flashError(request, result.isSuccessful() ? "未找到用户账户。" : result.getMessage());
|
||||||
response.sendRedirect(request.getContextPath() + "/admin/users");
|
response.sendRedirect(request.getContextPath() + "/admin/users");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
renderForm(request, response, "Edit user account", "/admin/users/update", result.getData().get(),
|
renderForm(request, response, "编辑用户账户", "/admin/users/update", result.getData().get(),
|
||||||
Collections.emptyMap(), Collections.emptyMap(), null);
|
Collections.emptyMap(), Collections.emptyMap(), null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,15 +121,15 @@ public class UserManagementServlet extends HttpServlet {
|
|||||||
throws ServletException, IOException {
|
throws ServletException, IOException {
|
||||||
UserForm form = readUserForm(request, false);
|
UserForm form = readUserForm(request, false);
|
||||||
if (!form.getErrors().isEmpty()) {
|
if (!form.getErrors().isEmpty()) {
|
||||||
renderForm(request, response, "Create user account", "/admin/users", form.getUser(), form.getValues(),
|
renderForm(request, response, "创建用户账户", "/admin/users", form.getUser(), form.getValues(),
|
||||||
form.getErrors(), "Please correct the highlighted account fields.");
|
form.getErrors(), "请修正高亮的账户字段。");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ServiceResult<Long> result = userAccountService.createUser(currentUser(request), form.getUser(),
|
ServiceResult<Long> result = userAccountService.createUser(currentUser(request), form.getUser(),
|
||||||
form.getPassword(), clientIp(request));
|
form.getPassword(), clientIp(request));
|
||||||
if (!result.isSuccessful()) {
|
if (!result.isSuccessful()) {
|
||||||
handleFormFailure(request, response, "Create user account", "/admin/users", form, result);
|
handleFormFailure(request, response, "创建用户账户", "/admin/users", form, result);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,15 +141,15 @@ public class UserManagementServlet extends HttpServlet {
|
|||||||
throws ServletException, IOException {
|
throws ServletException, IOException {
|
||||||
UserForm form = readUserForm(request, true);
|
UserForm form = readUserForm(request, true);
|
||||||
if (!form.getErrors().isEmpty()) {
|
if (!form.getErrors().isEmpty()) {
|
||||||
renderForm(request, response, "Edit user account", "/admin/users/update", form.getUser(), form.getValues(),
|
renderForm(request, response, "编辑用户账户", "/admin/users/update", form.getUser(), form.getValues(),
|
||||||
form.getErrors(), "Please correct the highlighted account fields.");
|
form.getErrors(), "请修正高亮的账户字段。");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ServiceResult<Void> result = userAccountService.updateUser(currentUser(request), form.getUser(),
|
ServiceResult<Void> result = userAccountService.updateUser(currentUser(request), form.getUser(),
|
||||||
form.getPassword(), clientIp(request));
|
form.getPassword(), clientIp(request));
|
||||||
if (!result.isSuccessful()) {
|
if (!result.isSuccessful()) {
|
||||||
handleFormFailure(request, response, "Edit user account", "/admin/users/update", form, result);
|
handleFormFailure(request, response, "编辑用户账户", "/admin/users/update", form, result);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,7 +206,7 @@ public class UserManagementServlet extends HttpServlet {
|
|||||||
User user = new User();
|
User user = new User();
|
||||||
|
|
||||||
if (requireId) {
|
if (requireId) {
|
||||||
user.setId(parseLong(values.get("id"), "id", "Select a valid user account.", errors));
|
user.setId(parseLong(values.get("id"), "id", "请选择有效的用户账户。", errors));
|
||||||
}
|
}
|
||||||
user.setUsername(values.get("username"));
|
user.setUsername(values.get("username"));
|
||||||
user.setDisplayName(values.get("displayName"));
|
user.setDisplayName(values.get("displayName"));
|
||||||
@@ -214,7 +214,7 @@ public class UserManagementServlet extends HttpServlet {
|
|||||||
try {
|
try {
|
||||||
user.setRole(Role.fromCode(values.get("role")));
|
user.setRole(Role.fromCode(values.get("role")));
|
||||||
} catch (IllegalArgumentException ex) {
|
} catch (IllegalArgumentException ex) {
|
||||||
errors.put("role", "Select a role.");
|
errors.put("role", "请选择角色。");
|
||||||
}
|
}
|
||||||
|
|
||||||
return new UserForm(user, values, errors, request.getParameter("password"));
|
return new UserForm(user, values, errors, request.getParameter("password"));
|
||||||
@@ -253,7 +253,7 @@ public class UserManagementServlet extends HttpServlet {
|
|||||||
if ("false".equals(normalized) || UserSearchCriteria.INACTIVE_STATUS.equals(normalized)) {
|
if ("false".equals(normalized) || UserSearchCriteria.INACTIVE_STATUS.equals(normalized)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
errors.put("active", "Select an active state.");
|
errors.put("active", "请选择启用状态。");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,18 @@ import java.util.Optional;
|
|||||||
public interface BookDao {
|
public interface BookDao {
|
||||||
List<BookCategory> findAllCategories();
|
List<BookCategory> findAllCategories();
|
||||||
|
|
||||||
|
Optional<BookCategory> findCategoryById(long id);
|
||||||
|
|
||||||
|
Optional<BookCategory> findCategoryByName(String name);
|
||||||
|
|
||||||
|
long createCategory(BookCategory category);
|
||||||
|
|
||||||
|
boolean updateCategory(BookCategory category);
|
||||||
|
|
||||||
|
boolean deleteCategory(long id);
|
||||||
|
|
||||||
|
int countBooksByCategoryId(long categoryId);
|
||||||
|
|
||||||
List<Book> search(BookSearchCriteria criteria);
|
List<Book> search(BookSearchCriteria criteria);
|
||||||
|
|
||||||
Optional<Book> findById(long id);
|
Optional<Book> findById(long id);
|
||||||
|
|||||||
@@ -33,6 +33,32 @@ public class JdbcBookDao implements BookDao {
|
|||||||
+ "FROM book_categories "
|
+ "FROM book_categories "
|
||||||
+ "ORDER BY name";
|
+ "ORDER BY name";
|
||||||
|
|
||||||
|
private static final String FIND_CATEGORY_BY_ID = ""
|
||||||
|
+ "SELECT id, name, description "
|
||||||
|
+ "FROM book_categories "
|
||||||
|
+ "WHERE id = ?";
|
||||||
|
|
||||||
|
private static final String FIND_CATEGORY_BY_NAME = ""
|
||||||
|
+ "SELECT id, name, description "
|
||||||
|
+ "FROM book_categories "
|
||||||
|
+ "WHERE name = ?";
|
||||||
|
|
||||||
|
private static final String CREATE_CATEGORY = ""
|
||||||
|
+ "INSERT INTO book_categories (name, description) "
|
||||||
|
+ "VALUES (?, ?)";
|
||||||
|
|
||||||
|
private static final String UPDATE_CATEGORY = ""
|
||||||
|
+ "UPDATE book_categories "
|
||||||
|
+ "SET name = ?, description = ? "
|
||||||
|
+ "WHERE id = ?";
|
||||||
|
|
||||||
|
private static final String DELETE_CATEGORY = "DELETE FROM book_categories WHERE id = ?";
|
||||||
|
|
||||||
|
private static final String COUNT_BOOKS_BY_CATEGORY = ""
|
||||||
|
+ "SELECT COUNT(*) "
|
||||||
|
+ "FROM books "
|
||||||
|
+ "WHERE category_id = ?";
|
||||||
|
|
||||||
private static final String FIND_BY_ID = "SELECT " + BOOK_COLUMNS + BOOK_FROM + "WHERE b.id = ?";
|
private static final String FIND_BY_ID = "SELECT " + BOOK_COLUMNS + BOOK_FROM + "WHERE b.id = ?";
|
||||||
|
|
||||||
private static final String FIND_BY_IDENTIFIER = "SELECT " + BOOK_COLUMNS + BOOK_FROM
|
private static final String FIND_BY_IDENTIFIER = "SELECT " + BOOK_COLUMNS + BOOK_FROM
|
||||||
@@ -66,6 +92,86 @@ public class JdbcBookDao implements BookDao {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<BookCategory> findCategoryById(long id) {
|
||||||
|
try (Connection connection = JdbcUtil.getConnection();
|
||||||
|
PreparedStatement statement = connection.prepareStatement(FIND_CATEGORY_BY_ID)) {
|
||||||
|
statement.setLong(1, id);
|
||||||
|
try (ResultSet resultSet = statement.executeQuery()) {
|
||||||
|
return resultSet.next() ? Optional.of(mapCategory(resultSet)) : Optional.empty();
|
||||||
|
}
|
||||||
|
} catch (SQLException ex) {
|
||||||
|
throw new DaoException("Unable to load book category by id", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<BookCategory> findCategoryByName(String name) {
|
||||||
|
try (Connection connection = JdbcUtil.getConnection();
|
||||||
|
PreparedStatement statement = connection.prepareStatement(FIND_CATEGORY_BY_NAME)) {
|
||||||
|
statement.setString(1, name);
|
||||||
|
try (ResultSet resultSet = statement.executeQuery()) {
|
||||||
|
return resultSet.next() ? Optional.of(mapCategory(resultSet)) : Optional.empty();
|
||||||
|
}
|
||||||
|
} catch (SQLException ex) {
|
||||||
|
throw new DaoException("Unable to load book category by name", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long createCategory(BookCategory category) {
|
||||||
|
try (Connection connection = JdbcUtil.getConnection();
|
||||||
|
PreparedStatement statement = connection.prepareStatement(CREATE_CATEGORY, Statement.RETURN_GENERATED_KEYS)) {
|
||||||
|
bindCategory(statement, category);
|
||||||
|
statement.executeUpdate();
|
||||||
|
|
||||||
|
try (ResultSet generatedKeys = statement.getGeneratedKeys()) {
|
||||||
|
if (generatedKeys.next()) {
|
||||||
|
return generatedKeys.getLong(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new DaoException("Unable to read generated book category id", null);
|
||||||
|
} catch (SQLException ex) {
|
||||||
|
throw new DaoException("Unable to create book category", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean updateCategory(BookCategory category) {
|
||||||
|
try (Connection connection = JdbcUtil.getConnection();
|
||||||
|
PreparedStatement statement = connection.prepareStatement(UPDATE_CATEGORY)) {
|
||||||
|
bindCategory(statement, category);
|
||||||
|
statement.setLong(3, category.getId());
|
||||||
|
return statement.executeUpdate() == 1;
|
||||||
|
} catch (SQLException ex) {
|
||||||
|
throw new DaoException("Unable to update book category", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean deleteCategory(long id) {
|
||||||
|
try (Connection connection = JdbcUtil.getConnection();
|
||||||
|
PreparedStatement statement = connection.prepareStatement(DELETE_CATEGORY)) {
|
||||||
|
statement.setLong(1, id);
|
||||||
|
return statement.executeUpdate() == 1;
|
||||||
|
} catch (SQLException ex) {
|
||||||
|
throw new DaoException("Unable to delete book category", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int countBooksByCategoryId(long categoryId) {
|
||||||
|
try (Connection connection = JdbcUtil.getConnection();
|
||||||
|
PreparedStatement statement = connection.prepareStatement(COUNT_BOOKS_BY_CATEGORY)) {
|
||||||
|
statement.setLong(1, categoryId);
|
||||||
|
try (ResultSet resultSet = statement.executeQuery()) {
|
||||||
|
return resultSet.next() ? resultSet.getInt(1) : 0;
|
||||||
|
}
|
||||||
|
} catch (SQLException ex) {
|
||||||
|
throw new DaoException("Unable to count books by category", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<Book> search(BookSearchCriteria criteria) {
|
public List<Book> search(BookSearchCriteria criteria) {
|
||||||
List<Object> parameters = new ArrayList<>();
|
List<Object> parameters = new ArrayList<>();
|
||||||
@@ -194,6 +300,11 @@ public class JdbcBookDao implements BookDao {
|
|||||||
statement.setString(7, book.getStatus().getCode());
|
statement.setString(7, book.getStatus().getCode());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void bindCategory(PreparedStatement statement, BookCategory category) throws SQLException {
|
||||||
|
statement.setString(1, category.getName());
|
||||||
|
statement.setString(2, category.getDescription());
|
||||||
|
}
|
||||||
|
|
||||||
private Book mapBook(ResultSet resultSet) throws SQLException {
|
private Book mapBook(ResultSet resultSet) throws SQLException {
|
||||||
Book book = new Book();
|
Book book = new Book();
|
||||||
book.setId(resultSet.getLong("id"));
|
book.setId(resultSet.getLong("id"));
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ package com.mzh.library.entity;
|
|||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
|
||||||
public enum BookStatus {
|
public enum BookStatus {
|
||||||
AVAILABLE("available", "Available"),
|
AVAILABLE("available", "可借"),
|
||||||
UNAVAILABLE("unavailable", "Unavailable"),
|
UNAVAILABLE("unavailable", "不可借"),
|
||||||
ARCHIVED("archived", "Archived");
|
ARCHIVED("archived", "已归档");
|
||||||
|
|
||||||
private final String code;
|
private final String code;
|
||||||
private final String displayName;
|
private final String displayName;
|
||||||
|
|||||||
@@ -145,7 +145,7 @@ public class BorrowRecord {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public String getDisplayStatusName() {
|
public String getDisplayStatusName() {
|
||||||
return isOverdue() ? "Overdue" : status.getDisplayName();
|
return isOverdue() ? "逾期" : status.getDisplayName();
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getBorrowedAtText() {
|
public String getBorrowedAtText() {
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ package com.mzh.library.entity;
|
|||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
|
||||||
public enum BorrowRecordStatus {
|
public enum BorrowRecordStatus {
|
||||||
ACTIVE("active", "Active"),
|
ACTIVE("active", "借阅中"),
|
||||||
RETURNED("returned", "Returned");
|
RETURNED("returned", "已归还");
|
||||||
|
|
||||||
private final String code;
|
private final String code;
|
||||||
private final String displayName;
|
private final String displayName;
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ package com.mzh.library.entity;
|
|||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
|
||||||
public enum ReaderStatus {
|
public enum ReaderStatus {
|
||||||
ACTIVE("active", "Active"),
|
ACTIVE("active", "正常"),
|
||||||
SUSPENDED("suspended", "Suspended"),
|
SUSPENDED("suspended", "暂停"),
|
||||||
INACTIVE("inactive", "Inactive");
|
INACTIVE("inactive", "停用");
|
||||||
|
|
||||||
private final String code;
|
private final String code;
|
||||||
private final String displayName;
|
private final String displayName;
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ package com.mzh.library.entity;
|
|||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
|
||||||
public enum Role {
|
public enum Role {
|
||||||
ADMINISTRATOR("administrator", "Administrator"),
|
ADMINISTRATOR("administrator", "管理员"),
|
||||||
LIBRARIAN("librarian", "Librarian"),
|
LIBRARIAN("librarian", "馆员"),
|
||||||
READER("reader", "Reader");
|
READER("reader", "读者");
|
||||||
|
|
||||||
private final String code;
|
private final String code;
|
||||||
private final String displayName;
|
private final String displayName;
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ public class SystemLog {
|
|||||||
return username;
|
return username;
|
||||||
}
|
}
|
||||||
|
|
||||||
return operatorId == null ? "System" : "User #" + operatorId;
|
return operatorId == null ? "系统" : "用户 #" + operatorId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getOperatorMetaText() {
|
public String getOperatorMetaText() {
|
||||||
@@ -144,7 +144,7 @@ public class SystemLog {
|
|||||||
if (operatorId != null && (!displayName.isEmpty() || !username.isEmpty())) {
|
if (operatorId != null && (!displayName.isEmpty() || !username.isEmpty())) {
|
||||||
appendMeta(meta, "#" + operatorId);
|
appendMeta(meta, "#" + operatorId);
|
||||||
}
|
}
|
||||||
appendMeta(meta, trim(operatorRole));
|
appendMeta(meta, displayRole(operatorRole));
|
||||||
return meta.toString();
|
return meta.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,8 +157,22 @@ public class SystemLog {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public String getResultStatusName() {
|
public String getResultStatusName() {
|
||||||
String trimmed = trim(resultStatus);
|
String normalized = trim(resultStatus).toLowerCase(Locale.ROOT);
|
||||||
return trimmed.isEmpty() ? "Unknown" : trimmed;
|
if ("success".equals(normalized)) {
|
||||||
|
return "成功";
|
||||||
|
}
|
||||||
|
if ("failure".equals(normalized)) {
|
||||||
|
return "失败";
|
||||||
|
}
|
||||||
|
return normalized.isEmpty() ? "未知" : trim(resultStatus);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getTargetTableName() {
|
||||||
|
String normalized = trim(targetTable).toLowerCase(Locale.ROOT);
|
||||||
|
if ("users".equals(normalized)) {
|
||||||
|
return "用户";
|
||||||
|
}
|
||||||
|
return trim(targetTable);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void appendMeta(StringBuilder meta, String value) {
|
private void appendMeta(StringBuilder meta, String value) {
|
||||||
@@ -174,4 +188,16 @@ public class SystemLog {
|
|||||||
private String trim(String value) {
|
private String trim(String value) {
|
||||||
return value == null ? "" : value.trim();
|
return value == null ? "" : value.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String displayRole(String roleCode) {
|
||||||
|
String normalized = trim(roleCode);
|
||||||
|
if (normalized.isEmpty()) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return Role.fromCode(normalized).getDisplayName();
|
||||||
|
} catch (IllegalArgumentException ex) {
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ public class User {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public String getActiveStatusName() {
|
public String getActiveStatusName() {
|
||||||
return active ? "Active" : "Inactive";
|
return active ? "启用" : "停用";
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getCreatedAtText() {
|
public String getCreatedAtText() {
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ public class AuthorizationFilter implements Filter {
|
|||||||
new PathRule("/admin/system-logs", Permission.VIEW_SYSTEM_LOGS),
|
new PathRule("/admin/system-logs", Permission.VIEW_SYSTEM_LOGS),
|
||||||
new PathRule("/reports", Permission.VIEW_REPORTS),
|
new PathRule("/reports", Permission.VIEW_REPORTS),
|
||||||
new PathRule("/borrowing", Permission.MANAGE_BORROWING),
|
new PathRule("/borrowing", Permission.MANAGE_BORROWING),
|
||||||
|
new PathRule("/book-categories", Permission.MANAGE_BOOKS),
|
||||||
new PathRule("/books", Permission.MANAGE_BOOKS),
|
new PathRule("/books", Permission.MANAGE_BOOKS),
|
||||||
new PathRule("/readers", Permission.MANAGE_READERS),
|
new PathRule("/readers", Permission.MANAGE_READERS),
|
||||||
new PathRule("/catalog", Permission.VIEW_CATALOG),
|
new PathRule("/catalog", Permission.VIEW_CATALOG),
|
||||||
@@ -61,7 +62,7 @@ public class AuthorizationFilter implements Filter {
|
|||||||
|
|
||||||
logDeniedAccess(user, requiredRule, path);
|
logDeniedAccess(user, requiredRule, path);
|
||||||
httpResponse.setStatus(HttpServletResponse.SC_FORBIDDEN);
|
httpResponse.setStatus(HttpServletResponse.SC_FORBIDDEN);
|
||||||
request.setAttribute("errorMessage", "You do not have permission to access this page.");
|
request.setAttribute("errorMessage", "您无权访问此页面。");
|
||||||
request.getRequestDispatcher(UNAUTHORIZED_JSP).forward(request, response);
|
request.getRequestDispatcher(UNAUTHORIZED_JSP).forward(request, response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,14 @@ import java.util.Optional;
|
|||||||
public interface BookService {
|
public interface BookService {
|
||||||
ServiceResult<List<BookCategory>> listCategories();
|
ServiceResult<List<BookCategory>> listCategories();
|
||||||
|
|
||||||
|
ServiceResult<Optional<BookCategory>> findCategory(long id);
|
||||||
|
|
||||||
|
ServiceResult<Long> createCategory(AuthenticatedUser actor, BookCategory category);
|
||||||
|
|
||||||
|
ServiceResult<Void> updateCategory(AuthenticatedUser actor, BookCategory category);
|
||||||
|
|
||||||
|
ServiceResult<Void> deleteCategory(AuthenticatedUser actor, long id);
|
||||||
|
|
||||||
ServiceResult<List<Book>> searchBooks(BookSearchCriteria criteria);
|
ServiceResult<List<Book>> searchBooks(BookSearchCriteria criteria);
|
||||||
|
|
||||||
ServiceResult<Optional<Book>> findBook(long id);
|
ServiceResult<Optional<Book>> findBook(long id);
|
||||||
|
|||||||
@@ -17,9 +17,9 @@ import java.util.logging.Logger;
|
|||||||
|
|
||||||
public class AuthServiceImpl implements AuthService {
|
public class AuthServiceImpl implements AuthService {
|
||||||
private static final Logger LOGGER = Logger.getLogger(AuthServiceImpl.class.getName());
|
private static final Logger LOGGER = Logger.getLogger(AuthServiceImpl.class.getName());
|
||||||
private static final String REQUIRED_MESSAGE = "Username and password are required.";
|
private static final String REQUIRED_MESSAGE = "请输入用户名和密码。";
|
||||||
private static final String INVALID_MESSAGE = "Invalid username or password.";
|
private static final String INVALID_MESSAGE = "用户名或密码不正确。";
|
||||||
private static final String UNAVAILABLE_MESSAGE = "Login service is temporarily unavailable. Please try again later.";
|
private static final String UNAVAILABLE_MESSAGE = "登录服务暂时不可用,请稍后重试。";
|
||||||
|
|
||||||
private final UserDao userDao;
|
private final UserDao userDao;
|
||||||
private final PermissionPolicy permissionPolicy;
|
private final PermissionPolicy permissionPolicy;
|
||||||
|
|||||||
@@ -21,9 +21,10 @@ import java.util.logging.Logger;
|
|||||||
public class BookServiceImpl implements BookService {
|
public class BookServiceImpl implements BookService {
|
||||||
private static final Logger LOGGER = Logger.getLogger(BookServiceImpl.class.getName());
|
private static final Logger LOGGER = Logger.getLogger(BookServiceImpl.class.getName());
|
||||||
private static final String UNAVAILABLE_MESSAGE =
|
private static final String UNAVAILABLE_MESSAGE =
|
||||||
"Book service is temporarily unavailable. Please try again later.";
|
"图书服务暂时不可用,请稍后重试。";
|
||||||
private static final String VALIDATION_MESSAGE = "Please correct the highlighted book fields.";
|
private static final String VALIDATION_MESSAGE = "请修正高亮的图书字段。";
|
||||||
private static final String DENIED_MESSAGE = "You do not have permission to manage books.";
|
private static final String CATEGORY_VALIDATION_MESSAGE = "请修正高亮的分类字段。";
|
||||||
|
private static final String DENIED_MESSAGE = "您无权管理图书。";
|
||||||
|
|
||||||
private final BookDao bookDao;
|
private final BookDao bookDao;
|
||||||
private final PermissionPolicy permissionPolicy;
|
private final PermissionPolicy permissionPolicy;
|
||||||
@@ -47,13 +48,118 @@ public class BookServiceImpl implements BookService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ServiceResult<Optional<BookCategory>> findCategory(long id) {
|
||||||
|
if (id <= 0) {
|
||||||
|
return ServiceResult.failure("请选择有效的分类。");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return ServiceResult.success(bookDao.findCategoryById(id));
|
||||||
|
} catch (DaoException ex) {
|
||||||
|
LOGGER.log(Level.SEVERE, "Unable to load book category id=" + id, ex);
|
||||||
|
return ServiceResult.failure(UNAVAILABLE_MESSAGE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ServiceResult<Long> createCategory(AuthenticatedUser actor, BookCategory category) {
|
||||||
|
if (!canManageBooks(actor)) {
|
||||||
|
return ServiceResult.failure(DENIED_MESSAGE);
|
||||||
|
}
|
||||||
|
|
||||||
|
normalize(category);
|
||||||
|
Map<String, String> errors = validate(category, false);
|
||||||
|
if (!errors.isEmpty()) {
|
||||||
|
return ServiceResult.validationFailure(CATEGORY_VALIDATION_MESSAGE, errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (bookDao.findCategoryByName(category.getName()).isPresent()) {
|
||||||
|
errors.put("name", "分类名称已被使用。");
|
||||||
|
return ServiceResult.validationFailure(CATEGORY_VALIDATION_MESSAGE, errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
long id = bookDao.createCategory(category);
|
||||||
|
LOGGER.info("Created book category id=" + id + " actorId=" + actor.getId());
|
||||||
|
return ServiceResult.success(id, "分类已创建。");
|
||||||
|
} catch (DaoException ex) {
|
||||||
|
LOGGER.log(Level.SEVERE, "Unable to create book category actorId=" + actor.getId()
|
||||||
|
+ " name=" + safeCategoryName(category), ex);
|
||||||
|
return ServiceResult.failure(UNAVAILABLE_MESSAGE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ServiceResult<Void> updateCategory(AuthenticatedUser actor, BookCategory category) {
|
||||||
|
if (!canManageBooks(actor)) {
|
||||||
|
return ServiceResult.failure(DENIED_MESSAGE);
|
||||||
|
}
|
||||||
|
|
||||||
|
normalize(category);
|
||||||
|
Map<String, String> errors = validate(category, true);
|
||||||
|
if (!errors.isEmpty()) {
|
||||||
|
return ServiceResult.validationFailure(CATEGORY_VALIDATION_MESSAGE, errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Optional<BookCategory> existingWithName = bookDao.findCategoryByName(category.getName());
|
||||||
|
if (existingWithName.isPresent() && existingWithName.get().getId() != category.getId()) {
|
||||||
|
errors.put("name", "分类名称已被使用。");
|
||||||
|
return ServiceResult.validationFailure(CATEGORY_VALIDATION_MESSAGE, errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!bookDao.updateCategory(category)) {
|
||||||
|
return ServiceResult.failure("未找到分类。");
|
||||||
|
}
|
||||||
|
|
||||||
|
LOGGER.info("Updated book category id=" + category.getId() + " actorId=" + actor.getId());
|
||||||
|
return ServiceResult.success(null, "分类已更新。");
|
||||||
|
} catch (DaoException ex) {
|
||||||
|
LOGGER.log(Level.SEVERE, "Unable to update book category id=" + category.getId()
|
||||||
|
+ " actorId=" + actor.getId(), ex);
|
||||||
|
return ServiceResult.failure(UNAVAILABLE_MESSAGE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ServiceResult<Void> deleteCategory(AuthenticatedUser actor, long id) {
|
||||||
|
if (!canManageBooks(actor)) {
|
||||||
|
return ServiceResult.failure(DENIED_MESSAGE);
|
||||||
|
}
|
||||||
|
if (id <= 0) {
|
||||||
|
return ServiceResult.failure("请选择有效的分类。");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!bookDao.findCategoryById(id).isPresent()) {
|
||||||
|
return ServiceResult.failure("未找到分类。");
|
||||||
|
}
|
||||||
|
if (bookDao.countBooksByCategoryId(id) > 0) {
|
||||||
|
Map<String, String> errors = new LinkedHashMap<>();
|
||||||
|
errors.put("category", "该分类已被现有图书使用,不能删除。");
|
||||||
|
return ServiceResult.validationFailure("该分类已被现有图书使用,不能删除。",
|
||||||
|
errors);
|
||||||
|
}
|
||||||
|
if (!bookDao.deleteCategory(id)) {
|
||||||
|
return ServiceResult.failure("未找到分类。");
|
||||||
|
}
|
||||||
|
|
||||||
|
LOGGER.info("Deleted book category id=" + id + " actorId=" + actor.getId());
|
||||||
|
return ServiceResult.success(null, "分类已删除。");
|
||||||
|
} catch (DaoException ex) {
|
||||||
|
LOGGER.log(Level.SEVERE, "Unable to delete book category id=" + id + " actorId=" + actor.getId(), ex);
|
||||||
|
return ServiceResult.failure(UNAVAILABLE_MESSAGE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ServiceResult<List<Book>> searchBooks(BookSearchCriteria criteria) {
|
public ServiceResult<List<Book>> searchBooks(BookSearchCriteria criteria) {
|
||||||
BookSearchCriteria normalized = criteria == null ? new BookSearchCriteria() : criteria;
|
BookSearchCriteria normalized = criteria == null ? new BookSearchCriteria() : criteria;
|
||||||
if (normalized.getCategoryId() != null && normalized.getCategoryId() <= 0) {
|
if (normalized.getCategoryId() != null && normalized.getCategoryId() <= 0) {
|
||||||
Map<String, String> errors = new LinkedHashMap<>();
|
Map<String, String> errors = new LinkedHashMap<>();
|
||||||
errors.put("categoryId", "Select a valid category.");
|
errors.put("categoryId", "请选择有效的分类。");
|
||||||
return ServiceResult.validationFailure("Please correct the catalog search filters.", errors);
|
return ServiceResult.validationFailure("请修正馆藏检索筛选条件。", errors);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -67,7 +173,7 @@ public class BookServiceImpl implements BookService {
|
|||||||
@Override
|
@Override
|
||||||
public ServiceResult<Optional<Book>> findBook(long id) {
|
public ServiceResult<Optional<Book>> findBook(long id) {
|
||||||
if (id <= 0) {
|
if (id <= 0) {
|
||||||
return ServiceResult.failure("Select a valid book.");
|
return ServiceResult.failure("请选择有效的图书。");
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -92,13 +198,13 @@ public class BookServiceImpl implements BookService {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if (bookDao.findByIdentifier(book.getIdentifier()).isPresent()) {
|
if (bookDao.findByIdentifier(book.getIdentifier()).isPresent()) {
|
||||||
errors.put("identifier", "Book identifier is already in use.");
|
errors.put("identifier", "图书编号已被使用。");
|
||||||
return ServiceResult.validationFailure(VALIDATION_MESSAGE, errors);
|
return ServiceResult.validationFailure(VALIDATION_MESSAGE, errors);
|
||||||
}
|
}
|
||||||
|
|
||||||
long id = bookDao.create(book);
|
long id = bookDao.create(book);
|
||||||
LOGGER.info("Created book id=" + id + " actorId=" + actor.getId());
|
LOGGER.info("Created book id=" + id + " actorId=" + actor.getId());
|
||||||
return ServiceResult.success(id, "Book created.");
|
return ServiceResult.success(id, "图书已创建。");
|
||||||
} catch (DaoException ex) {
|
} catch (DaoException ex) {
|
||||||
LOGGER.log(Level.SEVERE, "Unable to create book actorId=" + actor.getId(), ex);
|
LOGGER.log(Level.SEVERE, "Unable to create book actorId=" + actor.getId(), ex);
|
||||||
return ServiceResult.failure(UNAVAILABLE_MESSAGE);
|
return ServiceResult.failure(UNAVAILABLE_MESSAGE);
|
||||||
@@ -120,16 +226,16 @@ public class BookServiceImpl implements BookService {
|
|||||||
try {
|
try {
|
||||||
Optional<Book> existingWithIdentifier = bookDao.findByIdentifier(book.getIdentifier());
|
Optional<Book> existingWithIdentifier = bookDao.findByIdentifier(book.getIdentifier());
|
||||||
if (existingWithIdentifier.isPresent() && existingWithIdentifier.get().getId() != book.getId()) {
|
if (existingWithIdentifier.isPresent() && existingWithIdentifier.get().getId() != book.getId()) {
|
||||||
errors.put("identifier", "Book identifier is already in use.");
|
errors.put("identifier", "图书编号已被使用。");
|
||||||
return ServiceResult.validationFailure(VALIDATION_MESSAGE, errors);
|
return ServiceResult.validationFailure(VALIDATION_MESSAGE, errors);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!bookDao.update(book)) {
|
if (!bookDao.update(book)) {
|
||||||
return ServiceResult.failure("Book was not found.");
|
return ServiceResult.failure("未找到图书。");
|
||||||
}
|
}
|
||||||
|
|
||||||
LOGGER.info("Updated book id=" + book.getId() + " actorId=" + actor.getId());
|
LOGGER.info("Updated book id=" + book.getId() + " actorId=" + actor.getId());
|
||||||
return ServiceResult.success(null, "Book updated.");
|
return ServiceResult.success(null, "图书已更新。");
|
||||||
} catch (DaoException ex) {
|
} catch (DaoException ex) {
|
||||||
LOGGER.log(Level.SEVERE, "Unable to update book id=" + book.getId() + " actorId=" + actor.getId(), ex);
|
LOGGER.log(Level.SEVERE, "Unable to update book id=" + book.getId() + " actorId=" + actor.getId(), ex);
|
||||||
return ServiceResult.failure(UNAVAILABLE_MESSAGE);
|
return ServiceResult.failure(UNAVAILABLE_MESSAGE);
|
||||||
@@ -142,16 +248,16 @@ public class BookServiceImpl implements BookService {
|
|||||||
return ServiceResult.failure(DENIED_MESSAGE);
|
return ServiceResult.failure(DENIED_MESSAGE);
|
||||||
}
|
}
|
||||||
if (id <= 0) {
|
if (id <= 0) {
|
||||||
return ServiceResult.failure("Select a valid book.");
|
return ServiceResult.failure("请选择有效的图书。");
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!bookDao.delete(id)) {
|
if (!bookDao.delete(id)) {
|
||||||
return ServiceResult.failure("Book was not found.");
|
return ServiceResult.failure("未找到图书。");
|
||||||
}
|
}
|
||||||
|
|
||||||
LOGGER.info("Deleted book id=" + id + " actorId=" + actor.getId());
|
LOGGER.info("Deleted book id=" + id + " actorId=" + actor.getId());
|
||||||
return ServiceResult.success(null, "Book deleted.");
|
return ServiceResult.success(null, "图书已删除。");
|
||||||
} catch (DaoException ex) {
|
} catch (DaoException ex) {
|
||||||
LOGGER.log(Level.SEVERE, "Unable to delete book id=" + id + " actorId=" + actor.getId(), ex);
|
LOGGER.log(Level.SEVERE, "Unable to delete book id=" + id + " actorId=" + actor.getId(), ex);
|
||||||
return ServiceResult.failure(UNAVAILABLE_MESSAGE);
|
return ServiceResult.failure(UNAVAILABLE_MESSAGE);
|
||||||
@@ -171,44 +277,73 @@ public class BookServiceImpl implements BookService {
|
|||||||
book.setAuthor(trim(book.getAuthor()));
|
book.setAuthor(trim(book.getAuthor()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void normalize(BookCategory category) {
|
||||||
|
if (category == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
category.setName(trim(category.getName()));
|
||||||
|
category.setDescription(trim(category.getDescription()));
|
||||||
|
}
|
||||||
|
|
||||||
private Map<String, String> validate(Book book, boolean requireId) {
|
private Map<String, String> validate(Book book, boolean requireId) {
|
||||||
Map<String, String> errors = new LinkedHashMap<>();
|
Map<String, String> errors = new LinkedHashMap<>();
|
||||||
if (book == null) {
|
if (book == null) {
|
||||||
errors.put("book", "Book details are required.");
|
errors.put("book", "请填写图书详情。");
|
||||||
return errors;
|
return errors;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (requireId && book.getId() <= 0) {
|
if (requireId && book.getId() <= 0) {
|
||||||
errors.put("id", "Select a valid book.");
|
errors.put("id", "请选择有效的图书。");
|
||||||
}
|
}
|
||||||
requireLength(errors, "identifier", book.getIdentifier(), "Book identifier", 64);
|
requireLength(errors, "identifier", book.getIdentifier(), "图书编号", 64);
|
||||||
requireLength(errors, "title", book.getTitle(), "Title", 200);
|
requireLength(errors, "title", book.getTitle(), "书名", 200);
|
||||||
requireLength(errors, "author", book.getAuthor(), "Author", 120);
|
requireLength(errors, "author", book.getAuthor(), "作者", 120);
|
||||||
if (book.getCategoryId() <= 0) {
|
if (book.getCategoryId() <= 0) {
|
||||||
errors.put("categoryId", "Select a category.");
|
errors.put("categoryId", "请选择分类。");
|
||||||
}
|
}
|
||||||
if (book.getTotalCopies() < 0) {
|
if (book.getTotalCopies() < 0) {
|
||||||
errors.put("totalCopies", "Total copies cannot be negative.");
|
errors.put("totalCopies", "馆藏总数不能为负数。");
|
||||||
}
|
}
|
||||||
if (book.getAvailableCopies() < 0) {
|
if (book.getAvailableCopies() < 0) {
|
||||||
errors.put("availableCopies", "Available copies cannot be negative.");
|
errors.put("availableCopies", "可借数量不能为负数。");
|
||||||
}
|
}
|
||||||
if (book.getAvailableCopies() > book.getTotalCopies()) {
|
if (book.getAvailableCopies() > book.getTotalCopies()) {
|
||||||
errors.put("availableCopies", "Available copies cannot exceed total copies.");
|
errors.put("availableCopies", "可借数量不能超过馆藏总数。");
|
||||||
}
|
}
|
||||||
if (book.getStatus() == null) {
|
if (book.getStatus() == null) {
|
||||||
errors.put("status", "Select a status.");
|
errors.put("status", "请选择状态。");
|
||||||
}
|
}
|
||||||
return errors;
|
return errors;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Map<String, String> validate(BookCategory category, boolean requireId) {
|
||||||
|
Map<String, String> errors = new LinkedHashMap<>();
|
||||||
|
if (category == null) {
|
||||||
|
errors.put("category", "请填写分类详情。");
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requireId && category.getId() <= 0) {
|
||||||
|
errors.put("id", "请选择有效的分类。");
|
||||||
|
}
|
||||||
|
requireLength(errors, "name", category.getName(), "分类名称", 96);
|
||||||
|
if (category.getDescription() != null && category.getDescription().length() > 255) {
|
||||||
|
errors.put("description", "说明不能超过 255 个字符。");
|
||||||
|
}
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String safeCategoryName(BookCategory category) {
|
||||||
|
return category == null ? "" : category.getName();
|
||||||
|
}
|
||||||
|
|
||||||
private void requireLength(Map<String, String> errors, String field, String value, String label, int maxLength) {
|
private void requireLength(Map<String, String> errors, String field, String value, String label, int maxLength) {
|
||||||
if (value == null || value.isEmpty()) {
|
if (value == null || value.isEmpty()) {
|
||||||
errors.put(field, label + " is required.");
|
errors.put(field, "请填写" + label + "。");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (value.length() > maxLength) {
|
if (value.length() > maxLength) {
|
||||||
errors.put(field, label + " must be " + maxLength + " characters or fewer.");
|
errors.put(field, label + "不能超过 " + maxLength + " 个字符。");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,10 +35,10 @@ public class BorrowingServiceImpl implements BorrowingService {
|
|||||||
|
|
||||||
private static final Logger LOGGER = Logger.getLogger(BorrowingServiceImpl.class.getName());
|
private static final Logger LOGGER = Logger.getLogger(BorrowingServiceImpl.class.getName());
|
||||||
private static final String UNAVAILABLE_MESSAGE =
|
private static final String UNAVAILABLE_MESSAGE =
|
||||||
"Borrowing service is temporarily unavailable. Please try again later.";
|
"借阅服务暂时不可用,请稍后重试。";
|
||||||
private static final String VALIDATION_MESSAGE = "Please correct the highlighted borrowing fields.";
|
private static final String VALIDATION_MESSAGE = "请修正高亮的借阅字段。";
|
||||||
private static final String DENIED_MESSAGE = "You do not have permission to manage borrowing.";
|
private static final String DENIED_MESSAGE = "您无权管理借阅。";
|
||||||
private static final String HISTORY_DENIED_MESSAGE = "You do not have permission to view loan history.";
|
private static final String HISTORY_DENIED_MESSAGE = "您无权查看借阅历史。";
|
||||||
private static final int LOAN_DAYS = 14;
|
private static final int LOAN_DAYS = 14;
|
||||||
private static final int MAX_RENEWALS = 1;
|
private static final int MAX_RENEWALS = 1;
|
||||||
|
|
||||||
@@ -68,7 +68,7 @@ public class BorrowingServiceImpl implements BorrowingService {
|
|||||||
BorrowRecordSearchCriteria normalized = criteria == null ? new BorrowRecordSearchCriteria() : criteria;
|
BorrowRecordSearchCriteria normalized = criteria == null ? new BorrowRecordSearchCriteria() : criteria;
|
||||||
Map<String, String> errors = validateSearch(normalized);
|
Map<String, String> errors = validateSearch(normalized);
|
||||||
if (!errors.isEmpty()) {
|
if (!errors.isEmpty()) {
|
||||||
return ServiceResult.validationFailure("Please correct the borrowing search filters.", errors);
|
return ServiceResult.validationFailure("请修正借阅检索筛选条件。", errors);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -98,13 +98,13 @@ public class BorrowingServiceImpl implements BorrowingService {
|
|||||||
Optional<Reader> readerResult = borrowRecordDao.findReaderByIdentifierForUpdate(connection,
|
Optional<Reader> readerResult = borrowRecordDao.findReaderByIdentifierForUpdate(connection,
|
||||||
normalizedReaderIdentifier);
|
normalizedReaderIdentifier);
|
||||||
if (!readerResult.isPresent()) {
|
if (!readerResult.isPresent()) {
|
||||||
transactionErrors.put("readerIdentifier", "Reader was not found.");
|
transactionErrors.put("readerIdentifier", "未找到读者。");
|
||||||
}
|
}
|
||||||
|
|
||||||
Optional<Book> bookResult = borrowRecordDao.findBookByIdentifierForUpdate(connection,
|
Optional<Book> bookResult = borrowRecordDao.findBookByIdentifierForUpdate(connection,
|
||||||
normalizedBookIdentifier);
|
normalizedBookIdentifier);
|
||||||
if (!bookResult.isPresent()) {
|
if (!bookResult.isPresent()) {
|
||||||
transactionErrors.put("bookIdentifier", "Book was not found.");
|
transactionErrors.put("bookIdentifier", "未找到图书。");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!transactionErrors.isEmpty()) {
|
if (!transactionErrors.isEmpty()) {
|
||||||
@@ -134,7 +134,7 @@ public class BorrowingServiceImpl implements BorrowingService {
|
|||||||
|
|
||||||
LOGGER.info("Borrowed book recordId=" + id + " readerId=" + reader.getId()
|
LOGGER.info("Borrowed book recordId=" + id + " readerId=" + reader.getId()
|
||||||
+ " bookId=" + book.getId() + " actorId=" + actor.getId());
|
+ " bookId=" + book.getId() + " actorId=" + actor.getId());
|
||||||
return ServiceResult.success(id, "Book borrowed.");
|
return ServiceResult.success(id, "图书已借出。");
|
||||||
});
|
});
|
||||||
} catch (DaoException ex) {
|
} catch (DaoException ex) {
|
||||||
LOGGER.log(Level.SEVERE, "Unable to borrow book actorId=" + actor.getId()
|
LOGGER.log(Level.SEVERE, "Unable to borrow book actorId=" + actor.getId()
|
||||||
@@ -150,20 +150,20 @@ public class BorrowingServiceImpl implements BorrowingService {
|
|||||||
return ServiceResult.failure(DENIED_MESSAGE);
|
return ServiceResult.failure(DENIED_MESSAGE);
|
||||||
}
|
}
|
||||||
if (recordId <= 0) {
|
if (recordId <= 0) {
|
||||||
return ServiceResult.failure("Select a valid borrowing record.");
|
return ServiceResult.failure("请选择有效的借阅记录。");
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return transactionExecutor.execute(connection -> {
|
return transactionExecutor.execute(connection -> {
|
||||||
Optional<BorrowRecord> recordResult = borrowRecordDao.findByIdForUpdate(connection, recordId);
|
Optional<BorrowRecord> recordResult = borrowRecordDao.findByIdForUpdate(connection, recordId);
|
||||||
if (!recordResult.isPresent()) {
|
if (!recordResult.isPresent()) {
|
||||||
return ServiceResult.failure("Borrowing record was not found.");
|
return ServiceResult.failure("未找到借阅记录。");
|
||||||
}
|
}
|
||||||
|
|
||||||
BorrowRecord record = recordResult.get();
|
BorrowRecord record = recordResult.get();
|
||||||
Map<String, String> errors = validateActiveLoan(record);
|
Map<String, String> errors = validateActiveLoan(record);
|
||||||
if (!errors.isEmpty()) {
|
if (!errors.isEmpty()) {
|
||||||
return ServiceResult.validationFailure("Borrowing record cannot be returned.", errors);
|
return ServiceResult.validationFailure("借阅记录不能归还。", errors);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!borrowRecordDao.markReturned(connection, recordId, now())) {
|
if (!borrowRecordDao.markReturned(connection, recordId, now())) {
|
||||||
@@ -172,7 +172,7 @@ public class BorrowingServiceImpl implements BorrowingService {
|
|||||||
borrowRecordDao.incrementAvailableCopies(connection, record.getBookId());
|
borrowRecordDao.incrementAvailableCopies(connection, record.getBookId());
|
||||||
|
|
||||||
LOGGER.info("Returned borrow recordId=" + recordId + " actorId=" + actor.getId());
|
LOGGER.info("Returned borrow recordId=" + recordId + " actorId=" + actor.getId());
|
||||||
return ServiceResult.success(null, "Book returned.");
|
return ServiceResult.success(null, "图书已归还。");
|
||||||
});
|
});
|
||||||
} catch (DaoException ex) {
|
} catch (DaoException ex) {
|
||||||
LOGGER.log(Level.SEVERE, "Unable to return borrow record id=" + recordId + " actorId=" + actor.getId(), ex);
|
LOGGER.log(Level.SEVERE, "Unable to return borrow record id=" + recordId + " actorId=" + actor.getId(), ex);
|
||||||
@@ -186,23 +186,23 @@ public class BorrowingServiceImpl implements BorrowingService {
|
|||||||
return ServiceResult.failure(DENIED_MESSAGE);
|
return ServiceResult.failure(DENIED_MESSAGE);
|
||||||
}
|
}
|
||||||
if (recordId <= 0) {
|
if (recordId <= 0) {
|
||||||
return ServiceResult.failure("Select a valid borrowing record.");
|
return ServiceResult.failure("请选择有效的借阅记录。");
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return transactionExecutor.execute(connection -> {
|
return transactionExecutor.execute(connection -> {
|
||||||
Optional<BorrowRecord> recordResult = borrowRecordDao.findByIdForUpdate(connection, recordId);
|
Optional<BorrowRecord> recordResult = borrowRecordDao.findByIdForUpdate(connection, recordId);
|
||||||
if (!recordResult.isPresent()) {
|
if (!recordResult.isPresent()) {
|
||||||
return ServiceResult.failure("Borrowing record was not found.");
|
return ServiceResult.failure("未找到借阅记录。");
|
||||||
}
|
}
|
||||||
|
|
||||||
BorrowRecord record = recordResult.get();
|
BorrowRecord record = recordResult.get();
|
||||||
Map<String, String> errors = validateActiveLoan(record);
|
Map<String, String> errors = validateActiveLoan(record);
|
||||||
if (record.getRenewalCount() >= MAX_RENEWALS) {
|
if (record.getRenewalCount() >= MAX_RENEWALS) {
|
||||||
errors.put("renewalCount", "This loan has already reached the renewal limit.");
|
errors.put("renewalCount", "该借阅已达到续借次数上限。");
|
||||||
}
|
}
|
||||||
if (!errors.isEmpty()) {
|
if (!errors.isEmpty()) {
|
||||||
return ServiceResult.validationFailure("Borrowing record cannot be renewed.", errors);
|
return ServiceResult.validationFailure("借阅记录不能续借。", errors);
|
||||||
}
|
}
|
||||||
|
|
||||||
LocalDateTime currentDueAt = record.getDueAt() == null ? now() : record.getDueAt();
|
LocalDateTime currentDueAt = record.getDueAt() == null ? now() : record.getDueAt();
|
||||||
@@ -212,7 +212,7 @@ public class BorrowingServiceImpl implements BorrowingService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
LOGGER.info("Renewed borrow recordId=" + recordId + " actorId=" + actor.getId());
|
LOGGER.info("Renewed borrow recordId=" + recordId + " actorId=" + actor.getId());
|
||||||
return ServiceResult.success(null, "Loan renewed.");
|
return ServiceResult.success(null, "借阅已续借。");
|
||||||
});
|
});
|
||||||
} catch (DaoException ex) {
|
} catch (DaoException ex) {
|
||||||
LOGGER.log(Level.SEVERE, "Unable to renew borrow record id=" + recordId + " actorId=" + actor.getId(), ex);
|
LOGGER.log(Level.SEVERE, "Unable to renew borrow record id=" + recordId + " actorId=" + actor.getId(), ex);
|
||||||
@@ -229,7 +229,7 @@ public class BorrowingServiceImpl implements BorrowingService {
|
|||||||
try {
|
try {
|
||||||
Optional<Reader> readerResult = borrowRecordDao.findReaderByUserId(actor.getId());
|
Optional<Reader> readerResult = borrowRecordDao.findReaderByUserId(actor.getId());
|
||||||
if (!readerResult.isPresent()) {
|
if (!readerResult.isPresent()) {
|
||||||
return ServiceResult.success(Collections.emptyList(), "No reader profile is linked to your account.");
|
return ServiceResult.success(Collections.emptyList(), "您的账户未关联读者档案。");
|
||||||
}
|
}
|
||||||
|
|
||||||
return ServiceResult.success(borrowRecordDao.findByReaderId(readerResult.get().getId()));
|
return ServiceResult.success(borrowRecordDao.findByReaderId(readerResult.get().getId()));
|
||||||
@@ -246,16 +246,16 @@ public class BorrowingServiceImpl implements BorrowingService {
|
|||||||
private void validateBorrowEligibility(Map<String, String> errors, Reader reader, Book book,
|
private void validateBorrowEligibility(Map<String, String> errors, Reader reader, Book book,
|
||||||
java.sql.Connection connection) {
|
java.sql.Connection connection) {
|
||||||
if (reader.getStatus() != ReaderStatus.ACTIVE) {
|
if (reader.getStatus() != ReaderStatus.ACTIVE) {
|
||||||
errors.put("readerIdentifier", "Reader must be active to borrow books.");
|
errors.put("readerIdentifier", "读者状态必须为正常才能借阅图书。");
|
||||||
}
|
}
|
||||||
int activeLoans = borrowRecordDao.countActiveByReaderId(connection, reader.getId());
|
int activeLoans = borrowRecordDao.countActiveByReaderId(connection, reader.getId());
|
||||||
if (activeLoans >= reader.getMaxBorrowCount()) {
|
if (activeLoans >= reader.getMaxBorrowCount()) {
|
||||||
errors.put("readerIdentifier", "Reader has reached the active borrowing limit.");
|
errors.put("readerIdentifier", "读者已达到在借数量上限。");
|
||||||
}
|
}
|
||||||
if (book.getStatus() != BookStatus.AVAILABLE) {
|
if (book.getStatus() != BookStatus.AVAILABLE) {
|
||||||
errors.put("bookIdentifier", "Book status does not allow borrowing.");
|
errors.put("bookIdentifier", "图书状态不允许借阅。");
|
||||||
} else if (book.getAvailableCopies() <= 0) {
|
} else if (book.getAvailableCopies() <= 0) {
|
||||||
errors.put("bookIdentifier", "No available copies remain for this book.");
|
errors.put("bookIdentifier", "该图书没有可借副本。");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -270,7 +270,7 @@ public class BorrowingServiceImpl implements BorrowingService {
|
|||||||
try {
|
try {
|
||||||
BorrowRecordStatus.fromCode(statusCode);
|
BorrowRecordStatus.fromCode(statusCode);
|
||||||
} catch (IllegalArgumentException ex) {
|
} catch (IllegalArgumentException ex) {
|
||||||
errors.put("status", "Select a valid borrowing status.");
|
errors.put("status", "请选择有效的借阅状态。");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return errors;
|
return errors;
|
||||||
@@ -278,26 +278,26 @@ public class BorrowingServiceImpl implements BorrowingService {
|
|||||||
|
|
||||||
private Map<String, String> validateBorrowIdentifiers(String readerIdentifier, String bookIdentifier) {
|
private Map<String, String> validateBorrowIdentifiers(String readerIdentifier, String bookIdentifier) {
|
||||||
Map<String, String> errors = new LinkedHashMap<>();
|
Map<String, String> errors = new LinkedHashMap<>();
|
||||||
requireLength(errors, "readerIdentifier", readerIdentifier, "Reader ID", 64);
|
requireLength(errors, "readerIdentifier", readerIdentifier, "读者编号", 64);
|
||||||
requireLength(errors, "bookIdentifier", bookIdentifier, "Book ID", 64);
|
requireLength(errors, "bookIdentifier", bookIdentifier, "图书编号", 64);
|
||||||
return errors;
|
return errors;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Map<String, String> validateActiveLoan(BorrowRecord record) {
|
private Map<String, String> validateActiveLoan(BorrowRecord record) {
|
||||||
Map<String, String> errors = new LinkedHashMap<>();
|
Map<String, String> errors = new LinkedHashMap<>();
|
||||||
if (record.getStatus() != BorrowRecordStatus.ACTIVE || record.getReturnedAt() != null) {
|
if (record.getStatus() != BorrowRecordStatus.ACTIVE || record.getReturnedAt() != null) {
|
||||||
errors.put("status", "Only active loans can use this action.");
|
errors.put("status", "只有借阅中的记录可以执行此操作。");
|
||||||
}
|
}
|
||||||
return errors;
|
return errors;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void requireLength(Map<String, String> errors, String field, String value, String label, int maxLength) {
|
private void requireLength(Map<String, String> errors, String field, String value, String label, int maxLength) {
|
||||||
if (value == null || value.isEmpty()) {
|
if (value == null || value.isEmpty()) {
|
||||||
errors.put(field, label + " is required.");
|
errors.put(field, "请填写" + label + "。");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (value.length() > maxLength) {
|
if (value.length() > maxLength) {
|
||||||
errors.put(field, label + " must be " + maxLength + " characters or fewer.");
|
errors.put(field, label + "不能超过 " + maxLength + " 个字符。");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,10 +22,10 @@ import java.util.logging.Logger;
|
|||||||
public class ReaderServiceImpl implements ReaderService {
|
public class ReaderServiceImpl implements ReaderService {
|
||||||
private static final Logger LOGGER = Logger.getLogger(ReaderServiceImpl.class.getName());
|
private static final Logger LOGGER = Logger.getLogger(ReaderServiceImpl.class.getName());
|
||||||
private static final String UNAVAILABLE_MESSAGE =
|
private static final String UNAVAILABLE_MESSAGE =
|
||||||
"Reader service is temporarily unavailable. Please try again later.";
|
"读者服务暂时不可用,请稍后重试。";
|
||||||
private static final String VALIDATION_MESSAGE = "Please correct the highlighted reader fields.";
|
private static final String VALIDATION_MESSAGE = "请修正高亮的读者字段。";
|
||||||
private static final String SEARCH_VALIDATION_MESSAGE = "Please correct the reader search filters.";
|
private static final String SEARCH_VALIDATION_MESSAGE = "请修正读者检索筛选条件。";
|
||||||
private static final String DENIED_MESSAGE = "You do not have permission to manage readers.";
|
private static final String DENIED_MESSAGE = "您无权管理读者。";
|
||||||
private static final int MAX_BORROW_LIMIT = 50;
|
private static final int MAX_BORROW_LIMIT = 50;
|
||||||
private static final Pattern PHONE_PATTERN = Pattern.compile("(?=.*\\d)[0-9+()\\-\\s]{6,32}");
|
private static final Pattern PHONE_PATTERN = Pattern.compile("(?=.*\\d)[0-9+()\\-\\s]{6,32}");
|
||||||
private static final Pattern EMAIL_PATTERN = Pattern.compile("^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$");
|
private static final Pattern EMAIL_PATTERN = Pattern.compile("^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$");
|
||||||
@@ -61,7 +61,7 @@ public class ReaderServiceImpl implements ReaderService {
|
|||||||
@Override
|
@Override
|
||||||
public ServiceResult<Optional<Reader>> findReader(long id) {
|
public ServiceResult<Optional<Reader>> findReader(long id) {
|
||||||
if (id <= 0) {
|
if (id <= 0) {
|
||||||
return ServiceResult.failure("Select a valid reader.");
|
return ServiceResult.failure("请选择有效的读者。");
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -86,17 +86,17 @@ public class ReaderServiceImpl implements ReaderService {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if (readerDao.findByIdentifier(reader.getIdentifier()).isPresent()) {
|
if (readerDao.findByIdentifier(reader.getIdentifier()).isPresent()) {
|
||||||
errors.put("identifier", "Reader identifier is already in use.");
|
errors.put("identifier", "读者编号已被使用。");
|
||||||
return ServiceResult.validationFailure(VALIDATION_MESSAGE, errors);
|
return ServiceResult.validationFailure(VALIDATION_MESSAGE, errors);
|
||||||
}
|
}
|
||||||
if (reader.getUserId() != null && readerDao.findByUserId(reader.getUserId()).isPresent()) {
|
if (reader.getUserId() != null && readerDao.findByUserId(reader.getUserId()).isPresent()) {
|
||||||
errors.put("userId", "Linked account is already assigned to a reader profile.");
|
errors.put("userId", "关联账户已绑定到其他读者档案。");
|
||||||
return ServiceResult.validationFailure(VALIDATION_MESSAGE, errors);
|
return ServiceResult.validationFailure(VALIDATION_MESSAGE, errors);
|
||||||
}
|
}
|
||||||
|
|
||||||
long id = readerDao.create(reader);
|
long id = readerDao.create(reader);
|
||||||
LOGGER.info("Created reader id=" + id + " actorId=" + actor.getId());
|
LOGGER.info("Created reader id=" + id + " actorId=" + actor.getId());
|
||||||
return ServiceResult.success(id, "Reader profile created.");
|
return ServiceResult.success(id, "读者档案已创建。");
|
||||||
} catch (DaoException ex) {
|
} catch (DaoException ex) {
|
||||||
LOGGER.log(Level.SEVERE, "Unable to create reader actorId=" + actor.getId(), ex);
|
LOGGER.log(Level.SEVERE, "Unable to create reader actorId=" + actor.getId(), ex);
|
||||||
return ServiceResult.failure(UNAVAILABLE_MESSAGE);
|
return ServiceResult.failure(UNAVAILABLE_MESSAGE);
|
||||||
@@ -118,23 +118,23 @@ public class ReaderServiceImpl implements ReaderService {
|
|||||||
try {
|
try {
|
||||||
Optional<Reader> existingWithIdentifier = readerDao.findByIdentifier(reader.getIdentifier());
|
Optional<Reader> existingWithIdentifier = readerDao.findByIdentifier(reader.getIdentifier());
|
||||||
if (existingWithIdentifier.isPresent() && existingWithIdentifier.get().getId() != reader.getId()) {
|
if (existingWithIdentifier.isPresent() && existingWithIdentifier.get().getId() != reader.getId()) {
|
||||||
errors.put("identifier", "Reader identifier is already in use.");
|
errors.put("identifier", "读者编号已被使用。");
|
||||||
return ServiceResult.validationFailure(VALIDATION_MESSAGE, errors);
|
return ServiceResult.validationFailure(VALIDATION_MESSAGE, errors);
|
||||||
}
|
}
|
||||||
if (reader.getUserId() != null) {
|
if (reader.getUserId() != null) {
|
||||||
Optional<Reader> existingWithUser = readerDao.findByUserId(reader.getUserId());
|
Optional<Reader> existingWithUser = readerDao.findByUserId(reader.getUserId());
|
||||||
if (existingWithUser.isPresent() && existingWithUser.get().getId() != reader.getId()) {
|
if (existingWithUser.isPresent() && existingWithUser.get().getId() != reader.getId()) {
|
||||||
errors.put("userId", "Linked account is already assigned to a reader profile.");
|
errors.put("userId", "关联账户已绑定到其他读者档案。");
|
||||||
return ServiceResult.validationFailure(VALIDATION_MESSAGE, errors);
|
return ServiceResult.validationFailure(VALIDATION_MESSAGE, errors);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!readerDao.update(reader)) {
|
if (!readerDao.update(reader)) {
|
||||||
return ServiceResult.failure("Reader profile was not found.");
|
return ServiceResult.failure("未找到读者档案。");
|
||||||
}
|
}
|
||||||
|
|
||||||
LOGGER.info("Updated reader id=" + reader.getId() + " actorId=" + actor.getId());
|
LOGGER.info("Updated reader id=" + reader.getId() + " actorId=" + actor.getId());
|
||||||
return ServiceResult.success(null, "Reader profile updated.");
|
return ServiceResult.success(null, "读者档案已更新。");
|
||||||
} catch (DaoException ex) {
|
} catch (DaoException ex) {
|
||||||
LOGGER.log(Level.SEVERE, "Unable to update reader id=" + reader.getId() + " actorId=" + actor.getId(), ex);
|
LOGGER.log(Level.SEVERE, "Unable to update reader id=" + reader.getId() + " actorId=" + actor.getId(), ex);
|
||||||
return ServiceResult.failure(UNAVAILABLE_MESSAGE);
|
return ServiceResult.failure(UNAVAILABLE_MESSAGE);
|
||||||
@@ -147,16 +147,16 @@ public class ReaderServiceImpl implements ReaderService {
|
|||||||
return ServiceResult.failure(DENIED_MESSAGE);
|
return ServiceResult.failure(DENIED_MESSAGE);
|
||||||
}
|
}
|
||||||
if (id <= 0) {
|
if (id <= 0) {
|
||||||
return ServiceResult.failure("Select a valid reader.");
|
return ServiceResult.failure("请选择有效的读者。");
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!readerDao.deactivate(id)) {
|
if (!readerDao.deactivate(id)) {
|
||||||
return ServiceResult.failure("Reader profile was not found.");
|
return ServiceResult.failure("未找到读者档案。");
|
||||||
}
|
}
|
||||||
|
|
||||||
LOGGER.info("Deactivated reader id=" + id + " actorId=" + actor.getId());
|
LOGGER.info("Deactivated reader id=" + id + " actorId=" + actor.getId());
|
||||||
return ServiceResult.success(null, "Reader profile deactivated.");
|
return ServiceResult.success(null, "读者档案已停用。");
|
||||||
} catch (DaoException ex) {
|
} catch (DaoException ex) {
|
||||||
LOGGER.log(Level.SEVERE, "Unable to deactivate reader id=" + id + " actorId=" + actor.getId(), ex);
|
LOGGER.log(Level.SEVERE, "Unable to deactivate reader id=" + id + " actorId=" + actor.getId(), ex);
|
||||||
return ServiceResult.failure(UNAVAILABLE_MESSAGE);
|
return ServiceResult.failure(UNAVAILABLE_MESSAGE);
|
||||||
@@ -183,7 +183,7 @@ public class ReaderServiceImpl implements ReaderService {
|
|||||||
try {
|
try {
|
||||||
criteria.setStatusCode(ReaderStatus.fromCode(criteria.getStatusCode()).getCode());
|
criteria.setStatusCode(ReaderStatus.fromCode(criteria.getStatusCode()).getCode());
|
||||||
} catch (IllegalArgumentException ex) {
|
} catch (IllegalArgumentException ex) {
|
||||||
errors.put("status", "Select a valid status.");
|
errors.put("status", "请选择有效的状态。");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return errors;
|
return errors;
|
||||||
@@ -192,24 +192,24 @@ public class ReaderServiceImpl implements ReaderService {
|
|||||||
private Map<String, String> validate(Reader reader, boolean requireId) {
|
private Map<String, String> validate(Reader reader, boolean requireId) {
|
||||||
Map<String, String> errors = new LinkedHashMap<>();
|
Map<String, String> errors = new LinkedHashMap<>();
|
||||||
if (reader == null) {
|
if (reader == null) {
|
||||||
errors.put("reader", "Reader details are required.");
|
errors.put("reader", "请填写读者详情。");
|
||||||
return errors;
|
return errors;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (requireId && reader.getId() <= 0) {
|
if (requireId && reader.getId() <= 0) {
|
||||||
errors.put("id", "Select a valid reader.");
|
errors.put("id", "请选择有效的读者。");
|
||||||
}
|
}
|
||||||
requireLength(errors, "identifier", reader.getIdentifier(), "Reader identifier", 64);
|
requireLength(errors, "identifier", reader.getIdentifier(), "读者编号", 64);
|
||||||
requireLength(errors, "fullName", reader.getFullName(), "Full name", 100);
|
requireLength(errors, "fullName", reader.getFullName(), "姓名", 100);
|
||||||
if (reader.getUserId() != null && reader.getUserId() <= 0) {
|
if (reader.getUserId() != null && reader.getUserId() <= 0) {
|
||||||
errors.put("userId", "Linked account ID must be positive.");
|
errors.put("userId", "关联账户 ID 必须为正数。");
|
||||||
}
|
}
|
||||||
validateContact(errors, reader);
|
validateContact(errors, reader);
|
||||||
if (reader.getStatus() == null) {
|
if (reader.getStatus() == null) {
|
||||||
errors.put("status", "Select a status.");
|
errors.put("status", "请选择状态。");
|
||||||
}
|
}
|
||||||
if (reader.getMaxBorrowCount() < 1 || reader.getMaxBorrowCount() > MAX_BORROW_LIMIT) {
|
if (reader.getMaxBorrowCount() < 1 || reader.getMaxBorrowCount() > MAX_BORROW_LIMIT) {
|
||||||
errors.put("maxBorrowCount", "Max borrow count must be between 1 and " + MAX_BORROW_LIMIT + ".");
|
errors.put("maxBorrowCount", "最大借阅数量必须在 1 到 " + MAX_BORROW_LIMIT + " 之间。");
|
||||||
}
|
}
|
||||||
return errors;
|
return errors;
|
||||||
}
|
}
|
||||||
@@ -218,24 +218,24 @@ public class ReaderServiceImpl implements ReaderService {
|
|||||||
String phone = reader.getPhone();
|
String phone = reader.getPhone();
|
||||||
String email = reader.getEmail();
|
String email = reader.getEmail();
|
||||||
if ((phone == null || phone.isEmpty()) && (email == null || email.isEmpty())) {
|
if ((phone == null || phone.isEmpty()) && (email == null || email.isEmpty())) {
|
||||||
errors.put("phone", "Phone or email is required.");
|
errors.put("phone", "请填写电话或邮箱。");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (phone != null && !phone.isEmpty() && !PHONE_PATTERN.matcher(phone).matches()) {
|
if (phone != null && !phone.isEmpty() && !PHONE_PATTERN.matcher(phone).matches()) {
|
||||||
errors.put("phone", "Phone must include a digit and use 6 to 32 digits or common phone symbols.");
|
errors.put("phone", "电话必须包含数字,并使用 6 到 32 位数字或常见电话符号。");
|
||||||
}
|
}
|
||||||
if (email != null && !email.isEmpty() && !EMAIL_PATTERN.matcher(email).matches()) {
|
if (email != null && !email.isEmpty() && !EMAIL_PATTERN.matcher(email).matches()) {
|
||||||
errors.put("email", "Email must be a valid address.");
|
errors.put("email", "邮箱格式不正确。");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void requireLength(Map<String, String> errors, String field, String value, String label, int maxLength) {
|
private void requireLength(Map<String, String> errors, String field, String value, String label, int maxLength) {
|
||||||
if (value == null || value.isEmpty()) {
|
if (value == null || value.isEmpty()) {
|
||||||
errors.put(field, label + " is required.");
|
errors.put(field, "请填写" + label + "。");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (value.length() > maxLength) {
|
if (value.length() > maxLength) {
|
||||||
errors.put(field, label + " must be " + maxLength + " characters or fewer.");
|
errors.put(field, label + "不能超过 " + maxLength + " 个字符。");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ import java.util.logging.Logger;
|
|||||||
public class ReportServiceImpl implements ReportService {
|
public class ReportServiceImpl implements ReportService {
|
||||||
private static final Logger LOGGER = Logger.getLogger(ReportServiceImpl.class.getName());
|
private static final Logger LOGGER = Logger.getLogger(ReportServiceImpl.class.getName());
|
||||||
private static final String UNAVAILABLE_MESSAGE =
|
private static final String UNAVAILABLE_MESSAGE =
|
||||||
"Report service is temporarily unavailable. Please try again later.";
|
"报表服务暂时不可用,请稍后重试。";
|
||||||
private static final String DENIED_MESSAGE = "You do not have permission to view reports.";
|
private static final String DENIED_MESSAGE = "您无权查看报表。";
|
||||||
private static final int POPULAR_BOOK_LIMIT = 10;
|
private static final int POPULAR_BOOK_LIMIT = 10;
|
||||||
|
|
||||||
private final ReportDao reportDao;
|
private final ReportDao reportDao;
|
||||||
|
|||||||
@@ -20,9 +20,9 @@ import java.util.logging.Logger;
|
|||||||
public class SystemLogServiceImpl implements SystemLogService {
|
public class SystemLogServiceImpl implements SystemLogService {
|
||||||
private static final Logger LOGGER = Logger.getLogger(SystemLogServiceImpl.class.getName());
|
private static final Logger LOGGER = Logger.getLogger(SystemLogServiceImpl.class.getName());
|
||||||
private static final String UNAVAILABLE_MESSAGE =
|
private static final String UNAVAILABLE_MESSAGE =
|
||||||
"System log service is temporarily unavailable. Please try again later.";
|
"系统日志服务暂时不可用,请稍后重试。";
|
||||||
private static final String DENIED_MESSAGE = "You do not have permission to view system logs.";
|
private static final String DENIED_MESSAGE = "您无权查看系统日志。";
|
||||||
private static final String VALIDATION_MESSAGE = "Please correct the system log search filters.";
|
private static final String VALIDATION_MESSAGE = "请修正系统日志检索筛选条件。";
|
||||||
|
|
||||||
private final SystemLogDao systemLogDao;
|
private final SystemLogDao systemLogDao;
|
||||||
private final PermissionPolicy permissionPolicy;
|
private final PermissionPolicy permissionPolicy;
|
||||||
@@ -62,18 +62,18 @@ public class SystemLogServiceImpl implements SystemLogService {
|
|||||||
private Map<String, String> validate(SystemLogSearchCriteria criteria) {
|
private Map<String, String> validate(SystemLogSearchCriteria criteria) {
|
||||||
Map<String, String> errors = new LinkedHashMap<>();
|
Map<String, String> errors = new LinkedHashMap<>();
|
||||||
if (criteria.getOperationType().length() > 64) {
|
if (criteria.getOperationType().length() > 64) {
|
||||||
errors.put("operationType", "Operation type must be 64 characters or fewer.");
|
errors.put("operationType", "操作类型不能超过 64 个字符。");
|
||||||
}
|
}
|
||||||
if (criteria.getKeyword().length() > 120) {
|
if (criteria.getKeyword().length() > 120) {
|
||||||
errors.put("keyword", "Keyword must be 120 characters or fewer.");
|
errors.put("keyword", "关键词不能超过 120 个字符。");
|
||||||
}
|
}
|
||||||
|
|
||||||
parseDate(criteria.getCreatedFromText(), "createdFrom", "Start date", errors, criteria, true);
|
parseDate(criteria.getCreatedFromText(), "createdFrom", "开始日期", errors, criteria, true);
|
||||||
parseDate(criteria.getCreatedToText(), "createdTo", "End date", errors, criteria, false);
|
parseDate(criteria.getCreatedToText(), "createdTo", "结束日期", errors, criteria, false);
|
||||||
if (criteria.getCreatedFrom() != null
|
if (criteria.getCreatedFrom() != null
|
||||||
&& criteria.getCreatedTo() != null
|
&& criteria.getCreatedTo() != null
|
||||||
&& criteria.getCreatedFrom().isAfter(criteria.getCreatedTo())) {
|
&& criteria.getCreatedFrom().isAfter(criteria.getCreatedTo())) {
|
||||||
errors.put("createdTo", "End date must be on or after start date.");
|
errors.put("createdTo", "结束日期必须晚于或等于开始日期。");
|
||||||
}
|
}
|
||||||
return errors;
|
return errors;
|
||||||
}
|
}
|
||||||
@@ -91,7 +91,7 @@ public class SystemLogServiceImpl implements SystemLogService {
|
|||||||
criteria.setCreatedTo(parsed);
|
criteria.setCreatedTo(parsed);
|
||||||
}
|
}
|
||||||
} catch (DateTimeParseException ex) {
|
} catch (DateTimeParseException ex) {
|
||||||
errors.put(field, label + " must use YYYY-MM-DD.");
|
errors.put(field, label + "必须使用 YYYY-MM-DD 格式。");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,12 +30,12 @@ public class UserAccountServiceImpl implements UserAccountService {
|
|||||||
|
|
||||||
private static final Logger LOGGER = Logger.getLogger(UserAccountServiceImpl.class.getName());
|
private static final Logger LOGGER = Logger.getLogger(UserAccountServiceImpl.class.getName());
|
||||||
private static final String UNAVAILABLE_MESSAGE =
|
private static final String UNAVAILABLE_MESSAGE =
|
||||||
"User management service is temporarily unavailable. Please try again later.";
|
"用户管理服务暂时不可用,请稍后重试。";
|
||||||
private static final String VALIDATION_MESSAGE = "Please correct the highlighted account fields.";
|
private static final String VALIDATION_MESSAGE = "请修正高亮的账户字段。";
|
||||||
private static final String SEARCH_VALIDATION_MESSAGE = "Please correct the account search filters.";
|
private static final String SEARCH_VALIDATION_MESSAGE = "请修正账户检索筛选条件。";
|
||||||
private static final String DENIED_MESSAGE = "You do not have permission to manage users.";
|
private static final String DENIED_MESSAGE = "您无权管理用户。";
|
||||||
private static final String SELF_DEACTIVATE_MESSAGE = "You cannot deactivate your own administrator account.";
|
private static final String SELF_DEACTIVATE_MESSAGE = "不能停用您自己的管理员账户。";
|
||||||
private static final String SELF_ROLE_MESSAGE = "You cannot change your own administrator role.";
|
private static final String SELF_ROLE_MESSAGE = "不能修改您自己的管理员角色。";
|
||||||
|
|
||||||
private final UserAccountDao userAccountDao;
|
private final UserAccountDao userAccountDao;
|
||||||
private final SystemLogDao systemLogDao;
|
private final SystemLogDao systemLogDao;
|
||||||
@@ -80,7 +80,7 @@ public class UserAccountServiceImpl implements UserAccountService {
|
|||||||
return ServiceResult.failure(DENIED_MESSAGE);
|
return ServiceResult.failure(DENIED_MESSAGE);
|
||||||
}
|
}
|
||||||
if (id <= 0) {
|
if (id <= 0) {
|
||||||
return ServiceResult.failure("Select a valid user account.");
|
return ServiceResult.failure("请选择有效的用户账户。");
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -105,7 +105,7 @@ public class UserAccountServiceImpl implements UserAccountService {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if (userAccountDao.findByUsername(user.getUsername()).isPresent()) {
|
if (userAccountDao.findByUsername(user.getUsername()).isPresent()) {
|
||||||
errors.put("username", "Username is already in use.");
|
errors.put("username", "用户名已被使用。");
|
||||||
return ServiceResult.validationFailure(VALIDATION_MESSAGE, errors);
|
return ServiceResult.validationFailure(VALIDATION_MESSAGE, errors);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,10 +113,10 @@ public class UserAccountServiceImpl implements UserAccountService {
|
|||||||
return transactionExecutor.execute(connection -> {
|
return transactionExecutor.execute(connection -> {
|
||||||
long id = userAccountDao.create(connection, user);
|
long id = userAccountDao.create(connection, user);
|
||||||
systemLogDao.create(connection, auditLog(actor, "user.create", id,
|
systemLogDao.create(connection, auditLog(actor, "user.create", id,
|
||||||
"Created account username=" + user.getUsername() + " role=" + user.getRole().getCode(),
|
"创建账户:用户名=" + user.getUsername() + ",角色=" + user.getRole().getDisplayName(),
|
||||||
requestIp));
|
requestIp));
|
||||||
LOGGER.info("Created user id=" + id + " actorId=" + actor.getId());
|
LOGGER.info("Created user id=" + id + " actorId=" + actor.getId());
|
||||||
return ServiceResult.success(id, "User account created.");
|
return ServiceResult.success(id, "用户账户已创建。");
|
||||||
});
|
});
|
||||||
} catch (DaoException | IllegalStateException ex) {
|
} catch (DaoException | IllegalStateException ex) {
|
||||||
LOGGER.log(Level.SEVERE, "Unable to create user actorId=" + actor.getId()
|
LOGGER.log(Level.SEVERE, "Unable to create user actorId=" + actor.getId()
|
||||||
@@ -140,7 +140,7 @@ public class UserAccountServiceImpl implements UserAccountService {
|
|||||||
try {
|
try {
|
||||||
Optional<User> existingResult = userAccountDao.findById(user.getId());
|
Optional<User> existingResult = userAccountDao.findById(user.getId());
|
||||||
if (!existingResult.isPresent()) {
|
if (!existingResult.isPresent()) {
|
||||||
return ServiceResult.failure("User account was not found.");
|
return ServiceResult.failure("未找到用户账户。");
|
||||||
}
|
}
|
||||||
|
|
||||||
protectCurrentAdministrator(actor, user, errors);
|
protectCurrentAdministrator(actor, user, errors);
|
||||||
@@ -158,15 +158,15 @@ public class UserAccountServiceImpl implements UserAccountService {
|
|||||||
final boolean passwordChanged = updatePassword;
|
final boolean passwordChanged = updatePassword;
|
||||||
return transactionExecutor.execute(connection -> {
|
return transactionExecutor.execute(connection -> {
|
||||||
if (!userAccountDao.update(connection, user, passwordChanged)) {
|
if (!userAccountDao.update(connection, user, passwordChanged)) {
|
||||||
return ServiceResult.failure("User account was not found.");
|
return ServiceResult.failure("未找到用户账户。");
|
||||||
}
|
}
|
||||||
systemLogDao.create(connection, auditLog(actor, "user.update", user.getId(),
|
systemLogDao.create(connection, auditLog(actor, "user.update", user.getId(),
|
||||||
"Updated account username=" + user.getUsername() + " role=" + user.getRole().getCode()
|
"更新账户:用户名=" + user.getUsername() + ",角色=" + user.getRole().getDisplayName()
|
||||||
+ " active=" + user.isActive()
|
+ ",状态=" + (user.isActive() ? "启用" : "停用")
|
||||||
+ (passwordChanged ? " passwordReset=true" : ""),
|
+ (passwordChanged ? ",已重置密码" : ""),
|
||||||
requestIp));
|
requestIp));
|
||||||
LOGGER.info("Updated user id=" + user.getId() + " actorId=" + actor.getId());
|
LOGGER.info("Updated user id=" + user.getId() + " actorId=" + actor.getId());
|
||||||
return ServiceResult.success(null, "User account updated.");
|
return ServiceResult.success(null, "用户账户已更新。");
|
||||||
});
|
});
|
||||||
} catch (DaoException | IllegalStateException ex) {
|
} catch (DaoException | IllegalStateException ex) {
|
||||||
LOGGER.log(Level.SEVERE, "Unable to update user id=" + user.getId() + " actorId=" + actor.getId(), ex);
|
LOGGER.log(Level.SEVERE, "Unable to update user id=" + user.getId() + " actorId=" + actor.getId(), ex);
|
||||||
@@ -180,7 +180,7 @@ public class UserAccountServiceImpl implements UserAccountService {
|
|||||||
return ServiceResult.failure(DENIED_MESSAGE);
|
return ServiceResult.failure(DENIED_MESSAGE);
|
||||||
}
|
}
|
||||||
if (id <= 0) {
|
if (id <= 0) {
|
||||||
return ServiceResult.failure("Select a valid user account.");
|
return ServiceResult.failure("请选择有效的用户账户。");
|
||||||
}
|
}
|
||||||
if (actor.getId() == id) {
|
if (actor.getId() == id) {
|
||||||
Map<String, String> errors = new LinkedHashMap<>();
|
Map<String, String> errors = new LinkedHashMap<>();
|
||||||
@@ -191,20 +191,20 @@ public class UserAccountServiceImpl implements UserAccountService {
|
|||||||
try {
|
try {
|
||||||
Optional<User> existingResult = userAccountDao.findById(id);
|
Optional<User> existingResult = userAccountDao.findById(id);
|
||||||
if (!existingResult.isPresent()) {
|
if (!existingResult.isPresent()) {
|
||||||
return ServiceResult.failure("User account was not found.");
|
return ServiceResult.failure("未找到用户账户。");
|
||||||
}
|
}
|
||||||
|
|
||||||
User user = existingResult.get();
|
User user = existingResult.get();
|
||||||
user.setActive(false);
|
user.setActive(false);
|
||||||
return transactionExecutor.execute(connection -> {
|
return transactionExecutor.execute(connection -> {
|
||||||
if (!userAccountDao.update(connection, user, false)) {
|
if (!userAccountDao.update(connection, user, false)) {
|
||||||
return ServiceResult.failure("User account was not found.");
|
return ServiceResult.failure("未找到用户账户。");
|
||||||
}
|
}
|
||||||
systemLogDao.create(connection, auditLog(actor, "user.deactivate", id,
|
systemLogDao.create(connection, auditLog(actor, "user.deactivate", id,
|
||||||
"Deactivated account username=" + user.getUsername(),
|
"停用账户:用户名=" + user.getUsername(),
|
||||||
requestIp));
|
requestIp));
|
||||||
LOGGER.info("Deactivated user id=" + id + " actorId=" + actor.getId());
|
LOGGER.info("Deactivated user id=" + id + " actorId=" + actor.getId());
|
||||||
return ServiceResult.success(null, "User account deactivated.");
|
return ServiceResult.success(null, "用户账户已停用。");
|
||||||
});
|
});
|
||||||
} catch (DaoException ex) {
|
} catch (DaoException ex) {
|
||||||
LOGGER.log(Level.SEVERE, "Unable to deactivate user id=" + id + " actorId=" + actor.getId(), ex);
|
LOGGER.log(Level.SEVERE, "Unable to deactivate user id=" + id + " actorId=" + actor.getId(), ex);
|
||||||
@@ -218,7 +218,7 @@ public class UserAccountServiceImpl implements UserAccountService {
|
|||||||
try {
|
try {
|
||||||
criteria.setRoleCode(Role.fromCode(criteria.getRoleCode()).getCode());
|
criteria.setRoleCode(Role.fromCode(criteria.getRoleCode()).getCode());
|
||||||
} catch (IllegalArgumentException ex) {
|
} catch (IllegalArgumentException ex) {
|
||||||
errors.put("role", "Select a valid role.");
|
errors.put("role", "请选择有效的角色。");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -226,7 +226,7 @@ public class UserAccountServiceImpl implements UserAccountService {
|
|||||||
if (!activeStatus.isEmpty()
|
if (!activeStatus.isEmpty()
|
||||||
&& !UserSearchCriteria.ACTIVE_STATUS.equals(activeStatus)
|
&& !UserSearchCriteria.ACTIVE_STATUS.equals(activeStatus)
|
||||||
&& !UserSearchCriteria.INACTIVE_STATUS.equals(activeStatus)) {
|
&& !UserSearchCriteria.INACTIVE_STATUS.equals(activeStatus)) {
|
||||||
errors.put("active", "Select a valid active state.");
|
errors.put("active", "请选择有效的启用状态。");
|
||||||
}
|
}
|
||||||
return errors;
|
return errors;
|
||||||
}
|
}
|
||||||
@@ -234,19 +234,19 @@ public class UserAccountServiceImpl implements UserAccountService {
|
|||||||
private Map<String, String> validateUser(User user, boolean requireId, String password, boolean requirePassword) {
|
private Map<String, String> validateUser(User user, boolean requireId, String password, boolean requirePassword) {
|
||||||
Map<String, String> errors = new LinkedHashMap<>();
|
Map<String, String> errors = new LinkedHashMap<>();
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
errors.put("user", "User account details are required.");
|
errors.put("user", "请填写用户账户详情。");
|
||||||
return errors;
|
return errors;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (requireId && user.getId() <= 0) {
|
if (requireId && user.getId() <= 0) {
|
||||||
errors.put("id", "Select a valid user account.");
|
errors.put("id", "请选择有效的用户账户。");
|
||||||
}
|
}
|
||||||
if (!requireId) {
|
if (!requireId) {
|
||||||
requireLength(errors, "username", user.getUsername(), "Username", 64);
|
requireLength(errors, "username", user.getUsername(), "用户名", 64);
|
||||||
}
|
}
|
||||||
requireLength(errors, "displayName", user.getDisplayName(), "Display name", 100);
|
requireLength(errors, "displayName", user.getDisplayName(), "显示名称", 100);
|
||||||
if (user.getRole() == null) {
|
if (user.getRole() == null) {
|
||||||
errors.put("role", "Select a role.");
|
errors.put("role", "请选择角色。");
|
||||||
}
|
}
|
||||||
validatePassword(errors, password, requirePassword);
|
validatePassword(errors, password, requirePassword);
|
||||||
return errors;
|
return errors;
|
||||||
@@ -256,12 +256,12 @@ public class UserAccountServiceImpl implements UserAccountService {
|
|||||||
String trimmed = password == null ? "" : password.trim();
|
String trimmed = password == null ? "" : password.trim();
|
||||||
if (trimmed.isEmpty()) {
|
if (trimmed.isEmpty()) {
|
||||||
if (required) {
|
if (required) {
|
||||||
errors.put("password", "Password is required.");
|
errors.put("password", "请填写密码。");
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (password.length() > 128) {
|
if (password.length() > 128) {
|
||||||
errors.put("password", "Password must be 128 characters or fewer.");
|
errors.put("password", "密码不能超过 128 个字符。");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -279,11 +279,11 @@ public class UserAccountServiceImpl implements UserAccountService {
|
|||||||
|
|
||||||
private void requireLength(Map<String, String> errors, String field, String value, String label, int maxLength) {
|
private void requireLength(Map<String, String> errors, String field, String value, String label, int maxLength) {
|
||||||
if (value == null || value.isEmpty()) {
|
if (value == null || value.isEmpty()) {
|
||||||
errors.put(field, label + " is required.");
|
errors.put(field, "请填写" + label + "。");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (value.length() > maxLength) {
|
if (value.length() > maxLength) {
|
||||||
errors.put(field, label + " must be " + maxLength + " characters or fewer.");
|
errors.put(field, label + "不能超过 " + maxLength + " 个字符。");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,18 +2,18 @@
|
|||||||
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
|
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
|
||||||
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
|
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="zh-CN">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title><c:out value="${formTitle}" /> - MZH Library</title>
|
<title><c:out value="${formTitle}" /> - MZH 图书馆</title>
|
||||||
<link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/app.css">
|
<link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/app.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<%@ include file="/WEB-INF/jsp/common/header.jspf" %>
|
<%@ include file="/WEB-INF/jsp/common/header.jspf" %>
|
||||||
<main class="page-shell">
|
<main class="page-shell">
|
||||||
<section class="form-panel" aria-labelledby="user-form-title">
|
<section class="form-panel" aria-labelledby="user-form-title">
|
||||||
<p class="eyebrow">Administration</p>
|
<p class="eyebrow">系统管理</p>
|
||||||
<h1 id="user-form-title"><c:out value="${formTitle}" /></h1>
|
<h1 id="user-form-title"><c:out value="${formTitle}" /></h1>
|
||||||
|
|
||||||
<c:if test="${not empty errorMessage}">
|
<c:if test="${not empty errorMessage}">
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
|
|
||||||
<div class="form-grid">
|
<div class="form-grid">
|
||||||
<div class="form-field">
|
<div class="form-field">
|
||||||
<label for="username">Username</label>
|
<label for="username">用户名</label>
|
||||||
<c:choose>
|
<c:choose>
|
||||||
<c:when test="${user.id > 0}">
|
<c:when test="${user.id > 0}">
|
||||||
<input id="username" type="text" value="${fn:escapeXml(usernameValue)}" disabled>
|
<input id="username" type="text" value="${fn:escapeXml(usernameValue)}" disabled>
|
||||||
@@ -51,7 +51,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-field">
|
<div class="form-field">
|
||||||
<label for="displayName">Display name</label>
|
<label for="displayName">显示名称</label>
|
||||||
<input id="displayName" name="displayName" type="text"
|
<input id="displayName" name="displayName" type="text"
|
||||||
value="${fn:escapeXml(displayNameValue)}" required>
|
value="${fn:escapeXml(displayNameValue)}" required>
|
||||||
<c:if test="${not empty errors.displayName}">
|
<c:if test="${not empty errors.displayName}">
|
||||||
@@ -60,9 +60,9 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-field">
|
<div class="form-field">
|
||||||
<label for="role">Role</label>
|
<label for="role">角色</label>
|
||||||
<select id="role" name="role" required>
|
<select id="role" name="role" required>
|
||||||
<option value="">Select role</option>
|
<option value="">请选择角色</option>
|
||||||
<c:forEach var="role" items="${roles}">
|
<c:forEach var="role" items="${roles}">
|
||||||
<option value="${role.code}" <c:if test="${roleValue == role.code}">selected</c:if>>
|
<option value="${role.code}" <c:if test="${roleValue == role.code}">selected</c:if>>
|
||||||
<c:out value="${role.displayName}" />
|
<c:out value="${role.displayName}" />
|
||||||
@@ -75,13 +75,13 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-field">
|
<div class="form-field">
|
||||||
<label for="active">Active state</label>
|
<label for="active">启用状态</label>
|
||||||
<select id="active" name="active" required>
|
<select id="active" name="active" required>
|
||||||
<option value="true" <c:if test="${activeValue == true or activeValue == 'true'}">selected</c:if>>
|
<option value="true" <c:if test="${activeValue == true or activeValue == 'true'}">selected</c:if>>
|
||||||
Active
|
启用
|
||||||
</option>
|
</option>
|
||||||
<option value="false" <c:if test="${activeValue == false or activeValue == 'false'}">selected</c:if>>
|
<option value="false" <c:if test="${activeValue == false or activeValue == 'false'}">selected</c:if>>
|
||||||
Inactive
|
停用
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
<c:if test="${not empty errors.active}">
|
<c:if test="${not empty errors.active}">
|
||||||
@@ -92,8 +92,8 @@
|
|||||||
<div class="form-field">
|
<div class="form-field">
|
||||||
<label for="password">
|
<label for="password">
|
||||||
<c:choose>
|
<c:choose>
|
||||||
<c:when test="${user.id > 0}">New password</c:when>
|
<c:when test="${user.id > 0}">新密码</c:when>
|
||||||
<c:otherwise>Password</c:otherwise>
|
<c:otherwise>密码</c:otherwise>
|
||||||
</c:choose>
|
</c:choose>
|
||||||
</label>
|
</label>
|
||||||
<c:choose>
|
<c:choose>
|
||||||
@@ -111,8 +111,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
<button class="button button-primary" type="submit">Save</button>
|
<button class="button button-primary" type="submit">保存</button>
|
||||||
<a class="button button-secondary" href="${pageContext.request.contextPath}/admin/users">Cancel</a>
|
<a class="button button-secondary" href="${pageContext.request.contextPath}/admin/users">取消</a>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -2,11 +2,11 @@
|
|||||||
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
|
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
|
||||||
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
|
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="zh-CN">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>Manage Users - MZH Library</title>
|
<title>用户管理 - MZH 图书馆</title>
|
||||||
<link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/app.css">
|
<link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/app.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -14,11 +14,11 @@
|
|||||||
<main class="page-shell">
|
<main class="page-shell">
|
||||||
<section class="dashboard-hero catalog-hero" aria-labelledby="manage-users-title">
|
<section class="dashboard-hero catalog-hero" aria-labelledby="manage-users-title">
|
||||||
<div>
|
<div>
|
||||||
<p class="eyebrow">Administration</p>
|
<p class="eyebrow">系统管理</p>
|
||||||
<h1 id="manage-users-title">Manage users</h1>
|
<h1 id="manage-users-title">管理用户</h1>
|
||||||
<p>Create, update, deactivate, and review administrator, librarian, and reader accounts.</p>
|
<p>创建、更新、停用和查看管理员、馆员与读者账户。</p>
|
||||||
</div>
|
</div>
|
||||||
<a class="button button-primary" href="${pageContext.request.contextPath}/admin/users/new">New user</a>
|
<a class="button button-primary" href="${pageContext.request.contextPath}/admin/users/new">新增用户</a>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<c:if test="${not empty successMessage}">
|
<c:if test="${not empty successMessage}">
|
||||||
@@ -32,10 +32,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</c:if>
|
</c:if>
|
||||||
|
|
||||||
<section class="toolbar-panel" aria-label="User management search">
|
<section class="toolbar-panel" aria-label="用户管理检索">
|
||||||
<form class="search-form" action="${pageContext.request.contextPath}/admin/users" method="get">
|
<form class="search-form" action="${pageContext.request.contextPath}/admin/users" method="get">
|
||||||
<div class="search-field">
|
<div class="search-field">
|
||||||
<label for="keyword">Keyword</label>
|
<label for="keyword">关键词</label>
|
||||||
<input id="keyword" name="keyword" type="text" value="${fn:escapeXml(criteria.keyword)}">
|
<input id="keyword" name="keyword" type="text" value="${fn:escapeXml(criteria.keyword)}">
|
||||||
<c:if test="${not empty errors.keyword}">
|
<c:if test="${not empty errors.keyword}">
|
||||||
<span class="field-error"><c:out value="${errors.keyword}" /></span>
|
<span class="field-error"><c:out value="${errors.keyword}" /></span>
|
||||||
@@ -43,9 +43,9 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="search-field">
|
<div class="search-field">
|
||||||
<label for="role">Role</label>
|
<label for="role">角色</label>
|
||||||
<select id="role" name="role">
|
<select id="role" name="role">
|
||||||
<option value="">All roles</option>
|
<option value="">全部角色</option>
|
||||||
<c:forEach var="role" items="${roles}">
|
<c:forEach var="role" items="${roles}">
|
||||||
<option value="${role.code}" <c:if test="${criteria.roleCode == role.code}">selected</c:if>>
|
<option value="${role.code}" <c:if test="${criteria.roleCode == role.code}">selected</c:if>>
|
||||||
<c:out value="${role.displayName}" />
|
<c:out value="${role.displayName}" />
|
||||||
@@ -58,40 +58,40 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="search-field">
|
<div class="search-field">
|
||||||
<label for="active">Active state</label>
|
<label for="active">启用状态</label>
|
||||||
<select id="active" name="active">
|
<select id="active" name="active">
|
||||||
<option value="">All states</option>
|
<option value="">全部状态</option>
|
||||||
<option value="active" <c:if test="${criteria.activeStatus == 'active'}">selected</c:if>>Active</option>
|
<option value="active" <c:if test="${criteria.activeStatus == 'active'}">selected</c:if>>启用</option>
|
||||||
<option value="inactive" <c:if test="${criteria.activeStatus == 'inactive'}">selected</c:if>>Inactive</option>
|
<option value="inactive" <c:if test="${criteria.activeStatus == 'inactive'}">selected</c:if>>停用</option>
|
||||||
</select>
|
</select>
|
||||||
<c:if test="${not empty errors.active}">
|
<c:if test="${not empty errors.active}">
|
||||||
<span class="field-error"><c:out value="${errors.active}" /></span>
|
<span class="field-error"><c:out value="${errors.active}" /></span>
|
||||||
</c:if>
|
</c:if>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button class="button button-primary" type="submit">Search</button>
|
<button class="button button-primary" type="submit">检索</button>
|
||||||
<a class="button button-secondary" href="${pageContext.request.contextPath}/admin/users">Clear</a>
|
<a class="button button-secondary" href="${pageContext.request.contextPath}/admin/users">清空</a>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="table-panel" aria-labelledby="user-results-title">
|
<section class="table-panel" aria-labelledby="user-results-title">
|
||||||
<h2 id="user-results-title">User accounts</h2>
|
<h2 id="user-results-title">用户账户</h2>
|
||||||
<c:choose>
|
<c:choose>
|
||||||
<c:when test="${empty users}">
|
<c:when test="${empty users}">
|
||||||
<p class="empty-state">No user accounts match the current filters.</p>
|
<p class="empty-state">没有符合当前筛选条件的用户账户。</p>
|
||||||
</c:when>
|
</c:when>
|
||||||
<c:otherwise>
|
<c:otherwise>
|
||||||
<div class="table-scroll">
|
<div class="table-scroll">
|
||||||
<table class="data-table user-table">
|
<table class="data-table user-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col">Username</th>
|
<th scope="col">用户名</th>
|
||||||
<th scope="col">Display name</th>
|
<th scope="col">显示名称</th>
|
||||||
<th scope="col">Role</th>
|
<th scope="col">角色</th>
|
||||||
<th scope="col">State</th>
|
<th scope="col">状态</th>
|
||||||
<th scope="col">Created</th>
|
<th scope="col">创建时间</th>
|
||||||
<th scope="col">Updated</th>
|
<th scope="col">更新时间</th>
|
||||||
<th scope="col">Actions</th>
|
<th scope="col">操作</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -110,17 +110,17 @@
|
|||||||
<td>
|
<td>
|
||||||
<div class="table-actions">
|
<div class="table-actions">
|
||||||
<a class="button button-secondary"
|
<a class="button button-secondary"
|
||||||
href="${pageContext.request.contextPath}/admin/users/edit?id=${account.id}">Edit</a>
|
href="${pageContext.request.contextPath}/admin/users/edit?id=${account.id}">编辑</a>
|
||||||
<c:choose>
|
<c:choose>
|
||||||
<c:when test="${account.id == sessionScope.authenticatedUser.id or not account.active}">
|
<c:when test="${account.id == sessionScope.authenticatedUser.id or not account.active}">
|
||||||
<button class="button button-secondary" type="button" disabled>Deactivate</button>
|
<button class="button button-secondary" type="button" disabled>停用</button>
|
||||||
</c:when>
|
</c:when>
|
||||||
<c:otherwise>
|
<c:otherwise>
|
||||||
<form action="${pageContext.request.contextPath}/admin/users/deactivate"
|
<form action="${pageContext.request.contextPath}/admin/users/deactivate"
|
||||||
method="post"
|
method="post"
|
||||||
onsubmit="return confirm('Deactivate this user account?');">
|
onsubmit="return confirm('确定停用这个用户账户吗?');">
|
||||||
<input type="hidden" name="id" value="${account.id}">
|
<input type="hidden" name="id" value="${account.id}">
|
||||||
<button class="button button-danger" type="submit">Deactivate</button>
|
<button class="button button-danger" type="submit">停用</button>
|
||||||
</form>
|
</form>
|
||||||
</c:otherwise>
|
</c:otherwise>
|
||||||
</c:choose>
|
</c:choose>
|
||||||
|
|||||||
@@ -2,11 +2,11 @@
|
|||||||
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
|
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
|
||||||
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
|
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="zh-CN">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>Login - MZH Library</title>
|
<title>登录 - MZH 图书馆</title>
|
||||||
<link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/app.css">
|
<link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/app.css">
|
||||||
</head>
|
</head>
|
||||||
<body class="auth-page">
|
<body class="auth-page">
|
||||||
@@ -14,8 +14,8 @@
|
|||||||
<main class="auth-shell">
|
<main class="auth-shell">
|
||||||
<section class="login-panel" aria-labelledby="login-title">
|
<section class="login-panel" aria-labelledby="login-title">
|
||||||
<div>
|
<div>
|
||||||
<p class="eyebrow">Library Management</p>
|
<p class="eyebrow">图书馆管理</p>
|
||||||
<h1 id="login-title">Sign in</h1>
|
<h1 id="login-title">登录</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<c:if test="${not empty errorMessage}">
|
<c:if test="${not empty errorMessage}">
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
|
|
||||||
<form class="login-form" action="${pageContext.request.contextPath}/login" method="post" novalidate>
|
<form class="login-form" action="${pageContext.request.contextPath}/login" method="post" novalidate>
|
||||||
<input type="hidden" name="redirect" value="${fn:escapeXml(redirect)}">
|
<input type="hidden" name="redirect" value="${fn:escapeXml(redirect)}">
|
||||||
<label for="username">Username</label>
|
<label for="username">用户名</label>
|
||||||
<input id="username"
|
<input id="username"
|
||||||
name="username"
|
name="username"
|
||||||
type="text"
|
type="text"
|
||||||
@@ -34,14 +34,14 @@
|
|||||||
autocomplete="username"
|
autocomplete="username"
|
||||||
required>
|
required>
|
||||||
|
|
||||||
<label for="password">Password</label>
|
<label for="password">密码</label>
|
||||||
<input id="password"
|
<input id="password"
|
||||||
name="password"
|
name="password"
|
||||||
type="password"
|
type="password"
|
||||||
autocomplete="current-password"
|
autocomplete="current-password"
|
||||||
required>
|
required>
|
||||||
|
|
||||||
<button class="button button-primary" type="submit">Sign in</button>
|
<button class="button button-primary" type="submit">登录</button>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -1,27 +1,27 @@
|
|||||||
<%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" %>
|
<%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" %>
|
||||||
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
|
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="zh-CN">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>Unauthorized - MZH Library</title>
|
<title>无权限 - MZH 图书馆</title>
|
||||||
<link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/app.css">
|
<link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/app.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<%@ include file="/WEB-INF/jsp/common/header.jspf" %>
|
<%@ include file="/WEB-INF/jsp/common/header.jspf" %>
|
||||||
<main class="page-shell">
|
<main class="page-shell">
|
||||||
<section class="notice-panel" aria-labelledby="unauthorized-title">
|
<section class="notice-panel" aria-labelledby="unauthorized-title">
|
||||||
<h1 id="unauthorized-title">Access denied</h1>
|
<h1 id="unauthorized-title">无权访问</h1>
|
||||||
<p>
|
<p>
|
||||||
<c:choose>
|
<c:choose>
|
||||||
<c:when test="${not empty errorMessage}">
|
<c:when test="${not empty errorMessage}">
|
||||||
<c:out value="${errorMessage}" />
|
<c:out value="${errorMessage}" />
|
||||||
</c:when>
|
</c:when>
|
||||||
<c:otherwise>You do not have permission to access this page.</c:otherwise>
|
<c:otherwise>您无权访问此页面。</c:otherwise>
|
||||||
</c:choose>
|
</c:choose>
|
||||||
</p>
|
</p>
|
||||||
<a class="button button-primary" href="${pageContext.request.contextPath}/dashboard">Back to dashboard</a>
|
<a class="button button-primary" href="${pageContext.request.contextPath}/dashboard">返回控制台</a>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -2,20 +2,20 @@
|
|||||||
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
|
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
|
||||||
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
|
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="zh-CN">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>Catalog - MZH Library</title>
|
<title>馆藏检索 - MZH 图书馆</title>
|
||||||
<link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/app.css">
|
<link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/app.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<%@ include file="/WEB-INF/jsp/common/header.jspf" %>
|
<%@ include file="/WEB-INF/jsp/common/header.jspf" %>
|
||||||
<main class="page-shell">
|
<main class="page-shell">
|
||||||
<section class="dashboard-hero catalog-hero" aria-labelledby="catalog-title">
|
<section class="dashboard-hero catalog-hero" aria-labelledby="catalog-title">
|
||||||
<p class="eyebrow">Catalog</p>
|
<p class="eyebrow">馆藏</p>
|
||||||
<h1 id="catalog-title">Book catalog</h1>
|
<h1 id="catalog-title">馆藏检索</h1>
|
||||||
<p>Search the library collection by identifier, title, author, or category.</p>
|
<p>按图书编号、书名、作者或分类检索馆藏。</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<c:if test="${not empty errorMessage}">
|
<c:if test="${not empty errorMessage}">
|
||||||
@@ -24,27 +24,27 @@
|
|||||||
</div>
|
</div>
|
||||||
</c:if>
|
</c:if>
|
||||||
|
|
||||||
<section class="toolbar-panel" aria-label="Catalog search">
|
<section class="toolbar-panel" aria-label="馆藏检索">
|
||||||
<form class="search-form" action="${pageContext.request.contextPath}/catalog" method="get">
|
<form class="search-form" action="${pageContext.request.contextPath}/catalog" method="get">
|
||||||
<div class="search-field">
|
<div class="search-field">
|
||||||
<label for="identifier">Book ID</label>
|
<label for="identifier">图书编号</label>
|
||||||
<input id="identifier" name="identifier" type="text" value="${fn:escapeXml(criteria.identifier)}">
|
<input id="identifier" name="identifier" type="text" value="${fn:escapeXml(criteria.identifier)}">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="search-field">
|
<div class="search-field">
|
||||||
<label for="title">Title</label>
|
<label for="title">书名</label>
|
||||||
<input id="title" name="title" type="text" value="${fn:escapeXml(criteria.title)}">
|
<input id="title" name="title" type="text" value="${fn:escapeXml(criteria.title)}">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="search-field">
|
<div class="search-field">
|
||||||
<label for="author">Author</label>
|
<label for="author">作者</label>
|
||||||
<input id="author" name="author" type="text" value="${fn:escapeXml(criteria.author)}">
|
<input id="author" name="author" type="text" value="${fn:escapeXml(criteria.author)}">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="search-field">
|
<div class="search-field">
|
||||||
<label for="categoryId">Category</label>
|
<label for="categoryId">分类</label>
|
||||||
<select id="categoryId" name="categoryId">
|
<select id="categoryId" name="categoryId">
|
||||||
<option value="">All categories</option>
|
<option value="">全部分类</option>
|
||||||
<c:forEach var="category" items="${categories}">
|
<c:forEach var="category" items="${categories}">
|
||||||
<option value="${category.id}" <c:if test="${criteria.categoryId == category.id}">selected</c:if>>
|
<option value="${category.id}" <c:if test="${criteria.categoryId == category.id}">selected</c:if>>
|
||||||
<c:out value="${category.name}" />
|
<c:out value="${category.name}" />
|
||||||
@@ -56,31 +56,31 @@
|
|||||||
</c:if>
|
</c:if>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button class="button button-primary" type="submit">Search</button>
|
<button class="button button-primary" type="submit">检索</button>
|
||||||
<a class="button button-secondary" href="${pageContext.request.contextPath}/catalog">Clear</a>
|
<a class="button button-secondary" href="${pageContext.request.contextPath}/catalog">清空</a>
|
||||||
<c:if test="${canManageBooks}">
|
<c:if test="${canManageBooks}">
|
||||||
<a class="button button-secondary" href="${pageContext.request.contextPath}/books">Manage books</a>
|
<a class="button button-secondary" href="${pageContext.request.contextPath}/books">管理图书</a>
|
||||||
</c:if>
|
</c:if>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="table-panel" aria-labelledby="catalog-results-title">
|
<section class="table-panel" aria-labelledby="catalog-results-title">
|
||||||
<h2 id="catalog-results-title">Results</h2>
|
<h2 id="catalog-results-title">检索结果</h2>
|
||||||
<c:choose>
|
<c:choose>
|
||||||
<c:when test="${empty books}">
|
<c:when test="${empty books}">
|
||||||
<p class="empty-state">No books match the current filters.</p>
|
<p class="empty-state">没有符合当前筛选条件的图书。</p>
|
||||||
</c:when>
|
</c:when>
|
||||||
<c:otherwise>
|
<c:otherwise>
|
||||||
<div class="table-scroll">
|
<div class="table-scroll">
|
||||||
<table class="data-table">
|
<table class="data-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col">Book ID</th>
|
<th scope="col">图书编号</th>
|
||||||
<th scope="col">Title</th>
|
<th scope="col">书名</th>
|
||||||
<th scope="col">Author</th>
|
<th scope="col">作者</th>
|
||||||
<th scope="col">Category</th>
|
<th scope="col">分类</th>
|
||||||
<th scope="col">Copies</th>
|
<th scope="col">馆藏数量</th>
|
||||||
<th scope="col">Status</th>
|
<th scope="col">状态</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
<%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" %>
|
||||||
|
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>分类管理 - MZH 图书馆</title>
|
||||||
|
<link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/app.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<%@ include file="/WEB-INF/jsp/common/header.jspf" %>
|
||||||
|
<main class="page-shell">
|
||||||
|
<section class="dashboard-hero catalog-hero" aria-labelledby="category-title">
|
||||||
|
<div>
|
||||||
|
<p class="eyebrow">图书管理</p>
|
||||||
|
<h1 id="category-title">管理分类</h1>
|
||||||
|
<p>维护图书记录和检索筛选使用的馆藏分组。</p>
|
||||||
|
</div>
|
||||||
|
<div class="hero-actions">
|
||||||
|
<a class="button button-primary" href="${pageContext.request.contextPath}/book-categories/new">新增分类</a>
|
||||||
|
<a class="button button-secondary" href="${pageContext.request.contextPath}/books">管理图书</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<c:if test="${not empty successMessage}">
|
||||||
|
<div class="message message-success" role="status">
|
||||||
|
<c:out value="${successMessage}" />
|
||||||
|
</div>
|
||||||
|
</c:if>
|
||||||
|
<c:if test="${not empty errorMessage}">
|
||||||
|
<div class="message message-error" role="alert">
|
||||||
|
<c:out value="${errorMessage}" />
|
||||||
|
</div>
|
||||||
|
</c:if>
|
||||||
|
|
||||||
|
<section class="table-panel" aria-labelledby="category-results-title">
|
||||||
|
<h2 id="category-results-title">分类记录</h2>
|
||||||
|
<c:choose>
|
||||||
|
<c:when test="${empty categories}">
|
||||||
|
<p class="empty-state">尚未创建分类。</p>
|
||||||
|
</c:when>
|
||||||
|
<c:otherwise>
|
||||||
|
<div class="table-scroll">
|
||||||
|
<table class="data-table category-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col">名称</th>
|
||||||
|
<th scope="col">说明</th>
|
||||||
|
<th scope="col">操作</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<c:forEach var="category" items="${categories}">
|
||||||
|
<tr>
|
||||||
|
<td><c:out value="${category.name}" /></td>
|
||||||
|
<td>
|
||||||
|
<c:choose>
|
||||||
|
<c:when test="${empty category.description}">
|
||||||
|
<span class="muted-text">无说明</span>
|
||||||
|
</c:when>
|
||||||
|
<c:otherwise>
|
||||||
|
<c:out value="${category.description}" />
|
||||||
|
</c:otherwise>
|
||||||
|
</c:choose>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="table-actions">
|
||||||
|
<a class="button button-secondary"
|
||||||
|
href="${pageContext.request.contextPath}/book-categories/edit?id=${category.id}">编辑</a>
|
||||||
|
<form action="${pageContext.request.contextPath}/book-categories/delete"
|
||||||
|
method="post"
|
||||||
|
onsubmit="return confirm('确定删除这个分类吗?');">
|
||||||
|
<input type="hidden" name="id" value="${category.id}">
|
||||||
|
<button class="button button-danger" type="submit">删除</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</c:forEach>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</c:otherwise>
|
||||||
|
</c:choose>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
<%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" %>
|
||||||
|
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
|
||||||
|
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title><c:out value="${formTitle}" /> - MZH 图书馆</title>
|
||||||
|
<link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/app.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<%@ include file="/WEB-INF/jsp/common/header.jspf" %>
|
||||||
|
<main class="page-shell">
|
||||||
|
<section class="form-panel" aria-labelledby="category-form-title">
|
||||||
|
<p class="eyebrow">图书管理</p>
|
||||||
|
<h1 id="category-form-title"><c:out value="${formTitle}" /></h1>
|
||||||
|
|
||||||
|
<c:if test="${not empty errorMessage}">
|
||||||
|
<div class="message message-error" role="alert">
|
||||||
|
<c:out value="${errorMessage}" />
|
||||||
|
</div>
|
||||||
|
</c:if>
|
||||||
|
|
||||||
|
<c:set var="hasFormValues" value="${not empty formValues}" />
|
||||||
|
<c:set var="nameValue" value="${hasFormValues ? formValues.name : category.name}" />
|
||||||
|
<c:set var="descriptionValue" value="${hasFormValues ? formValues.description : category.description}" />
|
||||||
|
|
||||||
|
<form class="category-form" action="${pageContext.request.contextPath}${formAction}" method="post" novalidate>
|
||||||
|
<c:if test="${category.id > 0}">
|
||||||
|
<input type="hidden" name="id" value="${category.id}">
|
||||||
|
</c:if>
|
||||||
|
|
||||||
|
<div class="form-grid">
|
||||||
|
<div class="form-field">
|
||||||
|
<label for="name">分类名称</label>
|
||||||
|
<input id="name" name="name" type="text" value="${fn:escapeXml(nameValue)}" required>
|
||||||
|
<c:if test="${not empty errors.name}">
|
||||||
|
<span class="field-error"><c:out value="${errors.name}" /></span>
|
||||||
|
</c:if>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-field form-field-wide">
|
||||||
|
<label for="description">说明</label>
|
||||||
|
<textarea id="description" name="description" rows="4">${fn:escapeXml(descriptionValue)}</textarea>
|
||||||
|
<c:if test="${not empty errors.description}">
|
||||||
|
<span class="field-error"><c:out value="${errors.description}" /></span>
|
||||||
|
</c:if>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<c:if test="${not empty errors.category}">
|
||||||
|
<div class="message message-error" role="alert">
|
||||||
|
<c:out value="${errors.category}" />
|
||||||
|
</div>
|
||||||
|
</c:if>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<button class="button button-primary" type="submit">保存</button>
|
||||||
|
<a class="button button-secondary" href="${pageContext.request.contextPath}/book-categories">取消</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -2,18 +2,18 @@
|
|||||||
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
|
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
|
||||||
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
|
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="zh-CN">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title><c:out value="${formTitle}" /> - MZH Library</title>
|
<title><c:out value="${formTitle}" /> - MZH 图书馆</title>
|
||||||
<link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/app.css">
|
<link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/app.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<%@ include file="/WEB-INF/jsp/common/header.jspf" %>
|
<%@ include file="/WEB-INF/jsp/common/header.jspf" %>
|
||||||
<main class="page-shell">
|
<main class="page-shell">
|
||||||
<section class="form-panel" aria-labelledby="book-form-title">
|
<section class="form-panel" aria-labelledby="book-form-title">
|
||||||
<p class="eyebrow">Book Management</p>
|
<p class="eyebrow">图书管理</p>
|
||||||
<h1 id="book-form-title"><c:out value="${formTitle}" /></h1>
|
<h1 id="book-form-title"><c:out value="${formTitle}" /></h1>
|
||||||
|
|
||||||
<c:if test="${not empty errorMessage}">
|
<c:if test="${not empty errorMessage}">
|
||||||
@@ -38,7 +38,7 @@
|
|||||||
|
|
||||||
<div class="form-grid">
|
<div class="form-grid">
|
||||||
<div class="form-field">
|
<div class="form-field">
|
||||||
<label for="identifier">Book ID</label>
|
<label for="identifier">图书编号</label>
|
||||||
<input id="identifier" name="identifier" type="text" value="${fn:escapeXml(identifierValue)}" required>
|
<input id="identifier" name="identifier" type="text" value="${fn:escapeXml(identifierValue)}" required>
|
||||||
<c:if test="${not empty errors.identifier}">
|
<c:if test="${not empty errors.identifier}">
|
||||||
<span class="field-error"><c:out value="${errors.identifier}" /></span>
|
<span class="field-error"><c:out value="${errors.identifier}" /></span>
|
||||||
@@ -46,7 +46,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-field">
|
<div class="form-field">
|
||||||
<label for="title">Title</label>
|
<label for="title">书名</label>
|
||||||
<input id="title" name="title" type="text" value="${fn:escapeXml(titleValue)}" required>
|
<input id="title" name="title" type="text" value="${fn:escapeXml(titleValue)}" required>
|
||||||
<c:if test="${not empty errors.title}">
|
<c:if test="${not empty errors.title}">
|
||||||
<span class="field-error"><c:out value="${errors.title}" /></span>
|
<span class="field-error"><c:out value="${errors.title}" /></span>
|
||||||
@@ -54,7 +54,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-field">
|
<div class="form-field">
|
||||||
<label for="author">Author</label>
|
<label for="author">作者</label>
|
||||||
<input id="author" name="author" type="text" value="${fn:escapeXml(authorValue)}" required>
|
<input id="author" name="author" type="text" value="${fn:escapeXml(authorValue)}" required>
|
||||||
<c:if test="${not empty errors.author}">
|
<c:if test="${not empty errors.author}">
|
||||||
<span class="field-error"><c:out value="${errors.author}" /></span>
|
<span class="field-error"><c:out value="${errors.author}" /></span>
|
||||||
@@ -62,9 +62,9 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-field">
|
<div class="form-field">
|
||||||
<label for="categoryId">Category</label>
|
<label for="categoryId">分类</label>
|
||||||
<select id="categoryId" name="categoryId" required>
|
<select id="categoryId" name="categoryId" required>
|
||||||
<option value="">Select category</option>
|
<option value="">请选择分类</option>
|
||||||
<c:forEach var="category" items="${categories}">
|
<c:forEach var="category" items="${categories}">
|
||||||
<option value="${category.id}" <c:if test="${categoryValue == category.id}">selected</c:if>>
|
<option value="${category.id}" <c:if test="${categoryValue == category.id}">selected</c:if>>
|
||||||
<c:out value="${category.name}" />
|
<c:out value="${category.name}" />
|
||||||
@@ -77,7 +77,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-field">
|
<div class="form-field">
|
||||||
<label for="totalCopies">Total copies</label>
|
<label for="totalCopies">馆藏总数</label>
|
||||||
<input id="totalCopies" name="totalCopies" type="number" min="0" value="${fn:escapeXml(totalCopiesValue)}" required>
|
<input id="totalCopies" name="totalCopies" type="number" min="0" value="${fn:escapeXml(totalCopiesValue)}" required>
|
||||||
<c:if test="${not empty errors.totalCopies}">
|
<c:if test="${not empty errors.totalCopies}">
|
||||||
<span class="field-error"><c:out value="${errors.totalCopies}" /></span>
|
<span class="field-error"><c:out value="${errors.totalCopies}" /></span>
|
||||||
@@ -85,7 +85,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-field">
|
<div class="form-field">
|
||||||
<label for="availableCopies">Available copies</label>
|
<label for="availableCopies">可借数量</label>
|
||||||
<input id="availableCopies" name="availableCopies" type="number" min="0" value="${fn:escapeXml(availableCopiesValue)}" required>
|
<input id="availableCopies" name="availableCopies" type="number" min="0" value="${fn:escapeXml(availableCopiesValue)}" required>
|
||||||
<c:if test="${not empty errors.availableCopies}">
|
<c:if test="${not empty errors.availableCopies}">
|
||||||
<span class="field-error"><c:out value="${errors.availableCopies}" /></span>
|
<span class="field-error"><c:out value="${errors.availableCopies}" /></span>
|
||||||
@@ -93,9 +93,9 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-field">
|
<div class="form-field">
|
||||||
<label for="status">Status</label>
|
<label for="status">状态</label>
|
||||||
<select id="status" name="status" required>
|
<select id="status" name="status" required>
|
||||||
<option value="">Select status</option>
|
<option value="">请选择状态</option>
|
||||||
<c:forEach var="status" items="${statuses}">
|
<c:forEach var="status" items="${statuses}">
|
||||||
<option value="${status.code}" <c:if test="${statusValue == status.code}">selected</c:if>>
|
<option value="${status.code}" <c:if test="${statusValue == status.code}">selected</c:if>>
|
||||||
<c:out value="${status.displayName}" />
|
<c:out value="${status.displayName}" />
|
||||||
@@ -109,8 +109,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
<button class="button button-primary" type="submit">Save</button>
|
<button class="button button-primary" type="submit">保存</button>
|
||||||
<a class="button button-secondary" href="${pageContext.request.contextPath}/books">Cancel</a>
|
<a class="button button-secondary" href="${pageContext.request.contextPath}/books">取消</a>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -2,21 +2,24 @@
|
|||||||
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
|
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
|
||||||
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
|
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="zh-CN">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>Manage Books - MZH Library</title>
|
<title>图书管理 - MZH 图书馆</title>
|
||||||
<link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/app.css">
|
<link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/app.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<%@ include file="/WEB-INF/jsp/common/header.jspf" %>
|
<%@ include file="/WEB-INF/jsp/common/header.jspf" %>
|
||||||
<main class="page-shell">
|
<main class="page-shell">
|
||||||
<section class="dashboard-hero catalog-hero" aria-labelledby="manage-title">
|
<section class="dashboard-hero catalog-hero" aria-labelledby="manage-title">
|
||||||
<p class="eyebrow">Book Management</p>
|
<p class="eyebrow">图书管理</p>
|
||||||
<h1 id="manage-title">Manage books</h1>
|
<h1 id="manage-title">管理图书</h1>
|
||||||
<p>Create, update, delete, and review inventory for catalog records.</p>
|
<p>创建、更新、删除和查看馆藏记录的库存信息。</p>
|
||||||
<a class="button button-primary" href="${pageContext.request.contextPath}/books/new">New book</a>
|
<div class="hero-actions">
|
||||||
|
<a class="button button-primary" href="${pageContext.request.contextPath}/books/new">新增图书</a>
|
||||||
|
<a class="button button-secondary" href="${pageContext.request.contextPath}/book-categories">分类</a>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<c:if test="${not empty successMessage}">
|
<c:if test="${not empty successMessage}">
|
||||||
@@ -30,27 +33,27 @@
|
|||||||
</div>
|
</div>
|
||||||
</c:if>
|
</c:if>
|
||||||
|
|
||||||
<section class="toolbar-panel" aria-label="Book management search">
|
<section class="toolbar-panel" aria-label="图书管理检索">
|
||||||
<form class="search-form" action="${pageContext.request.contextPath}/books" method="get">
|
<form class="search-form" action="${pageContext.request.contextPath}/books" method="get">
|
||||||
<div class="search-field">
|
<div class="search-field">
|
||||||
<label for="identifier">Book ID</label>
|
<label for="identifier">图书编号</label>
|
||||||
<input id="identifier" name="identifier" type="text" value="${fn:escapeXml(criteria.identifier)}">
|
<input id="identifier" name="identifier" type="text" value="${fn:escapeXml(criteria.identifier)}">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="search-field">
|
<div class="search-field">
|
||||||
<label for="title">Title</label>
|
<label for="title">书名</label>
|
||||||
<input id="title" name="title" type="text" value="${fn:escapeXml(criteria.title)}">
|
<input id="title" name="title" type="text" value="${fn:escapeXml(criteria.title)}">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="search-field">
|
<div class="search-field">
|
||||||
<label for="author">Author</label>
|
<label for="author">作者</label>
|
||||||
<input id="author" name="author" type="text" value="${fn:escapeXml(criteria.author)}">
|
<input id="author" name="author" type="text" value="${fn:escapeXml(criteria.author)}">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="search-field">
|
<div class="search-field">
|
||||||
<label for="categoryId">Category</label>
|
<label for="categoryId">分类</label>
|
||||||
<select id="categoryId" name="categoryId">
|
<select id="categoryId" name="categoryId">
|
||||||
<option value="">All categories</option>
|
<option value="">全部分类</option>
|
||||||
<c:forEach var="category" items="${categories}">
|
<c:forEach var="category" items="${categories}">
|
||||||
<option value="${category.id}" <c:if test="${criteria.categoryId == category.id}">selected</c:if>>
|
<option value="${category.id}" <c:if test="${criteria.categoryId == category.id}">selected</c:if>>
|
||||||
<c:out value="${category.name}" />
|
<c:out value="${category.name}" />
|
||||||
@@ -62,30 +65,31 @@
|
|||||||
</c:if>
|
</c:if>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button class="button button-primary" type="submit">Search</button>
|
<button class="button button-primary" type="submit">检索</button>
|
||||||
<a class="button button-secondary" href="${pageContext.request.contextPath}/books">Clear</a>
|
<a class="button button-secondary" href="${pageContext.request.contextPath}/books">清空</a>
|
||||||
<a class="button button-secondary" href="${pageContext.request.contextPath}/catalog">View catalog</a>
|
<a class="button button-secondary" href="${pageContext.request.contextPath}/catalog">查看馆藏</a>
|
||||||
|
<a class="button button-secondary" href="${pageContext.request.contextPath}/book-categories">分类</a>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="table-panel" aria-labelledby="management-results-title">
|
<section class="table-panel" aria-labelledby="management-results-title">
|
||||||
<h2 id="management-results-title">Book records</h2>
|
<h2 id="management-results-title">图书记录</h2>
|
||||||
<c:choose>
|
<c:choose>
|
||||||
<c:when test="${empty books}">
|
<c:when test="${empty books}">
|
||||||
<p class="empty-state">No book records match the current filters.</p>
|
<p class="empty-state">没有符合当前筛选条件的图书记录。</p>
|
||||||
</c:when>
|
</c:when>
|
||||||
<c:otherwise>
|
<c:otherwise>
|
||||||
<div class="table-scroll">
|
<div class="table-scroll">
|
||||||
<table class="data-table">
|
<table class="data-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col">Book ID</th>
|
<th scope="col">图书编号</th>
|
||||||
<th scope="col">Title</th>
|
<th scope="col">书名</th>
|
||||||
<th scope="col">Author</th>
|
<th scope="col">作者</th>
|
||||||
<th scope="col">Category</th>
|
<th scope="col">分类</th>
|
||||||
<th scope="col">Copies</th>
|
<th scope="col">馆藏数量</th>
|
||||||
<th scope="col">Status</th>
|
<th scope="col">状态</th>
|
||||||
<th scope="col">Actions</th>
|
<th scope="col">操作</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -104,12 +108,12 @@
|
|||||||
<td>
|
<td>
|
||||||
<div class="table-actions">
|
<div class="table-actions">
|
||||||
<a class="button button-secondary"
|
<a class="button button-secondary"
|
||||||
href="${pageContext.request.contextPath}/books/edit?id=${book.id}">Edit</a>
|
href="${pageContext.request.contextPath}/books/edit?id=${book.id}">编辑</a>
|
||||||
<form action="${pageContext.request.contextPath}/books/delete"
|
<form action="${pageContext.request.contextPath}/books/delete"
|
||||||
method="post"
|
method="post"
|
||||||
onsubmit="return confirm('Delete this book record?');">
|
onsubmit="return confirm('确定删除这条图书记录吗?');">
|
||||||
<input type="hidden" name="id" value="${book.id}">
|
<input type="hidden" name="id" value="${book.id}">
|
||||||
<button class="button button-danger" type="submit">Delete</button>
|
<button class="button button-danger" type="submit">删除</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -2,19 +2,19 @@
|
|||||||
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
|
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
|
||||||
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
|
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="zh-CN">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>New Borrow - MZH Library</title>
|
<title>新增借阅 - MZH 图书馆</title>
|
||||||
<link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/app.css">
|
<link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/app.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<%@ include file="/WEB-INF/jsp/common/header.jspf" %>
|
<%@ include file="/WEB-INF/jsp/common/header.jspf" %>
|
||||||
<main class="page-shell">
|
<main class="page-shell">
|
||||||
<section class="form-panel" aria-labelledby="borrow-form-title">
|
<section class="form-panel" aria-labelledby="borrow-form-title">
|
||||||
<p class="eyebrow">Borrowing Management</p>
|
<p class="eyebrow">借阅管理</p>
|
||||||
<h1 id="borrow-form-title">New borrow</h1>
|
<h1 id="borrow-form-title">新增借阅</h1>
|
||||||
|
|
||||||
<c:if test="${not empty errorMessage}">
|
<c:if test="${not empty errorMessage}">
|
||||||
<div class="message message-error" role="alert">
|
<div class="message message-error" role="alert">
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
<form class="borrow-form" action="${pageContext.request.contextPath}/borrowing/create" method="post" novalidate>
|
<form class="borrow-form" action="${pageContext.request.contextPath}/borrowing/create" method="post" novalidate>
|
||||||
<div class="form-grid">
|
<div class="form-grid">
|
||||||
<div class="form-field">
|
<div class="form-field">
|
||||||
<label for="readerIdentifier">Reader ID</label>
|
<label for="readerIdentifier">读者编号</label>
|
||||||
<input id="readerIdentifier" name="readerIdentifier" type="text"
|
<input id="readerIdentifier" name="readerIdentifier" type="text"
|
||||||
value="${fn:escapeXml(readerIdentifierValue)}" required>
|
value="${fn:escapeXml(readerIdentifierValue)}" required>
|
||||||
<c:if test="${not empty errors.readerIdentifier}">
|
<c:if test="${not empty errors.readerIdentifier}">
|
||||||
@@ -37,7 +37,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-field">
|
<div class="form-field">
|
||||||
<label for="bookIdentifier">Book ID</label>
|
<label for="bookIdentifier">图书编号</label>
|
||||||
<input id="bookIdentifier" name="bookIdentifier" type="text"
|
<input id="bookIdentifier" name="bookIdentifier" type="text"
|
||||||
value="${fn:escapeXml(bookIdentifierValue)}" required>
|
value="${fn:escapeXml(bookIdentifierValue)}" required>
|
||||||
<c:if test="${not empty errors.bookIdentifier}">
|
<c:if test="${not empty errors.bookIdentifier}">
|
||||||
@@ -47,8 +47,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
<button class="button button-primary" type="submit">Borrow</button>
|
<button class="button button-primary" type="submit">借出</button>
|
||||||
<a class="button button-secondary" href="${pageContext.request.contextPath}/borrowing">Cancel</a>
|
<a class="button button-secondary" href="${pageContext.request.contextPath}/borrowing">取消</a>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -2,11 +2,11 @@
|
|||||||
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
|
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
|
||||||
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
|
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="zh-CN">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>Borrowing Management - MZH Library</title>
|
<title>借阅管理 - MZH 图书馆</title>
|
||||||
<link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/app.css">
|
<link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/app.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -14,11 +14,11 @@
|
|||||||
<main class="page-shell">
|
<main class="page-shell">
|
||||||
<section class="dashboard-hero catalog-hero" aria-labelledby="borrowing-title">
|
<section class="dashboard-hero catalog-hero" aria-labelledby="borrowing-title">
|
||||||
<div>
|
<div>
|
||||||
<p class="eyebrow">Borrowing Management</p>
|
<p class="eyebrow">借阅管理</p>
|
||||||
<h1 id="borrowing-title">Manage borrowing</h1>
|
<h1 id="borrowing-title">管理借阅</h1>
|
||||||
<p>Create borrow records, process returns, renew active loans, and review overdue items.</p>
|
<p>创建借阅记录、处理归还、续借有效借阅并查看逾期项目。</p>
|
||||||
</div>
|
</div>
|
||||||
<a class="button button-primary" href="${pageContext.request.contextPath}/borrowing/new">New borrow</a>
|
<a class="button button-primary" href="${pageContext.request.contextPath}/borrowing/new">新增借阅</a>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<c:if test="${not empty successMessage}">
|
<c:if test="${not empty successMessage}">
|
||||||
@@ -32,31 +32,31 @@
|
|||||||
</div>
|
</div>
|
||||||
</c:if>
|
</c:if>
|
||||||
|
|
||||||
<section class="toolbar-panel" aria-label="Borrowing search">
|
<section class="toolbar-panel" aria-label="借阅检索">
|
||||||
<form class="search-form borrowing-search-form" action="${pageContext.request.contextPath}/borrowing" method="get">
|
<form class="search-form borrowing-search-form" action="${pageContext.request.contextPath}/borrowing" method="get">
|
||||||
<div class="search-field">
|
<div class="search-field">
|
||||||
<label for="readerIdentifier">Reader ID</label>
|
<label for="readerIdentifier">读者编号</label>
|
||||||
<input id="readerIdentifier" name="readerIdentifier" type="text"
|
<input id="readerIdentifier" name="readerIdentifier" type="text"
|
||||||
value="${fn:escapeXml(criteria.readerIdentifier)}">
|
value="${fn:escapeXml(criteria.readerIdentifier)}">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="search-field">
|
<div class="search-field">
|
||||||
<label for="bookIdentifier">Book ID</label>
|
<label for="bookIdentifier">图书编号</label>
|
||||||
<input id="bookIdentifier" name="bookIdentifier" type="text"
|
<input id="bookIdentifier" name="bookIdentifier" type="text"
|
||||||
value="${fn:escapeXml(criteria.bookIdentifier)}">
|
value="${fn:escapeXml(criteria.bookIdentifier)}">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="search-field">
|
<div class="search-field">
|
||||||
<label for="status">Status</label>
|
<label for="status">状态</label>
|
||||||
<select id="status" name="status">
|
<select id="status" name="status">
|
||||||
<option value="">All statuses</option>
|
<option value="">全部状态</option>
|
||||||
<c:forEach var="status" items="${statuses}">
|
<c:forEach var="status" items="${statuses}">
|
||||||
<option value="${status.code}" <c:if test="${criteria.statusCode == status.code}">selected</c:if>>
|
<option value="${status.code}" <c:if test="${criteria.statusCode == status.code}">selected</c:if>>
|
||||||
<c:out value="${status.displayName}" />
|
<c:out value="${status.displayName}" />
|
||||||
</option>
|
</option>
|
||||||
</c:forEach>
|
</c:forEach>
|
||||||
<option value="${overdueStatus}" <c:if test="${criteria.statusCode == overdueStatus}">selected</c:if>>
|
<option value="${overdueStatus}" <c:if test="${criteria.statusCode == overdueStatus}">selected</c:if>>
|
||||||
Overdue
|
逾期
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
<c:if test="${not empty errors.status}">
|
<c:if test="${not empty errors.status}">
|
||||||
@@ -64,30 +64,30 @@
|
|||||||
</c:if>
|
</c:if>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button class="button button-primary" type="submit">Search</button>
|
<button class="button button-primary" type="submit">检索</button>
|
||||||
<a class="button button-secondary" href="${pageContext.request.contextPath}/borrowing">Clear</a>
|
<a class="button button-secondary" href="${pageContext.request.contextPath}/borrowing">清空</a>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="table-panel" aria-labelledby="borrowing-results-title">
|
<section class="table-panel" aria-labelledby="borrowing-results-title">
|
||||||
<h2 id="borrowing-results-title">Borrowing records</h2>
|
<h2 id="borrowing-results-title">借阅记录</h2>
|
||||||
<c:choose>
|
<c:choose>
|
||||||
<c:when test="${empty borrowRecords}">
|
<c:when test="${empty borrowRecords}">
|
||||||
<p class="empty-state">No borrowing records match the current filters.</p>
|
<p class="empty-state">没有符合当前筛选条件的借阅记录。</p>
|
||||||
</c:when>
|
</c:when>
|
||||||
<c:otherwise>
|
<c:otherwise>
|
||||||
<div class="table-scroll">
|
<div class="table-scroll">
|
||||||
<table class="data-table borrowing-table">
|
<table class="data-table borrowing-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col">Reader</th>
|
<th scope="col">读者</th>
|
||||||
<th scope="col">Book</th>
|
<th scope="col">图书</th>
|
||||||
<th scope="col">Borrowed</th>
|
<th scope="col">借出时间</th>
|
||||||
<th scope="col">Due</th>
|
<th scope="col">应还时间</th>
|
||||||
<th scope="col">Returned</th>
|
<th scope="col">归还时间</th>
|
||||||
<th scope="col">Renewals</th>
|
<th scope="col">续借次数</th>
|
||||||
<th scope="col">Status</th>
|
<th scope="col">状态</th>
|
||||||
<th scope="col">Actions</th>
|
<th scope="col">操作</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -108,7 +108,7 @@
|
|||||||
<c:when test="${not empty record.returnedAtText}">
|
<c:when test="${not empty record.returnedAtText}">
|
||||||
<c:out value="${record.returnedAtText}" />
|
<c:out value="${record.returnedAtText}" />
|
||||||
</c:when>
|
</c:when>
|
||||||
<c:otherwise>Not returned</c:otherwise>
|
<c:otherwise>未归还</c:otherwise>
|
||||||
</c:choose>
|
</c:choose>
|
||||||
</td>
|
</td>
|
||||||
<td><c:out value="${record.renewalCount}" /> / <c:out value="${maxRenewals}" /></td>
|
<td><c:out value="${record.renewalCount}" /> / <c:out value="${maxRenewals}" /></td>
|
||||||
@@ -123,22 +123,22 @@
|
|||||||
<div class="table-actions">
|
<div class="table-actions">
|
||||||
<form action="${pageContext.request.contextPath}/borrowing/return"
|
<form action="${pageContext.request.contextPath}/borrowing/return"
|
||||||
method="post"
|
method="post"
|
||||||
onsubmit="return confirm('Return this book?');">
|
onsubmit="return confirm('确定归还这本书吗?');">
|
||||||
<input type="hidden" name="id" value="${record.id}">
|
<input type="hidden" name="id" value="${record.id}">
|
||||||
<button class="button button-secondary" type="submit">Return</button>
|
<button class="button button-secondary" type="submit">归还</button>
|
||||||
</form>
|
</form>
|
||||||
<c:if test="${record.renewalCount < maxRenewals}">
|
<c:if test="${record.renewalCount < maxRenewals}">
|
||||||
<form action="${pageContext.request.contextPath}/borrowing/renew"
|
<form action="${pageContext.request.contextPath}/borrowing/renew"
|
||||||
method="post"
|
method="post"
|
||||||
onsubmit="return confirm('Renew this loan?');">
|
onsubmit="return confirm('确定续借这条记录吗?');">
|
||||||
<input type="hidden" name="id" value="${record.id}">
|
<input type="hidden" name="id" value="${record.id}">
|
||||||
<button class="button button-secondary" type="submit">Renew</button>
|
<button class="button button-secondary" type="submit">续借</button>
|
||||||
</form>
|
</form>
|
||||||
</c:if>
|
</c:if>
|
||||||
</div>
|
</div>
|
||||||
</c:when>
|
</c:when>
|
||||||
<c:otherwise>
|
<c:otherwise>
|
||||||
<span class="muted-text">Complete</span>
|
<span class="muted-text">已完成</span>
|
||||||
</c:otherwise>
|
</c:otherwise>
|
||||||
</c:choose>
|
</c:choose>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -1,30 +1,31 @@
|
|||||||
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
|
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
|
||||||
<header class="app-header">
|
<header class="app-header">
|
||||||
<a class="brand" href="${pageContext.request.contextPath}/dashboard">MZH Library</a>
|
<a class="brand" href="${pageContext.request.contextPath}/dashboard">MZH 图书馆</a>
|
||||||
<c:if test="${not empty sessionScope.authenticatedUser}">
|
<c:if test="${not empty sessionScope.authenticatedUser}">
|
||||||
<nav class="top-nav" aria-label="Primary">
|
<nav class="top-nav" aria-label="主导航">
|
||||||
<a href="${pageContext.request.contextPath}/dashboard">Dashboard</a>
|
<a href="${pageContext.request.contextPath}/dashboard">控制台</a>
|
||||||
<a href="${pageContext.request.contextPath}/catalog">Catalog</a>
|
<a href="${pageContext.request.contextPath}/catalog">馆藏检索</a>
|
||||||
<c:if test="${sessionScope.userRole == 'administrator'}">
|
<c:if test="${sessionScope.userRole == 'administrator'}">
|
||||||
<a href="${pageContext.request.contextPath}/admin/home">Admin</a>
|
<a href="${pageContext.request.contextPath}/admin/home">管理</a>
|
||||||
<a href="${pageContext.request.contextPath}/admin/users">Users</a>
|
<a href="${pageContext.request.contextPath}/admin/users">用户</a>
|
||||||
<a href="${pageContext.request.contextPath}/admin/system-logs">Logs</a>
|
<a href="${pageContext.request.contextPath}/admin/system-logs">日志</a>
|
||||||
</c:if>
|
</c:if>
|
||||||
<c:if test="${sessionScope.userRole == 'administrator' or sessionScope.userRole == 'librarian'}">
|
<c:if test="${sessionScope.userRole == 'administrator' or sessionScope.userRole == 'librarian'}">
|
||||||
<a href="${pageContext.request.contextPath}/librarian/home">Librarian</a>
|
<a href="${pageContext.request.contextPath}/librarian/home">馆员</a>
|
||||||
<a href="${pageContext.request.contextPath}/books">Books</a>
|
<a href="${pageContext.request.contextPath}/books">图书</a>
|
||||||
<a href="${pageContext.request.contextPath}/readers">Readers</a>
|
<a href="${pageContext.request.contextPath}/book-categories">分类</a>
|
||||||
<a href="${pageContext.request.contextPath}/borrowing">Borrowing</a>
|
<a href="${pageContext.request.contextPath}/readers">读者</a>
|
||||||
<a href="${pageContext.request.contextPath}/reports">Reports</a>
|
<a href="${pageContext.request.contextPath}/borrowing">借阅</a>
|
||||||
|
<a href="${pageContext.request.contextPath}/reports">报表</a>
|
||||||
</c:if>
|
</c:if>
|
||||||
<a href="${pageContext.request.contextPath}/reader/home">Reader</a>
|
<a href="${pageContext.request.contextPath}/reader/home">读者中心</a>
|
||||||
<c:if test="${sessionScope.userRole == 'reader'}">
|
<c:if test="${sessionScope.userRole == 'reader'}">
|
||||||
<a href="${pageContext.request.contextPath}/reader/loans">My Loans</a>
|
<a href="${pageContext.request.contextPath}/reader/loans">我的借阅</a>
|
||||||
</c:if>
|
</c:if>
|
||||||
<span class="user-pill">
|
<span class="user-pill">
|
||||||
<c:out value="${sessionScope.authenticatedUser.displayName}" />
|
<c:out value="${sessionScope.authenticatedUser.displayName}" />
|
||||||
</span>
|
</span>
|
||||||
<a class="button button-secondary" href="${pageContext.request.contextPath}/logout">Logout</a>
|
<a class="button button-secondary" href="${pageContext.request.contextPath}/logout">退出</a>
|
||||||
</nav>
|
</nav>
|
||||||
</c:if>
|
</c:if>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" %>
|
<%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" %>
|
||||||
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
|
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="zh-CN">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>Dashboard - MZH Library</title>
|
<title>控制台 - MZH 图书馆</title>
|
||||||
<link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/app.css">
|
<link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/app.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -15,80 +15,86 @@
|
|||||||
<p class="eyebrow">
|
<p class="eyebrow">
|
||||||
<c:out value="${sessionScope.authenticatedUser.role.displayName}" />
|
<c:out value="${sessionScope.authenticatedUser.role.displayName}" />
|
||||||
</p>
|
</p>
|
||||||
<h1 id="dashboard-title">Dashboard</h1>
|
<h1 id="dashboard-title">控制台</h1>
|
||||||
<p>Signed in as <strong><c:out value="${sessionScope.authenticatedUser.displayName}" /></strong>.</p>
|
<p>当前登录:<strong><c:out value="${sessionScope.authenticatedUser.displayName}" /></strong></p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="card-grid" aria-label="Role workspaces">
|
<section class="card-grid" aria-label="角色工作区">
|
||||||
<c:if test="${sessionScope.userRole == 'administrator'}">
|
<c:if test="${sessionScope.userRole == 'administrator'}">
|
||||||
<article class="workspace-card">
|
<article class="workspace-card">
|
||||||
<h2>Administration</h2>
|
<h2>系统管理</h2>
|
||||||
<p>Account, role, permission, and system-maintenance entry point.</p>
|
<p>账户、角色、权限和系统维护入口。</p>
|
||||||
<a class="button button-secondary" href="${pageContext.request.contextPath}/admin/home">Open</a>
|
<a class="button button-secondary" href="${pageContext.request.contextPath}/admin/home">打开</a>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<article class="workspace-card">
|
<article class="workspace-card">
|
||||||
<h2>User Management</h2>
|
<h2>用户管理</h2>
|
||||||
<p>Create, update, deactivate, and review login accounts.</p>
|
<p>创建、更新、停用和查看登录账户。</p>
|
||||||
<a class="button button-secondary" href="${pageContext.request.contextPath}/admin/users">Open</a>
|
<a class="button button-secondary" href="${pageContext.request.contextPath}/admin/users">打开</a>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<article class="workspace-card">
|
<article class="workspace-card">
|
||||||
<h2>System Logs</h2>
|
<h2>系统日志</h2>
|
||||||
<p>Review read-only audit entries for account and maintenance actions.</p>
|
<p>查看账户与维护操作的只读审计记录。</p>
|
||||||
<a class="button button-secondary" href="${pageContext.request.contextPath}/admin/system-logs">Open</a>
|
<a class="button button-secondary" href="${pageContext.request.contextPath}/admin/system-logs">打开</a>
|
||||||
</article>
|
</article>
|
||||||
</c:if>
|
</c:if>
|
||||||
|
|
||||||
<c:if test="${sessionScope.userRole == 'administrator' or sessionScope.userRole == 'librarian'}">
|
<c:if test="${sessionScope.userRole == 'administrator' or sessionScope.userRole == 'librarian'}">
|
||||||
<article class="workspace-card">
|
<article class="workspace-card">
|
||||||
<h2>Librarian Workspace</h2>
|
<h2>馆员工作台</h2>
|
||||||
<p>Book, reader, borrowing, return, renewal, and overdue entry point.</p>
|
<p>图书、读者、借阅、归还、续借和逾期处理入口。</p>
|
||||||
<a class="button button-secondary" href="${pageContext.request.contextPath}/librarian/home">Open</a>
|
<a class="button button-secondary" href="${pageContext.request.contextPath}/librarian/home">打开</a>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<article class="workspace-card">
|
<article class="workspace-card">
|
||||||
<h2>Book Management</h2>
|
<h2>图书管理</h2>
|
||||||
<p>Create, update, delete, and review book inventory records.</p>
|
<p>创建、更新、删除和查看图书库存记录。</p>
|
||||||
<a class="button button-secondary" href="${pageContext.request.contextPath}/books">Open</a>
|
<a class="button button-secondary" href="${pageContext.request.contextPath}/books">打开</a>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<article class="workspace-card">
|
<article class="workspace-card">
|
||||||
<h2>Reader Management</h2>
|
<h2>分类维护</h2>
|
||||||
<p>Create, update, deactivate, and review reader eligibility records.</p>
|
<p>维护图书记录和检索筛选使用的馆藏分类。</p>
|
||||||
<a class="button button-secondary" href="${pageContext.request.contextPath}/readers">Open</a>
|
<a class="button button-secondary" href="${pageContext.request.contextPath}/book-categories">打开</a>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<article class="workspace-card">
|
<article class="workspace-card">
|
||||||
<h2>Borrowing Management</h2>
|
<h2>读者管理</h2>
|
||||||
<p>Create loans, process returns, renew active records, and review overdue items.</p>
|
<p>创建、更新、停用和查看读者借阅资格记录。</p>
|
||||||
<a class="button button-secondary" href="${pageContext.request.contextPath}/borrowing">Open</a>
|
<a class="button button-secondary" href="${pageContext.request.contextPath}/readers">打开</a>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<article class="workspace-card">
|
<article class="workspace-card">
|
||||||
<h2>Report Center</h2>
|
<h2>借阅管理</h2>
|
||||||
<p>Review inventory health, borrowing counts, overdue records, and popular books.</p>
|
<p>创建借阅、处理归还、续借有效记录并查看逾期项目。</p>
|
||||||
<a class="button button-secondary" href="${pageContext.request.contextPath}/reports">Open</a>
|
<a class="button button-secondary" href="${pageContext.request.contextPath}/borrowing">打开</a>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="workspace-card">
|
||||||
|
<h2>报表中心</h2>
|
||||||
|
<p>查看库存状况、借阅统计、逾期记录和热门图书。</p>
|
||||||
|
<a class="button button-secondary" href="${pageContext.request.contextPath}/reports">打开</a>
|
||||||
</article>
|
</article>
|
||||||
</c:if>
|
</c:if>
|
||||||
|
|
||||||
<article class="workspace-card">
|
<article class="workspace-card">
|
||||||
<h2>Book Catalog</h2>
|
<h2>馆藏检索</h2>
|
||||||
<p>Search books by title, author, category, or book identifier.</p>
|
<p>按书名、作者、分类或图书编号检索图书。</p>
|
||||||
<a class="button button-secondary" href="${pageContext.request.contextPath}/catalog">Search</a>
|
<a class="button button-secondary" href="${pageContext.request.contextPath}/catalog">检索</a>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<article class="workspace-card">
|
<article class="workspace-card">
|
||||||
<h2>Reader Center</h2>
|
<h2>读者中心</h2>
|
||||||
<p>Reader self-service entry point for catalog access and loan history.</p>
|
<p>读者自助访问馆藏和借阅历史的入口。</p>
|
||||||
<a class="button button-secondary" href="${pageContext.request.contextPath}/reader/home">Open</a>
|
<a class="button button-secondary" href="${pageContext.request.contextPath}/reader/home">打开</a>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<c:if test="${sessionScope.userRole == 'reader'}">
|
<c:if test="${sessionScope.userRole == 'reader'}">
|
||||||
<article class="workspace-card">
|
<article class="workspace-card">
|
||||||
<h2>My Loan History</h2>
|
<h2>我的借阅历史</h2>
|
||||||
<p>Review your active, returned, and overdue borrowing records.</p>
|
<p>查看您的在借、已还和逾期借阅记录。</p>
|
||||||
<a class="button button-secondary" href="${pageContext.request.contextPath}/reader/loans">Open</a>
|
<a class="button button-secondary" href="${pageContext.request.contextPath}/reader/loans">打开</a>
|
||||||
</article>
|
</article>
|
||||||
</c:if>
|
</c:if>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -2,11 +2,11 @@
|
|||||||
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
|
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
|
||||||
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
|
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="zh-CN">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>System Logs - MZH Library</title>
|
<title>系统日志 - MZH 图书馆</title>
|
||||||
<link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/app.css">
|
<link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/app.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -14,9 +14,9 @@
|
|||||||
<main class="page-shell">
|
<main class="page-shell">
|
||||||
<section class="dashboard-hero catalog-hero" aria-labelledby="system-logs-title">
|
<section class="dashboard-hero catalog-hero" aria-labelledby="system-logs-title">
|
||||||
<div>
|
<div>
|
||||||
<p class="eyebrow">System Maintenance</p>
|
<p class="eyebrow">系统维护</p>
|
||||||
<h1 id="system-logs-title">System logs</h1>
|
<h1 id="system-logs-title">系统日志</h1>
|
||||||
<p>Review administrative account changes and maintenance audit records.</p>
|
<p>查看管理账户变更和维护审计记录。</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -26,17 +26,22 @@
|
|||||||
</div>
|
</div>
|
||||||
</c:if>
|
</c:if>
|
||||||
|
|
||||||
<section class="toolbar-panel" aria-label="System log search">
|
<section class="toolbar-panel" aria-label="系统日志检索">
|
||||||
<form class="search-form system-log-search-form"
|
<form class="search-form system-log-search-form"
|
||||||
action="${pageContext.request.contextPath}/admin/system-logs" method="get">
|
action="${pageContext.request.contextPath}/admin/system-logs" method="get">
|
||||||
<div class="search-field">
|
<div class="search-field">
|
||||||
<label for="operationType">Operation</label>
|
<label for="operationType">操作</label>
|
||||||
<select id="operationType" name="operationType">
|
<select id="operationType" name="operationType">
|
||||||
<option value="">All operations</option>
|
<option value="">全部操作</option>
|
||||||
<c:forEach var="operationType" items="${operationTypes}">
|
<c:forEach var="operationType" items="${operationTypes}">
|
||||||
<option value="${fn:escapeXml(operationType)}"
|
<option value="${fn:escapeXml(operationType)}"
|
||||||
<c:if test="${criteria.operationType == operationType}">selected</c:if>>
|
<c:if test="${criteria.operationType == operationType}">selected</c:if>>
|
||||||
<c:out value="${operationType}" />
|
<c:choose>
|
||||||
|
<c:when test="${operationType == 'user.create'}">创建用户</c:when>
|
||||||
|
<c:when test="${operationType == 'user.update'}">更新用户</c:when>
|
||||||
|
<c:when test="${operationType == 'user.deactivate'}">停用用户</c:when>
|
||||||
|
<c:otherwise><c:out value="${operationType}" /></c:otherwise>
|
||||||
|
</c:choose>
|
||||||
</option>
|
</option>
|
||||||
</c:forEach>
|
</c:forEach>
|
||||||
<c:if test="${not empty criteria.operationType and empty operationTypes}">
|
<c:if test="${not empty criteria.operationType and empty operationTypes}">
|
||||||
@@ -51,7 +56,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="search-field">
|
<div class="search-field">
|
||||||
<label for="keyword">Keyword</label>
|
<label for="keyword">关键词</label>
|
||||||
<input id="keyword" name="keyword" type="text" value="${fn:escapeXml(criteria.keyword)}">
|
<input id="keyword" name="keyword" type="text" value="${fn:escapeXml(criteria.keyword)}">
|
||||||
<c:if test="${not empty errors.keyword}">
|
<c:if test="${not empty errors.keyword}">
|
||||||
<span class="field-error"><c:out value="${errors.keyword}" /></span>
|
<span class="field-error"><c:out value="${errors.keyword}" /></span>
|
||||||
@@ -59,7 +64,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="search-field">
|
<div class="search-field">
|
||||||
<label for="createdFrom">From</label>
|
<label for="createdFrom">开始日期</label>
|
||||||
<input id="createdFrom" name="createdFrom" type="date"
|
<input id="createdFrom" name="createdFrom" type="date"
|
||||||
value="${fn:escapeXml(criteria.createdFromText)}">
|
value="${fn:escapeXml(criteria.createdFromText)}">
|
||||||
<c:if test="${not empty errors.createdFrom}">
|
<c:if test="${not empty errors.createdFrom}">
|
||||||
@@ -68,7 +73,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="search-field">
|
<div class="search-field">
|
||||||
<label for="createdTo">To</label>
|
<label for="createdTo">结束日期</label>
|
||||||
<input id="createdTo" name="createdTo" type="date"
|
<input id="createdTo" name="createdTo" type="date"
|
||||||
value="${fn:escapeXml(criteria.createdToText)}">
|
value="${fn:escapeXml(criteria.createdToText)}">
|
||||||
<c:if test="${not empty errors.createdTo}">
|
<c:if test="${not empty errors.createdTo}">
|
||||||
@@ -76,29 +81,29 @@
|
|||||||
</c:if>
|
</c:if>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button class="button button-primary" type="submit">Search</button>
|
<button class="button button-primary" type="submit">检索</button>
|
||||||
<a class="button button-secondary" href="${pageContext.request.contextPath}/admin/system-logs">Clear</a>
|
<a class="button button-secondary" href="${pageContext.request.contextPath}/admin/system-logs">清空</a>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="table-panel" aria-labelledby="system-log-results-title">
|
<section class="table-panel" aria-labelledby="system-log-results-title">
|
||||||
<h2 id="system-log-results-title">Log entries</h2>
|
<h2 id="system-log-results-title">日志记录</h2>
|
||||||
<c:choose>
|
<c:choose>
|
||||||
<c:when test="${empty logs}">
|
<c:when test="${empty logs}">
|
||||||
<p class="empty-state">No system logs match the current filters.</p>
|
<p class="empty-state">没有符合当前筛选条件的系统日志。</p>
|
||||||
</c:when>
|
</c:when>
|
||||||
<c:otherwise>
|
<c:otherwise>
|
||||||
<div class="table-scroll">
|
<div class="table-scroll">
|
||||||
<table class="data-table system-log-table">
|
<table class="data-table system-log-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col">Time</th>
|
<th scope="col">时间</th>
|
||||||
<th scope="col">Operator</th>
|
<th scope="col">操作人</th>
|
||||||
<th scope="col">Operation</th>
|
<th scope="col">操作</th>
|
||||||
<th scope="col">Target</th>
|
<th scope="col">目标</th>
|
||||||
<th scope="col">Result</th>
|
<th scope="col">结果</th>
|
||||||
<th scope="col">IP address</th>
|
<th scope="col">IP 地址</th>
|
||||||
<th scope="col">Detail</th>
|
<th scope="col">详情</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -111,9 +116,16 @@
|
|||||||
<div class="muted-text"><c:out value="${log.operatorMetaText}" /></div>
|
<div class="muted-text"><c:out value="${log.operatorMetaText}" /></div>
|
||||||
</c:if>
|
</c:if>
|
||||||
</td>
|
</td>
|
||||||
<td><c:out value="${log.operationType}" /></td>
|
|
||||||
<td>
|
<td>
|
||||||
<c:out value="${log.targetTable}" />
|
<c:choose>
|
||||||
|
<c:when test="${log.operationType == 'user.create'}">创建用户</c:when>
|
||||||
|
<c:when test="${log.operationType == 'user.update'}">更新用户</c:when>
|
||||||
|
<c:when test="${log.operationType == 'user.deactivate'}">停用用户</c:when>
|
||||||
|
<c:otherwise><c:out value="${log.operationType}" /></c:otherwise>
|
||||||
|
</c:choose>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<c:out value="${log.targetTableName}" />
|
||||||
<c:if test="${not empty log.targetId}">
|
<c:if test="${not empty log.targetId}">
|
||||||
#<c:out value="${log.targetId}" />
|
#<c:out value="${log.targetId}" />
|
||||||
</c:if>
|
</c:if>
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" %>
|
<%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" %>
|
||||||
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
|
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="zh-CN">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>Loan History - MZH Library</title>
|
<title>借阅历史 - MZH 图书馆</title>
|
||||||
<link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/app.css">
|
<link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/app.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -13,11 +13,11 @@
|
|||||||
<main class="page-shell">
|
<main class="page-shell">
|
||||||
<section class="dashboard-hero catalog-hero" aria-labelledby="loan-history-title">
|
<section class="dashboard-hero catalog-hero" aria-labelledby="loan-history-title">
|
||||||
<div>
|
<div>
|
||||||
<p class="eyebrow">Reader Center</p>
|
<p class="eyebrow">读者中心</p>
|
||||||
<h1 id="loan-history-title">Loan history</h1>
|
<h1 id="loan-history-title">借阅历史</h1>
|
||||||
<p>Review your active, returned, and overdue borrowing records.</p>
|
<p>查看您的在借、已还和逾期借阅记录。</p>
|
||||||
</div>
|
</div>
|
||||||
<a class="button button-secondary" href="${pageContext.request.contextPath}/catalog">Search catalog</a>
|
<a class="button button-secondary" href="${pageContext.request.contextPath}/catalog">检索馆藏</a>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<c:if test="${not empty successMessage}">
|
<c:if test="${not empty successMessage}">
|
||||||
@@ -32,23 +32,23 @@
|
|||||||
</c:if>
|
</c:if>
|
||||||
|
|
||||||
<section class="table-panel" aria-labelledby="loan-results-title">
|
<section class="table-panel" aria-labelledby="loan-results-title">
|
||||||
<h2 id="loan-results-title">Borrowing records</h2>
|
<h2 id="loan-results-title">借阅记录</h2>
|
||||||
<c:choose>
|
<c:choose>
|
||||||
<c:when test="${empty borrowRecords}">
|
<c:when test="${empty borrowRecords}">
|
||||||
<p class="empty-state">No borrowing records are available for this account.</p>
|
<p class="empty-state">此账户暂无借阅记录。</p>
|
||||||
</c:when>
|
</c:when>
|
||||||
<c:otherwise>
|
<c:otherwise>
|
||||||
<div class="table-scroll">
|
<div class="table-scroll">
|
||||||
<table class="data-table borrowing-table">
|
<table class="data-table borrowing-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col">Book ID</th>
|
<th scope="col">图书编号</th>
|
||||||
<th scope="col">Title</th>
|
<th scope="col">书名</th>
|
||||||
<th scope="col">Borrowed</th>
|
<th scope="col">借出时间</th>
|
||||||
<th scope="col">Due</th>
|
<th scope="col">应还时间</th>
|
||||||
<th scope="col">Returned</th>
|
<th scope="col">归还时间</th>
|
||||||
<th scope="col">Renewals</th>
|
<th scope="col">续借次数</th>
|
||||||
<th scope="col">Status</th>
|
<th scope="col">状态</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -63,7 +63,7 @@
|
|||||||
<c:when test="${not empty record.returnedAtText}">
|
<c:when test="${not empty record.returnedAtText}">
|
||||||
<c:out value="${record.returnedAtText}" />
|
<c:out value="${record.returnedAtText}" />
|
||||||
</c:when>
|
</c:when>
|
||||||
<c:otherwise>Not returned</c:otherwise>
|
<c:otherwise>未归还</c:otherwise>
|
||||||
</c:choose>
|
</c:choose>
|
||||||
</td>
|
</td>
|
||||||
<td><c:out value="${record.renewalCount}" /></td>
|
<td><c:out value="${record.renewalCount}" /></td>
|
||||||
|
|||||||
@@ -2,18 +2,18 @@
|
|||||||
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
|
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
|
||||||
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
|
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="zh-CN">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title><c:out value="${formTitle}" /> - MZH Library</title>
|
<title><c:out value="${formTitle}" /> - MZH 图书馆</title>
|
||||||
<link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/app.css">
|
<link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/app.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<%@ include file="/WEB-INF/jsp/common/header.jspf" %>
|
<%@ include file="/WEB-INF/jsp/common/header.jspf" %>
|
||||||
<main class="page-shell">
|
<main class="page-shell">
|
||||||
<section class="form-panel" aria-labelledby="reader-form-title">
|
<section class="form-panel" aria-labelledby="reader-form-title">
|
||||||
<p class="eyebrow">Reader Management</p>
|
<p class="eyebrow">读者管理</p>
|
||||||
<h1 id="reader-form-title"><c:out value="${formTitle}" /></h1>
|
<h1 id="reader-form-title"><c:out value="${formTitle}" /></h1>
|
||||||
|
|
||||||
<c:if test="${not empty errorMessage}">
|
<c:if test="${not empty errorMessage}">
|
||||||
@@ -38,7 +38,7 @@
|
|||||||
|
|
||||||
<div class="form-grid">
|
<div class="form-grid">
|
||||||
<div class="form-field">
|
<div class="form-field">
|
||||||
<label for="identifier">Reader ID</label>
|
<label for="identifier">读者编号</label>
|
||||||
<input id="identifier" name="identifier" type="text" value="${fn:escapeXml(identifierValue)}" required>
|
<input id="identifier" name="identifier" type="text" value="${fn:escapeXml(identifierValue)}" required>
|
||||||
<c:if test="${not empty errors.identifier}">
|
<c:if test="${not empty errors.identifier}">
|
||||||
<span class="field-error"><c:out value="${errors.identifier}" /></span>
|
<span class="field-error"><c:out value="${errors.identifier}" /></span>
|
||||||
@@ -46,7 +46,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-field">
|
<div class="form-field">
|
||||||
<label for="fullName">Full name</label>
|
<label for="fullName">姓名</label>
|
||||||
<input id="fullName" name="fullName" type="text" value="${fn:escapeXml(fullNameValue)}" required>
|
<input id="fullName" name="fullName" type="text" value="${fn:escapeXml(fullNameValue)}" required>
|
||||||
<c:if test="${not empty errors.fullName}">
|
<c:if test="${not empty errors.fullName}">
|
||||||
<span class="field-error"><c:out value="${errors.fullName}" /></span>
|
<span class="field-error"><c:out value="${errors.fullName}" /></span>
|
||||||
@@ -54,7 +54,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-field">
|
<div class="form-field">
|
||||||
<label for="phone">Phone</label>
|
<label for="phone">电话</label>
|
||||||
<input id="phone" name="phone" type="tel" value="${fn:escapeXml(phoneValue)}">
|
<input id="phone" name="phone" type="tel" value="${fn:escapeXml(phoneValue)}">
|
||||||
<c:if test="${not empty errors.phone}">
|
<c:if test="${not empty errors.phone}">
|
||||||
<span class="field-error"><c:out value="${errors.phone}" /></span>
|
<span class="field-error"><c:out value="${errors.phone}" /></span>
|
||||||
@@ -62,7 +62,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-field">
|
<div class="form-field">
|
||||||
<label for="email">Email</label>
|
<label for="email">邮箱</label>
|
||||||
<input id="email" name="email" type="email" value="${fn:escapeXml(emailValue)}">
|
<input id="email" name="email" type="email" value="${fn:escapeXml(emailValue)}">
|
||||||
<c:if test="${not empty errors.email}">
|
<c:if test="${not empty errors.email}">
|
||||||
<span class="field-error"><c:out value="${errors.email}" /></span>
|
<span class="field-error"><c:out value="${errors.email}" /></span>
|
||||||
@@ -70,7 +70,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-field">
|
<div class="form-field">
|
||||||
<label for="userId">Linked account ID</label>
|
<label for="userId">关联账户 ID</label>
|
||||||
<input id="userId" name="userId" type="number" min="1" value="${fn:escapeXml(userIdValue)}">
|
<input id="userId" name="userId" type="number" min="1" value="${fn:escapeXml(userIdValue)}">
|
||||||
<c:if test="${not empty errors.userId}">
|
<c:if test="${not empty errors.userId}">
|
||||||
<span class="field-error"><c:out value="${errors.userId}" /></span>
|
<span class="field-error"><c:out value="${errors.userId}" /></span>
|
||||||
@@ -78,7 +78,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-field">
|
<div class="form-field">
|
||||||
<label for="maxBorrowCount">Max borrow count</label>
|
<label for="maxBorrowCount">最大借阅数量</label>
|
||||||
<input id="maxBorrowCount" name="maxBorrowCount" type="number" min="1" max="50"
|
<input id="maxBorrowCount" name="maxBorrowCount" type="number" min="1" max="50"
|
||||||
value="${fn:escapeXml(maxBorrowCountValue)}" required>
|
value="${fn:escapeXml(maxBorrowCountValue)}" required>
|
||||||
<c:if test="${not empty errors.maxBorrowCount}">
|
<c:if test="${not empty errors.maxBorrowCount}">
|
||||||
@@ -87,9 +87,9 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-field">
|
<div class="form-field">
|
||||||
<label for="status">Status</label>
|
<label for="status">状态</label>
|
||||||
<select id="status" name="status" required>
|
<select id="status" name="status" required>
|
||||||
<option value="">Select status</option>
|
<option value="">请选择状态</option>
|
||||||
<c:forEach var="status" items="${statuses}">
|
<c:forEach var="status" items="${statuses}">
|
||||||
<option value="${status.code}" <c:if test="${statusValue == status.code}">selected</c:if>>
|
<option value="${status.code}" <c:if test="${statusValue == status.code}">selected</c:if>>
|
||||||
<c:out value="${status.displayName}" />
|
<c:out value="${status.displayName}" />
|
||||||
@@ -103,8 +103,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
<button class="button button-primary" type="submit">Save</button>
|
<button class="button button-primary" type="submit">保存</button>
|
||||||
<a class="button button-secondary" href="${pageContext.request.contextPath}/readers">Cancel</a>
|
<a class="button button-secondary" href="${pageContext.request.contextPath}/readers">取消</a>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -2,21 +2,21 @@
|
|||||||
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
|
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
|
||||||
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
|
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="zh-CN">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>Manage Readers - MZH Library</title>
|
<title>读者管理 - MZH 图书馆</title>
|
||||||
<link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/app.css">
|
<link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/app.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<%@ include file="/WEB-INF/jsp/common/header.jspf" %>
|
<%@ include file="/WEB-INF/jsp/common/header.jspf" %>
|
||||||
<main class="page-shell">
|
<main class="page-shell">
|
||||||
<section class="dashboard-hero catalog-hero" aria-labelledby="manage-readers-title">
|
<section class="dashboard-hero catalog-hero" aria-labelledby="manage-readers-title">
|
||||||
<p class="eyebrow">Reader Management</p>
|
<p class="eyebrow">读者管理</p>
|
||||||
<h1 id="manage-readers-title">Manage readers</h1>
|
<h1 id="manage-readers-title">管理读者</h1>
|
||||||
<p>Create, update, and review reader eligibility and contact records.</p>
|
<p>创建、更新和查看读者资格及联系方式记录。</p>
|
||||||
<a class="button button-primary" href="${pageContext.request.contextPath}/readers/new">New reader</a>
|
<a class="button button-primary" href="${pageContext.request.contextPath}/readers/new">新增读者</a>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<c:if test="${not empty successMessage}">
|
<c:if test="${not empty successMessage}">
|
||||||
@@ -30,27 +30,27 @@
|
|||||||
</div>
|
</div>
|
||||||
</c:if>
|
</c:if>
|
||||||
|
|
||||||
<section class="toolbar-panel" aria-label="Reader management search">
|
<section class="toolbar-panel" aria-label="读者管理检索">
|
||||||
<form class="search-form" action="${pageContext.request.contextPath}/readers" method="get">
|
<form class="search-form" action="${pageContext.request.contextPath}/readers" method="get">
|
||||||
<div class="search-field">
|
<div class="search-field">
|
||||||
<label for="identifier">Reader ID</label>
|
<label for="identifier">读者编号</label>
|
||||||
<input id="identifier" name="identifier" type="text" value="${fn:escapeXml(criteria.identifier)}">
|
<input id="identifier" name="identifier" type="text" value="${fn:escapeXml(criteria.identifier)}">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="search-field">
|
<div class="search-field">
|
||||||
<label for="name">Name</label>
|
<label for="name">姓名</label>
|
||||||
<input id="name" name="name" type="text" value="${fn:escapeXml(criteria.name)}">
|
<input id="name" name="name" type="text" value="${fn:escapeXml(criteria.name)}">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="search-field">
|
<div class="search-field">
|
||||||
<label for="contact">Phone or email</label>
|
<label for="contact">电话或邮箱</label>
|
||||||
<input id="contact" name="contact" type="text" value="${fn:escapeXml(criteria.contact)}">
|
<input id="contact" name="contact" type="text" value="${fn:escapeXml(criteria.contact)}">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="search-field">
|
<div class="search-field">
|
||||||
<label for="status">Status</label>
|
<label for="status">状态</label>
|
||||||
<select id="status" name="status">
|
<select id="status" name="status">
|
||||||
<option value="">All statuses</option>
|
<option value="">全部状态</option>
|
||||||
<c:forEach var="status" items="${statuses}">
|
<c:forEach var="status" items="${statuses}">
|
||||||
<option value="${status.code}" <c:if test="${criteria.statusCode == status.code}">selected</c:if>>
|
<option value="${status.code}" <c:if test="${criteria.statusCode == status.code}">selected</c:if>>
|
||||||
<c:out value="${status.displayName}" />
|
<c:out value="${status.displayName}" />
|
||||||
@@ -62,29 +62,29 @@
|
|||||||
</c:if>
|
</c:if>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button class="button button-primary" type="submit">Search</button>
|
<button class="button button-primary" type="submit">检索</button>
|
||||||
<a class="button button-secondary" href="${pageContext.request.contextPath}/readers">Clear</a>
|
<a class="button button-secondary" href="${pageContext.request.contextPath}/readers">清空</a>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="table-panel" aria-labelledby="reader-results-title">
|
<section class="table-panel" aria-labelledby="reader-results-title">
|
||||||
<h2 id="reader-results-title">Reader records</h2>
|
<h2 id="reader-results-title">读者记录</h2>
|
||||||
<c:choose>
|
<c:choose>
|
||||||
<c:when test="${empty readers}">
|
<c:when test="${empty readers}">
|
||||||
<p class="empty-state">No reader records match the current filters.</p>
|
<p class="empty-state">没有符合当前筛选条件的读者记录。</p>
|
||||||
</c:when>
|
</c:when>
|
||||||
<c:otherwise>
|
<c:otherwise>
|
||||||
<div class="table-scroll">
|
<div class="table-scroll">
|
||||||
<table class="data-table">
|
<table class="data-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col">Reader ID</th>
|
<th scope="col">读者编号</th>
|
||||||
<th scope="col">Name</th>
|
<th scope="col">姓名</th>
|
||||||
<th scope="col">Contact</th>
|
<th scope="col">联系方式</th>
|
||||||
<th scope="col">Account</th>
|
<th scope="col">关联账户</th>
|
||||||
<th scope="col">Borrow limit</th>
|
<th scope="col">借阅上限</th>
|
||||||
<th scope="col">Status</th>
|
<th scope="col">状态</th>
|
||||||
<th scope="col">Actions</th>
|
<th scope="col">操作</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -105,7 +105,7 @@
|
|||||||
<c:when test="${not empty reader.username}">
|
<c:when test="${not empty reader.username}">
|
||||||
<c:out value="${reader.username}" />
|
<c:out value="${reader.username}" />
|
||||||
</c:when>
|
</c:when>
|
||||||
<c:otherwise>Unlinked</c:otherwise>
|
<c:otherwise>未关联</c:otherwise>
|
||||||
</c:choose>
|
</c:choose>
|
||||||
</td>
|
</td>
|
||||||
<td><c:out value="${reader.maxBorrowCount}" /></td>
|
<td><c:out value="${reader.maxBorrowCount}" /></td>
|
||||||
@@ -117,12 +117,12 @@
|
|||||||
<td>
|
<td>
|
||||||
<div class="table-actions">
|
<div class="table-actions">
|
||||||
<a class="button button-secondary"
|
<a class="button button-secondary"
|
||||||
href="${pageContext.request.contextPath}/readers/edit?id=${reader.id}">Edit</a>
|
href="${pageContext.request.contextPath}/readers/edit?id=${reader.id}">编辑</a>
|
||||||
<form action="${pageContext.request.contextPath}/readers/delete"
|
<form action="${pageContext.request.contextPath}/readers/delete"
|
||||||
method="post"
|
method="post"
|
||||||
onsubmit="return confirm('Deactivate this reader profile?');">
|
onsubmit="return confirm('确定停用这个读者档案吗?');">
|
||||||
<input type="hidden" name="id" value="${reader.id}">
|
<input type="hidden" name="id" value="${reader.id}">
|
||||||
<button class="button button-danger" type="submit">Deactivate</button>
|
<button class="button button-danger" type="submit">停用</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" %>
|
<%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" %>
|
||||||
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
|
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="zh-CN">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>Reports - MZH Library</title>
|
<title>报表 - MZH 图书馆</title>
|
||||||
<link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/app.css">
|
<link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/app.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -13,11 +13,11 @@
|
|||||||
<main class="page-shell">
|
<main class="page-shell">
|
||||||
<section class="dashboard-hero catalog-hero" aria-labelledby="reports-title">
|
<section class="dashboard-hero catalog-hero" aria-labelledby="reports-title">
|
||||||
<div>
|
<div>
|
||||||
<p class="eyebrow">Reports</p>
|
<p class="eyebrow">报表</p>
|
||||||
<h1 id="reports-title">Report center</h1>
|
<h1 id="reports-title">报表中心</h1>
|
||||||
<p>Review collection inventory, borrowing health, overdue loans, and popular books.</p>
|
<p>查看馆藏库存、借阅状况、逾期借阅和热门图书。</p>
|
||||||
</div>
|
</div>
|
||||||
<a class="button button-secondary" href="${pageContext.request.contextPath}/borrowing">Borrowing records</a>
|
<a class="button button-secondary" href="${pageContext.request.contextPath}/borrowing">借阅记录</a>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<c:if test="${not empty errorMessage}">
|
<c:if test="${not empty errorMessage}">
|
||||||
@@ -27,59 +27,59 @@
|
|||||||
</c:if>
|
</c:if>
|
||||||
|
|
||||||
<c:if test="${not empty reportCenter}">
|
<c:if test="${not empty reportCenter}">
|
||||||
<section class="report-grid" aria-label="Report summary">
|
<section class="report-grid" aria-label="报表摘要">
|
||||||
<article class="report-card">
|
<article class="report-card">
|
||||||
<p class="eyebrow">Inventory</p>
|
<p class="eyebrow">库存</p>
|
||||||
<h2>Total titles</h2>
|
<h2>图书种类总数</h2>
|
||||||
<p class="report-metric"><c:out value="${reportCenter.inventorySummary.totalTitles}" /></p>
|
<p class="report-metric"><c:out value="${reportCenter.inventorySummary.totalTitles}" /></p>
|
||||||
</article>
|
</article>
|
||||||
<article class="report-card">
|
<article class="report-card">
|
||||||
<p class="eyebrow">Inventory</p>
|
<p class="eyebrow">库存</p>
|
||||||
<h2>Total copies</h2>
|
<h2>馆藏总册数</h2>
|
||||||
<p class="report-metric"><c:out value="${reportCenter.inventorySummary.totalCopies}" /></p>
|
<p class="report-metric"><c:out value="${reportCenter.inventorySummary.totalCopies}" /></p>
|
||||||
</article>
|
</article>
|
||||||
<article class="report-card">
|
<article class="report-card">
|
||||||
<p class="eyebrow">Inventory</p>
|
<p class="eyebrow">库存</p>
|
||||||
<h2>Available copies</h2>
|
<h2>可借册数</h2>
|
||||||
<p class="report-metric"><c:out value="${reportCenter.inventorySummary.availableCopies}" /></p>
|
<p class="report-metric"><c:out value="${reportCenter.inventorySummary.availableCopies}" /></p>
|
||||||
</article>
|
</article>
|
||||||
<article class="report-card">
|
<article class="report-card">
|
||||||
<p class="eyebrow">Attention</p>
|
<p class="eyebrow">需关注</p>
|
||||||
<h2>Unavailable or empty</h2>
|
<h2>不可借或无库存</h2>
|
||||||
<p class="report-metric"><c:out value="${reportCenter.inventorySummary.unavailableOrEmptyTitles}" /></p>
|
<p class="report-metric"><c:out value="${reportCenter.inventorySummary.unavailableOrEmptyTitles}" /></p>
|
||||||
</article>
|
</article>
|
||||||
<article class="report-card">
|
<article class="report-card">
|
||||||
<p class="eyebrow">Borrowing</p>
|
<p class="eyebrow">借阅</p>
|
||||||
<h2>Currently borrowed</h2>
|
<h2>当前借出</h2>
|
||||||
<p class="report-metric"><c:out value="${reportCenter.borrowingSummary.activeLoans}" /></p>
|
<p class="report-metric"><c:out value="${reportCenter.borrowingSummary.activeLoans}" /></p>
|
||||||
</article>
|
</article>
|
||||||
<article class="report-card">
|
<article class="report-card">
|
||||||
<p class="eyebrow">Borrowing</p>
|
<p class="eyebrow">借阅</p>
|
||||||
<h2>Returned records</h2>
|
<h2>已归还记录</h2>
|
||||||
<p class="report-metric"><c:out value="${reportCenter.borrowingSummary.returnedLoans}" /></p>
|
<p class="report-metric"><c:out value="${reportCenter.borrowingSummary.returnedLoans}" /></p>
|
||||||
</article>
|
</article>
|
||||||
<article class="report-card report-card-alert">
|
<article class="report-card report-card-alert">
|
||||||
<p class="eyebrow">Borrowing</p>
|
<p class="eyebrow">借阅</p>
|
||||||
<h2>Overdue loans</h2>
|
<h2>逾期借阅</h2>
|
||||||
<p class="report-metric"><c:out value="${reportCenter.borrowingSummary.overdueLoans}" /></p>
|
<p class="report-metric"><c:out value="${reportCenter.borrowingSummary.overdueLoans}" /></p>
|
||||||
</article>
|
</article>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="table-panel" aria-labelledby="overdue-report-title">
|
<section class="table-panel" aria-labelledby="overdue-report-title">
|
||||||
<h2 id="overdue-report-title">Overdue list</h2>
|
<h2 id="overdue-report-title">逾期列表</h2>
|
||||||
<c:choose>
|
<c:choose>
|
||||||
<c:when test="${empty reportCenter.overdueRows}">
|
<c:when test="${empty reportCenter.overdueRows}">
|
||||||
<p class="empty-state">No active overdue borrowing records.</p>
|
<p class="empty-state">当前没有逾期未还的借阅记录。</p>
|
||||||
</c:when>
|
</c:when>
|
||||||
<c:otherwise>
|
<c:otherwise>
|
||||||
<div class="table-scroll">
|
<div class="table-scroll">
|
||||||
<table class="data-table">
|
<table class="data-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col">Reader</th>
|
<th scope="col">读者</th>
|
||||||
<th scope="col">Book</th>
|
<th scope="col">图书</th>
|
||||||
<th scope="col">Due date</th>
|
<th scope="col">应还日期</th>
|
||||||
<th scope="col">Overdue days</th>
|
<th scope="col">逾期天数</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -96,7 +96,7 @@
|
|||||||
<td><c:out value="${row.dueAtText}" /></td>
|
<td><c:out value="${row.dueAtText}" /></td>
|
||||||
<td>
|
<td>
|
||||||
<span class="status-pill status-overdue">
|
<span class="status-pill status-overdue">
|
||||||
<c:out value="${row.overdueDays}" /> days
|
<c:out value="${row.overdueDays}" /> 天
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -109,19 +109,19 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="table-panel" aria-labelledby="popular-report-title">
|
<section class="table-panel" aria-labelledby="popular-report-title">
|
||||||
<h2 id="popular-report-title">Popular borrowing ranking</h2>
|
<h2 id="popular-report-title">热门借阅排行</h2>
|
||||||
<c:choose>
|
<c:choose>
|
||||||
<c:when test="${empty reportCenter.popularBooks}">
|
<c:when test="${empty reportCenter.popularBooks}">
|
||||||
<p class="empty-state">No borrowing records are available for ranking yet.</p>
|
<p class="empty-state">暂无可用于排行的借阅记录。</p>
|
||||||
</c:when>
|
</c:when>
|
||||||
<c:otherwise>
|
<c:otherwise>
|
||||||
<div class="table-scroll">
|
<div class="table-scroll">
|
||||||
<table class="data-table">
|
<table class="data-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col">Book</th>
|
<th scope="col">图书</th>
|
||||||
<th scope="col">Author</th>
|
<th scope="col">作者</th>
|
||||||
<th scope="col">Borrow records</th>
|
<th scope="col">借阅次数</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" %>
|
<%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" %>
|
||||||
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
|
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="zh-CN">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title><c:out value="${areaName}" /> - MZH Library</title>
|
<title><c:out value="${areaName}" /> - MZH 图书馆</title>
|
||||||
<link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/app.css">
|
<link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/app.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -17,61 +17,67 @@
|
|||||||
</p>
|
</p>
|
||||||
<h1 id="area-title"><c:out value="${areaName}" /></h1>
|
<h1 id="area-title"><c:out value="${areaName}" /></h1>
|
||||||
<p><c:out value="${areaSummary}" /></p>
|
<p><c:out value="${areaSummary}" /></p>
|
||||||
<a class="button button-primary" href="${pageContext.request.contextPath}/dashboard">Back to dashboard</a>
|
<a class="button button-primary" href="${pageContext.request.contextPath}/dashboard">返回控制台</a>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="card-grid role-actions" aria-label="Workspace actions">
|
<section class="card-grid role-actions" aria-label="工作区操作">
|
||||||
<article class="workspace-card">
|
<article class="workspace-card">
|
||||||
<h2>Book Catalog</h2>
|
<h2>馆藏检索</h2>
|
||||||
<p>Search available collection records by title, author, category, or book identifier.</p>
|
<p>按书名、作者、分类或图书编号检索可用馆藏记录。</p>
|
||||||
<a class="button button-secondary" href="${pageContext.request.contextPath}/catalog">Search catalog</a>
|
<a class="button button-secondary" href="${pageContext.request.contextPath}/catalog">检索馆藏</a>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<c:if test="${sessionScope.userRole == 'administrator' or sessionScope.userRole == 'librarian'}">
|
<c:if test="${sessionScope.userRole == 'administrator' or sessionScope.userRole == 'librarian'}">
|
||||||
<c:if test="${sessionScope.userRole == 'administrator'}">
|
<c:if test="${sessionScope.userRole == 'administrator'}">
|
||||||
<article class="workspace-card">
|
<article class="workspace-card">
|
||||||
<h2>User Management</h2>
|
<h2>用户管理</h2>
|
||||||
<p>Create, update, deactivate, and review login accounts.</p>
|
<p>创建、更新、停用和查看登录账户。</p>
|
||||||
<a class="button button-secondary" href="${pageContext.request.contextPath}/admin/users">Manage users</a>
|
<a class="button button-secondary" href="${pageContext.request.contextPath}/admin/users">管理用户</a>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<article class="workspace-card">
|
<article class="workspace-card">
|
||||||
<h2>System Logs</h2>
|
<h2>系统日志</h2>
|
||||||
<p>Review read-only audit entries for account and maintenance actions.</p>
|
<p>查看账户与维护操作的只读审计记录。</p>
|
||||||
<a class="button button-secondary" href="${pageContext.request.contextPath}/admin/system-logs">View logs</a>
|
<a class="button button-secondary" href="${pageContext.request.contextPath}/admin/system-logs">查看日志</a>
|
||||||
</article>
|
</article>
|
||||||
</c:if>
|
</c:if>
|
||||||
|
|
||||||
<article class="workspace-card">
|
<article class="workspace-card">
|
||||||
<h2>Book Management</h2>
|
<h2>图书管理</h2>
|
||||||
<p>Create, update, delete, and review inventory fields for book records.</p>
|
<p>创建、更新、删除和查看图书记录的库存字段。</p>
|
||||||
<a class="button button-secondary" href="${pageContext.request.contextPath}/books">Manage books</a>
|
<a class="button button-secondary" href="${pageContext.request.contextPath}/books">管理图书</a>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<article class="workspace-card">
|
<article class="workspace-card">
|
||||||
<h2>Reader Management</h2>
|
<h2>分类维护</h2>
|
||||||
<p>Create, update, deactivate, and review eligibility fields for reader records.</p>
|
<p>创建、更新和停用图书记录使用的馆藏分类。</p>
|
||||||
<a class="button button-secondary" href="${pageContext.request.contextPath}/readers">Manage readers</a>
|
<a class="button button-secondary" href="${pageContext.request.contextPath}/book-categories">管理分类</a>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<article class="workspace-card">
|
<article class="workspace-card">
|
||||||
<h2>Borrowing Management</h2>
|
<h2>读者管理</h2>
|
||||||
<p>Create loans, process returns, renew records, and review overdue items.</p>
|
<p>创建、更新、停用和查看读者记录的资格字段。</p>
|
||||||
<a class="button button-secondary" href="${pageContext.request.contextPath}/borrowing">Manage borrowing</a>
|
<a class="button button-secondary" href="${pageContext.request.contextPath}/readers">管理读者</a>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<article class="workspace-card">
|
<article class="workspace-card">
|
||||||
<h2>Report Center</h2>
|
<h2>借阅管理</h2>
|
||||||
<p>Review inventory summaries, borrowing health, overdue lists, and popular books.</p>
|
<p>创建借阅、处理归还、续借记录并查看逾期项目。</p>
|
||||||
<a class="button button-secondary" href="${pageContext.request.contextPath}/reports">View reports</a>
|
<a class="button button-secondary" href="${pageContext.request.contextPath}/borrowing">管理借阅</a>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="workspace-card">
|
||||||
|
<h2>报表中心</h2>
|
||||||
|
<p>查看库存摘要、借阅状况、逾期列表和热门图书。</p>
|
||||||
|
<a class="button button-secondary" href="${pageContext.request.contextPath}/reports">查看报表</a>
|
||||||
</article>
|
</article>
|
||||||
</c:if>
|
</c:if>
|
||||||
|
|
||||||
<c:if test="${sessionScope.userRole == 'reader'}">
|
<c:if test="${sessionScope.userRole == 'reader'}">
|
||||||
<article class="workspace-card">
|
<article class="workspace-card">
|
||||||
<h2>My Loan History</h2>
|
<h2>我的借阅历史</h2>
|
||||||
<p>Review active loans, returned records, renewal counts, and overdue status.</p>
|
<p>查看在借记录、已还记录、续借次数和逾期状态。</p>
|
||||||
<a class="button button-secondary" href="${pageContext.request.contextPath}/reader/loans">View history</a>
|
<a class="button button-secondary" href="${pageContext.request.contextPath}/reader/loans">查看历史</a>
|
||||||
</article>
|
</article>
|
||||||
</c:if>
|
</c:if>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
|
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
|
||||||
version="4.0">
|
version="4.0">
|
||||||
|
|
||||||
<display-name>MZH Library Management</display-name>
|
<display-name>MZH 图书馆管理系统</display-name>
|
||||||
|
|
||||||
<filter>
|
<filter>
|
||||||
<filter-name>CharacterEncodingFilter</filter-name>
|
<filter-name>CharacterEncodingFilter</filter-name>
|
||||||
@@ -117,6 +117,11 @@
|
|||||||
<url-pattern>/books/edit</url-pattern>
|
<url-pattern>/books/edit</url-pattern>
|
||||||
<url-pattern>/books/update</url-pattern>
|
<url-pattern>/books/update</url-pattern>
|
||||||
<url-pattern>/books/delete</url-pattern>
|
<url-pattern>/books/delete</url-pattern>
|
||||||
|
<url-pattern>/book-categories</url-pattern>
|
||||||
|
<url-pattern>/book-categories/new</url-pattern>
|
||||||
|
<url-pattern>/book-categories/edit</url-pattern>
|
||||||
|
<url-pattern>/book-categories/update</url-pattern>
|
||||||
|
<url-pattern>/book-categories/delete</url-pattern>
|
||||||
</servlet-mapping>
|
</servlet-mapping>
|
||||||
|
|
||||||
<servlet>
|
<servlet>
|
||||||
|
|||||||
@@ -313,6 +313,12 @@ h2 {
|
|||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hero-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
.toolbar-panel,
|
.toolbar-panel,
|
||||||
.table-panel,
|
.table-panel,
|
||||||
.form-panel {
|
.form-panel {
|
||||||
@@ -350,6 +356,8 @@ h2 {
|
|||||||
.search-form select,
|
.search-form select,
|
||||||
.book-form input,
|
.book-form input,
|
||||||
.book-form select,
|
.book-form select,
|
||||||
|
.category-form input,
|
||||||
|
.category-form textarea,
|
||||||
.reader-form input,
|
.reader-form input,
|
||||||
.reader-form select,
|
.reader-form select,
|
||||||
.user-form input,
|
.user-form input,
|
||||||
@@ -368,6 +376,8 @@ h2 {
|
|||||||
.search-form select:focus,
|
.search-form select:focus,
|
||||||
.book-form input:focus,
|
.book-form input:focus,
|
||||||
.book-form select:focus,
|
.book-form select:focus,
|
||||||
|
.category-form input:focus,
|
||||||
|
.category-form textarea:focus,
|
||||||
.reader-form input:focus,
|
.reader-form input:focus,
|
||||||
.reader-form select:focus,
|
.reader-form select:focus,
|
||||||
.user-form input:focus,
|
.user-form input:focus,
|
||||||
@@ -394,7 +404,8 @@ h2 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.user-table,
|
.user-table,
|
||||||
.system-log-table {
|
.system-log-table,
|
||||||
|
.category-table {
|
||||||
min-width: 980px;
|
min-width: 980px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -504,6 +515,7 @@ h2 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.book-form,
|
.book-form,
|
||||||
|
.category-form,
|
||||||
.reader-form,
|
.reader-form,
|
||||||
.user-form,
|
.user-form,
|
||||||
.borrow-form {
|
.borrow-form {
|
||||||
@@ -522,6 +534,15 @@ h2 {
|
|||||||
gap: 6px;
|
gap: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form-field-wide {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-form textarea {
|
||||||
|
min-height: 112px;
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
.form-field label {
|
.form-field label {
|
||||||
color: var(--color-muted);
|
color: var(--color-muted);
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="1440" height="960" viewBox="0 0 1440 960" role="img" aria-label="Library shelves">
|
<svg xmlns="http://www.w3.org/2000/svg" width="1440" height="960" viewBox="0 0 1440 960" role="img" aria-label="图书馆书架">
|
||||||
<rect width="1440" height="960" fill="#e8edf1"/>
|
<rect width="1440" height="960" fill="#e8edf1"/>
|
||||||
<rect x="0" y="705" width="1440" height="255" fill="#d4ddd8"/>
|
<rect x="0" y="705" width="1440" height="255" fill="#d4ddd8"/>
|
||||||
<g opacity="0.92">
|
<g opacity="0.92">
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
@@ -14,10 +14,10 @@ import java.util.logging.Level;
|
|||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
public final class AuthServiceCheck {
|
public final class AuthServiceCheck {
|
||||||
private static final String REQUIRED_MESSAGE = "Username and password are required.";
|
private static final String REQUIRED_MESSAGE = "请输入用户名和密码。";
|
||||||
private static final String INVALID_MESSAGE = "Invalid username or password.";
|
private static final String INVALID_MESSAGE = "用户名或密码不正确。";
|
||||||
private static final String UNAVAILABLE_MESSAGE =
|
private static final String UNAVAILABLE_MESSAGE =
|
||||||
"Login service is temporarily unavailable. Please try again later.";
|
"登录服务暂时不可用,请稍后重试。";
|
||||||
|
|
||||||
private AuthServiceCheck() {
|
private AuthServiceCheck() {
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import com.mzh.library.exception.DaoException;
|
|||||||
import com.mzh.library.service.impl.BookServiceImpl;
|
import com.mzh.library.service.impl.BookServiceImpl;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.EnumSet;
|
import java.util.EnumSet;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -23,7 +22,7 @@ import java.util.logging.Logger;
|
|||||||
|
|
||||||
public final class BookServiceCheck {
|
public final class BookServiceCheck {
|
||||||
private static final String UNAVAILABLE_MESSAGE =
|
private static final String UNAVAILABLE_MESSAGE =
|
||||||
"Book service is temporarily unavailable. Please try again later.";
|
"图书服务暂时不可用,请稍后重试。";
|
||||||
|
|
||||||
private BookServiceCheck() {
|
private BookServiceCheck() {
|
||||||
}
|
}
|
||||||
@@ -45,9 +44,15 @@ public final class BookServiceCheck {
|
|||||||
ServiceResult<Long> denied = service.createBook(reader,
|
ServiceResult<Long> denied = service.createBook(reader,
|
||||||
book(0L, "BK-1001", "Reader Write", "Test Author", 1L, 1, 1, BookStatus.AVAILABLE));
|
book(0L, "BK-1001", "Reader Write", "Test Author", 1L, 1, 1, BookStatus.AVAILABLE));
|
||||||
require(!denied.isSuccessful(), "reader write should fail");
|
require(!denied.isSuccessful(), "reader write should fail");
|
||||||
require("You do not have permission to manage books.".equals(denied.getMessage()),
|
require("您无权管理图书。".equals(denied.getMessage()),
|
||||||
"reader write should return permission message");
|
"reader write should return permission message");
|
||||||
|
|
||||||
|
ServiceResult<Long> deniedCategory = service.createCategory(reader,
|
||||||
|
category(0L, "Reader Category", "Denied category"));
|
||||||
|
require(!deniedCategory.isSuccessful(), "reader category create should fail");
|
||||||
|
require("您无权管理图书。".equals(deniedCategory.getMessage()),
|
||||||
|
"reader category write should return permission message");
|
||||||
|
|
||||||
ServiceResult<Long> created = service.createBook(librarian,
|
ServiceResult<Long> created = service.createBook(librarian,
|
||||||
book(0L, "BK-1002", "Service Test", "Test Author", 1L, 2, 1, BookStatus.AVAILABLE));
|
book(0L, "BK-1002", "Service Test", "Test Author", 1L, 2, 1, BookStatus.AVAILABLE));
|
||||||
require(created.isSuccessful(), "librarian should create a valid book");
|
require(created.isSuccessful(), "librarian should create a valid book");
|
||||||
@@ -63,6 +68,13 @@ public final class BookServiceCheck {
|
|||||||
require(updated.isSuccessful(), "librarian should update a valid book");
|
require(updated.isSuccessful(), "librarian should update a valid book");
|
||||||
require(dao.findById(createdId).get().getAvailableCopies() == 3, "update should persist available copies");
|
require(dao.findById(createdId).get().getAvailableCopies() == 3, "update should persist available copies");
|
||||||
|
|
||||||
|
ServiceResult<Void> deleteUsedCategory = service.deleteCategory(librarian, 1L);
|
||||||
|
require(!deleteUsedCategory.isSuccessful(), "used category delete should fail");
|
||||||
|
require("该分类已被现有图书使用,不能删除。".equals(deleteUsedCategory.getMessage()),
|
||||||
|
"used category delete should return a safe specific message");
|
||||||
|
require(deleteUsedCategory.getErrors().containsKey("category"),
|
||||||
|
"used category delete should return a category-level field error");
|
||||||
|
|
||||||
ServiceResult<List<Book>> search = service.searchBooks(new BookSearchCriteria("BK-1003", "", "", null));
|
ServiceResult<List<Book>> search = service.searchBooks(new BookSearchCriteria("BK-1003", "", "", null));
|
||||||
require(search.isSuccessful(), "search should succeed");
|
require(search.isSuccessful(), "search should succeed");
|
||||||
require(search.getData().size() == 1, "search should find updated identifier");
|
require(search.getData().size() == 1, "search should find updated identifier");
|
||||||
@@ -71,6 +83,26 @@ public final class BookServiceCheck {
|
|||||||
require(deleted.isSuccessful(), "librarian should delete a book");
|
require(deleted.isSuccessful(), "librarian should delete a book");
|
||||||
require(!dao.findById(createdId).isPresent(), "delete should remove the record");
|
require(!dao.findById(createdId).isPresent(), "delete should remove the record");
|
||||||
|
|
||||||
|
ServiceResult<Long> createdCategory = service.createCategory(librarian,
|
||||||
|
category(0L, "Architecture", "Design and systems"));
|
||||||
|
require(createdCategory.isSuccessful(), "librarian should create a category");
|
||||||
|
long categoryId = createdCategory.getData();
|
||||||
|
|
||||||
|
ServiceResult<Long> duplicateCategory = service.createCategory(librarian,
|
||||||
|
category(0L, "Architecture", "Duplicate category"));
|
||||||
|
require(!duplicateCategory.isSuccessful(), "duplicate category should fail");
|
||||||
|
require(duplicateCategory.getErrors().containsKey("name"), "duplicate category should target name field");
|
||||||
|
|
||||||
|
ServiceResult<Void> updatedCategory = service.updateCategory(librarian,
|
||||||
|
category(categoryId, "Software Architecture", "Updated category"));
|
||||||
|
require(updatedCategory.isSuccessful(), "librarian should update a category");
|
||||||
|
require("Software Architecture".equals(dao.findCategoryById(categoryId).get().getName()),
|
||||||
|
"category update should persist the new name");
|
||||||
|
|
||||||
|
ServiceResult<Void> deletedCategory = service.deleteCategory(librarian, categoryId);
|
||||||
|
require(deletedCategory.isSuccessful(), "unused category should be deleted");
|
||||||
|
require(!dao.findCategoryById(categoryId).isPresent(), "category delete should remove unused category");
|
||||||
|
|
||||||
BookService failingService = new BookServiceImpl(new FailingBookDao());
|
BookService failingService = new BookServiceImpl(new FailingBookDao());
|
||||||
ServiceResult<List<Book>> unavailable = failingService.searchBooks(new BookSearchCriteria());
|
ServiceResult<List<Book>> unavailable = failingService.searchBooks(new BookSearchCriteria());
|
||||||
require(!unavailable.isSuccessful(), "DAO failure should not escape service");
|
require(!unavailable.isSuccessful(), "DAO failure should not escape service");
|
||||||
@@ -98,6 +130,14 @@ public final class BookServiceCheck {
|
|||||||
return book;
|
return book;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static BookCategory category(long id, String name, String description) {
|
||||||
|
BookCategory category = new BookCategory();
|
||||||
|
category.setId(id);
|
||||||
|
category.setName(name);
|
||||||
|
category.setDescription(description);
|
||||||
|
return category;
|
||||||
|
}
|
||||||
|
|
||||||
private static void require(boolean condition, String message) {
|
private static void require(boolean condition, String message) {
|
||||||
if (!condition) {
|
if (!condition) {
|
||||||
throw new AssertionError(message);
|
throw new AssertionError(message);
|
||||||
@@ -106,14 +146,70 @@ public final class BookServiceCheck {
|
|||||||
|
|
||||||
private static final class InMemoryBookDao implements BookDao {
|
private static final class InMemoryBookDao implements BookDao {
|
||||||
private final Map<Long, Book> books = new LinkedHashMap<>();
|
private final Map<Long, Book> books = new LinkedHashMap<>();
|
||||||
|
private final Map<Long, BookCategory> categories = new LinkedHashMap<>();
|
||||||
private long nextId = 1L;
|
private long nextId = 1L;
|
||||||
|
private long nextCategoryId = 2L;
|
||||||
|
|
||||||
|
private InMemoryBookDao() {
|
||||||
|
categories.put(1L, category(1L, "Computer Science", "Programming books"));
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<BookCategory> findAllCategories() {
|
public List<BookCategory> findAllCategories() {
|
||||||
BookCategory category = new BookCategory();
|
List<BookCategory> results = new ArrayList<>();
|
||||||
category.setId(1L);
|
for (BookCategory category : categories.values()) {
|
||||||
category.setName("Computer Science");
|
results.add(copy(category));
|
||||||
return Collections.singletonList(category);
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<BookCategory> findCategoryById(long id) {
|
||||||
|
return Optional.ofNullable(categories.get(id)).map(this::copy);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<BookCategory> findCategoryByName(String name) {
|
||||||
|
for (BookCategory category : categories.values()) {
|
||||||
|
if (category.getName().equals(name)) {
|
||||||
|
return Optional.of(copy(category));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long createCategory(BookCategory category) {
|
||||||
|
long id = nextCategoryId++;
|
||||||
|
BookCategory stored = copy(category);
|
||||||
|
stored.setId(id);
|
||||||
|
categories.put(id, stored);
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean updateCategory(BookCategory category) {
|
||||||
|
if (!categories.containsKey(category.getId())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
categories.put(category.getId(), copy(category));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean deleteCategory(long id) {
|
||||||
|
return categories.remove(id) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int countBooksByCategoryId(long categoryId) {
|
||||||
|
int count = 0;
|
||||||
|
for (Book book : books.values()) {
|
||||||
|
if (book.getCategoryId() == categoryId) {
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -178,6 +274,10 @@ public final class BookServiceCheck {
|
|||||||
copy.setCategoryName(source.getCategoryName());
|
copy.setCategoryName(source.getCategoryName());
|
||||||
return copy;
|
return copy;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private BookCategory copy(BookCategory source) {
|
||||||
|
return category(source.getId(), source.getName(), source.getDescription());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final class FailingBookDao implements BookDao {
|
private static final class FailingBookDao implements BookDao {
|
||||||
@@ -186,6 +286,36 @@ public final class BookServiceCheck {
|
|||||||
throw new DaoException("Simulated category failure", null);
|
throw new DaoException("Simulated category failure", null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<BookCategory> findCategoryById(long id) {
|
||||||
|
throw new DaoException("Simulated category find failure", null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<BookCategory> findCategoryByName(String name) {
|
||||||
|
throw new DaoException("Simulated category find failure", null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long createCategory(BookCategory category) {
|
||||||
|
throw new DaoException("Simulated category create failure", null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean updateCategory(BookCategory category) {
|
||||||
|
throw new DaoException("Simulated category update failure", null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean deleteCategory(long id) {
|
||||||
|
throw new DaoException("Simulated category delete failure", null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int countBooksByCategoryId(long categoryId) {
|
||||||
|
throw new DaoException("Simulated category count failure", null);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<Book> search(BookSearchCriteria criteria) {
|
public List<Book> search(BookSearchCriteria criteria) {
|
||||||
throw new DaoException("Simulated search failure", null);
|
throw new DaoException("Simulated search failure", null);
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ import java.util.logging.Logger;
|
|||||||
|
|
||||||
public final class BorrowingServiceCheck {
|
public final class BorrowingServiceCheck {
|
||||||
private static final String UNAVAILABLE_MESSAGE =
|
private static final String UNAVAILABLE_MESSAGE =
|
||||||
"Borrowing service is temporarily unavailable. Please try again later.";
|
"借阅服务暂时不可用,请稍后重试。";
|
||||||
private static final Clock FIXED_CLOCK = Clock.fixed(
|
private static final Clock FIXED_CLOCK = Clock.fixed(
|
||||||
Instant.parse("2026-04-27T00:00:00Z"),
|
Instant.parse("2026-04-27T00:00:00Z"),
|
||||||
ZoneId.of("UTC")
|
ZoneId.of("UTC")
|
||||||
@@ -62,7 +62,7 @@ public final class BorrowingServiceCheck {
|
|||||||
|
|
||||||
ServiceResult<Long> denied = service.borrowBook(readerUser, "RD-1000", "BK-1000");
|
ServiceResult<Long> denied = service.borrowBook(readerUser, "RD-1000", "BK-1000");
|
||||||
require(!denied.isSuccessful(), "reader should not manage borrow creation");
|
require(!denied.isSuccessful(), "reader should not manage borrow creation");
|
||||||
require("You do not have permission to manage borrowing.".equals(denied.getMessage()),
|
require("您无权管理借阅。".equals(denied.getMessage()),
|
||||||
"reader borrow creation should return permission message");
|
"reader borrow creation should return permission message");
|
||||||
|
|
||||||
ServiceResult<Long> suspended = service.borrowBook(librarian, "RD-1001", "BK-1000");
|
ServiceResult<Long> suspended = service.borrowBook(librarian, "RD-1001", "BK-1000");
|
||||||
@@ -96,7 +96,7 @@ public final class BorrowingServiceCheck {
|
|||||||
|
|
||||||
ServiceResult<List<BorrowRecord>> staffHistory = service.listCurrentReaderHistory(administrator);
|
ServiceResult<List<BorrowRecord>> staffHistory = service.listCurrentReaderHistory(administrator);
|
||||||
require(!staffHistory.isSuccessful(), "staff should use management history, not reader loan history");
|
require(!staffHistory.isSuccessful(), "staff should use management history, not reader loan history");
|
||||||
require("You do not have permission to view loan history.".equals(staffHistory.getMessage()),
|
require("您无权查看借阅历史。".equals(staffHistory.getMessage()),
|
||||||
"staff reader-history access should return permission message");
|
"staff reader-history access should return permission message");
|
||||||
|
|
||||||
ServiceResult<Void> returned = service.returnBook(librarian, borrowedId);
|
ServiceResult<Void> returned = service.returnBook(librarian, borrowedId);
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import java.util.logging.Logger;
|
|||||||
|
|
||||||
public final class ReaderServiceCheck {
|
public final class ReaderServiceCheck {
|
||||||
private static final String UNAVAILABLE_MESSAGE =
|
private static final String UNAVAILABLE_MESSAGE =
|
||||||
"Reader service is temporarily unavailable. Please try again later.";
|
"读者服务暂时不可用,请稍后重试。";
|
||||||
|
|
||||||
private ReaderServiceCheck() {
|
private ReaderServiceCheck() {
|
||||||
}
|
}
|
||||||
@@ -64,7 +64,7 @@ public final class ReaderServiceCheck {
|
|||||||
ServiceResult<Long> denied = service.createReader(readerUser,
|
ServiceResult<Long> denied = service.createReader(readerUser,
|
||||||
reader(0L, "RD-1006", null, "Reader Write", "13800000007", "", ReaderStatus.ACTIVE, 5));
|
reader(0L, "RD-1006", null, "Reader Write", "13800000007", "", ReaderStatus.ACTIVE, 5));
|
||||||
require(!denied.isSuccessful(), "reader write should fail");
|
require(!denied.isSuccessful(), "reader write should fail");
|
||||||
require("You do not have permission to manage readers.".equals(denied.getMessage()),
|
require("您无权管理读者。".equals(denied.getMessage()),
|
||||||
"reader write should return permission message");
|
"reader write should return permission message");
|
||||||
|
|
||||||
ServiceResult<Long> created = service.createReader(librarian,
|
ServiceResult<Long> created = service.createReader(librarian,
|
||||||
|
|||||||
@@ -21,9 +21,9 @@ import java.util.logging.Level;
|
|||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
public final class ReportServiceCheck {
|
public final class ReportServiceCheck {
|
||||||
private static final String DENIED_MESSAGE = "You do not have permission to view reports.";
|
private static final String DENIED_MESSAGE = "您无权查看报表。";
|
||||||
private static final String UNAVAILABLE_MESSAGE =
|
private static final String UNAVAILABLE_MESSAGE =
|
||||||
"Report service is temporarily unavailable. Please try again later.";
|
"报表服务暂时不可用,请稍后重试。";
|
||||||
|
|
||||||
private ReportServiceCheck() {
|
private ReportServiceCheck() {
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,9 +20,9 @@ import java.util.logging.Level;
|
|||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
public final class SystemLogServiceCheck {
|
public final class SystemLogServiceCheck {
|
||||||
private static final String DENIED_MESSAGE = "You do not have permission to view system logs.";
|
private static final String DENIED_MESSAGE = "您无权查看系统日志。";
|
||||||
private static final String UNAVAILABLE_MESSAGE =
|
private static final String UNAVAILABLE_MESSAGE =
|
||||||
"System log service is temporarily unavailable. Please try again later.";
|
"系统日志服务暂时不可用,请稍后重试。";
|
||||||
|
|
||||||
private SystemLogServiceCheck() {
|
private SystemLogServiceCheck() {
|
||||||
}
|
}
|
||||||
@@ -59,10 +59,10 @@ public final class SystemLogServiceCheck {
|
|||||||
SystemLog orphanedOperator = log(99L, "user.update", "Updated orphaned operator account");
|
SystemLog orphanedOperator = log(99L, "user.update", "Updated orphaned operator account");
|
||||||
orphanedOperator.setOperatorUsername("");
|
orphanedOperator.setOperatorUsername("");
|
||||||
orphanedOperator.setOperatorDisplayName("");
|
orphanedOperator.setOperatorDisplayName("");
|
||||||
require("User #1".equals(orphanedOperator.getOperatorLabel()),
|
require("用户 #1".equals(orphanedOperator.getOperatorLabel()),
|
||||||
"operator id should still render when joined user names are unavailable");
|
"operator id should still render when joined user names are unavailable");
|
||||||
require("administrator".equals(orphanedOperator.getOperatorMetaText()),
|
require("管理员".equals(orphanedOperator.getOperatorMetaText()),
|
||||||
"operator meta should preserve role when names are unavailable");
|
"operator meta should display role when names are unavailable");
|
||||||
|
|
||||||
SystemLog unsafeStatus = log(100L, "user.update", "Unsafe status check");
|
SystemLog unsafeStatus = log(100L, "user.update", "Unsafe status check");
|
||||||
unsafeStatus.setResultStatus("success\" onclick=\"x");
|
unsafeStatus.setResultStatus("success\" onclick=\"x");
|
||||||
|
|||||||
@@ -26,9 +26,9 @@ import java.util.logging.Logger;
|
|||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
public final class UserAccountServiceCheck {
|
public final class UserAccountServiceCheck {
|
||||||
private static final String DENIED_MESSAGE = "You do not have permission to manage users.";
|
private static final String DENIED_MESSAGE = "您无权管理用户。";
|
||||||
private static final String UNAVAILABLE_MESSAGE =
|
private static final String UNAVAILABLE_MESSAGE =
|
||||||
"User management service is temporarily unavailable. Please try again later.";
|
"用户管理服务暂时不可用,请稍后重试。";
|
||||||
|
|
||||||
private UserAccountServiceCheck() {
|
private UserAccountServiceCheck() {
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user