Compare commits

..

10 Commits

Author SHA1 Message Date
Zzzz 3efcb394fb chore: record journal 2026-04-28 11:37:36 +08:00
Zzzz 46efa3b781 chore(task): archive 04-28-chinese-detailed-readme 2026-04-28 11:37:32 +08:00
Zzzz 2d4a7e2cdd add readme 2026-04-28 11:36:41 +08:00
Zzzz 23470ebda3 trellis finish frontend chinese ui 2026-04-28 10:56:18 +08:00
Zzzz ff044e6aab +chinese 2026-04-28 10:53:09 +08:00
Zzzz 5dc91a4e8e add .gitignore 2026-04-28 10:22:17 +08:00
Zzzz 8dc208d77d trellis元数据更改 2026-04-27 23:44:27 +08:00
Zzzz d917a6247c trellis元数据更改 2026-04-27 23:42:38 +08:00
Zzzz 63738f108a 维护入口 2026-04-27 23:38:19 +08:00
Zzzz 4155d5b1ea Trellis 元数据改动 2026-04-27 23:11:28 +08:00
80 changed files with 2326 additions and 599 deletions
+1 -1
View File
@@ -14,7 +14,7 @@ project_doc_fallback_filenames = ["AGENTS.md"]
# Without this flag, hooks.json is ignored and Trellis context won't
# be injected into Codex sessions.
sandbox_mode = "workspace-write"
sandbox_mode = "danger-full-access"
[sandbox_workspace_write]
network_access = true
+3
View File
@@ -1,2 +1,5 @@
# Maven build output
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
```
## 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
### 1. Scope / Trigger
+7
View File
@@ -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,
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
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
@@ -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 should post to Servlet controller endpoints, not directly to DAOs or
@@ -3,7 +3,7 @@
"name": "continue-development",
"title": "brainstorm: 继续开发程序",
"description": "",
"status": "in_progress",
"status": "completed",
"dev_type": null,
"scope": null,
"package": null,
@@ -11,7 +11,7 @@
"creator": "Zzzz",
"assignee": "Zzzz",
"createdAt": "2026-04-27",
"completedAt": null,
"completedAt": "2026-04-27",
"branch": null,
"base_branch": "master",
"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": {}
}
+7 -3
View File
@@ -8,8 +8,8 @@
<!-- @@@auto:current-status -->
- **Active File**: `journal-1.md`
- **Total Sessions**: 6
- **Last Active**: 2026-04-27
- **Total Sessions**: 10
- **Last Active**: 2026-04-28
<!-- @@@/auto:current-status -->
---
@@ -19,7 +19,7 @@
<!-- @@@auto:active-documents -->
| File | Lines | Status |
|------|-------|--------|
| `journal-1.md` | ~275 | Active |
| `journal-1.md` | ~408 | Active |
<!-- @@@/auto:active-documents -->
---
@@ -29,6 +29,10 @@
<!-- @@@auto:session-history -->
| # | 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` |
| 5 | 2026-04-27 | Borrowing circulation management | `7502890` | `master` |
| 4 | 2026-04-27 | Reader information management slice | `eff118e` | `master` |
+133
View File
@@ -273,3 +273,136 @@ Implemented the staff report center with inventory, borrowing, overdue, and popu
### Next Steps
- 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
+325 -14
View File
@@ -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` |
| 构建工具 | MavenWAR 项目 |
| Web 技术 | Servlet 4.0、JSP、JSTL |
| Servlet API | `javax.servlet:javax.servlet-api:4.0.1``provided` |
| JSP 标签库 | `javax.servlet:jstl:1.2` |
| 数据库 | MySQLJDBC 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-8Web 请求通过 `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
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 {
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 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 FLASH_SUCCESS = "flashSuccess";
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 {
String path = request.getServletPath();
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);
return;
}
@@ -50,6 +52,19 @@ public class BookManagementServlet extends HttpServlet {
showEditForm(request, response);
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)) {
response.sendError(HttpServletResponse.SC_NOT_FOUND);
return;
@@ -73,6 +88,18 @@ public class BookManagementServlet extends HttpServlet {
deleteBook(request, response);
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);
}
@@ -107,26 +134,52 @@ public class BookManagementServlet extends HttpServlet {
long id = requiredLong(request.getParameter("id"), -1L);
ServiceResult<Optional<Book>> result = bookService.findBook(id);
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");
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);
}
private void createBook(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
BookForm form = readBookForm(request, false);
if (!form.getErrors().isEmpty()) {
renderForm(request, response, "Create book", "/books", form.getBook(), form.getValues(),
form.getErrors(), "Please correct the highlighted book fields.");
renderForm(request, response, "创建图书", "/books", form.getBook(), form.getValues(),
form.getErrors(), "请修正高亮的图书字段。");
return;
}
ServiceResult<Long> result = bookService.createBook(currentUser(request), form.getBook());
if (!result.isSuccessful()) {
handleFormFailure(request, response, "Create book", "/books", form, result);
handleFormFailure(request, response, "创建图书", "/books", form, result);
return;
}
@@ -137,14 +190,14 @@ public class BookManagementServlet extends HttpServlet {
private void updateBook(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
BookForm form = readBookForm(request, true);
if (!form.getErrors().isEmpty()) {
renderForm(request, response, "Edit book", "/books/update", form.getBook(), form.getValues(),
form.getErrors(), "Please correct the highlighted book fields.");
renderForm(request, response, "编辑图书", "/books/update", form.getBook(), form.getValues(),
form.getErrors(), "请修正高亮的图书字段。");
return;
}
ServiceResult<Void> result = bookService.updateBook(currentUser(request), form.getBook());
if (!result.isSuccessful()) {
handleFormFailure(request, response, "Edit book", "/books/update", form, result);
handleFormFailure(request, response, "编辑图书", "/books/update", form, result);
return;
}
@@ -167,6 +220,60 @@ public class BookManagementServlet extends HttpServlet {
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,
String action, BookForm form, ServiceResult<?> result)
throws ServletException, IOException {
@@ -178,6 +285,17 @@ public class BookManagementServlet extends HttpServlet {
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,
Book book, Map<String, String> formValues, Map<String, String> errors, String errorMessage)
throws ServletException, IOException {
@@ -199,26 +317,41 @@ public class BookManagementServlet extends HttpServlet {
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) {
Map<String, String> values = formValues(request);
Map<String, String> errors = new LinkedHashMap<>();
Book book = new Book();
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.setTitle(values.get("title"));
book.setAuthor(values.get("author"));
book.setCategoryId(parseLong(values.get("categoryId"), "categoryId", "Select a category.", errors));
book.setTotalCopies(parseInt(values.get("totalCopies"), "totalCopies", "Enter a valid total copy count.", errors));
book.setCategoryId(parseLong(values.get("categoryId"), "categoryId", "请选择分类。", errors));
book.setTotalCopies(parseInt(values.get("totalCopies"), "totalCopies", "请输入有效的馆藏总数。", errors));
book.setAvailableCopies(parseInt(values.get("availableCopies"), "availableCopies",
"Enter a valid available copy count.", errors));
"请输入有效的可借数量。", errors));
try {
book.setStatus(BookStatus.fromCode(values.get("status")));
} catch (IllegalArgumentException ex) {
errors.put("status", "Select a status.");
errors.put("status", "请选择状态。");
}
return new BookForm(book, values, errors);
@@ -237,6 +370,27 @@ public class BookManagementServlet extends HttpServlet {
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) {
return new BookSearchCriteria(
request.getParameter("identifier"),
@@ -300,7 +454,7 @@ public class BookManagementServlet extends HttpServlet {
}
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)
@@ -368,4 +522,28 @@ public class BookManagementServlet extends HttpServlet {
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 {
long id = requiredLong(request.getParameter("id"), -1L);
ServiceResult<Void> result = id <= 0
? ServiceResult.failure("Select a valid borrowing record.")
? ServiceResult.failure("请选择有效的借阅记录。")
: borrowingService.returnBook(currentUser(request), id);
redirectWithResult(request, response, result);
}
@@ -124,7 +124,7 @@ public class BorrowingManagementServlet extends HttpServlet {
throws IOException, ServletException {
long id = requiredLong(request.getParameter("id"), -1L);
ServiceResult<Void> result = id <= 0
? ServiceResult.failure("Select a valid borrowing record.")
? ServiceResult.failure("请选择有效的借阅记录。")
: borrowingService.renewLoan(currentUser(request), id);
redirectWithResult(request, response, result);
}
@@ -185,12 +185,12 @@ public class BorrowingManagementServlet extends HttpServlet {
if (result.hasErrors()) {
return result.getErrors().values().iterator().next();
}
return "Borrowing action failed.";
return "借阅操作失败。";
}
private boolean isPermissionDenied(ServiceResult<?> result) {
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)
@@ -20,7 +20,7 @@ import javax.servlet.http.HttpSession;
public class ReaderLoanHistoryServlet extends HttpServlet {
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 HISTORY_DENIED_MESSAGE = "You do not have permission to view loan history.";
private static final String HISTORY_DENIED_MESSAGE = "您无权查看借阅历史。";
private BorrowingServiceImpl borrowingService;
@@ -41,7 +41,7 @@ public class ReaderManagementServlet extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String path = request.getServletPath();
if ("/readers/new".equals(path)) {
renderForm(request, response, "Create reader", "/readers", defaultReader(), Collections.emptyMap(),
renderForm(request, response, "创建读者", "/readers", defaultReader(), Collections.emptyMap(),
Collections.emptyMap(), null);
return;
}
@@ -99,12 +99,12 @@ public class ReaderManagementServlet extends HttpServlet {
long id = requiredLong(request.getParameter("id"), -1L);
ServiceResult<Optional<Reader>> result = readerService.findReader(id);
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");
return;
}
renderForm(request, response, "Edit reader", "/readers/update", result.getData().get(),
renderForm(request, response, "编辑读者", "/readers/update", result.getData().get(),
Collections.emptyMap(), Collections.emptyMap(), null);
}
@@ -112,14 +112,14 @@ public class ReaderManagementServlet extends HttpServlet {
throws ServletException, IOException {
ReaderForm form = readReaderForm(request, false);
if (!form.getErrors().isEmpty()) {
renderForm(request, response, "Create reader", "/readers", form.getReader(), form.getValues(),
form.getErrors(), "Please correct the highlighted reader fields.");
renderForm(request, response, "创建读者", "/readers", form.getReader(), form.getValues(),
form.getErrors(), "请修正高亮的读者字段。");
return;
}
ServiceResult<Long> result = readerService.createReader(currentUser(request), form.getReader());
if (!result.isSuccessful()) {
handleFormFailure(request, response, "Create reader", "/readers", form, result);
handleFormFailure(request, response, "创建读者", "/readers", form, result);
return;
}
@@ -131,14 +131,14 @@ public class ReaderManagementServlet extends HttpServlet {
throws ServletException, IOException {
ReaderForm form = readReaderForm(request, true);
if (!form.getErrors().isEmpty()) {
renderForm(request, response, "Edit reader", "/readers/update", form.getReader(), form.getValues(),
form.getErrors(), "Please correct the highlighted reader fields.");
renderForm(request, response, "编辑读者", "/readers/update", form.getReader(), form.getValues(),
form.getErrors(), "请修正高亮的读者字段。");
return;
}
ServiceResult<Void> result = readerService.updateReader(currentUser(request), form.getReader());
if (!result.isSuccessful()) {
handleFormFailure(request, response, "Edit reader", "/readers/update", form, result);
handleFormFailure(request, response, "编辑读者", "/readers/update", form, result);
return;
}
@@ -195,21 +195,21 @@ public class ReaderManagementServlet extends HttpServlet {
Reader reader = new Reader();
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.setUserId(optionalPositiveLong(values.get("userId"), "userId",
"Enter a valid linked account ID.", errors));
"请输入有效的关联账户 ID", errors));
reader.setFullName(values.get("fullName"));
reader.setPhone(values.get("phone"));
reader.setEmail(values.get("email"));
reader.setMaxBorrowCount(parseInt(values.get("maxBorrowCount"), "maxBorrowCount",
"Enter a valid max borrow count.", errors));
"请输入有效的最大借阅数量。", errors));
try {
reader.setStatus(ReaderStatus.fromCode(values.get("status")));
} catch (IllegalArgumentException ex) {
errors.put("status", "Select a status.");
errors.put("status", "请选择状态。");
}
return new ReaderForm(reader, values, errors);
@@ -304,7 +304,7 @@ public class ReaderManagementServlet extends HttpServlet {
}
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)
@@ -46,7 +46,7 @@ public class ReportServlet extends HttpServlet {
private boolean isPermissionDenied(ServiceResult<?> result) {
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)
@@ -14,14 +14,14 @@ public class RoleAreaServlet extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String servletPath = request.getServletPath();
if (servletPath.startsWith("/admin")) {
request.setAttribute("areaName", "Administration");
request.setAttribute("areaSummary", "Account, role, permission, and system-maintenance entry point.");
request.setAttribute("areaName", "系统管理");
request.setAttribute("areaSummary", "账户、角色、权限和系统维护入口。");
} else if (servletPath.startsWith("/librarian")) {
request.setAttribute("areaName", "Librarian Workspace");
request.setAttribute("areaSummary", "Book, reader, borrowing, return, renewal, and overdue entry point.");
request.setAttribute("areaName", "馆员工作台");
request.setAttribute("areaSummary", "图书、读者、借阅、归还、续借和逾期处理入口。");
} else {
request.setAttribute("areaName", "Reader Center");
request.setAttribute("areaSummary", "Catalog search and reader self-service entry point.");
request.setAttribute("areaName", "读者中心");
request.setAttribute("areaSummary", "馆藏检索和读者自助服务入口。");
}
request.getRequestDispatcher(ROLE_HOME_JSP).forward(request, response);
@@ -21,7 +21,7 @@ import javax.servlet.http.HttpSession;
public class SystemLogServlet extends HttpServlet {
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 DENIED_MESSAGE = "You do not have permission to view system logs.";
private static final String DENIED_MESSAGE = "您无权查看系统日志。";
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 FLASH_SUCCESS = "flashSuccess";
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;
@@ -44,7 +44,7 @@ public class UserManagementServlet extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String path = request.getServletPath();
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);
return;
}
@@ -108,12 +108,12 @@ public class UserManagementServlet extends HttpServlet {
return;
}
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");
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);
}
@@ -121,15 +121,15 @@ public class UserManagementServlet extends HttpServlet {
throws ServletException, IOException {
UserForm form = readUserForm(request, false);
if (!form.getErrors().isEmpty()) {
renderForm(request, response, "Create user account", "/admin/users", form.getUser(), form.getValues(),
form.getErrors(), "Please correct the highlighted account fields.");
renderForm(request, response, "创建用户账户", "/admin/users", form.getUser(), form.getValues(),
form.getErrors(), "请修正高亮的账户字段。");
return;
}
ServiceResult<Long> result = userAccountService.createUser(currentUser(request), form.getUser(),
form.getPassword(), clientIp(request));
if (!result.isSuccessful()) {
handleFormFailure(request, response, "Create user account", "/admin/users", form, result);
handleFormFailure(request, response, "创建用户账户", "/admin/users", form, result);
return;
}
@@ -141,15 +141,15 @@ public class UserManagementServlet extends HttpServlet {
throws ServletException, IOException {
UserForm form = readUserForm(request, true);
if (!form.getErrors().isEmpty()) {
renderForm(request, response, "Edit user account", "/admin/users/update", form.getUser(), form.getValues(),
form.getErrors(), "Please correct the highlighted account fields.");
renderForm(request, response, "编辑用户账户", "/admin/users/update", form.getUser(), form.getValues(),
form.getErrors(), "请修正高亮的账户字段。");
return;
}
ServiceResult<Void> result = userAccountService.updateUser(currentUser(request), form.getUser(),
form.getPassword(), clientIp(request));
if (!result.isSuccessful()) {
handleFormFailure(request, response, "Edit user account", "/admin/users/update", form, result);
handleFormFailure(request, response, "编辑用户账户", "/admin/users/update", form, result);
return;
}
@@ -206,7 +206,7 @@ public class UserManagementServlet extends HttpServlet {
User user = new User();
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.setDisplayName(values.get("displayName"));
@@ -214,7 +214,7 @@ public class UserManagementServlet extends HttpServlet {
try {
user.setRole(Role.fromCode(values.get("role")));
} catch (IllegalArgumentException ex) {
errors.put("role", "Select a role.");
errors.put("role", "请选择角色。");
}
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)) {
return false;
}
errors.put("active", "Select an active state.");
errors.put("active", "请选择启用状态。");
return false;
}
@@ -10,6 +10,18 @@ import java.util.Optional;
public interface BookDao {
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);
Optional<Book> findById(long id);
@@ -33,6 +33,32 @@ public class JdbcBookDao implements BookDao {
+ "FROM book_categories "
+ "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_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
public List<Book> search(BookSearchCriteria criteria) {
List<Object> parameters = new ArrayList<>();
@@ -194,6 +300,11 @@ public class JdbcBookDao implements BookDao {
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 {
Book book = new Book();
book.setId(resultSet.getLong("id"));
@@ -3,9 +3,9 @@ package com.mzh.library.entity;
import java.util.Locale;
public enum BookStatus {
AVAILABLE("available", "Available"),
UNAVAILABLE("unavailable", "Unavailable"),
ARCHIVED("archived", "Archived");
AVAILABLE("available", "可借"),
UNAVAILABLE("unavailable", "不可借"),
ARCHIVED("archived", "已归档");
private final String code;
private final String displayName;
@@ -145,7 +145,7 @@ public class BorrowRecord {
}
public String getDisplayStatusName() {
return isOverdue() ? "Overdue" : status.getDisplayName();
return isOverdue() ? "逾期" : status.getDisplayName();
}
public String getBorrowedAtText() {
@@ -3,8 +3,8 @@ package com.mzh.library.entity;
import java.util.Locale;
public enum BorrowRecordStatus {
ACTIVE("active", "Active"),
RETURNED("returned", "Returned");
ACTIVE("active", "借阅中"),
RETURNED("returned", "已归还");
private final String code;
private final String displayName;
@@ -3,9 +3,9 @@ package com.mzh.library.entity;
import java.util.Locale;
public enum ReaderStatus {
ACTIVE("active", "Active"),
SUSPENDED("suspended", "Suspended"),
INACTIVE("inactive", "Inactive");
ACTIVE("active", "正常"),
SUSPENDED("suspended", "暂停"),
INACTIVE("inactive", "停用");
private final String code;
private final String displayName;
@@ -3,9 +3,9 @@ package com.mzh.library.entity;
import java.util.Locale;
public enum Role {
ADMINISTRATOR("administrator", "Administrator"),
LIBRARIAN("librarian", "Librarian"),
READER("reader", "Reader");
ADMINISTRATOR("administrator", "管理员"),
LIBRARIAN("librarian", "馆员"),
READER("reader", "读者");
private final String code;
private final String displayName;
@@ -131,7 +131,7 @@ public class SystemLog {
return username;
}
return operatorId == null ? "System" : "User #" + operatorId;
return operatorId == null ? "系统" : "用户 #" + operatorId;
}
public String getOperatorMetaText() {
@@ -144,7 +144,7 @@ public class SystemLog {
if (operatorId != null && (!displayName.isEmpty() || !username.isEmpty())) {
appendMeta(meta, "#" + operatorId);
}
appendMeta(meta, trim(operatorRole));
appendMeta(meta, displayRole(operatorRole));
return meta.toString();
}
@@ -157,8 +157,22 @@ public class SystemLog {
}
public String getResultStatusName() {
String trimmed = trim(resultStatus);
return trimmed.isEmpty() ? "Unknown" : trimmed;
String normalized = trim(resultStatus).toLowerCase(Locale.ROOT);
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) {
@@ -174,4 +188,16 @@ public class SystemLog {
private String trim(String value) {
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() {
return active ? "Active" : "Inactive";
return active ? "启用" : "停用";
}
public String getCreatedAtText() {
@@ -29,6 +29,7 @@ public class AuthorizationFilter implements Filter {
new PathRule("/admin/system-logs", Permission.VIEW_SYSTEM_LOGS),
new PathRule("/reports", Permission.VIEW_REPORTS),
new PathRule("/borrowing", Permission.MANAGE_BORROWING),
new PathRule("/book-categories", Permission.MANAGE_BOOKS),
new PathRule("/books", Permission.MANAGE_BOOKS),
new PathRule("/readers", Permission.MANAGE_READERS),
new PathRule("/catalog", Permission.VIEW_CATALOG),
@@ -61,7 +62,7 @@ public class AuthorizationFilter implements Filter {
logDeniedAccess(user, requiredRule, path);
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);
}
@@ -11,6 +11,14 @@ import java.util.Optional;
public interface BookService {
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<Optional<Book>> findBook(long id);
@@ -17,9 +17,9 @@ import java.util.logging.Logger;
public class AuthServiceImpl implements AuthService {
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 INVALID_MESSAGE = "Invalid username or password.";
private static final String UNAVAILABLE_MESSAGE = "Login service is temporarily unavailable. Please try again later.";
private static final String REQUIRED_MESSAGE = "请输入用户名和密码。";
private static final String INVALID_MESSAGE = "用户名或密码不正确。";
private static final String UNAVAILABLE_MESSAGE = "登录服务暂时不可用,请稍后重试。";
private final UserDao userDao;
private final PermissionPolicy permissionPolicy;
@@ -21,9 +21,10 @@ import java.util.logging.Logger;
public class BookServiceImpl implements BookService {
private static final Logger LOGGER = Logger.getLogger(BookServiceImpl.class.getName());
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 DENIED_MESSAGE = "You do not have permission to manage books.";
"图书服务暂时不可用,请稍后重试。";
private static final String VALIDATION_MESSAGE = "请修正高亮的图书字段。";
private static final String CATEGORY_VALIDATION_MESSAGE = "请修正高亮的分类字段。";
private static final String DENIED_MESSAGE = "您无权管理图书。";
private final BookDao bookDao;
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
public ServiceResult<List<Book>> searchBooks(BookSearchCriteria criteria) {
BookSearchCriteria normalized = criteria == null ? new BookSearchCriteria() : criteria;
if (normalized.getCategoryId() != null && normalized.getCategoryId() <= 0) {
Map<String, String> errors = new LinkedHashMap<>();
errors.put("categoryId", "Select a valid category.");
return ServiceResult.validationFailure("Please correct the catalog search filters.", errors);
errors.put("categoryId", "请选择有效的分类。");
return ServiceResult.validationFailure("请修正馆藏检索筛选条件。", errors);
}
try {
@@ -67,7 +173,7 @@ public class BookServiceImpl implements BookService {
@Override
public ServiceResult<Optional<Book>> findBook(long id) {
if (id <= 0) {
return ServiceResult.failure("Select a valid book.");
return ServiceResult.failure("请选择有效的图书。");
}
try {
@@ -92,13 +198,13 @@ public class BookServiceImpl implements BookService {
try {
if (bookDao.findByIdentifier(book.getIdentifier()).isPresent()) {
errors.put("identifier", "Book identifier is already in use.");
errors.put("identifier", "图书编号已被使用。");
return ServiceResult.validationFailure(VALIDATION_MESSAGE, errors);
}
long id = bookDao.create(book);
LOGGER.info("Created book id=" + id + " actorId=" + actor.getId());
return ServiceResult.success(id, "Book created.");
return ServiceResult.success(id, "图书已创建。");
} catch (DaoException ex) {
LOGGER.log(Level.SEVERE, "Unable to create book actorId=" + actor.getId(), ex);
return ServiceResult.failure(UNAVAILABLE_MESSAGE);
@@ -120,16 +226,16 @@ public class BookServiceImpl implements BookService {
try {
Optional<Book> existingWithIdentifier = bookDao.findByIdentifier(book.getIdentifier());
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);
}
if (!bookDao.update(book)) {
return ServiceResult.failure("Book was not found.");
return ServiceResult.failure("未找到图书。");
}
LOGGER.info("Updated book id=" + book.getId() + " actorId=" + actor.getId());
return ServiceResult.success(null, "Book updated.");
return ServiceResult.success(null, "图书已更新。");
} catch (DaoException ex) {
LOGGER.log(Level.SEVERE, "Unable to update book id=" + book.getId() + " actorId=" + actor.getId(), ex);
return ServiceResult.failure(UNAVAILABLE_MESSAGE);
@@ -142,16 +248,16 @@ public class BookServiceImpl implements BookService {
return ServiceResult.failure(DENIED_MESSAGE);
}
if (id <= 0) {
return ServiceResult.failure("Select a valid book.");
return ServiceResult.failure("请选择有效的图书。");
}
try {
if (!bookDao.delete(id)) {
return ServiceResult.failure("Book was not found.");
return ServiceResult.failure("未找到图书。");
}
LOGGER.info("Deleted book id=" + id + " actorId=" + actor.getId());
return ServiceResult.success(null, "Book deleted.");
return ServiceResult.success(null, "图书已删除。");
} catch (DaoException ex) {
LOGGER.log(Level.SEVERE, "Unable to delete book id=" + id + " actorId=" + actor.getId(), ex);
return ServiceResult.failure(UNAVAILABLE_MESSAGE);
@@ -171,44 +277,73 @@ public class BookServiceImpl implements BookService {
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) {
Map<String, String> errors = new LinkedHashMap<>();
if (book == null) {
errors.put("book", "Book details are required.");
errors.put("book", "请填写图书详情。");
return errors;
}
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, "title", book.getTitle(), "Title", 200);
requireLength(errors, "author", book.getAuthor(), "Author", 120);
requireLength(errors, "identifier", book.getIdentifier(), "图书编号", 64);
requireLength(errors, "title", book.getTitle(), "书名", 200);
requireLength(errors, "author", book.getAuthor(), "作者", 120);
if (book.getCategoryId() <= 0) {
errors.put("categoryId", "Select a category.");
errors.put("categoryId", "请选择分类。");
}
if (book.getTotalCopies() < 0) {
errors.put("totalCopies", "Total copies cannot be negative.");
errors.put("totalCopies", "馆藏总数不能为负数。");
}
if (book.getAvailableCopies() < 0) {
errors.put("availableCopies", "Available copies cannot be negative.");
errors.put("availableCopies", "可借数量不能为负数。");
}
if (book.getAvailableCopies() > book.getTotalCopies()) {
errors.put("availableCopies", "Available copies cannot exceed total copies.");
errors.put("availableCopies", "可借数量不能超过馆藏总数。");
}
if (book.getStatus() == null) {
errors.put("status", "Select a status.");
errors.put("status", "请选择状态。");
}
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) {
if (value == null || value.isEmpty()) {
errors.put(field, label + " is required.");
errors.put(field, "请填写" + label + "");
return;
}
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 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 DENIED_MESSAGE = "You do not have permission to manage borrowing.";
private static final String HISTORY_DENIED_MESSAGE = "You do not have permission to view loan history.";
"借阅服务暂时不可用,请稍后重试。";
private static final String VALIDATION_MESSAGE = "请修正高亮的借阅字段。";
private static final String DENIED_MESSAGE = "您无权管理借阅。";
private static final String HISTORY_DENIED_MESSAGE = "您无权查看借阅历史。";
private static final int LOAN_DAYS = 14;
private static final int MAX_RENEWALS = 1;
@@ -68,7 +68,7 @@ public class BorrowingServiceImpl implements BorrowingService {
BorrowRecordSearchCriteria normalized = criteria == null ? new BorrowRecordSearchCriteria() : criteria;
Map<String, String> errors = validateSearch(normalized);
if (!errors.isEmpty()) {
return ServiceResult.validationFailure("Please correct the borrowing search filters.", errors);
return ServiceResult.validationFailure("请修正借阅检索筛选条件。", errors);
}
try {
@@ -98,13 +98,13 @@ public class BorrowingServiceImpl implements BorrowingService {
Optional<Reader> readerResult = borrowRecordDao.findReaderByIdentifierForUpdate(connection,
normalizedReaderIdentifier);
if (!readerResult.isPresent()) {
transactionErrors.put("readerIdentifier", "Reader was not found.");
transactionErrors.put("readerIdentifier", "未找到读者。");
}
Optional<Book> bookResult = borrowRecordDao.findBookByIdentifierForUpdate(connection,
normalizedBookIdentifier);
if (!bookResult.isPresent()) {
transactionErrors.put("bookIdentifier", "Book was not found.");
transactionErrors.put("bookIdentifier", "未找到图书。");
}
if (!transactionErrors.isEmpty()) {
@@ -134,7 +134,7 @@ public class BorrowingServiceImpl implements BorrowingService {
LOGGER.info("Borrowed book recordId=" + id + " readerId=" + reader.getId()
+ " bookId=" + book.getId() + " actorId=" + actor.getId());
return ServiceResult.success(id, "Book borrowed.");
return ServiceResult.success(id, "图书已借出。");
});
} catch (DaoException ex) {
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);
}
if (recordId <= 0) {
return ServiceResult.failure("Select a valid borrowing record.");
return ServiceResult.failure("请选择有效的借阅记录。");
}
try {
return transactionExecutor.execute(connection -> {
Optional<BorrowRecord> recordResult = borrowRecordDao.findByIdForUpdate(connection, recordId);
if (!recordResult.isPresent()) {
return ServiceResult.failure("Borrowing record was not found.");
return ServiceResult.failure("未找到借阅记录。");
}
BorrowRecord record = recordResult.get();
Map<String, String> errors = validateActiveLoan(record);
if (!errors.isEmpty()) {
return ServiceResult.validationFailure("Borrowing record cannot be returned.", errors);
return ServiceResult.validationFailure("借阅记录不能归还。", errors);
}
if (!borrowRecordDao.markReturned(connection, recordId, now())) {
@@ -172,7 +172,7 @@ public class BorrowingServiceImpl implements BorrowingService {
borrowRecordDao.incrementAvailableCopies(connection, record.getBookId());
LOGGER.info("Returned borrow recordId=" + recordId + " actorId=" + actor.getId());
return ServiceResult.success(null, "Book returned.");
return ServiceResult.success(null, "图书已归还。");
});
} catch (DaoException 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);
}
if (recordId <= 0) {
return ServiceResult.failure("Select a valid borrowing record.");
return ServiceResult.failure("请选择有效的借阅记录。");
}
try {
return transactionExecutor.execute(connection -> {
Optional<BorrowRecord> recordResult = borrowRecordDao.findByIdForUpdate(connection, recordId);
if (!recordResult.isPresent()) {
return ServiceResult.failure("Borrowing record was not found.");
return ServiceResult.failure("未找到借阅记录。");
}
BorrowRecord record = recordResult.get();
Map<String, String> errors = validateActiveLoan(record);
if (record.getRenewalCount() >= MAX_RENEWALS) {
errors.put("renewalCount", "This loan has already reached the renewal limit.");
errors.put("renewalCount", "该借阅已达到续借次数上限。");
}
if (!errors.isEmpty()) {
return ServiceResult.validationFailure("Borrowing record cannot be renewed.", errors);
return ServiceResult.validationFailure("借阅记录不能续借。", errors);
}
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());
return ServiceResult.success(null, "Loan renewed.");
return ServiceResult.success(null, "借阅已续借。");
});
} catch (DaoException 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 {
Optional<Reader> readerResult = borrowRecordDao.findReaderByUserId(actor.getId());
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()));
@@ -246,16 +246,16 @@ public class BorrowingServiceImpl implements BorrowingService {
private void validateBorrowEligibility(Map<String, String> errors, Reader reader, Book book,
java.sql.Connection connection) {
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());
if (activeLoans >= reader.getMaxBorrowCount()) {
errors.put("readerIdentifier", "Reader has reached the active borrowing limit.");
errors.put("readerIdentifier", "读者已达到在借数量上限。");
}
if (book.getStatus() != BookStatus.AVAILABLE) {
errors.put("bookIdentifier", "Book status does not allow borrowing.");
errors.put("bookIdentifier", "图书状态不允许借阅。");
} 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 {
BorrowRecordStatus.fromCode(statusCode);
} catch (IllegalArgumentException ex) {
errors.put("status", "Select a valid borrowing status.");
errors.put("status", "请选择有效的借阅状态。");
}
}
return errors;
@@ -278,26 +278,26 @@ public class BorrowingServiceImpl implements BorrowingService {
private Map<String, String> validateBorrowIdentifiers(String readerIdentifier, String bookIdentifier) {
Map<String, String> errors = new LinkedHashMap<>();
requireLength(errors, "readerIdentifier", readerIdentifier, "Reader ID", 64);
requireLength(errors, "bookIdentifier", bookIdentifier, "Book ID", 64);
requireLength(errors, "readerIdentifier", readerIdentifier, "读者编号", 64);
requireLength(errors, "bookIdentifier", bookIdentifier, "图书编号", 64);
return errors;
}
private Map<String, String> validateActiveLoan(BorrowRecord record) {
Map<String, String> errors = new LinkedHashMap<>();
if (record.getStatus() != BorrowRecordStatus.ACTIVE || record.getReturnedAt() != null) {
errors.put("status", "Only active loans can use this action.");
errors.put("status", "只有借阅中的记录可以执行此操作。");
}
return errors;
}
private void requireLength(Map<String, String> errors, String field, String value, String label, int maxLength) {
if (value == null || value.isEmpty()) {
errors.put(field, label + " is required.");
errors.put(field, "请填写" + label + "");
return;
}
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 {
private static final Logger LOGGER = Logger.getLogger(ReaderServiceImpl.class.getName());
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 SEARCH_VALIDATION_MESSAGE = "Please correct the reader search filters.";
private static final String DENIED_MESSAGE = "You do not have permission to manage readers.";
"读者服务暂时不可用,请稍后重试。";
private static final String VALIDATION_MESSAGE = "请修正高亮的读者字段。";
private static final String SEARCH_VALIDATION_MESSAGE = "请修正读者检索筛选条件。";
private static final String DENIED_MESSAGE = "您无权管理读者。";
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 EMAIL_PATTERN = Pattern.compile("^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$");
@@ -61,7 +61,7 @@ public class ReaderServiceImpl implements ReaderService {
@Override
public ServiceResult<Optional<Reader>> findReader(long id) {
if (id <= 0) {
return ServiceResult.failure("Select a valid reader.");
return ServiceResult.failure("请选择有效的读者。");
}
try {
@@ -86,17 +86,17 @@ public class ReaderServiceImpl implements ReaderService {
try {
if (readerDao.findByIdentifier(reader.getIdentifier()).isPresent()) {
errors.put("identifier", "Reader identifier is already in use.");
errors.put("identifier", "读者编号已被使用。");
return ServiceResult.validationFailure(VALIDATION_MESSAGE, errors);
}
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);
}
long id = readerDao.create(reader);
LOGGER.info("Created reader id=" + id + " actorId=" + actor.getId());
return ServiceResult.success(id, "Reader profile created.");
return ServiceResult.success(id, "读者档案已创建。");
} catch (DaoException ex) {
LOGGER.log(Level.SEVERE, "Unable to create reader actorId=" + actor.getId(), ex);
return ServiceResult.failure(UNAVAILABLE_MESSAGE);
@@ -118,23 +118,23 @@ public class ReaderServiceImpl implements ReaderService {
try {
Optional<Reader> existingWithIdentifier = readerDao.findByIdentifier(reader.getIdentifier());
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);
}
if (reader.getUserId() != null) {
Optional<Reader> existingWithUser = readerDao.findByUserId(reader.getUserId());
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);
}
}
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());
return ServiceResult.success(null, "Reader profile updated.");
return ServiceResult.success(null, "读者档案已更新。");
} catch (DaoException ex) {
LOGGER.log(Level.SEVERE, "Unable to update reader id=" + reader.getId() + " actorId=" + actor.getId(), ex);
return ServiceResult.failure(UNAVAILABLE_MESSAGE);
@@ -147,16 +147,16 @@ public class ReaderServiceImpl implements ReaderService {
return ServiceResult.failure(DENIED_MESSAGE);
}
if (id <= 0) {
return ServiceResult.failure("Select a valid reader.");
return ServiceResult.failure("请选择有效的读者。");
}
try {
if (!readerDao.deactivate(id)) {
return ServiceResult.failure("Reader profile was not found.");
return ServiceResult.failure("未找到读者档案。");
}
LOGGER.info("Deactivated reader id=" + id + " actorId=" + actor.getId());
return ServiceResult.success(null, "Reader profile deactivated.");
return ServiceResult.success(null, "读者档案已停用。");
} catch (DaoException ex) {
LOGGER.log(Level.SEVERE, "Unable to deactivate reader id=" + id + " actorId=" + actor.getId(), ex);
return ServiceResult.failure(UNAVAILABLE_MESSAGE);
@@ -183,7 +183,7 @@ public class ReaderServiceImpl implements ReaderService {
try {
criteria.setStatusCode(ReaderStatus.fromCode(criteria.getStatusCode()).getCode());
} catch (IllegalArgumentException ex) {
errors.put("status", "Select a valid status.");
errors.put("status", "请选择有效的状态。");
}
}
return errors;
@@ -192,24 +192,24 @@ public class ReaderServiceImpl implements ReaderService {
private Map<String, String> validate(Reader reader, boolean requireId) {
Map<String, String> errors = new LinkedHashMap<>();
if (reader == null) {
errors.put("reader", "Reader details are required.");
errors.put("reader", "请填写读者详情。");
return errors;
}
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, "fullName", reader.getFullName(), "Full name", 100);
requireLength(errors, "identifier", reader.getIdentifier(), "读者编号", 64);
requireLength(errors, "fullName", reader.getFullName(), "姓名", 100);
if (reader.getUserId() != null && reader.getUserId() <= 0) {
errors.put("userId", "Linked account ID must be positive.");
errors.put("userId", "关联账户 ID 必须为正数。");
}
validateContact(errors, reader);
if (reader.getStatus() == null) {
errors.put("status", "Select a status.");
errors.put("status", "请选择状态。");
}
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;
}
@@ -218,24 +218,24 @@ public class ReaderServiceImpl implements ReaderService {
String phone = reader.getPhone();
String email = reader.getEmail();
if ((phone == null || phone.isEmpty()) && (email == null || email.isEmpty())) {
errors.put("phone", "Phone or email is required.");
errors.put("phone", "请填写电话或邮箱。");
return;
}
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()) {
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) {
if (value == null || value.isEmpty()) {
errors.put(field, label + " is required.");
errors.put(field, "请填写" + label + "");
return;
}
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 {
private static final Logger LOGGER = Logger.getLogger(ReportServiceImpl.class.getName());
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 final ReportDao reportDao;
@@ -20,9 +20,9 @@ import java.util.logging.Logger;
public class SystemLogServiceImpl implements SystemLogService {
private static final Logger LOGGER = Logger.getLogger(SystemLogServiceImpl.class.getName());
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 VALIDATION_MESSAGE = "Please correct the system log search filters.";
"系统日志服务暂时不可用,请稍后重试。";
private static final String DENIED_MESSAGE = "您无权查看系统日志。";
private static final String VALIDATION_MESSAGE = "请修正系统日志检索筛选条件。";
private final SystemLogDao systemLogDao;
private final PermissionPolicy permissionPolicy;
@@ -62,18 +62,18 @@ public class SystemLogServiceImpl implements SystemLogService {
private Map<String, String> validate(SystemLogSearchCriteria criteria) {
Map<String, String> errors = new LinkedHashMap<>();
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) {
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.getCreatedToText(), "createdTo", "End date", errors, criteria, false);
parseDate(criteria.getCreatedFromText(), "createdFrom", "开始日期", errors, criteria, true);
parseDate(criteria.getCreatedToText(), "createdTo", "结束日期", errors, criteria, false);
if (criteria.getCreatedFrom() != null
&& criteria.getCreatedTo() != null
&& criteria.getCreatedFrom().isAfter(criteria.getCreatedTo())) {
errors.put("createdTo", "End date must be on or after start date.");
errors.put("createdTo", "结束日期必须晚于或等于开始日期。");
}
return errors;
}
@@ -91,7 +91,7 @@ public class SystemLogServiceImpl implements SystemLogService {
criteria.setCreatedTo(parsed);
}
} 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 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 SEARCH_VALIDATION_MESSAGE = "Please correct the account search filters.";
private static final String DENIED_MESSAGE = "You do not have permission to manage users.";
private static final String SELF_DEACTIVATE_MESSAGE = "You cannot deactivate your own administrator account.";
private static final String SELF_ROLE_MESSAGE = "You cannot change your own administrator role.";
"用户管理服务暂时不可用,请稍后重试。";
private static final String VALIDATION_MESSAGE = "请修正高亮的账户字段。";
private static final String SEARCH_VALIDATION_MESSAGE = "请修正账户检索筛选条件。";
private static final String DENIED_MESSAGE = "您无权管理用户。";
private static final String SELF_DEACTIVATE_MESSAGE = "不能停用您自己的管理员账户。";
private static final String SELF_ROLE_MESSAGE = "不能修改您自己的管理员角色。";
private final UserAccountDao userAccountDao;
private final SystemLogDao systemLogDao;
@@ -80,7 +80,7 @@ public class UserAccountServiceImpl implements UserAccountService {
return ServiceResult.failure(DENIED_MESSAGE);
}
if (id <= 0) {
return ServiceResult.failure("Select a valid user account.");
return ServiceResult.failure("请选择有效的用户账户。");
}
try {
@@ -105,7 +105,7 @@ public class UserAccountServiceImpl implements UserAccountService {
try {
if (userAccountDao.findByUsername(user.getUsername()).isPresent()) {
errors.put("username", "Username is already in use.");
errors.put("username", "用户名已被使用。");
return ServiceResult.validationFailure(VALIDATION_MESSAGE, errors);
}
@@ -113,10 +113,10 @@ public class UserAccountServiceImpl implements UserAccountService {
return transactionExecutor.execute(connection -> {
long id = userAccountDao.create(connection, user);
systemLogDao.create(connection, auditLog(actor, "user.create", id,
"Created account username=" + user.getUsername() + " role=" + user.getRole().getCode(),
"创建账户:用户名=" + user.getUsername() + ",角色=" + user.getRole().getDisplayName(),
requestIp));
LOGGER.info("Created user id=" + id + " actorId=" + actor.getId());
return ServiceResult.success(id, "User account created.");
return ServiceResult.success(id, "用户账户已创建。");
});
} catch (DaoException | IllegalStateException ex) {
LOGGER.log(Level.SEVERE, "Unable to create user actorId=" + actor.getId()
@@ -140,7 +140,7 @@ public class UserAccountServiceImpl implements UserAccountService {
try {
Optional<User> existingResult = userAccountDao.findById(user.getId());
if (!existingResult.isPresent()) {
return ServiceResult.failure("User account was not found.");
return ServiceResult.failure("未找到用户账户。");
}
protectCurrentAdministrator(actor, user, errors);
@@ -158,15 +158,15 @@ public class UserAccountServiceImpl implements UserAccountService {
final boolean passwordChanged = updatePassword;
return transactionExecutor.execute(connection -> {
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(),
"Updated account username=" + user.getUsername() + " role=" + user.getRole().getCode()
+ " active=" + user.isActive()
+ (passwordChanged ? " passwordReset=true" : ""),
"更新账户:用户名=" + user.getUsername() + ",角色=" + user.getRole().getDisplayName()
+ ",状态=" + (user.isActive() ? "启用" : "停用")
+ (passwordChanged ? ",已重置密码" : ""),
requestIp));
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) {
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);
}
if (id <= 0) {
return ServiceResult.failure("Select a valid user account.");
return ServiceResult.failure("请选择有效的用户账户。");
}
if (actor.getId() == id) {
Map<String, String> errors = new LinkedHashMap<>();
@@ -191,20 +191,20 @@ public class UserAccountServiceImpl implements UserAccountService {
try {
Optional<User> existingResult = userAccountDao.findById(id);
if (!existingResult.isPresent()) {
return ServiceResult.failure("User account was not found.");
return ServiceResult.failure("未找到用户账户。");
}
User user = existingResult.get();
user.setActive(false);
return transactionExecutor.execute(connection -> {
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,
"Deactivated account username=" + user.getUsername(),
"停用账户:用户名=" + user.getUsername(),
requestIp));
LOGGER.info("Deactivated user id=" + id + " actorId=" + actor.getId());
return ServiceResult.success(null, "User account deactivated.");
return ServiceResult.success(null, "用户账户已停用。");
});
} catch (DaoException 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 {
criteria.setRoleCode(Role.fromCode(criteria.getRoleCode()).getCode());
} 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()
&& !UserSearchCriteria.ACTIVE_STATUS.equals(activeStatus)
&& !UserSearchCriteria.INACTIVE_STATUS.equals(activeStatus)) {
errors.put("active", "Select a valid active state.");
errors.put("active", "请选择有效的启用状态。");
}
return errors;
}
@@ -234,19 +234,19 @@ public class UserAccountServiceImpl implements UserAccountService {
private Map<String, String> validateUser(User user, boolean requireId, String password, boolean requirePassword) {
Map<String, String> errors = new LinkedHashMap<>();
if (user == null) {
errors.put("user", "User account details are required.");
errors.put("user", "请填写用户账户详情。");
return errors;
}
if (requireId && user.getId() <= 0) {
errors.put("id", "Select a valid user account.");
errors.put("id", "请选择有效的用户账户。");
}
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) {
errors.put("role", "Select a role.");
errors.put("role", "请选择角色。");
}
validatePassword(errors, password, requirePassword);
return errors;
@@ -256,12 +256,12 @@ public class UserAccountServiceImpl implements UserAccountService {
String trimmed = password == null ? "" : password.trim();
if (trimmed.isEmpty()) {
if (required) {
errors.put("password", "Password is required.");
errors.put("password", "请填写密码。");
}
return;
}
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) {
if (value == null || value.isEmpty()) {
errors.put(field, label + " is required.");
errors.put(field, "请填写" + label + "");
return;
}
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="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
<!doctype html>
<html lang="en">
<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 Library</title>
<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="user-form-title">
<p class="eyebrow">Administration</p>
<p class="eyebrow">系统管理</p>
<h1 id="user-form-title"><c:out value="${formTitle}" /></h1>
<c:if test="${not empty errorMessage}">
@@ -36,7 +36,7 @@
<div class="form-grid">
<div class="form-field">
<label for="username">Username</label>
<label for="username">用户名</label>
<c:choose>
<c:when test="${user.id > 0}">
<input id="username" type="text" value="${fn:escapeXml(usernameValue)}" disabled>
@@ -51,7 +51,7 @@
</div>
<div class="form-field">
<label for="displayName">Display name</label>
<label for="displayName">显示名称</label>
<input id="displayName" name="displayName" type="text"
value="${fn:escapeXml(displayNameValue)}" required>
<c:if test="${not empty errors.displayName}">
@@ -60,9 +60,9 @@
</div>
<div class="form-field">
<label for="role">Role</label>
<label for="role">角色</label>
<select id="role" name="role" required>
<option value="">Select role</option>
<option value="">请选择角色</option>
<c:forEach var="role" items="${roles}">
<option value="${role.code}" <c:if test="${roleValue == role.code}">selected</c:if>>
<c:out value="${role.displayName}" />
@@ -75,13 +75,13 @@
</div>
<div class="form-field">
<label for="active">Active state</label>
<label for="active">启用状态</label>
<select id="active" name="active" required>
<option value="true" <c:if test="${activeValue == true or activeValue == 'true'}">selected</c:if>>
Active
启用
</option>
<option value="false" <c:if test="${activeValue == false or activeValue == 'false'}">selected</c:if>>
Inactive
停用
</option>
</select>
<c:if test="${not empty errors.active}">
@@ -92,8 +92,8 @@
<div class="form-field">
<label for="password">
<c:choose>
<c:when test="${user.id > 0}">New password</c:when>
<c:otherwise>Password</c:otherwise>
<c:when test="${user.id > 0}">新密码</c:when>
<c:otherwise>密码</c:otherwise>
</c:choose>
</label>
<c:choose>
@@ -111,8 +111,8 @@
</div>
<div class="form-actions">
<button class="button button-primary" type="submit">Save</button>
<a class="button button-secondary" href="${pageContext.request.contextPath}/admin/users">Cancel</a>
<button class="button button-primary" type="submit">保存</button>
<a class="button button-secondary" href="${pageContext.request.contextPath}/admin/users">取消</a>
</div>
</form>
</section>
@@ -2,11 +2,11 @@
<%@ 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="en">
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<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">
</head>
<body>
@@ -14,11 +14,11 @@
<main class="page-shell">
<section class="dashboard-hero catalog-hero" aria-labelledby="manage-users-title">
<div>
<p class="eyebrow">Administration</p>
<h1 id="manage-users-title">Manage users</h1>
<p>Create, update, deactivate, and review administrator, librarian, and reader accounts.</p>
<p class="eyebrow">系统管理</p>
<h1 id="manage-users-title">管理用户</h1>
<p>创建、更新、停用和查看管理员、馆员与读者账户。</p>
</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>
<c:if test="${not empty successMessage}">
@@ -32,10 +32,10 @@
</div>
</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">
<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)}">
<c:if test="${not empty errors.keyword}">
<span class="field-error"><c:out value="${errors.keyword}" /></span>
@@ -43,9 +43,9 @@
</div>
<div class="search-field">
<label for="role">Role</label>
<label for="role">角色</label>
<select id="role" name="role">
<option value="">All roles</option>
<option value="">全部角色</option>
<c:forEach var="role" items="${roles}">
<option value="${role.code}" <c:if test="${criteria.roleCode == role.code}">selected</c:if>>
<c:out value="${role.displayName}" />
@@ -58,40 +58,40 @@
</div>
<div class="search-field">
<label for="active">Active state</label>
<label for="active">启用状态</label>
<select id="active" name="active">
<option value="">All states</option>
<option value="active" <c:if test="${criteria.activeStatus == 'active'}">selected</c:if>>Active</option>
<option value="inactive" <c:if test="${criteria.activeStatus == 'inactive'}">selected</c:if>>Inactive</option>
<option value="">全部状态</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>>停用</option>
</select>
<c:if test="${not empty errors.active}">
<span class="field-error"><c:out value="${errors.active}" /></span>
</c:if>
</div>
<button class="button button-primary" type="submit">Search</button>
<a class="button button-secondary" href="${pageContext.request.contextPath}/admin/users">Clear</a>
<button class="button button-primary" type="submit">检索</button>
<a class="button button-secondary" href="${pageContext.request.contextPath}/admin/users">清空</a>
</form>
</section>
<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:when test="${empty users}">
<p class="empty-state">No user accounts match the current filters.</p>
<p class="empty-state">没有符合当前筛选条件的用户账户。</p>
</c:when>
<c:otherwise>
<div class="table-scroll">
<table class="data-table user-table">
<thead>
<tr>
<th scope="col">Username</th>
<th scope="col">Display name</th>
<th scope="col">Role</th>
<th scope="col">State</th>
<th scope="col">Created</th>
<th scope="col">Updated</th>
<th scope="col">Actions</th>
<th scope="col">用户名</th>
<th scope="col">显示名称</th>
<th scope="col">角色</th>
<th scope="col">状态</th>
<th scope="col">创建时间</th>
<th scope="col">更新时间</th>
<th scope="col">操作</th>
</tr>
</thead>
<tbody>
@@ -110,17 +110,17 @@
<td>
<div class="table-actions">
<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: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:otherwise>
<form action="${pageContext.request.contextPath}/admin/users/deactivate"
method="post"
onsubmit="return confirm('Deactivate this user account?');">
onsubmit="return confirm('确定停用这个用户账户吗?');">
<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>
</c:otherwise>
</c:choose>
+7 -7
View File
@@ -2,11 +2,11 @@
<%@ 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="en">
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<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">
</head>
<body class="auth-page">
@@ -14,8 +14,8 @@
<main class="auth-shell">
<section class="login-panel" aria-labelledby="login-title">
<div>
<p class="eyebrow">Library Management</p>
<h1 id="login-title">Sign in</h1>
<p class="eyebrow">图书馆管理</p>
<h1 id="login-title">登录</h1>
</div>
<c:if test="${not empty errorMessage}">
@@ -26,7 +26,7 @@
<form class="login-form" action="${pageContext.request.contextPath}/login" method="post" novalidate>
<input type="hidden" name="redirect" value="${fn:escapeXml(redirect)}">
<label for="username">Username</label>
<label for="username">用户名</label>
<input id="username"
name="username"
type="text"
@@ -34,14 +34,14 @@
autocomplete="username"
required>
<label for="password">Password</label>
<label for="password">密码</label>
<input id="password"
name="password"
type="password"
autocomplete="current-password"
required>
<button class="button button-primary" type="submit">Sign in</button>
<button class="button button-primary" type="submit">登录</button>
</form>
</section>
</main>
@@ -1,27 +1,27 @@
<%@ 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="en">
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<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">
</head>
<body>
<%@ include file="/WEB-INF/jsp/common/header.jspf" %>
<main class="page-shell">
<section class="notice-panel" aria-labelledby="unauthorized-title">
<h1 id="unauthorized-title">Access denied</h1>
<h1 id="unauthorized-title">无权访问</h1>
<p>
<c:choose>
<c:when test="${not empty errorMessage}">
<c:out value="${errorMessage}" />
</c:when>
<c:otherwise>You do not have permission to access this page.</c:otherwise>
<c:otherwise>您无权访问此页面。</c:otherwise>
</c:choose>
</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>
</main>
</body>
+22 -22
View File
@@ -2,20 +2,20 @@
<%@ 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="en">
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<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">
</head>
<body>
<%@ include file="/WEB-INF/jsp/common/header.jspf" %>
<main class="page-shell">
<section class="dashboard-hero catalog-hero" aria-labelledby="catalog-title">
<p class="eyebrow">Catalog</p>
<h1 id="catalog-title">Book catalog</h1>
<p>Search the library collection by identifier, title, author, or category.</p>
<p class="eyebrow">馆藏</p>
<h1 id="catalog-title">馆藏检索</h1>
<p>按图书编号、书名、作者或分类检索馆藏。</p>
</section>
<c:if test="${not empty errorMessage}">
@@ -24,27 +24,27 @@
</div>
</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">
<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)}">
</div>
<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)}">
</div>
<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)}">
</div>
<div class="search-field">
<label for="categoryId">Category</label>
<label for="categoryId">分类</label>
<select id="categoryId" name="categoryId">
<option value="">All categories</option>
<option value="">全部分类</option>
<c:forEach var="category" items="${categories}">
<option value="${category.id}" <c:if test="${criteria.categoryId == category.id}">selected</c:if>>
<c:out value="${category.name}" />
@@ -56,31 +56,31 @@
</c:if>
</div>
<button class="button button-primary" type="submit">Search</button>
<a class="button button-secondary" href="${pageContext.request.contextPath}/catalog">Clear</a>
<button class="button button-primary" type="submit">检索</button>
<a class="button button-secondary" href="${pageContext.request.contextPath}/catalog">清空</a>
<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>
</form>
</section>
<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:when test="${empty books}">
<p class="empty-state">No books match the current filters.</p>
<p class="empty-state">没有符合当前筛选条件的图书。</p>
</c:when>
<c:otherwise>
<div class="table-scroll">
<table class="data-table">
<thead>
<tr>
<th scope="col">Book ID</th>
<th scope="col">Title</th>
<th scope="col">Author</th>
<th scope="col">Category</th>
<th scope="col">Copies</th>
<th scope="col">Status</th>
<th scope="col">图书编号</th>
<th scope="col">书名</th>
<th scope="col">作者</th>
<th scope="col">分类</th>
<th scope="col">馆藏数量</th>
<th scope="col">状态</th>
</tr>
</thead>
<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>
+14 -14
View File
@@ -2,18 +2,18 @@
<%@ 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="en">
<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 Library</title>
<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="book-form-title">
<p class="eyebrow">Book Management</p>
<p class="eyebrow">图书管理</p>
<h1 id="book-form-title"><c:out value="${formTitle}" /></h1>
<c:if test="${not empty errorMessage}">
@@ -38,7 +38,7 @@
<div class="form-grid">
<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>
<c:if test="${not empty errors.identifier}">
<span class="field-error"><c:out value="${errors.identifier}" /></span>
@@ -46,7 +46,7 @@
</div>
<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>
<c:if test="${not empty errors.title}">
<span class="field-error"><c:out value="${errors.title}" /></span>
@@ -54,7 +54,7 @@
</div>
<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>
<c:if test="${not empty errors.author}">
<span class="field-error"><c:out value="${errors.author}" /></span>
@@ -62,9 +62,9 @@
</div>
<div class="form-field">
<label for="categoryId">Category</label>
<label for="categoryId">分类</label>
<select id="categoryId" name="categoryId" required>
<option value="">Select category</option>
<option value="">请选择分类</option>
<c:forEach var="category" items="${categories}">
<option value="${category.id}" <c:if test="${categoryValue == category.id}">selected</c:if>>
<c:out value="${category.name}" />
@@ -77,7 +77,7 @@
</div>
<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>
<c:if test="${not empty errors.totalCopies}">
<span class="field-error"><c:out value="${errors.totalCopies}" /></span>
@@ -85,7 +85,7 @@
</div>
<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>
<c:if test="${not empty errors.availableCopies}">
<span class="field-error"><c:out value="${errors.availableCopies}" /></span>
@@ -93,9 +93,9 @@
</div>
<div class="form-field">
<label for="status">Status</label>
<label for="status">状态</label>
<select id="status" name="status" required>
<option value="">Select status</option>
<option value="">请选择状态</option>
<c:forEach var="status" items="${statuses}">
<option value="${status.code}" <c:if test="${statusValue == status.code}">selected</c:if>>
<c:out value="${status.displayName}" />
@@ -109,8 +109,8 @@
</div>
<div class="form-actions">
<button class="button button-primary" type="submit">Save</button>
<a class="button button-secondary" href="${pageContext.request.contextPath}/books">Cancel</a>
<button class="button button-primary" type="submit">保存</button>
<a class="button button-secondary" href="${pageContext.request.contextPath}/books">取消</a>
</div>
</form>
</section>
+31 -27
View File
@@ -2,21 +2,24 @@
<%@ 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="en">
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<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">
</head>
<body>
<%@ include file="/WEB-INF/jsp/common/header.jspf" %>
<main class="page-shell">
<section class="dashboard-hero catalog-hero" aria-labelledby="manage-title">
<p class="eyebrow">Book Management</p>
<h1 id="manage-title">Manage books</h1>
<p>Create, update, delete, and review inventory for catalog records.</p>
<a class="button button-primary" href="${pageContext.request.contextPath}/books/new">New book</a>
<p class="eyebrow">图书管理</p>
<h1 id="manage-title">管理图书</h1>
<p>创建、更新、删除和查看馆藏记录的库存信息。</p>
<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>
<c:if test="${not empty successMessage}">
@@ -30,27 +33,27 @@
</div>
</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">
<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)}">
</div>
<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)}">
</div>
<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)}">
</div>
<div class="search-field">
<label for="categoryId">Category</label>
<label for="categoryId">分类</label>
<select id="categoryId" name="categoryId">
<option value="">All categories</option>
<option value="">全部分类</option>
<c:forEach var="category" items="${categories}">
<option value="${category.id}" <c:if test="${criteria.categoryId == category.id}">selected</c:if>>
<c:out value="${category.name}" />
@@ -62,30 +65,31 @@
</c:if>
</div>
<button class="button button-primary" type="submit">Search</button>
<a class="button button-secondary" href="${pageContext.request.contextPath}/books">Clear</a>
<a class="button button-secondary" href="${pageContext.request.contextPath}/catalog">View catalog</a>
<button class="button button-primary" type="submit">检索</button>
<a class="button button-secondary" href="${pageContext.request.contextPath}/books">清空</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>
</section>
<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:when test="${empty books}">
<p class="empty-state">No book records match the current filters.</p>
<p class="empty-state">没有符合当前筛选条件的图书记录。</p>
</c:when>
<c:otherwise>
<div class="table-scroll">
<table class="data-table">
<thead>
<tr>
<th scope="col">Book ID</th>
<th scope="col">Title</th>
<th scope="col">Author</th>
<th scope="col">Category</th>
<th scope="col">Copies</th>
<th scope="col">Status</th>
<th scope="col">Actions</th>
<th scope="col">图书编号</th>
<th scope="col">书名</th>
<th scope="col">作者</th>
<th scope="col">分类</th>
<th scope="col">馆藏数量</th>
<th scope="col">状态</th>
<th scope="col">操作</th>
</tr>
</thead>
<tbody>
@@ -104,12 +108,12 @@
<td>
<div class="table-actions">
<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"
method="post"
onsubmit="return confirm('Delete this book record?');">
onsubmit="return confirm('确定删除这条图书记录吗?');">
<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>
</div>
</td>
@@ -2,19 +2,19 @@
<%@ 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="en">
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<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">
</head>
<body>
<%@ include file="/WEB-INF/jsp/common/header.jspf" %>
<main class="page-shell">
<section class="form-panel" aria-labelledby="borrow-form-title">
<p class="eyebrow">Borrowing Management</p>
<h1 id="borrow-form-title">New borrow</h1>
<p class="eyebrow">借阅管理</p>
<h1 id="borrow-form-title">新增借阅</h1>
<c:if test="${not empty errorMessage}">
<div class="message message-error" role="alert">
@@ -28,7 +28,7 @@
<form class="borrow-form" action="${pageContext.request.contextPath}/borrowing/create" method="post" novalidate>
<div class="form-grid">
<div class="form-field">
<label for="readerIdentifier">Reader ID</label>
<label for="readerIdentifier">读者编号</label>
<input id="readerIdentifier" name="readerIdentifier" type="text"
value="${fn:escapeXml(readerIdentifierValue)}" required>
<c:if test="${not empty errors.readerIdentifier}">
@@ -37,7 +37,7 @@
</div>
<div class="form-field">
<label for="bookIdentifier">Book ID</label>
<label for="bookIdentifier">图书编号</label>
<input id="bookIdentifier" name="bookIdentifier" type="text"
value="${fn:escapeXml(bookIdentifierValue)}" required>
<c:if test="${not empty errors.bookIdentifier}">
@@ -47,8 +47,8 @@
</div>
<div class="form-actions">
<button class="button button-primary" type="submit">Borrow</button>
<a class="button button-secondary" href="${pageContext.request.contextPath}/borrowing">Cancel</a>
<button class="button button-primary" type="submit">借出</button>
<a class="button button-secondary" href="${pageContext.request.contextPath}/borrowing">取消</a>
</div>
</form>
</section>
@@ -2,11 +2,11 @@
<%@ 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="en">
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<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">
</head>
<body>
@@ -14,11 +14,11 @@
<main class="page-shell">
<section class="dashboard-hero catalog-hero" aria-labelledby="borrowing-title">
<div>
<p class="eyebrow">Borrowing Management</p>
<h1 id="borrowing-title">Manage borrowing</h1>
<p>Create borrow records, process returns, renew active loans, and review overdue items.</p>
<p class="eyebrow">借阅管理</p>
<h1 id="borrowing-title">管理借阅</h1>
<p>创建借阅记录、处理归还、续借有效借阅并查看逾期项目。</p>
</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>
<c:if test="${not empty successMessage}">
@@ -32,31 +32,31 @@
</div>
</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">
<div class="search-field">
<label for="readerIdentifier">Reader ID</label>
<label for="readerIdentifier">读者编号</label>
<input id="readerIdentifier" name="readerIdentifier" type="text"
value="${fn:escapeXml(criteria.readerIdentifier)}">
</div>
<div class="search-field">
<label for="bookIdentifier">Book ID</label>
<label for="bookIdentifier">图书编号</label>
<input id="bookIdentifier" name="bookIdentifier" type="text"
value="${fn:escapeXml(criteria.bookIdentifier)}">
</div>
<div class="search-field">
<label for="status">Status</label>
<label for="status">状态</label>
<select id="status" name="status">
<option value="">All statuses</option>
<option value="">全部状态</option>
<c:forEach var="status" items="${statuses}">
<option value="${status.code}" <c:if test="${criteria.statusCode == status.code}">selected</c:if>>
<c:out value="${status.displayName}" />
</option>
</c:forEach>
<option value="${overdueStatus}" <c:if test="${criteria.statusCode == overdueStatus}">selected</c:if>>
Overdue
逾期
</option>
</select>
<c:if test="${not empty errors.status}">
@@ -64,30 +64,30 @@
</c:if>
</div>
<button class="button button-primary" type="submit">Search</button>
<a class="button button-secondary" href="${pageContext.request.contextPath}/borrowing">Clear</a>
<button class="button button-primary" type="submit">检索</button>
<a class="button button-secondary" href="${pageContext.request.contextPath}/borrowing">清空</a>
</form>
</section>
<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:when test="${empty borrowRecords}">
<p class="empty-state">No borrowing records match the current filters.</p>
<p class="empty-state">没有符合当前筛选条件的借阅记录。</p>
</c:when>
<c:otherwise>
<div class="table-scroll">
<table class="data-table borrowing-table">
<thead>
<tr>
<th scope="col">Reader</th>
<th scope="col">Book</th>
<th scope="col">Borrowed</th>
<th scope="col">Due</th>
<th scope="col">Returned</th>
<th scope="col">Renewals</th>
<th scope="col">Status</th>
<th scope="col">Actions</th>
<th scope="col">读者</th>
<th scope="col">图书</th>
<th scope="col">借出时间</th>
<th scope="col">应还时间</th>
<th scope="col">归还时间</th>
<th scope="col">续借次数</th>
<th scope="col">状态</th>
<th scope="col">操作</th>
</tr>
</thead>
<tbody>
@@ -108,7 +108,7 @@
<c:when test="${not empty record.returnedAtText}">
<c:out value="${record.returnedAtText}" />
</c:when>
<c:otherwise>Not returned</c:otherwise>
<c:otherwise>未归还</c:otherwise>
</c:choose>
</td>
<td><c:out value="${record.renewalCount}" /> / <c:out value="${maxRenewals}" /></td>
@@ -123,22 +123,22 @@
<div class="table-actions">
<form action="${pageContext.request.contextPath}/borrowing/return"
method="post"
onsubmit="return confirm('Return this book?');">
onsubmit="return confirm('确定归还这本书吗?');">
<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>
<c:if test="${record.renewalCount < maxRenewals}">
<form action="${pageContext.request.contextPath}/borrowing/renew"
method="post"
onsubmit="return confirm('Renew this loan?');">
onsubmit="return confirm('确定续借这条记录吗?');">
<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>
</c:if>
</div>
</c:when>
<c:otherwise>
<span class="muted-text">Complete</span>
<span class="muted-text">已完成</span>
</c:otherwise>
</c:choose>
</td>
+16 -15
View File
@@ -1,30 +1,31 @@
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<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}">
<nav class="top-nav" aria-label="Primary">
<a href="${pageContext.request.contextPath}/dashboard">Dashboard</a>
<a href="${pageContext.request.contextPath}/catalog">Catalog</a>
<nav class="top-nav" aria-label="主导航">
<a href="${pageContext.request.contextPath}/dashboard">控制台</a>
<a href="${pageContext.request.contextPath}/catalog">馆藏检索</a>
<c:if test="${sessionScope.userRole == 'administrator'}">
<a href="${pageContext.request.contextPath}/admin/home">Admin</a>
<a href="${pageContext.request.contextPath}/admin/users">Users</a>
<a href="${pageContext.request.contextPath}/admin/system-logs">Logs</a>
<a href="${pageContext.request.contextPath}/admin/home">管理</a>
<a href="${pageContext.request.contextPath}/admin/users">用户</a>
<a href="${pageContext.request.contextPath}/admin/system-logs">日志</a>
</c:if>
<c:if test="${sessionScope.userRole == 'administrator' or sessionScope.userRole == 'librarian'}">
<a href="${pageContext.request.contextPath}/librarian/home">Librarian</a>
<a href="${pageContext.request.contextPath}/books">Books</a>
<a href="${pageContext.request.contextPath}/readers">Readers</a>
<a href="${pageContext.request.contextPath}/borrowing">Borrowing</a>
<a href="${pageContext.request.contextPath}/reports">Reports</a>
<a href="${pageContext.request.contextPath}/librarian/home">馆员</a>
<a href="${pageContext.request.contextPath}/books">图书</a>
<a href="${pageContext.request.contextPath}/book-categories">分类</a>
<a href="${pageContext.request.contextPath}/readers">读者</a>
<a href="${pageContext.request.contextPath}/borrowing">借阅</a>
<a href="${pageContext.request.contextPath}/reports">报表</a>
</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'}">
<a href="${pageContext.request.contextPath}/reader/loans">My Loans</a>
<a href="${pageContext.request.contextPath}/reader/loans">我的借阅</a>
</c:if>
<span class="user-pill">
<c:out value="${sessionScope.authenticatedUser.displayName}" />
</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>
</c:if>
</header>
+44 -38
View File
@@ -1,11 +1,11 @@
<%@ 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="en">
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<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">
</head>
<body>
@@ -15,80 +15,86 @@
<p class="eyebrow">
<c:out value="${sessionScope.authenticatedUser.role.displayName}" />
</p>
<h1 id="dashboard-title">Dashboard</h1>
<p>Signed in as <strong><c:out value="${sessionScope.authenticatedUser.displayName}" /></strong>.</p>
<h1 id="dashboard-title">控制台</h1>
<p>当前登录:<strong><c:out value="${sessionScope.authenticatedUser.displayName}" /></strong></p>
</section>
<section class="card-grid" aria-label="Role workspaces">
<section class="card-grid" aria-label="角色工作区">
<c:if test="${sessionScope.userRole == 'administrator'}">
<article class="workspace-card">
<h2>Administration</h2>
<p>Account, role, permission, and system-maintenance entry point.</p>
<a class="button button-secondary" href="${pageContext.request.contextPath}/admin/home">Open</a>
<h2>系统管理</h2>
<p>账户、角色、权限和系统维护入口。</p>
<a class="button button-secondary" href="${pageContext.request.contextPath}/admin/home">打开</a>
</article>
<article class="workspace-card">
<h2>User Management</h2>
<p>Create, update, deactivate, and review login accounts.</p>
<a class="button button-secondary" href="${pageContext.request.contextPath}/admin/users">Open</a>
<h2>用户管理</h2>
<p>创建、更新、停用和查看登录账户。</p>
<a class="button button-secondary" href="${pageContext.request.contextPath}/admin/users">打开</a>
</article>
<article class="workspace-card">
<h2>System Logs</h2>
<p>Review read-only audit entries for account and maintenance actions.</p>
<a class="button button-secondary" href="${pageContext.request.contextPath}/admin/system-logs">Open</a>
<h2>系统日志</h2>
<p>查看账户与维护操作的只读审计记录。</p>
<a class="button button-secondary" href="${pageContext.request.contextPath}/admin/system-logs">打开</a>
</article>
</c:if>
<c:if test="${sessionScope.userRole == 'administrator' or sessionScope.userRole == 'librarian'}">
<article class="workspace-card">
<h2>Librarian Workspace</h2>
<p>Book, reader, borrowing, return, renewal, and overdue entry point.</p>
<a class="button button-secondary" href="${pageContext.request.contextPath}/librarian/home">Open</a>
<h2>馆员工作台</h2>
<p>图书、读者、借阅、归还、续借和逾期处理入口。</p>
<a class="button button-secondary" href="${pageContext.request.contextPath}/librarian/home">打开</a>
</article>
<article class="workspace-card">
<h2>Book Management</h2>
<p>Create, update, delete, and review book inventory records.</p>
<a class="button button-secondary" href="${pageContext.request.contextPath}/books">Open</a>
<h2>图书管理</h2>
<p>创建、更新、删除和查看图书库存记录。</p>
<a class="button button-secondary" href="${pageContext.request.contextPath}/books">打开</a>
</article>
<article class="workspace-card">
<h2>Reader Management</h2>
<p>Create, update, deactivate, and review reader eligibility records.</p>
<a class="button button-secondary" href="${pageContext.request.contextPath}/readers">Open</a>
<h2>分类维护</h2>
<p>维护图书记录和检索筛选使用的馆藏分类。</p>
<a class="button button-secondary" href="${pageContext.request.contextPath}/book-categories">打开</a>
</article>
<article class="workspace-card">
<h2>Borrowing Management</h2>
<p>Create loans, process returns, renew active records, and review overdue items.</p>
<a class="button button-secondary" href="${pageContext.request.contextPath}/borrowing">Open</a>
<h2>读者管理</h2>
<p>创建、更新、停用和查看读者借阅资格记录。</p>
<a class="button button-secondary" href="${pageContext.request.contextPath}/readers">打开</a>
</article>
<article class="workspace-card">
<h2>Report Center</h2>
<p>Review inventory health, borrowing counts, overdue records, and popular books.</p>
<a class="button button-secondary" href="${pageContext.request.contextPath}/reports">Open</a>
<h2>借阅管理</h2>
<p>创建借阅、处理归还、续借有效记录并查看逾期项目。</p>
<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>
</c:if>
<article class="workspace-card">
<h2>Book Catalog</h2>
<p>Search books by title, author, category, or book identifier.</p>
<a class="button button-secondary" href="${pageContext.request.contextPath}/catalog">Search</a>
<h2>馆藏检索</h2>
<p>按书名、作者、分类或图书编号检索图书。</p>
<a class="button button-secondary" href="${pageContext.request.contextPath}/catalog">检索</a>
</article>
<article class="workspace-card">
<h2>Reader Center</h2>
<p>Reader self-service entry point for catalog access and loan history.</p>
<a class="button button-secondary" href="${pageContext.request.contextPath}/reader/home">Open</a>
<h2>读者中心</h2>
<p>读者自助访问馆藏和借阅历史的入口。</p>
<a class="button button-secondary" href="${pageContext.request.contextPath}/reader/home">打开</a>
</article>
<c:if test="${sessionScope.userRole == 'reader'}">
<article class="workspace-card">
<h2>My Loan History</h2>
<p>Review your active, returned, and overdue borrowing records.</p>
<a class="button button-secondary" href="${pageContext.request.contextPath}/reader/loans">Open</a>
<h2>我的借阅历史</h2>
<p>查看您的在借、已还和逾期借阅记录。</p>
<a class="button button-secondary" href="${pageContext.request.contextPath}/reader/loans">打开</a>
</article>
</c:if>
</section>
@@ -2,11 +2,11 @@
<%@ 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="en">
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<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">
</head>
<body>
@@ -14,9 +14,9 @@
<main class="page-shell">
<section class="dashboard-hero catalog-hero" aria-labelledby="system-logs-title">
<div>
<p class="eyebrow">System Maintenance</p>
<h1 id="system-logs-title">System logs</h1>
<p>Review administrative account changes and maintenance audit records.</p>
<p class="eyebrow">系统维护</p>
<h1 id="system-logs-title">系统日志</h1>
<p>查看管理账户变更和维护审计记录。</p>
</div>
</section>
@@ -26,17 +26,22 @@
</div>
</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"
action="${pageContext.request.contextPath}/admin/system-logs" method="get">
<div class="search-field">
<label for="operationType">Operation</label>
<label for="operationType">操作</label>
<select id="operationType" name="operationType">
<option value="">All operations</option>
<option value="">全部操作</option>
<c:forEach var="operationType" items="${operationTypes}">
<option value="${fn:escapeXml(operationType)}"
<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>
</c:forEach>
<c:if test="${not empty criteria.operationType and empty operationTypes}">
@@ -51,7 +56,7 @@
</div>
<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)}">
<c:if test="${not empty errors.keyword}">
<span class="field-error"><c:out value="${errors.keyword}" /></span>
@@ -59,7 +64,7 @@
</div>
<div class="search-field">
<label for="createdFrom">From</label>
<label for="createdFrom">开始日期</label>
<input id="createdFrom" name="createdFrom" type="date"
value="${fn:escapeXml(criteria.createdFromText)}">
<c:if test="${not empty errors.createdFrom}">
@@ -68,7 +73,7 @@
</div>
<div class="search-field">
<label for="createdTo">To</label>
<label for="createdTo">结束日期</label>
<input id="createdTo" name="createdTo" type="date"
value="${fn:escapeXml(criteria.createdToText)}">
<c:if test="${not empty errors.createdTo}">
@@ -76,29 +81,29 @@
</c:if>
</div>
<button class="button button-primary" type="submit">Search</button>
<a class="button button-secondary" href="${pageContext.request.contextPath}/admin/system-logs">Clear</a>
<button class="button button-primary" type="submit">检索</button>
<a class="button button-secondary" href="${pageContext.request.contextPath}/admin/system-logs">清空</a>
</form>
</section>
<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:when test="${empty logs}">
<p class="empty-state">No system logs match the current filters.</p>
<p class="empty-state">没有符合当前筛选条件的系统日志。</p>
</c:when>
<c:otherwise>
<div class="table-scroll">
<table class="data-table system-log-table">
<thead>
<tr>
<th scope="col">Time</th>
<th scope="col">Operator</th>
<th scope="col">Operation</th>
<th scope="col">Target</th>
<th scope="col">Result</th>
<th scope="col">IP address</th>
<th scope="col">Detail</th>
<th scope="col">时间</th>
<th scope="col">操作人</th>
<th scope="col">操作</th>
<th scope="col">目标</th>
<th scope="col">结果</th>
<th scope="col">IP 地址</th>
<th scope="col">详情</th>
</tr>
</thead>
<tbody>
@@ -111,9 +116,16 @@
<div class="muted-text"><c:out value="${log.operatorMetaText}" /></div>
</c:if>
</td>
<td><c:out value="${log.operationType}" /></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:out value="${log.targetId}" />
</c:if>
+16 -16
View File
@@ -1,11 +1,11 @@
<%@ 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="en">
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<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">
</head>
<body>
@@ -13,11 +13,11 @@
<main class="page-shell">
<section class="dashboard-hero catalog-hero" aria-labelledby="loan-history-title">
<div>
<p class="eyebrow">Reader Center</p>
<h1 id="loan-history-title">Loan history</h1>
<p>Review your active, returned, and overdue borrowing records.</p>
<p class="eyebrow">读者中心</p>
<h1 id="loan-history-title">借阅历史</h1>
<p>查看您的在借、已还和逾期借阅记录。</p>
</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>
<c:if test="${not empty successMessage}">
@@ -32,23 +32,23 @@
</c:if>
<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: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:otherwise>
<div class="table-scroll">
<table class="data-table borrowing-table">
<thead>
<tr>
<th scope="col">Book ID</th>
<th scope="col">Title</th>
<th scope="col">Borrowed</th>
<th scope="col">Due</th>
<th scope="col">Returned</th>
<th scope="col">Renewals</th>
<th scope="col">Status</th>
<th scope="col">图书编号</th>
<th scope="col">书名</th>
<th scope="col">借出时间</th>
<th scope="col">应还时间</th>
<th scope="col">归还时间</th>
<th scope="col">续借次数</th>
<th scope="col">状态</th>
</tr>
</thead>
<tbody>
@@ -63,7 +63,7 @@
<c:when test="${not empty record.returnedAtText}">
<c:out value="${record.returnedAtText}" />
</c:when>
<c:otherwise>Not returned</c:otherwise>
<c:otherwise>未归还</c:otherwise>
</c:choose>
</td>
<td><c:out value="${record.renewalCount}" /></td>
+13 -13
View File
@@ -2,18 +2,18 @@
<%@ 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="en">
<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 Library</title>
<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="reader-form-title">
<p class="eyebrow">Reader Management</p>
<p class="eyebrow">读者管理</p>
<h1 id="reader-form-title"><c:out value="${formTitle}" /></h1>
<c:if test="${not empty errorMessage}">
@@ -38,7 +38,7 @@
<div class="form-grid">
<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>
<c:if test="${not empty errors.identifier}">
<span class="field-error"><c:out value="${errors.identifier}" /></span>
@@ -46,7 +46,7 @@
</div>
<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>
<c:if test="${not empty errors.fullName}">
<span class="field-error"><c:out value="${errors.fullName}" /></span>
@@ -54,7 +54,7 @@
</div>
<div class="form-field">
<label for="phone">Phone</label>
<label for="phone">电话</label>
<input id="phone" name="phone" type="tel" value="${fn:escapeXml(phoneValue)}">
<c:if test="${not empty errors.phone}">
<span class="field-error"><c:out value="${errors.phone}" /></span>
@@ -62,7 +62,7 @@
</div>
<div class="form-field">
<label for="email">Email</label>
<label for="email">邮箱</label>
<input id="email" name="email" type="email" value="${fn:escapeXml(emailValue)}">
<c:if test="${not empty errors.email}">
<span class="field-error"><c:out value="${errors.email}" /></span>
@@ -70,7 +70,7 @@
</div>
<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)}">
<c:if test="${not empty errors.userId}">
<span class="field-error"><c:out value="${errors.userId}" /></span>
@@ -78,7 +78,7 @@
</div>
<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"
value="${fn:escapeXml(maxBorrowCountValue)}" required>
<c:if test="${not empty errors.maxBorrowCount}">
@@ -87,9 +87,9 @@
</div>
<div class="form-field">
<label for="status">Status</label>
<label for="status">状态</label>
<select id="status" name="status" required>
<option value="">Select status</option>
<option value="">请选择状态</option>
<c:forEach var="status" items="${statuses}">
<option value="${status.code}" <c:if test="${statusValue == status.code}">selected</c:if>>
<c:out value="${status.displayName}" />
@@ -103,8 +103,8 @@
</div>
<div class="form-actions">
<button class="button button-primary" type="submit">Save</button>
<a class="button button-secondary" href="${pageContext.request.contextPath}/readers">Cancel</a>
<button class="button button-primary" type="submit">保存</button>
<a class="button button-secondary" href="${pageContext.request.contextPath}/readers">取消</a>
</div>
</form>
</section>
+27 -27
View File
@@ -2,21 +2,21 @@
<%@ 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="en">
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<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">
</head>
<body>
<%@ include file="/WEB-INF/jsp/common/header.jspf" %>
<main class="page-shell">
<section class="dashboard-hero catalog-hero" aria-labelledby="manage-readers-title">
<p class="eyebrow">Reader Management</p>
<h1 id="manage-readers-title">Manage readers</h1>
<p>Create, update, and review reader eligibility and contact records.</p>
<a class="button button-primary" href="${pageContext.request.contextPath}/readers/new">New reader</a>
<p class="eyebrow">读者管理</p>
<h1 id="manage-readers-title">管理读者</h1>
<p>创建、更新和查看读者资格及联系方式记录。</p>
<a class="button button-primary" href="${pageContext.request.contextPath}/readers/new">新增读者</a>
</section>
<c:if test="${not empty successMessage}">
@@ -30,27 +30,27 @@
</div>
</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">
<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)}">
</div>
<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)}">
</div>
<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)}">
</div>
<div class="search-field">
<label for="status">Status</label>
<label for="status">状态</label>
<select id="status" name="status">
<option value="">All statuses</option>
<option value="">全部状态</option>
<c:forEach var="status" items="${statuses}">
<option value="${status.code}" <c:if test="${criteria.statusCode == status.code}">selected</c:if>>
<c:out value="${status.displayName}" />
@@ -62,29 +62,29 @@
</c:if>
</div>
<button class="button button-primary" type="submit">Search</button>
<a class="button button-secondary" href="${pageContext.request.contextPath}/readers">Clear</a>
<button class="button button-primary" type="submit">检索</button>
<a class="button button-secondary" href="${pageContext.request.contextPath}/readers">清空</a>
</form>
</section>
<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:when test="${empty readers}">
<p class="empty-state">No reader records match the current filters.</p>
<p class="empty-state">没有符合当前筛选条件的读者记录。</p>
</c:when>
<c:otherwise>
<div class="table-scroll">
<table class="data-table">
<thead>
<tr>
<th scope="col">Reader ID</th>
<th scope="col">Name</th>
<th scope="col">Contact</th>
<th scope="col">Account</th>
<th scope="col">Borrow limit</th>
<th scope="col">Status</th>
<th scope="col">Actions</th>
<th scope="col">读者编号</th>
<th scope="col">姓名</th>
<th scope="col">联系方式</th>
<th scope="col">关联账户</th>
<th scope="col">借阅上限</th>
<th scope="col">状态</th>
<th scope="col">操作</th>
</tr>
</thead>
<tbody>
@@ -105,7 +105,7 @@
<c:when test="${not empty reader.username}">
<c:out value="${reader.username}" />
</c:when>
<c:otherwise>Unlinked</c:otherwise>
<c:otherwise>未关联</c:otherwise>
</c:choose>
</td>
<td><c:out value="${reader.maxBorrowCount}" /></td>
@@ -117,12 +117,12 @@
<td>
<div class="table-actions">
<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"
method="post"
onsubmit="return confirm('Deactivate this reader profile?');">
onsubmit="return confirm('确定停用这个读者档案吗?');">
<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>
</div>
</td>
@@ -1,11 +1,11 @@
<%@ 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="en">
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<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">
</head>
<body>
@@ -13,11 +13,11 @@
<main class="page-shell">
<section class="dashboard-hero catalog-hero" aria-labelledby="reports-title">
<div>
<p class="eyebrow">Reports</p>
<h1 id="reports-title">Report center</h1>
<p>Review collection inventory, borrowing health, overdue loans, and popular books.</p>
<p class="eyebrow">报表</p>
<h1 id="reports-title">报表中心</h1>
<p>查看馆藏库存、借阅状况、逾期借阅和热门图书。</p>
</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>
<c:if test="${not empty errorMessage}">
@@ -27,59 +27,59 @@
</c:if>
<c:if test="${not empty reportCenter}">
<section class="report-grid" aria-label="Report summary">
<section class="report-grid" aria-label="报表摘要">
<article class="report-card">
<p class="eyebrow">Inventory</p>
<h2>Total titles</h2>
<p class="eyebrow">库存</p>
<h2>图书种类总数</h2>
<p class="report-metric"><c:out value="${reportCenter.inventorySummary.totalTitles}" /></p>
</article>
<article class="report-card">
<p class="eyebrow">Inventory</p>
<h2>Total copies</h2>
<p class="eyebrow">库存</p>
<h2>馆藏总册数</h2>
<p class="report-metric"><c:out value="${reportCenter.inventorySummary.totalCopies}" /></p>
</article>
<article class="report-card">
<p class="eyebrow">Inventory</p>
<h2>Available copies</h2>
<p class="eyebrow">库存</p>
<h2>可借册数</h2>
<p class="report-metric"><c:out value="${reportCenter.inventorySummary.availableCopies}" /></p>
</article>
<article class="report-card">
<p class="eyebrow">Attention</p>
<h2>Unavailable or empty</h2>
<p class="eyebrow">需关注</p>
<h2>不可借或无库存</h2>
<p class="report-metric"><c:out value="${reportCenter.inventorySummary.unavailableOrEmptyTitles}" /></p>
</article>
<article class="report-card">
<p class="eyebrow">Borrowing</p>
<h2>Currently borrowed</h2>
<p class="eyebrow">借阅</p>
<h2>当前借出</h2>
<p class="report-metric"><c:out value="${reportCenter.borrowingSummary.activeLoans}" /></p>
</article>
<article class="report-card">
<p class="eyebrow">Borrowing</p>
<h2>Returned records</h2>
<p class="eyebrow">借阅</p>
<h2>已归还记录</h2>
<p class="report-metric"><c:out value="${reportCenter.borrowingSummary.returnedLoans}" /></p>
</article>
<article class="report-card report-card-alert">
<p class="eyebrow">Borrowing</p>
<h2>Overdue loans</h2>
<p class="eyebrow">借阅</p>
<h2>逾期借阅</h2>
<p class="report-metric"><c:out value="${reportCenter.borrowingSummary.overdueLoans}" /></p>
</article>
</section>
<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:when test="${empty reportCenter.overdueRows}">
<p class="empty-state">No active overdue borrowing records.</p>
<p class="empty-state">当前没有逾期未还的借阅记录。</p>
</c:when>
<c:otherwise>
<div class="table-scroll">
<table class="data-table">
<thead>
<tr>
<th scope="col">Reader</th>
<th scope="col">Book</th>
<th scope="col">Due date</th>
<th scope="col">Overdue days</th>
<th scope="col">读者</th>
<th scope="col">图书</th>
<th scope="col">应还日期</th>
<th scope="col">逾期天数</th>
</tr>
</thead>
<tbody>
@@ -96,7 +96,7 @@
<td><c:out value="${row.dueAtText}" /></td>
<td>
<span class="status-pill status-overdue">
<c:out value="${row.overdueDays}" /> days
<c:out value="${row.overdueDays}" />
</span>
</td>
</tr>
@@ -109,19 +109,19 @@
</section>
<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: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:otherwise>
<div class="table-scroll">
<table class="data-table">
<thead>
<tr>
<th scope="col">Book</th>
<th scope="col">Author</th>
<th scope="col">Borrow records</th>
<th scope="col">图书</th>
<th scope="col">作者</th>
<th scope="col">借阅次数</th>
</tr>
</thead>
<tbody>
+34 -28
View File
@@ -1,11 +1,11 @@
<%@ 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="en">
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<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">
</head>
<body>
@@ -17,61 +17,67 @@
</p>
<h1 id="area-title"><c:out value="${areaName}" /></h1>
<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 class="card-grid role-actions" aria-label="Workspace actions">
<section class="card-grid role-actions" aria-label="工作区操作">
<article class="workspace-card">
<h2>Book Catalog</h2>
<p>Search available collection records by title, author, category, or book identifier.</p>
<a class="button button-secondary" href="${pageContext.request.contextPath}/catalog">Search catalog</a>
<h2>馆藏检索</h2>
<p>按书名、作者、分类或图书编号检索可用馆藏记录。</p>
<a class="button button-secondary" href="${pageContext.request.contextPath}/catalog">检索馆藏</a>
</article>
<c:if test="${sessionScope.userRole == 'administrator' or sessionScope.userRole == 'librarian'}">
<c:if test="${sessionScope.userRole == 'administrator'}">
<article class="workspace-card">
<h2>User Management</h2>
<p>Create, update, deactivate, and review login accounts.</p>
<a class="button button-secondary" href="${pageContext.request.contextPath}/admin/users">Manage users</a>
<h2>用户管理</h2>
<p>创建、更新、停用和查看登录账户。</p>
<a class="button button-secondary" href="${pageContext.request.contextPath}/admin/users">管理用户</a>
</article>
<article class="workspace-card">
<h2>System Logs</h2>
<p>Review read-only audit entries for account and maintenance actions.</p>
<a class="button button-secondary" href="${pageContext.request.contextPath}/admin/system-logs">View logs</a>
<h2>系统日志</h2>
<p>查看账户与维护操作的只读审计记录。</p>
<a class="button button-secondary" href="${pageContext.request.contextPath}/admin/system-logs">查看日志</a>
</article>
</c:if>
<article class="workspace-card">
<h2>Book Management</h2>
<p>Create, update, delete, and review inventory fields for book records.</p>
<a class="button button-secondary" href="${pageContext.request.contextPath}/books">Manage books</a>
<h2>图书管理</h2>
<p>创建、更新、删除和查看图书记录的库存字段。</p>
<a class="button button-secondary" href="${pageContext.request.contextPath}/books">管理图书</a>
</article>
<article class="workspace-card">
<h2>Reader Management</h2>
<p>Create, update, deactivate, and review eligibility fields for reader records.</p>
<a class="button button-secondary" href="${pageContext.request.contextPath}/readers">Manage readers</a>
<h2>分类维护</h2>
<p>创建、更新和停用图书记录使用的馆藏分类。</p>
<a class="button button-secondary" href="${pageContext.request.contextPath}/book-categories">管理分类</a>
</article>
<article class="workspace-card">
<h2>Borrowing Management</h2>
<p>Create loans, process returns, renew records, and review overdue items.</p>
<a class="button button-secondary" href="${pageContext.request.contextPath}/borrowing">Manage borrowing</a>
<h2>读者管理</h2>
<p>创建、更新、停用和查看读者记录的资格字段。</p>
<a class="button button-secondary" href="${pageContext.request.contextPath}/readers">管理读者</a>
</article>
<article class="workspace-card">
<h2>Report Center</h2>
<p>Review inventory summaries, borrowing health, overdue lists, and popular books.</p>
<a class="button button-secondary" href="${pageContext.request.contextPath}/reports">View reports</a>
<h2>借阅管理</h2>
<p>创建借阅、处理归还、续借记录并查看逾期项目。</p>
<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>
</c:if>
<c:if test="${sessionScope.userRole == 'reader'}">
<article class="workspace-card">
<h2>My Loan History</h2>
<p>Review active loans, returned records, renewal counts, and overdue status.</p>
<a class="button button-secondary" href="${pageContext.request.contextPath}/reader/loans">View history</a>
<h2>我的借阅历史</h2>
<p>查看在借记录、已还记录、续借次数和逾期状态。</p>
<a class="button button-secondary" href="${pageContext.request.contextPath}/reader/loans">查看历史</a>
</article>
</c:if>
</section>
+6 -1
View File
@@ -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"
version="4.0">
<display-name>MZH Library Management</display-name>
<display-name>MZH 图书馆管理系统</display-name>
<filter>
<filter-name>CharacterEncodingFilter</filter-name>
@@ -117,6 +117,11 @@
<url-pattern>/books/edit</url-pattern>
<url-pattern>/books/update</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>
+22 -1
View File
@@ -313,6 +313,12 @@ h2 {
margin-top: 0;
}
.hero-actions {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.toolbar-panel,
.table-panel,
.form-panel {
@@ -350,6 +356,8 @@ h2 {
.search-form select,
.book-form input,
.book-form select,
.category-form input,
.category-form textarea,
.reader-form input,
.reader-form select,
.user-form input,
@@ -368,6 +376,8 @@ h2 {
.search-form select:focus,
.book-form input:focus,
.book-form select:focus,
.category-form input:focus,
.category-form textarea:focus,
.reader-form input:focus,
.reader-form select:focus,
.user-form input:focus,
@@ -394,7 +404,8 @@ h2 {
}
.user-table,
.system-log-table {
.system-log-table,
.category-table {
min-width: 980px;
}
@@ -504,6 +515,7 @@ h2 {
}
.book-form,
.category-form,
.reader-form,
.user-form,
.borrow-form {
@@ -522,6 +534,15 @@ h2 {
gap: 6px;
}
.form-field-wide {
grid-column: 1 / -1;
}
.category-form textarea {
min-height: 112px;
resize: vertical;
}
.form-field label {
color: var(--color-muted);
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 x="0" y="705" width="1440" height="255" fill="#d4ddd8"/>
<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;
public final class AuthServiceCheck {
private static final String REQUIRED_MESSAGE = "Username and password are required.";
private static final String INVALID_MESSAGE = "Invalid username or password.";
private static final String REQUIRED_MESSAGE = "请输入用户名和密码。";
private static final String INVALID_MESSAGE = "用户名或密码不正确。";
private static final String UNAVAILABLE_MESSAGE =
"Login service is temporarily unavailable. Please try again later.";
"登录服务暂时不可用,请稍后重试。";
private AuthServiceCheck() {
}
@@ -12,7 +12,6 @@ import com.mzh.library.exception.DaoException;
import com.mzh.library.service.impl.BookServiceImpl;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumSet;
import java.util.LinkedHashMap;
import java.util.List;
@@ -23,7 +22,7 @@ import java.util.logging.Logger;
public final class BookServiceCheck {
private static final String UNAVAILABLE_MESSAGE =
"Book service is temporarily unavailable. Please try again later.";
"图书服务暂时不可用,请稍后重试。";
private BookServiceCheck() {
}
@@ -45,9 +44,15 @@ public final class BookServiceCheck {
ServiceResult<Long> denied = service.createBook(reader,
book(0L, "BK-1001", "Reader Write", "Test Author", 1L, 1, 1, BookStatus.AVAILABLE));
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");
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,
book(0L, "BK-1002", "Service Test", "Test Author", 1L, 2, 1, BookStatus.AVAILABLE));
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(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));
require(search.isSuccessful(), "search should succeed");
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(!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());
ServiceResult<List<Book>> unavailable = failingService.searchBooks(new BookSearchCriteria());
require(!unavailable.isSuccessful(), "DAO failure should not escape service");
@@ -98,6 +130,14 @@ public final class BookServiceCheck {
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) {
if (!condition) {
throw new AssertionError(message);
@@ -106,14 +146,70 @@ public final class BookServiceCheck {
private static final class InMemoryBookDao implements BookDao {
private final Map<Long, Book> books = new LinkedHashMap<>();
private final Map<Long, BookCategory> categories = new LinkedHashMap<>();
private long nextId = 1L;
private long nextCategoryId = 2L;
private InMemoryBookDao() {
categories.put(1L, category(1L, "Computer Science", "Programming books"));
}
@Override
public List<BookCategory> findAllCategories() {
BookCategory category = new BookCategory();
category.setId(1L);
category.setName("Computer Science");
return Collections.singletonList(category);
List<BookCategory> results = new ArrayList<>();
for (BookCategory category : categories.values()) {
results.add(copy(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
@@ -178,6 +274,10 @@ public final class BookServiceCheck {
copy.setCategoryName(source.getCategoryName());
return copy;
}
private BookCategory copy(BookCategory source) {
return category(source.getId(), source.getName(), source.getDescription());
}
}
private static final class FailingBookDao implements BookDao {
@@ -186,6 +286,36 @@ public final class BookServiceCheck {
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
public List<Book> search(BookSearchCriteria criteria) {
throw new DaoException("Simulated search failure", null);
@@ -30,7 +30,7 @@ import java.util.logging.Logger;
public final class BorrowingServiceCheck {
private static final String UNAVAILABLE_MESSAGE =
"Borrowing service is temporarily unavailable. Please try again later.";
"借阅服务暂时不可用,请稍后重试。";
private static final Clock FIXED_CLOCK = Clock.fixed(
Instant.parse("2026-04-27T00:00:00Z"),
ZoneId.of("UTC")
@@ -62,7 +62,7 @@ public final class BorrowingServiceCheck {
ServiceResult<Long> denied = service.borrowBook(readerUser, "RD-1000", "BK-1000");
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");
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);
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");
ServiceResult<Void> returned = service.returnBook(librarian, borrowedId);
@@ -21,7 +21,7 @@ import java.util.logging.Logger;
public final class ReaderServiceCheck {
private static final String UNAVAILABLE_MESSAGE =
"Reader service is temporarily unavailable. Please try again later.";
"读者服务暂时不可用,请稍后重试。";
private ReaderServiceCheck() {
}
@@ -64,7 +64,7 @@ public final class ReaderServiceCheck {
ServiceResult<Long> denied = service.createReader(readerUser,
reader(0L, "RD-1006", null, "Reader Write", "13800000007", "", ReaderStatus.ACTIVE, 5));
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");
ServiceResult<Long> created = service.createReader(librarian,
@@ -21,9 +21,9 @@ import java.util.logging.Level;
import java.util.logging.Logger;
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 =
"Report service is temporarily unavailable. Please try again later.";
"报表服务暂时不可用,请稍后重试。";
private ReportServiceCheck() {
}
@@ -20,9 +20,9 @@ import java.util.logging.Level;
import java.util.logging.Logger;
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 =
"System log service is temporarily unavailable. Please try again later.";
"系统日志服务暂时不可用,请稍后重试。";
private SystemLogServiceCheck() {
}
@@ -59,10 +59,10 @@ public final class SystemLogServiceCheck {
SystemLog orphanedOperator = log(99L, "user.update", "Updated orphaned operator account");
orphanedOperator.setOperatorUsername("");
orphanedOperator.setOperatorDisplayName("");
require("User #1".equals(orphanedOperator.getOperatorLabel()),
require("用户 #1".equals(orphanedOperator.getOperatorLabel()),
"operator id should still render when joined user names are unavailable");
require("administrator".equals(orphanedOperator.getOperatorMetaText()),
"operator meta should preserve role when names are unavailable");
require("管理员".equals(orphanedOperator.getOperatorMetaText()),
"operator meta should display role when names are unavailable");
SystemLog unsafeStatus = log(100L, "user.update", "Unsafe status check");
unsafeStatus.setResultStatus("success\" onclick=\"x");
@@ -26,9 +26,9 @@ import java.util.logging.Logger;
import java.util.stream.Collectors;
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 =
"User management service is temporarily unavailable. Please try again later.";
"用户管理服务暂时不可用,请稍后重试。";
private UserAccountServiceCheck() {
}