Compare commits
27 Commits
934ea1fc0d
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
| a37d37945b | |||
| d1f32b9d52 | |||
| 44b72d3959 | |||
| 8535b4804b | |||
| acbd873fbc | |||
| e8c46311b9 | |||
| da610644d7 | |||
| d0e71f2aa9 | |||
| 0face72b8d | |||
| cc9636e48a | |||
| 0a386b81f9 | |||
| 36db197e75 | |||
| 781ce4697e | |||
| cc32c222a4 | |||
| dc192e8223 | |||
| fdf0eba506 | |||
| 89b6dd1f85 | |||
| 3efcb394fb | |||
| 46efa3b781 | |||
| 2d4a7e2cdd | |||
| 23470ebda3 | |||
| ff044e6aab | |||
| 5dc91a4e8e | |||
| 8dc208d77d | |||
| d917a6247c | |||
| 63738f108a | |||
| 4155d5b1ea |
+1
-1
@@ -14,7 +14,7 @@ project_doc_fallback_filenames = ["AGENTS.md"]
|
||||
# Without this flag, hooks.json is ignored and Trellis context won't
|
||||
# be injected into Codex sessions.
|
||||
|
||||
sandbox_mode = "workspace-write"
|
||||
sandbox_mode = "danger-full-access"
|
||||
|
||||
[sandbox_workspace_write]
|
||||
network_access = true
|
||||
|
||||
@@ -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
|
||||
@@ -541,6 +630,14 @@ reports/dashboard.jsp <- ReportServlet <- ReportService <- ReportDao <- books/re
|
||||
- `users.username`: unique login identifier submitted by `LoginServlet`.
|
||||
- `users.password_hash`: PBKDF2 hash in
|
||||
`pbkdf2_sha256$iterations$saltBase64$hashBase64` format.
|
||||
- Local scaffold demo users must have documented, known initial passwords for
|
||||
new deployments: `admin/admin123`, `librarian/librarian123`, and
|
||||
`reader/reader123`. Their `schema.sql` hashes must verify through
|
||||
`PasswordHasher.verify` and must be treated as local/demo-only credentials,
|
||||
never production credentials.
|
||||
- `schema.sql` uses `INSERT IGNORE` for demo `users` rows. Replaying the schema
|
||||
must not be assumed to reset existing account passwords; README reset
|
||||
guidance must call this out explicitly.
|
||||
- `users.role_code`: foreign key to `roles.code`; supported scaffold values
|
||||
are `administrator`, `librarian`, and `reader`.
|
||||
- `users.active`: only rows with `active = 1` can authenticate.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -144,3 +144,74 @@ the server-side exception.
|
||||
<c:out value="${log.resultStatusName}" />
|
||||
</span>
|
||||
```
|
||||
|
||||
## Scenario: Login Diagnostic Logging
|
||||
|
||||
### 1. Scope / Trigger
|
||||
|
||||
- Trigger: Windows deployment login failures need server-side diagnostics across
|
||||
`LoginServlet -> AuthServiceImpl -> JdbcUserDao -> JdbcUtil` without changing
|
||||
the generic user-facing login messages.
|
||||
|
||||
### 2. Signatures
|
||||
|
||||
- Servlet route: `POST /login` with `username`, `password`, and optional
|
||||
same-application `redirect`.
|
||||
- Service signature: `AuthService.authenticate(String username, String password)`.
|
||||
- DAO signature: `UserDao.findActiveByUsername(String username)`.
|
||||
- DB config keys: `db.driver`, `db.url`, `db.username`, and `db.password`.
|
||||
|
||||
### 3. Contracts
|
||||
|
||||
- Login request logs may include remote address, context path, redirect presence,
|
||||
username presence/length, sanitized username, and whether normalization changed
|
||||
the username.
|
||||
- Authentication logs must distinguish missing required fields, active user not
|
||||
found, password mismatch, service error, and success.
|
||||
- JDBC logs must confirm `db.properties` loading, required key resolution,
|
||||
connection attempts, successful connections, and driver/connection failures.
|
||||
- Logs must never include raw passwords, password hashes, salts, database
|
||||
passwords, or unredacted password-like JDBC URL parameters.
|
||||
|
||||
### 4. Validation & Error Matrix
|
||||
|
||||
- Missing username or password -> log missing-field category and return the
|
||||
existing required-field message.
|
||||
- Unknown or inactive username -> log `active-user-not-found` and return the
|
||||
existing invalid-credentials message.
|
||||
- Existing user with bad password -> log `password-mismatch` and return the
|
||||
existing invalid-credentials message.
|
||||
- Missing DB config or JDBC failure -> log server-side details with credentials
|
||||
redacted and return the existing service-unavailable message.
|
||||
|
||||
### 5. Good/Base/Bad Cases
|
||||
|
||||
- Good: a failed login shows whether the request reached the servlet, whether
|
||||
the username was normalized, whether the active user row was found, and
|
||||
whether password verification failed.
|
||||
- Base: successful login keeps logging user ID and role only.
|
||||
- Bad: a diagnostic log writes `password`, `password_hash`, salt, or a JDBC URL
|
||||
containing `password=secret`.
|
||||
|
||||
### 6. Tests Required
|
||||
|
||||
- Run `mvn test` or the documented Maven path to compile Servlet, service, DAO,
|
||||
and utility code.
|
||||
- Scan changed logs for password/hash/salt/database-password output before
|
||||
finishing.
|
||||
- Keep `AuthServiceCheck` behavior expectations unchanged for required fields,
|
||||
invalid credentials, success, permission checks, and DAO failure fallback.
|
||||
|
||||
### 7. Wrong vs Correct
|
||||
|
||||
#### Wrong
|
||||
|
||||
```java
|
||||
LOGGER.info("Login failed password=" + password + " hash=" + user.getPasswordHash());
|
||||
```
|
||||
|
||||
#### Correct
|
||||
|
||||
```java
|
||||
LOGGER.info("Login failed reason=password-mismatch userId=" + user.getId());
|
||||
```
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -16,13 +16,52 @@ the reusable UI units.
|
||||
|
||||
- Use shared fragments for repeated layout pieces such as header, navigation,
|
||||
sidebar, footer, pagination, and message banners.
|
||||
- Prefer `.jspf` includes or JSP tag files once the project chooses one
|
||||
pattern; document the actual paths after implementation.
|
||||
- Use `.jspf` includes for the current JSP presentation layer. The authenticated
|
||||
application frame lives in `src/main/webapp/WEB-INF/jsp/common/header.jspf`
|
||||
and owns the dark sidebar, top utility bar, module navigation, global search,
|
||||
user display, and logout link.
|
||||
- Any `.jspf` fragment that contains user-visible Simplified Chinese text must
|
||||
declare `<%@ page pageEncoding="UTF-8" %>` at the top. Do not rely only on the
|
||||
including JSP page or response `Content-Type`; Tomcat/Jasper can otherwise
|
||||
compile the fragment with a non-UTF-8 default and render mojibake.
|
||||
- JSP-rendered HTML responses must be served as `text/html;charset=UTF-8` by
|
||||
the encoding filter or the JSP page directive. Request/response character
|
||||
encoding alone is not enough for browsers to decode Simplified Chinese safely.
|
||||
- Preserve role-conditioned navigation in that shared frame: administrator-only
|
||||
links stay inside `sessionScope.userRole == 'administrator'`; staff links stay
|
||||
inside `administrator or librarian`; reader-only links stay inside
|
||||
`sessionScope.userRole == 'reader'`.
|
||||
- For active navigation in forwarded JSPs, derive the current location from the
|
||||
public Servlet path before falling back to the JSP servlet path. Use exact
|
||||
matches or slash-delimited prefixes; do not use broad `fn:contains` checks
|
||||
against `requestURI`, because forwarded pages expose `/WEB-INF/jsp/...` paths
|
||||
and can activate unrelated sidebar items.
|
||||
|
||||
```jsp
|
||||
<c:set var="currentPath" value="${requestScope['javax.servlet.forward.servlet_path']}" />
|
||||
<c:if test="${empty currentPath}">
|
||||
<c:set var="currentPath" value="${pageContext.request.servletPath}" />
|
||||
</c:if>
|
||||
<a class="${(currentPath == '/books' or fn:startsWith(currentPath, '/books/')) ? 'is-active' : ''}">
|
||||
```
|
||||
- Keep fragments presentation-focused. They should not open database
|
||||
connections or call DAOs.
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
|
||||
@@ -45,6 +45,9 @@ image-first design and preserve the Servlet/JSP layered architecture.
|
||||
- Do not implement UI only from text descriptions when an approved image
|
||||
reference exists.
|
||||
- Do not put SQL, DAO calls, or business workflows in JSP pages.
|
||||
- Do not hard-code operational dashboard/report metrics, sample people, fixed
|
||||
borrow dates, or fake table rows in JSP pages; use Servlet-provided request
|
||||
attributes and empty states.
|
||||
- Do not rely only on browser validation for protected workflows.
|
||||
|
||||
---
|
||||
|
||||
@@ -36,6 +36,100 @@ changes the frontend architecture.
|
||||
|
||||
---
|
||||
|
||||
## Scenario: Dashboard Workbench Request Contract
|
||||
|
||||
### 1. Scope / Trigger
|
||||
|
||||
- Trigger: the authenticated workbench spans Servlet request attributes,
|
||||
service-derived report/catalog/borrowing data, and role-specific JSP display.
|
||||
- Route: `GET /dashboard`.
|
||||
- JSP path: `WEB-INF/jsp/dashboard.jsp`.
|
||||
|
||||
### 2. Signatures
|
||||
|
||||
- Servlet: `DashboardServlet.doGet(HttpServletRequest, HttpServletResponse)`.
|
||||
- Services used for page data:
|
||||
- `BookService.listCategories()`.
|
||||
- `BookService.searchBooks(new BookSearchCriteria())`.
|
||||
- `ReaderService.searchReaders(new ReaderSearchCriteria())` for staff reader
|
||||
totals.
|
||||
- `ReportService.loadReportCenter(AuthenticatedUser actor)` for
|
||||
administrator/librarian users.
|
||||
- `BorrowingService.searchRecords(actor, new BorrowRecordSearchCriteria())`
|
||||
for administrator/librarian users.
|
||||
- Request attributes:
|
||||
- `currentUser: AuthenticatedUser`.
|
||||
- `categories: List<BookCategory>`.
|
||||
- `dashboardBooks: List<Book>`.
|
||||
- `dashboardMetrics: List<DashboardMetric>`.
|
||||
- `reportCenter: ReportCenter` for staff users when report loading succeeds.
|
||||
- `dashboardBorrowRecords: List<BorrowRecord>` for staff users.
|
||||
- `errorMessage: String` when a service returns a safe failure.
|
||||
|
||||
### 3. Contracts
|
||||
|
||||
- Workbench values must come from request attributes populated by the Servlet;
|
||||
JSP must not embed operational sample rows, fixed dates, or fake totals.
|
||||
- Staff metrics use `ReportCenter` values derived from `books` and
|
||||
`borrow_records`, plus reader totals from `ReaderService`; reader fallback
|
||||
metrics may derive from `dashboardBooks`.
|
||||
- Popular ranking, overdue rows, and borrowing rows render only real service
|
||||
results and show empty states when lists are empty.
|
||||
- Category filters render from `categories`, the same source used by catalog and
|
||||
book-management pages.
|
||||
- Role-gated sections stay in JSP conditionals based on `sessionScope.userRole`;
|
||||
staff-only data is not requested for reader users.
|
||||
|
||||
### 4. Validation & Error Matrix
|
||||
|
||||
- Category load failure -> `categories` is an empty list and `errorMessage` is
|
||||
set.
|
||||
- Book search failure -> `dashboardBooks` is an empty list and `errorMessage`
|
||||
is set.
|
||||
- Reader total load failure -> staff metrics fall back to another real
|
||||
service-derived metric and `errorMessage` is set.
|
||||
- Staff report load failure -> report-backed sections show empty states and
|
||||
`errorMessage` is set.
|
||||
- Staff borrowing search failure -> `dashboardBorrowRecords` is an empty list
|
||||
and `errorMessage` is set.
|
||||
- Empty service result -> render a stable empty state, not hard-coded fallback
|
||||
sample data.
|
||||
|
||||
### 5. Good/Base/Bad Cases
|
||||
|
||||
- Good: a librarian opens `/dashboard` and sees report-backed metrics, current
|
||||
borrowing rows, overdue rows, popular ranking, and real book rows.
|
||||
- Base: no borrow records exist; the workbench keeps the layout and shows empty
|
||||
states for ranking, borrowing, and overdue panels.
|
||||
- Bad: `dashboard.jsp` contains names, book IDs, 2024 dates, or counts that do
|
||||
not come from request attributes.
|
||||
|
||||
### 6. Tests Required
|
||||
|
||||
- Run Maven compile/test for Servlet and JavaBean contract checks.
|
||||
- Run standalone service checks covering report, borrowing, catalog/book, and
|
||||
permission policy behavior when available.
|
||||
- Scan `dashboard.jsp` for static sample names, fixed dates, and decorative
|
||||
sample-only values after dashboard changes.
|
||||
- Verify staff and reader role conditionals still show only the intended
|
||||
sections.
|
||||
|
||||
### 7. Wrong vs Correct
|
||||
|
||||
#### Wrong
|
||||
|
||||
```text
|
||||
dashboard.jsp -> hard-coded metric "12,586" and fixed rows like "L20240521001"
|
||||
```
|
||||
|
||||
#### Correct
|
||||
|
||||
```text
|
||||
dashboard.jsp <- DashboardServlet <- ReportService/BookService/ReaderService/BorrowingService
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Page Scripts
|
||||
|
||||
Small JavaScript can improve interaction, such as confirm dialogs or local form
|
||||
|
||||
@@ -33,7 +33,11 @@ rendering.
|
||||
### 2. Signatures
|
||||
|
||||
- Login form: `POST /login`.
|
||||
- Request fields: `username`, `password`, and optional `redirect`.
|
||||
- Request fields consumed by `LoginServlet`: `username`, `password`, and
|
||||
optional `redirect`.
|
||||
- Presentation-only login controls may submit auxiliary fields such as
|
||||
`rememberUsername`; these must not participate in authentication or
|
||||
authorization unless the Servlet/service contract is deliberately changed.
|
||||
- Login JSP request attributes: `errorMessage`, `username`, and `redirect`.
|
||||
- Dashboard/role JSP session attributes: `authenticatedUser`, `userRole`, and
|
||||
`userPermissions`.
|
||||
@@ -47,6 +51,12 @@ rendering.
|
||||
attribute or session attribute.
|
||||
- `redirect` must be a same-application path beginning with one `/`; invalid
|
||||
values are ignored.
|
||||
- Login pages must not include a client-side role selector. The authenticated
|
||||
role is determined by the `users.role_code` row returned through
|
||||
`AuthService`, not by client-submitted form state.
|
||||
- Remember-me behavior may persist only the username in browser storage. It must
|
||||
never persist passwords, password hashes, redirects, permission state, or
|
||||
extend the server session.
|
||||
- JSPs render data with JSP EL/JSTL, not scriptlet Java.
|
||||
- JSPs may read safe session snapshots, but they must not call DAOs or inspect
|
||||
password hashes.
|
||||
@@ -67,10 +77,12 @@ rendering.
|
||||
|
||||
- Good: failed login keeps the escaped username and never redisplays the
|
||||
password.
|
||||
- Good: checking remember-me does not change the server-side authentication
|
||||
decision.
|
||||
- Base: dashboard reads `sessionScope.authenticatedUser.displayName` and
|
||||
`sessionScope.userRole` only for display/navigation.
|
||||
- Bad: JSP uses scriptlets, JDBC, or raw request parameters to decide
|
||||
authentication.
|
||||
- Bad: JSP, JavaScript, or Servlet code trusts a client-submitted role field to
|
||||
grant a role or stores the password in browser storage.
|
||||
|
||||
### 6. Tests Required
|
||||
|
||||
@@ -79,6 +91,8 @@ rendering.
|
||||
files.
|
||||
- Run service-level auth checks for required fields, invalid credentials,
|
||||
success, DAO fallback, and permission checks.
|
||||
- When login page scripts change, scan them to confirm only usernames can be
|
||||
stored client-side and `password` is never persisted.
|
||||
- When Maven/Tomcat is available, run a Servlet/JSP compile or package check.
|
||||
|
||||
### 7. Wrong vs Correct
|
||||
@@ -87,6 +101,7 @@ rendering.
|
||||
|
||||
```jsp
|
||||
<%-- JSP checks request.getParameter("password") or runs SQL directly. --%>
|
||||
<%-- JavaScript stores the password or LoginServlet trusts a submitted role. --%>
|
||||
```
|
||||
|
||||
#### Correct
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}
|
||||
{"file": ".trellis/spec/frontend/index.md", "reason": "Frontend JSP/CSS context for verifying the refreshed preview"}
|
||||
@@ -0,0 +1,2 @@
|
||||
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}
|
||||
{"file": ".trellis/spec/frontend/index.md", "reason": "Frontend JSP/CSS context for preview verification"}
|
||||
@@ -0,0 +1,47 @@
|
||||
# Rebuild Current Frontend Preview
|
||||
|
||||
## Goal
|
||||
|
||||
Rebuild the current Java Web application and refresh the local Tomcat deployment so the user can view the latest frontend effect in the browser.
|
||||
|
||||
## What I already know
|
||||
|
||||
* The user asked to rebuild the program to inspect the new frontend.
|
||||
* The project is a Java 11 Maven WAR application.
|
||||
* Maven produces `target/library-management.war`.
|
||||
* Frontend JSP/CSS assets live under `src/main/webapp`.
|
||||
* Local Tomcat path recorded by prior work is `/home/sjy/apps/tomcat/apache-tomcat-9.0.117/apache-tomcat-9.0.117`.
|
||||
* The Tomcat context should be `/library-management`.
|
||||
|
||||
## Requirements
|
||||
|
||||
* Run a clean Maven package build.
|
||||
* Deploy the new WAR to the local Tomcat `webapps` directory.
|
||||
* Remove the expanded old deployment directory before restart so stale frontend assets are not reused.
|
||||
* Start or restart Tomcat.
|
||||
* Verify the frontend login URL is reachable.
|
||||
* Provide the local browser URL to the user.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
* [x] `mvn clean package` succeeds.
|
||||
* [x] `target/library-management.war` exists.
|
||||
* [x] Tomcat deployment is refreshed with the new WAR.
|
||||
* [x] `/library-management/login` returns HTTP 200.
|
||||
* [x] User receives the local preview URL.
|
||||
|
||||
## Out of Scope
|
||||
|
||||
* Source code changes.
|
||||
* New UI requirements or redesign decisions.
|
||||
* Database schema or seed data changes.
|
||||
|
||||
## Technical Notes
|
||||
|
||||
* Build command from README: `mvn clean package`; fallback Maven path: `/home/sjy/.sdkman/candidates/maven/current/bin/mvn clean package`.
|
||||
* Deployment target: `/home/sjy/apps/tomcat/apache-tomcat-9.0.117/apache-tomcat-9.0.117/webapps/library-management.war`.
|
||||
* Build completed at 2026-04-28 20:21 +0800.
|
||||
* Previous Tomcat deployment was moved to `/home/sjy/apps/tomcat/apache-tomcat-9.0.117/apache-tomcat-9.0.117/deploy-backups/_pre-preview-20260428-202206/`.
|
||||
* Tomcat is running in tmux session `mzh-library-tomcat`.
|
||||
* Verified `http://localhost:8080/library-management/login` returns `HTTP 200` with `Content-Type: text/html;charset=UTF-8`.
|
||||
* Verified demo login redirects to `/library-management/dashboard`, and the authenticated dashboard returns `HTTP 200`.
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"id": "04-28-rebuild-current-frontend-preview",
|
||||
"name": "04-28-rebuild-current-frontend-preview",
|
||||
"title": "rebuild current frontend preview",
|
||||
"description": "",
|
||||
"status": "in_progress",
|
||||
"dev_type": null,
|
||||
"scope": null,
|
||||
"package": null,
|
||||
"priority": "P2",
|
||||
"creator": "Zzzz",
|
||||
"assignee": "Zzzz",
|
||||
"createdAt": "2026-04-28",
|
||||
"completedAt": null,
|
||||
"branch": null,
|
||||
"base_branch": "master",
|
||||
"worktree_path": null,
|
||||
"commit": null,
|
||||
"pr_url": null,
|
||||
"subtasks": [],
|
||||
"children": [],
|
||||
"parent": null,
|
||||
"relatedFiles": [],
|
||||
"notes": "",
|
||||
"meta": {}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}
|
||||
{"file": ".trellis/spec/frontend/index.md", "reason": "Frontend checklist for reviewing login page UI changes"}
|
||||
{"file": ".trellis/spec/frontend/type-safety.md", "reason": "Verify login form contract remains unchanged"}
|
||||
{"file": ".trellis/spec/frontend/quality-guidelines.md", "reason": "Verify UI layout quality after removal"}
|
||||
@@ -0,0 +1,7 @@
|
||||
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}
|
||||
{"file": ".trellis/spec/frontend/index.md", "reason": "Frontend JSP/CSS guidelines for login page UI changes"}
|
||||
{"file": ".trellis/spec/frontend/directory-structure.md", "reason": "JSP and static asset placement conventions"}
|
||||
{"file": ".trellis/spec/frontend/component-guidelines.md", "reason": "Form and page component conventions"}
|
||||
{"file": ".trellis/spec/frontend/state-management.md", "reason": "Server-rendered form state conventions"}
|
||||
{"file": ".trellis/spec/frontend/type-safety.md", "reason": "Login form request contract and loginRole behavior"}
|
||||
{"file": ".trellis/spec/frontend/quality-guidelines.md", "reason": "UI quality checks for JSP/CSS changes"}
|
||||
@@ -0,0 +1,52 @@
|
||||
# 调整登录页登录选项与布局
|
||||
|
||||
## Goal
|
||||
|
||||
简化登录界面:移除登录身份单选项和标题旁的图书图标,并微调表单布局,让登录卡片在元素减少后仍保持居中、紧凑和视觉平衡。
|
||||
|
||||
## What I Already Know
|
||||
|
||||
* 用户要求删除登录界面中的“登录身份”选项。
|
||||
* 用户要求删除登录界面中的图书图标。
|
||||
* 登录页 JSP 位于 `src/main/webapp/WEB-INF/jsp/auth/login.jsp`。
|
||||
* 登录页样式位于 `src/main/webapp/static/css/app.css`。
|
||||
* 登录页脚本位于 `src/main/webapp/static/js/login.js`,当前主要处理记住用户名、密码显示切换和忘记密码提示。
|
||||
* 前端规范说明登录页不应包含客户端角色选择,认证后的角色由 `AuthService` 返回的用户角色决定。
|
||||
|
||||
## Assumptions
|
||||
|
||||
* “图书的图标”指登录页标题旁内联 SVG 的 `login-brand-mark`,不是背景插画 `static/images/library-login.svg`。
|
||||
* “微调布局”指因移除图标和登录身份单选后,调整标题区域、表单间距和卡片留白,不做整页视觉重设计。
|
||||
|
||||
## Requirements
|
||||
|
||||
* 移除登录页的登录身份单选区域,包括“登录身份”“管理员”“馆员”“读者”选项。
|
||||
* 移除登录页标题旁的图书图标。
|
||||
* 保留用户名、密码、记住我、忘记密码提示和登录提交功能。
|
||||
* 表单提交仍只依赖后端已消费的 `username`、`password`、可选 `redirect`,不改变认证/授权逻辑。
|
||||
* 调整登录页布局,使标题、副标题、输入框、选项行和按钮在桌面与移动端都保持合理间距。
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
* [x] 登录页不再渲染“登录身份”文案和角色单选按钮。
|
||||
* [x] 登录页标题旁不再渲染图书 SVG 图标。
|
||||
* [x] 登录页在桌面和移动端没有明显空洞、错位或文本重叠。
|
||||
* [x] 用户名/密码登录表单仍可提交到 `POST /login`。
|
||||
* [x] 项目可通过 Maven 构建或等价检查。
|
||||
|
||||
## Definition of Done
|
||||
|
||||
* JSP/CSS 改动范围聚焦在登录页 UI。
|
||||
* Lint/typecheck/build 可用检查已运行;如无法运行,记录原因。
|
||||
* 不修改后端认证授权逻辑。
|
||||
|
||||
## Out of Scope
|
||||
|
||||
* 不重做整套登录页视觉风格。
|
||||
* 不修改用户角色、权限、认证服务或数据库。
|
||||
* 不删除登录页背景插画,除非代码检查证明它就是用户所指图标。
|
||||
|
||||
## Technical Notes
|
||||
|
||||
* 前端规范入口: `.trellis/spec/frontend/index.md`。
|
||||
* 相关规范: `.trellis/spec/frontend/type-safety.md` 中说明 `LoginServlet` 消费 `username`、`password` 和可选 `redirect`,登录角色不由客户端表单状态决定。
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"id": "login-page-simplify-layout",
|
||||
"name": "login-page-simplify-layout",
|
||||
"title": "调整登录页登录选项与布局",
|
||||
"description": "",
|
||||
"status": "in_progress",
|
||||
"dev_type": null,
|
||||
"scope": null,
|
||||
"package": null,
|
||||
"priority": "P2",
|
||||
"creator": "Zzzz",
|
||||
"assignee": "Zzzz",
|
||||
"createdAt": "2026-04-28",
|
||||
"completedAt": null,
|
||||
"branch": null,
|
||||
"base_branch": "master",
|
||||
"worktree_path": null,
|
||||
"commit": null,
|
||||
"pr_url": null,
|
||||
"subtasks": [],
|
||||
"children": [],
|
||||
"parent": null,
|
||||
"relatedFiles": [],
|
||||
"notes": "",
|
||||
"meta": {}
|
||||
}
|
||||
+2
-2
@@ -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,6 @@
|
||||
{"file": ".trellis/spec/frontend/index.md", "reason": "Frontend checklist for reviewing JSP/CSS presentation changes."}
|
||||
{"file": ".trellis/spec/frontend/component-guidelines.md", "reason": "Verify shared frame role navigation and Simplified Chinese copy remain correct."}
|
||||
{"file": ".trellis/spec/frontend/quality-guidelines.md", "reason": "Verify layout, accessibility basics, and no obvious overlap/clipping."}
|
||||
{"file": ".trellis/spec/backend/index.md", "reason": "Verify encoding changes remain within Servlet/JSP architecture."}
|
||||
{"file": ".trellis/spec/backend/quality-guidelines.md", "reason": "Verify layer boundaries and Maven build/test expectations."}
|
||||
{"file": ".trellis/tasks/archive/2026-04/00-bootstrap-guidelines/research/project-requirements.md", "reason": "Confirm the change preserves the agreed JSP + Servlet + Tomcat stack."}
|
||||
@@ -0,0 +1,7 @@
|
||||
{"file": ".trellis/spec/frontend/index.md", "reason": "Frontend JSP/CSS presentation conventions and checklist for authenticated UI work."}
|
||||
{"file": ".trellis/spec/frontend/directory-structure.md", "reason": "JSP fragment and static asset placement constraints."}
|
||||
{"file": ".trellis/spec/frontend/component-guidelines.md", "reason": "Shared header/sidebar fragment rules, role-conditioned navigation, and Simplified Chinese copy requirements."}
|
||||
{"file": ".trellis/spec/frontend/quality-guidelines.md", "reason": "UI quality checks for JSP/CSS layout, accessibility basics, and visual consistency."}
|
||||
{"file": ".trellis/spec/backend/index.md", "reason": "Servlet/JSP/Tomcat architecture context for the encoding filter change."}
|
||||
{"file": ".trellis/spec/backend/quality-guidelines.md", "reason": "Layer-boundary and Maven verification requirements for backend-adjacent changes."}
|
||||
{"file": ".trellis/tasks/archive/2026-04/00-bootstrap-guidelines/research/project-requirements.md", "reason": "Original project stack and presentation-layer requirements."}
|
||||
@@ -0,0 +1,83 @@
|
||||
# Fix Frontend Encoding And Layout
|
||||
|
||||
## Goal
|
||||
|
||||
Fix the authenticated JSP frontend so Simplified Chinese text renders correctly in the browser, then refine the shared application frame and dashboard layout so navigation, role workbench links, search, user identity, and page content look coordinated at common desktop and mobile widths.
|
||||
|
||||
## What I Already Know
|
||||
|
||||
* The user wants to view the actual frontend UI after the frontend refactor.
|
||||
* The running Tomcat application appears to show the pre-refactor UI.
|
||||
* The project is a Java 11 Maven WAR application.
|
||||
* Maven produces `target/library-management.war`.
|
||||
* Frontend assets and JSPs live under `src/main/webapp`.
|
||||
* Local Tomcat path is `/home/sjy/apps/tomcat/apache-tomcat-9.0.117/apache-tomcat-9.0.117`.
|
||||
* Local MySQL is running at `127.0.0.1:3306`.
|
||||
* The user now sees severe mojibake such as `书 å¾ä¹¦ç®¡çç³»ç»`, which is UTF-8 Chinese content being decoded as a non-UTF-8 encoding.
|
||||
* The visible broken area is the authenticated shell: sidebar brand, role workbench, module navigation, topbar search, notification, user pill, and role label.
|
||||
* JSP pages already declare `contentType="text/html;charset=UTF-8"` and `<meta charset="UTF-8">`; `CharacterEncodingFilter` currently sets request/response character encoding but does not force an HTML content type.
|
||||
* Follow-up user screenshot shows the authenticated shell still renders as ordinary document-flow text: the dark fixed sidebar is missing, the topbar is loose, and the dashboard metric cards collapse into a vertical text column. HTML and CSS endpoint checks alone are not sufficient.
|
||||
|
||||
## Requirements
|
||||
|
||||
* Ensure every JSP-rendered HTML response is explicitly served as UTF-8 so Chinese labels, placeholders, headings, and role names do not render as mojibake.
|
||||
* Preserve the JSP + Servlet + CSS stack; do not introduce a frontend framework.
|
||||
* Keep all user-facing JSP copy in Simplified Chinese.
|
||||
* Refine the shared authenticated frame in `header.jspf`/CSS:
|
||||
* Sidebar brand and role workbench should be readable and not visually crowded.
|
||||
* Navigation links should align consistently and avoid repeated glyph/text collisions.
|
||||
* Topbar search, notification, user display, and role label should fit without overlap.
|
||||
* Refine dashboard layout in CSS so metric cards, search/ranking panels, tables, and shortcut cards have balanced spacing and degrade cleanly on narrower viewports.
|
||||
* Rebuild and redeploy the WAR to the local Tomcat instance after source changes.
|
||||
* Verify `/library-management/login` is reachable and a known login reaches `/library-management/dashboard`.
|
||||
* Verify the dashboard HTML/headers indicate UTF-8 and the rendered shell text is readable Chinese.
|
||||
* Verify the rendered page visually in a browser at desktop width:
|
||||
* A dark fixed left sidebar must be visible.
|
||||
* The topbar must start to the right of the sidebar and align search/user controls.
|
||||
* Dashboard content must start below the topbar and to the right of the sidebar.
|
||||
* Metrics must render as cards in a grid on desktop, not as plain vertical text.
|
||||
* No overlapping, no unstyled header text, and no horizontal crowding at 1920px-wide desktop.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
* [x] `mvn clean package` succeeds after the frontend/encoding changes.
|
||||
* [x] Tomcat `webapps/library-management.war` is refreshed from `target/library-management.war`.
|
||||
* [x] Old expanded deployment directory is removed before restart.
|
||||
* [x] Tomcat listens on port `8080`.
|
||||
* [x] `/library-management/login` returns HTTP 200.
|
||||
* [x] `admin/admin123` login redirects to `/library-management/dashboard`.
|
||||
* [x] Authenticated dashboard response uses UTF-8 and no longer displays mojibake for Chinese UI text.
|
||||
* [ ] Sidebar, role chips, topbar search/actions, dashboard panels, and responsive layout avoid obvious overlap, clipping, duplicated visual noise, and unstyled document-flow rendering in an actual browser screenshot.
|
||||
|
||||
## Definition Of Done
|
||||
|
||||
* Encoding fix implemented in source.
|
||||
* Layout refinement implemented in JSP/CSS source.
|
||||
* Rebuild and deploy completed.
|
||||
* Verification results reported to the user.
|
||||
* No database schema changes or unrelated backend behavior changes.
|
||||
|
||||
## Out Of Scope
|
||||
|
||||
* Replacing the JSP/CSS frontend with React, Vue, or another SPA framework.
|
||||
* Changing database schema or seed data.
|
||||
* Committing build artifacts.
|
||||
* Reworking business workflows beyond what is needed to render the current pages correctly.
|
||||
|
||||
## Technical Notes
|
||||
|
||||
* Build command from README: `/home/sjy/.sdkman/candidates/maven/current/bin/mvn clean package` if `mvn` is unavailable.
|
||||
* Deployment target: `/home/sjy/apps/tomcat/apache-tomcat-9.0.117/apache-tomcat-9.0.117/webapps/library-management.war`.
|
||||
* Rebuild completed at 2026-04-28 16:55 +0800; deployed WAR size is 4,489,937 bytes.
|
||||
* Previous deployment was moved to `/home/sjy/apps/tomcat/apache-tomcat-9.0.117/apache-tomcat-9.0.117/deploy-backups/_pre-rebuild-20260428-1656/`.
|
||||
* Deployed `static/css/app.css` is byte-for-byte identical to `src/main/webapp/static/css/app.css`.
|
||||
* Likely impacted files from inspection:
|
||||
* `src/main/java/com/mzh/library/filter/CharacterEncodingFilter.java`
|
||||
* `src/main/webapp/WEB-INF/jsp/common/header.jspf`
|
||||
* `src/main/webapp/WEB-INF/jsp/dashboard.jsp`
|
||||
* `src/main/webapp/static/css/app.css`
|
||||
* Final verification completed at 2026-04-28 17:34 +0800:
|
||||
* `/library-management/login` returns `200` with `Content-Type: text/html;charset=UTF-8`.
|
||||
* `admin/admin123` login reaches `/library-management/dashboard` with UTF-8 content.
|
||||
* Dashboard HTML contains readable Chinese markers including `图书管理系统`, `角色工作台`, `管理员工作台`, `馆藏检索`, `用户管理`, `系统日志`, and `退出登录`.
|
||||
* Follow-up visual regression reported after that verification: screenshot shows the authenticated shell unstyled at desktop width despite readable Chinese. Future verification must include a browser screenshot or equivalent computed-style/layout assertion, not only `curl`.
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"id": "04-28-rebuild-current-frontend",
|
||||
"name": "04-28-rebuild-current-frontend",
|
||||
"title": "rebuild and redeploy current frontend",
|
||||
"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,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": {}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{"file": ".trellis/spec/frontend/index.md", "reason": "Frontend JSP/CSS stack and checklist for review."}
|
||||
{"file": ".trellis/spec/frontend/component-guidelines.md", "reason": "Review shared JSP fragments, forms, tables, navigation, and Chinese copy."}
|
||||
{"file": ".trellis/spec/frontend/quality-guidelines.md", "reason": "Review visual fidelity to image-first workflow and forbidden patterns."}
|
||||
{"file": ".trellis/spec/frontend/state-management.md", "reason": "Verify server-rendered state and existing data flow remain intact."}
|
||||
{"file": ".trellis/spec/frontend/type-safety.md", "reason": "Verify display contracts and validation handling remain safe."}
|
||||
{"file": ".trellis/tasks/04-28-frontend-reference-redesign/research/reference-dashboard-visual-notes.md", "reason": "Compare implementation against extracted visual requirements."}
|
||||
@@ -0,0 +1,8 @@
|
||||
{"file": ".trellis/spec/frontend/index.md", "reason": "Frontend JSP/CSS stack and pre-development checklist for this redesign."}
|
||||
{"file": ".trellis/spec/frontend/directory-structure.md", "reason": "JSP and static asset organization constraints."}
|
||||
{"file": ".trellis/spec/frontend/component-guidelines.md", "reason": "Shared JSP fragment, form, table, navigation, and Chinese copy conventions."}
|
||||
{"file": ".trellis/spec/frontend/quality-guidelines.md", "reason": "Image-to-JSP restoration quality bar and forbidden patterns."}
|
||||
{"file": ".trellis/spec/frontend/hook-guidelines.md", "reason": "Confirms no React/Vue hook conventions should be introduced."}
|
||||
{"file": ".trellis/spec/frontend/state-management.md", "reason": "Server-rendered request/session/form state conventions to preserve."}
|
||||
{"file": ".trellis/spec/frontend/type-safety.md", "reason": "JSP/Servlet validation and JavaBean display contract constraints."}
|
||||
{"file": ".trellis/tasks/04-28-frontend-reference-redesign/research/reference-dashboard-visual-notes.md", "reason": "Visual requirements extracted from the provided reference image."}
|
||||
@@ -0,0 +1,71 @@
|
||||
# brainstorm: Redesign Frontend From Reference Image
|
||||
|
||||
## Goal
|
||||
|
||||
Refactor the JSP/CSS frontend so the library-management application visually matches the provided dashboard reference as closely as practical while preserving existing Servlet/JSP behavior, routes, role-based navigation, forms, tables, and Simplified Chinese interface copy.
|
||||
|
||||
## What I Already Know
|
||||
|
||||
* The user wants the frontend rebuilt to imitate the attached reference image as closely as possible.
|
||||
* The reference image is a Chinese library management dashboard with a dark left sidebar, white top bar, dense white card panels, blue primary actions, rounded statistics cards, table sections, and operational shortcut tiles.
|
||||
* The application is a JSP + Servlet + Maven WAR project, not a React/Vue SPA.
|
||||
* Existing frontend files live under `src/main/webapp/WEB-INF/jsp/` with shared CSS in `src/main/webapp/static/css/app.css`.
|
||||
* Current shared header fragment is `src/main/webapp/WEB-INF/jsp/common/header.jspf`.
|
||||
* Existing pages include dashboard, login, catalog, book management, reader management, borrowing, reports, system logs, and user management JSPs.
|
||||
* Frontend spec requires image-first implementation and Simplified Chinese display copy.
|
||||
|
||||
## Assumptions
|
||||
|
||||
* "Frontend" means the shared visual system across JSP pages, with the dashboard receiving the closest match because the reference image is a dashboard screenshot.
|
||||
* The redesign should keep current endpoints, request parameter names, JSTL conditions, and server-rendered data contracts unchanged.
|
||||
* New CSS classes and JSP structure are allowed when they improve visual fidelity, but no new frontend framework should be introduced.
|
||||
* The reference image should be stored with the task for implementation/check agents.
|
||||
|
||||
## Open Questions
|
||||
|
||||
* Confirm scope: apply the reference style across the whole JSP frontend, not only the dashboard page.
|
||||
|
||||
## Requirements
|
||||
|
||||
* Build a left dark sidebar similar to the reference, including brand/title, role workbench buttons, module navigation, and compact footer/menu area.
|
||||
* Build a top utility bar similar to the reference, including breadcrumb/location text, search field, notification/avatar/user controls where appropriate.
|
||||
* Restyle the dashboard as a dense admin workspace with:
|
||||
* Large white dashboard shell.
|
||||
* Four metric cards with colored icon blocks and month-over-month text.
|
||||
* Search/filter panel.
|
||||
* Ranking/chart-like panel matching the screenshot's simple blue bar chart look.
|
||||
* Recent borrowing table and overdue table.
|
||||
* Book-management table.
|
||||
* Shortcut cards for reader management, report center, borrowing circulation, and system logs.
|
||||
* Restyle shared tables, forms, buttons, badges, panels, empty states, and navigation so secondary pages feel consistent with the reference.
|
||||
* Preserve existing JSP/Servlet behavior and role-based visibility.
|
||||
* Keep user-visible copy in Simplified Chinese.
|
||||
* Keep responsive behavior usable on narrower screens.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
* [ ] Dashboard layout visibly matches the reference image's structure, spacing, palette, and density.
|
||||
* [ ] Shared navigation changes are reflected across existing JSP pages without breaking role-based links.
|
||||
* [ ] Existing forms and tables remain functional and readable after the redesign.
|
||||
* [ ] No React/Vue/SPA tooling is introduced.
|
||||
* [ ] Maven build succeeds.
|
||||
|
||||
## Definition Of Done
|
||||
|
||||
* Tests/build run where available.
|
||||
* JSP/CSS changes reviewed against frontend specs and the reference image.
|
||||
* Task context files are curated for implement/check agents.
|
||||
* Any reusable convention learned during the work is considered for spec update.
|
||||
|
||||
## Out Of Scope
|
||||
|
||||
* Backend behavior changes.
|
||||
* Database schema changes.
|
||||
* Replacing JSP with a JavaScript framework.
|
||||
* Exact live charting libraries unless needed; a CSS/HTML approximation is acceptable for this visual refactor.
|
||||
|
||||
## Technical Notes
|
||||
|
||||
* Reference image copied to `.trellis/tasks/04-28-frontend-reference-redesign/research/reference-dashboard.png`.
|
||||
* Visual notes are recorded in `.trellis/tasks/04-28-frontend-reference-redesign/research/reference-dashboard-visual-notes.md`.
|
||||
* Relevant spec index: `.trellis/spec/frontend/index.md`.
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
# Reference Dashboard Visual Notes
|
||||
|
||||
## Source
|
||||
|
||||
Reference image: `.trellis/tasks/04-28-frontend-reference-redesign/research/reference-dashboard.png`
|
||||
|
||||
Original accessible path during planning: `/mnt/d/qq/聊天文件/2535624881/nt_qq/nt_data/Pic/2026-04/Ori/ab6d1035bac12c469acaffea0e6db1c8.png`
|
||||
|
||||
## Overall Layout
|
||||
|
||||
* Full application frame with a fixed-width dark navy sidebar on the left and a light gray workspace on the right.
|
||||
* Sidebar width is about 250 px in the reference. It contains the system name at top, role workbench buttons, module navigation links, and a compact menu icon near the bottom.
|
||||
* Main area has a white top bar with breadcrumb text on the left and search, notification, avatar, role label, and dropdown affordance on the right.
|
||||
* Content area uses a light gray page background with white cards, small border radius, subtle shadows, and dense spacing.
|
||||
|
||||
## Palette And Typography
|
||||
|
||||
* Sidebar: very dark navy gradient or solid dark blue-black.
|
||||
* Primary action blue: medium royal blue.
|
||||
* Secondary accent colors: teal, orange, purple, red, and green for icon/stat/status accents.
|
||||
* Cards: white with light gray borders and soft shadows.
|
||||
* Text: dark slate/near black for headings, gray for helper copy and metadata.
|
||||
* Typography is compact, Chinese UI oriented, and dashboard-like rather than marketing-like.
|
||||
|
||||
## Dashboard Structure
|
||||
|
||||
* Top hero panel starts with "管理员工作台" heading and short explanatory copy.
|
||||
* Four statistic cards in one row:
|
||||
* 馆藏总量
|
||||
* 在借数量
|
||||
* 逾期数量
|
||||
* 读者总数
|
||||
* Middle area:
|
||||
* Left: 馆藏检索 form with two-column labels/inputs/select and blue search button plus reset button.
|
||||
* Right: 热门图书排行 bar chart with blue vertical bars and small labels.
|
||||
* Table area:
|
||||
* 借阅流通 recent records table.
|
||||
* 逾期列表 pending overdue table.
|
||||
* 图书管理 book list table.
|
||||
* Bottom/right shortcut tiles:
|
||||
* 读者管理
|
||||
* 报表中心
|
||||
* 借阅流通
|
||||
* 系统日志
|
||||
|
||||
## Interaction And Reuse Targets
|
||||
|
||||
* Preserve existing links and routes in navigation.
|
||||
* Sidebar active/hover states should use blue filled pills.
|
||||
* Role workbench entries should be prominent colored pills near the top of the sidebar.
|
||||
* Tables should be compact with subtle row separators and badge-like statuses.
|
||||
* Forms should use compact inputs with borders and clear focus states.
|
||||
* Existing JSP pages can reuse shared classes for panels, toolbar forms, tables, badges, action links, and cards.
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"id": "frontend-reference-redesign",
|
||||
"name": "frontend-reference-redesign",
|
||||
"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": {}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}
|
||||
{"file": ".trellis/spec/frontend/index.md", "reason": "Verify frontend work follows JSP/CSS conventions"}
|
||||
{"file": ".trellis/spec/frontend/quality-guidelines.md", "reason": "Verify responsive UI and simplified authenticated shell"}
|
||||
{"file": ".trellis/spec/backend/quality-guidelines.md", "reason": "Verify backend layering and no static data regressions"}
|
||||
{"file": ".trellis/spec/backend/database-guidelines.md", "reason": "Verify dashboard uses existing derived report data correctly"}
|
||||
@@ -0,0 +1,8 @@
|
||||
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}
|
||||
{"file": ".trellis/spec/frontend/index.md", "reason": "Frontend JSP/CSS conventions for authenticated shell and dashboard UI"}
|
||||
{"file": ".trellis/spec/backend/index.md", "reason": "Backend Servlet/service/DAO layering for dashboard real data"}
|
||||
{"file": ".trellis/spec/frontend/component-guidelines.md", "reason": "JSP fragments, cards, tables, and reusable presentation rules"}
|
||||
{"file": ".trellis/spec/frontend/state-management.md", "reason": "Server-rendered request/session state conventions"}
|
||||
{"file": ".trellis/spec/frontend/quality-guidelines.md", "reason": "UI quality checks for JSP/CSS changes"}
|
||||
{"file": ".trellis/spec/backend/database-guidelines.md", "reason": "Existing report data contracts and database-derived summary rules"}
|
||||
{"file": ".trellis/spec/backend/error-handling.md", "reason": "ServiceResult and servlet error handling conventions"}
|
||||
@@ -0,0 +1,54 @@
|
||||
# Fix Frontend Workbench Display
|
||||
|
||||
## Goal
|
||||
|
||||
Make the authenticated workbench reflect real application data and simplify the navigation-heavy UI so it does not duplicate the sidebar.
|
||||
|
||||
## What I Already Know
|
||||
|
||||
- The user reported that the frontend workbench data does not match actual data.
|
||||
- The current `dashboard.jsp` hard-codes metric values, popular book ranking rows, borrowing rows, overdue rows, and book rows.
|
||||
- The workbench shortcut cards for 读者管理, 报表中心, 借阅流通, and 系统日志 duplicate links already present in the sidebar.
|
||||
- The UI uses circular single-character markers beside text in metrics, shortcut cards, sidebar links, role chips, and topbar user summary.
|
||||
- The sidebar is fixed on desktop, but responsive CSS changes `.app-sidebar` and `.app-topbar` to static layout under 960px, effectively removing the persistent sidebar behavior.
|
||||
- Existing report infrastructure already exposes actual inventory summary, borrowing summary, overdue rows, and popular books through `ReportService.loadReportCenter(...)`.
|
||||
|
||||
## Assumptions
|
||||
|
||||
- "UI text beside an unnecessary circle with one character" applies to decorative single-character icon circles in the authenticated shell and workbench, not to plain text labels or table status pills.
|
||||
- The workbench should reuse existing server-rendered JSP/Servlet patterns rather than introducing client-side state.
|
||||
- When a specific real data source does not yet exist, prefer showing an existing real metric over keeping a static fake metric.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Replace hard-coded workbench summary metrics with real data.
|
||||
- Replace the hard-coded popular book ranking with real ranking data.
|
||||
- Replace hard-coded borrowing/overdue/book table samples with real data or remove the fake sample rows in favor of empty states.
|
||||
- Keep the workbench catalog search category selector populated from real categories.
|
||||
- Remove the workbench shortcut entry block containing 读者管理, 报表中心, 借阅流通, and 系统日志.
|
||||
- Remove the decorative circular single-character UI markers around text in the authenticated shell/workbench where they are not functionally necessary.
|
||||
- Ensure the sidebar cannot be hidden or collapsed by responsive layout rules.
|
||||
- Keep role-based visibility and permissions intact for administrator, librarian, and reader users.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] Workbench metrics are rendered from request attributes populated by backend services, not hard-coded numbers.
|
||||
- [ ] Popular ranking and table content no longer contain static sample records such as 张晓明, 活着, 三体, or fixed 2024 dates unless those values come from the database.
|
||||
- [ ] The workbench no longer shows shortcut cards for 读者管理, 报表中心, 借阅流通, or 系统日志.
|
||||
- [ ] Decorative single-character circles next to UI text are removed or restyled as plain text/spacing without circular badges.
|
||||
- [ ] Sidebar remains visible and occupies its sidebar column across responsive breakpoints.
|
||||
- [ ] Existing navigation links still work and remain role-aware.
|
||||
- [ ] Project lint/type-check or the closest available Java build/test command passes.
|
||||
|
||||
## Out Of Scope
|
||||
|
||||
- Adding new major dashboard modules beyond the current workbench content.
|
||||
- Redesigning unrelated pages outside the shared authenticated shell and workbench.
|
||||
- Changing database schema unless necessary to replace static workbench data.
|
||||
|
||||
## Technical Notes
|
||||
|
||||
- Likely files: `src/main/java/com/mzh/library/controller/DashboardServlet.java`, `src/main/webapp/WEB-INF/jsp/dashboard.jsp`, `src/main/webapp/WEB-INF/jsp/common/header.jspf`, and `src/main/webapp/static/css/app.css`.
|
||||
- Existing actual report data: `ReportServiceImpl`, `JdbcReportDao`, `ReportCenter`, `InventorySummary`, `BorrowingSummary`, `OverdueReportRow`, and `PopularBookReportRow`.
|
||||
- Existing category/book patterns: `BookServiceImpl`, `JdbcBookDao`, and `BookCatalogServlet`.
|
||||
- Existing borrowing list pattern: `BorrowingServiceImpl.searchRecords(...)` and `BorrowingManagementServlet`.
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"id": "frontend-workbench-display-fix",
|
||||
"name": "frontend-workbench-display-fix",
|
||||
"title": "修复前端工作台展示",
|
||||
"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,7 @@
|
||||
{"file": ".trellis/spec/frontend/index.md", "reason": "Review JSP changes against presentation-layer conventions."}
|
||||
{"file": ".trellis/spec/frontend/component-guidelines.md", "reason": "Verify button/action removal keeps page composition and primary operations intact."}
|
||||
{"file": ".trellis/spec/frontend/state-management.md", "reason": "Ensure JSP changes do not alter request/session contracts."}
|
||||
{"file": ".trellis/spec/frontend/quality-guidelines.md", "reason": "Run UI-oriented quality review for removed redundant actions."}
|
||||
{"file": ".trellis/spec/backend/database-guidelines.md", "reason": "Review Chinese demo data against schema and seed-data conventions."}
|
||||
{"file": ".trellis/spec/backend/quality-guidelines.md", "reason": "Verify backend layer boundaries and checks for schema-only data changes."}
|
||||
{"file": ".trellis/spec/frontend/type-safety.md", "reason": "Verify the login JSP keeps the POST /login contract, request fields, and safe rendering behavior."}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
{"file": ".trellis/spec/frontend/index.md", "reason": "JSP presentation-layer conventions and required pre-development checklist for removing redundant page actions."}
|
||||
{"file": ".trellis/spec/frontend/directory-structure.md", "reason": "Location and ownership of JSP pages, shared fragments, and static assets."}
|
||||
{"file": ".trellis/spec/frontend/component-guidelines.md", "reason": "Rules for JSP fragments, forms, tables, buttons, and page composition."}
|
||||
{"file": ".trellis/spec/frontend/state-management.md", "reason": "Server-rendered request/session/form state constraints for JSP changes."}
|
||||
{"file": ".trellis/spec/frontend/type-safety.md", "reason": "JSP/Servlet display contracts and safe rendering expectations."}
|
||||
{"file": ".trellis/spec/frontend/quality-guidelines.md", "reason": "UI quality checks for JSP/CSS changes."}
|
||||
{"file": ".trellis/spec/backend/index.md", "reason": "Backend architecture overview for database initialization changes."}
|
||||
{"file": ".trellis/spec/backend/database-guidelines.md", "reason": "MySQL schema and seed-data conventions for readers, categories, and books."}
|
||||
{"file": ".trellis/spec/backend/quality-guidelines.md", "reason": "Layer-boundary and verification expectations for database-only backend changes."}
|
||||
@@ -0,0 +1,70 @@
|
||||
# remove redundant page actions and add Chinese demo data
|
||||
|
||||
## Goal
|
||||
|
||||
精简已登录页面中与侧边栏重复的右侧跨模块跳转按钮,补充更贴近中文图书馆场景的演示图书与读者数据,并按参考截图重构真实可用的登录界面。
|
||||
|
||||
## What I already know
|
||||
|
||||
* 用户希望移除以下重复入口:报表中心右侧“借阅记录”;馆藏检索右侧“管理图书”;图书管理右侧“分类”“查看馆藏”;管理分类右侧“管理图书”;读者档案右侧“管理登录账户”;用户账户与角色右侧“读者档案”。
|
||||
* 侧边栏已经提供这些模块之间的跳转,因此页面标题栏和工具栏中的跨模块二级入口会显得重复。
|
||||
* “新增图书”“新增分类”“新增读者档案”“新增账户”等当前页面内的主要操作仍应保留。
|
||||
* 演示数据位于 `src/main/resources/db/schema.sql`,当前包含英文读者名、英文分类和英文图书。
|
||||
* 项目是 JSP + Servlet + MySQL 架构,前端页面在 `src/main/webapp/WEB-INF/jsp/`,数据库初始化脚本使用 `utf8mb4`。
|
||||
* 用户补充要求:仿照参考截图重构登录界面,必须是真实可用的登录表单,而不是静态展示页。
|
||||
* 参考截图特征:浅色模糊图书馆背景、居中的白色登录卡片、蓝色书本图标与“图书管理系统”标题、用户名/密码输入框图标、密码显隐按钮、身份单选项、记住我和忘记密码入口、蓝色主登录按钮。
|
||||
|
||||
## Assumptions
|
||||
|
||||
* 本任务只调整冗余跨模块按钮,不改变侧边栏导航、权限控制、Servlet 路由或业务流程。
|
||||
* 数据初始化仍使用 `INSERT IGNORE` / `ON DUPLICATE KEY UPDATE` 的现有风格,避免重复执行脚本破坏已有本地数据。
|
||||
* 中文演示数据可以替换或扩充现有英文样例,但登录测试账号用户名和密码保持不变。
|
||||
|
||||
## Requirements
|
||||
|
||||
* 报表中心页面不再显示跳转到借阅记录的右侧按钮。
|
||||
* 馆藏检索页面不再显示跳转到管理图书的右侧按钮。
|
||||
* 图书管理页面不再显示跳转到分类管理或馆藏检索的右侧按钮;保留新增图书入口。
|
||||
* 分类管理页面不再显示跳转到管理图书的右侧按钮;保留新增分类入口。
|
||||
* 读者档案页面不再显示跳转到管理登录账户的右侧按钮;保留新增读者档案入口。
|
||||
* 用户账户与角色页面不再显示跳转到读者档案的右侧按钮;保留新增账户入口。
|
||||
* 数据库初始化脚本加入中文图书分类、中文书名、中文作者和中文读者姓名。
|
||||
* 本地演示账号仍能用于登录验证。
|
||||
* 登录页按参考截图重构视觉,但保留现有 `POST /login`、`username`、`password`、`redirect`、错误提示和回填用户名等真实登录能力。
|
||||
* 登录页新增或保留真实可交互控件:密码显隐切换、登录身份单选项、记住我选项和忘记密码入口。
|
||||
* 登录身份选择不应破坏现有服务端认证;当前后端仍以账号密码和账号角色为准,前端角色选项仅作为登录意图提示或表单辅助字段。
|
||||
* 登录页需要在桌面和移动端保持可用,输入框、按钮和错误提示不能溢出或遮挡。
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
* [x] 指定页面中的重复跨模块按钮被移除,侧边栏仍能导航到对应模块。
|
||||
* [x] 页面内新增操作按钮未被误删。
|
||||
* [x] `schema.sql` 包含多条中文图书数据和多条中文读者数据。
|
||||
* [x] 中文演示数据使用 `utf8mb4` 兼容的文本,不引入新表或迁移机制。
|
||||
* [x] 相关检查或可用的构建验证通过;若环境缺少 Maven,记录 fallback 验证。
|
||||
* [x] 登录页视觉接近参考截图,并使用真实表单提交到现有 `/login`。
|
||||
* [x] 密码显隐、记住我、身份单选项在浏览器中可交互且不破坏登录流程。
|
||||
* [x] 登录失败时继续显示服务端错误提示并保留用户名/redirect。
|
||||
* [x] 登录页在移动端和桌面端布局稳定,无文字或控件重叠。
|
||||
|
||||
## Definition of Done
|
||||
|
||||
* Tests/checks run where available.
|
||||
* Lint/typecheck/build status reported.
|
||||
* Specs reviewed for whether new conventions need recording.
|
||||
|
||||
## Out of Scope
|
||||
|
||||
* 不重设计侧边栏或整体视觉风格。
|
||||
* 不新增页面、权限、路由或服务层能力。
|
||||
* 不改变借阅记录、报表、用户账户或读者档案的业务逻辑。
|
||||
* 不实现真实找回密码流程;忘记密码入口可展示当前系统暂未开放或指向安全的占位交互。
|
||||
|
||||
## Technical Notes
|
||||
|
||||
* Likely JSP files: `src/main/webapp/WEB-INF/jsp/reports/dashboard.jsp`, `src/main/webapp/WEB-INF/jsp/books/catalog.jsp`, `src/main/webapp/WEB-INF/jsp/books/manage.jsp`, `src/main/webapp/WEB-INF/jsp/books/categories.jsp`, `src/main/webapp/WEB-INF/jsp/readers/manage.jsp`, `src/main/webapp/WEB-INF/jsp/admin/users/manage.jsp`.
|
||||
* Login files: `src/main/webapp/WEB-INF/jsp/auth/login.jsp`, `src/main/webapp/static/css/app.css`, and possibly small inline or static JavaScript for password visibility/remember-me interactions.
|
||||
* Data file: `src/main/resources/db/schema.sql`.
|
||||
* Relevant specs: frontend JSP/component/state/quality guidelines and backend database/quality guidelines.
|
||||
* Final verification: `git diff --check`, `node --check src/main/webapp/static/js/login.js`, JSP scriptlet/SQL/JDBC scans, removed-link scan, password persistence scan, and `/home/sjy/.sdkman/candidates/maven/current/bin/mvn clean package` passed.
|
||||
* Spec update decision: `.trellis/spec/frontend/type-safety.md` documents the new presentation-only login controls (`loginRole`, `rememberUsername`) and the username-only remember-me constraint.
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"id": "remove-redundant-actions-add-cn-data",
|
||||
"name": "remove-redundant-actions-add-cn-data",
|
||||
"title": "remove redundant page actions and add Chinese demo data",
|
||||
"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": "Frontend stack and checklist for final review."}
|
||||
{"file": ".trellis/spec/frontend/component-guidelines.md", "reason": "Check JSP fragments, role-conditioned navigation, Chinese copy, and reusable UI patterns."}
|
||||
{"file": ".trellis/spec/frontend/state-management.md", "reason": "Check session/request state usage remains server-rendered and safe."}
|
||||
{"file": ".trellis/spec/frontend/type-safety.md", "reason": "Check JSP/Servlet display contracts and safe EL/JSTL rendering."}
|
||||
{"file": ".trellis/spec/frontend/quality-guidelines.md", "reason": "Check navigation, layout, accessibility, and JSP/CSS architecture quality."}
|
||||
{"file": ".trellis/tasks/archive/2026-04/00-bootstrap-guidelines/research/project-requirements.md", "reason": "Check the change preserves the agreed JSP + Servlet + Tomcat stack."}
|
||||
@@ -0,0 +1,6 @@
|
||||
{"file": ".trellis/spec/frontend/index.md", "reason": "Frontend stack and checklist for JSP/CSS implementation."}
|
||||
{"file": ".trellis/spec/frontend/component-guidelines.md", "reason": "Shared JSP fragment, role-conditioned navigation, Simplified Chinese copy, form, table, and CSS conventions."}
|
||||
{"file": ".trellis/spec/frontend/state-management.md", "reason": "Server-rendered request/session state conventions while using session role data in navigation."}
|
||||
{"file": ".trellis/spec/frontend/type-safety.md", "reason": "JSP/Servlet display contracts and safe EL/JSTL rendering constraints."}
|
||||
{"file": ".trellis/spec/frontend/quality-guidelines.md", "reason": "Frontend verification expectations for navigation, layout, accessibility, and JSP/CSS boundaries."}
|
||||
{"file": ".trellis/tasks/archive/2026-04/00-bootstrap-guidelines/research/project-requirements.md", "reason": "Project stack constraints for JSP, Servlet, MySQL, and Tomcat."}
|
||||
@@ -0,0 +1,87 @@
|
||||
# Sidebar Active State And Management UX Cleanup
|
||||
|
||||
## Goal
|
||||
|
||||
Fix several visible JSP/CSS navigation and layout issues in the authenticated library-management UI, and reduce confusion between reader profile management and user account management without changing the backend data model.
|
||||
|
||||
## What I Already Know
|
||||
|
||||
* The application is a Java 11 Maven WAR using JSP, Servlet, JSTL, CSS, and Tomcat.
|
||||
* Authenticated navigation lives in `src/main/webapp/WEB-INF/jsp/common/header.jspf`.
|
||||
* Sidebar active state currently uses `fn:contains(currentUri, ...)`, but rendered JSP paths can differ from public servlet paths after `RequestDispatcher.forward`.
|
||||
* This explains reported false positives and false negatives:
|
||||
* `/catalog` can render through `/WEB-INF/jsp/books/catalog.jsp`, causing the books nav item to look active.
|
||||
* `/book-categories` renders through `/WEB-INF/jsp/books/categories.jsp`, causing books to look active while categories may not.
|
||||
* `/reports` renders `reports/dashboard.jsp`, which can make dashboard/workbench look active.
|
||||
* `/admin/system-logs` renders `maintenance/system-logs.jsp`, so the system log item may not activate.
|
||||
* The catalog, book management, and reader management hero sections put eyebrow/title/body/actions directly under a flex container; pages that wrap text in a child `<div>` avoid the horizontal layout break.
|
||||
* `dashboard.jsp` contains the small technical sentence the user wants removed.
|
||||
* `ReaderManagementServlet` manages reader profiles/eligibility/contact/borrowing limits; `UserManagementServlet` manages login accounts/roles/active status. These are overlapping concepts to users but distinct backend workflows.
|
||||
|
||||
## Requirements
|
||||
|
||||
* Sidebar active state must be based on the original public servlet path, not the forwarded JSP path.
|
||||
* Only the matching sidebar item should be active for catalog, books, book categories, reports, and system logs.
|
||||
* Remove the sidebar "角色工作台" block.
|
||||
* Remove the sidebar "工作台" nav item.
|
||||
* Move "报表中心" to the top of the main module navigation for administrator/librarian roles.
|
||||
* Fix the header/hero layout on catalog, book management, and reader management so eyebrow/title/description stay grouped vertically.
|
||||
* Remove the dashboard sentence: `登录后进入 Dashboard,会话仅保存安全的 AuthenticatedUser 快照、角色代码与权限代码集合。`
|
||||
* Reduce the perceived duplication between reader management and user management using conservative UI changes:
|
||||
* Treat reader management as reader profile/borrowing eligibility management.
|
||||
* Treat user management as account/role/login status management.
|
||||
* Prefer clearer labels, descriptions, and cross-links over merging backend flows.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
* [x] Opening `/catalog` highlights only "馆藏检索".
|
||||
* [x] Opening `/books` highlights only "图书管理".
|
||||
* [x] Opening `/book-categories` highlights only "图书分类管理".
|
||||
* [x] Opening `/reports` highlights only "报表中心" and does not highlight "工作台".
|
||||
* [x] Opening `/admin/system-logs` highlights "系统日志".
|
||||
* [x] The sidebar no longer displays the role workbench cards or a "工作台" nav item.
|
||||
* [x] "报表中心" appears before catalog/books/readers/borrowing for administrator/librarian navigation.
|
||||
* [x] Catalog, book management, and reader management hero copy is vertically grouped and does not lay out as separate horizontal items.
|
||||
* [x] The dashboard technical session sentence is absent.
|
||||
* [x] Reader/user management labels and descriptions make the distinction between reader profiles and user accounts clearer.
|
||||
* [x] Maven verification passes or the closest available build command is reported.
|
||||
|
||||
## Definition Of Done
|
||||
|
||||
* Focused JSP/CSS changes only unless a backend change is required by verification.
|
||||
* Existing Servlet/JSP rendering and JSTL escaping behavior remains intact.
|
||||
* Maven build/test verification run where available.
|
||||
* Trellis quality check completed before final response.
|
||||
|
||||
## Technical Approach
|
||||
|
||||
* In `header.jspf`, derive a `currentPath` from `requestScope['javax.servlet.forward.servlet_path']` with a fallback to `pageContext.request.servletPath`.
|
||||
* Replace broad `fn:contains` checks with exact or prefix checks against public servlet paths.
|
||||
* Reorder and trim sidebar markup according to the requested information architecture.
|
||||
* Wrap catalog/book/reader hero text in a child `<div>` to match pages that already render correctly.
|
||||
* Remove only the requested dashboard small text, leaving role-specific workbench headings and metrics intact.
|
||||
* Use copy changes and cross-links to clarify reader profiles versus user accounts without changing controllers, entities, DAOs, or database schema.
|
||||
|
||||
## Out Of Scope
|
||||
|
||||
* Merging reader and user management into a single page.
|
||||
* Changing authentication, authorization, database schema, or service-layer behavior.
|
||||
* Redesigning the whole dashboard or adding new frontend libraries.
|
||||
|
||||
## Technical Notes
|
||||
|
||||
* Relevant frontend spec index: `.trellis/spec/frontend/index.md`.
|
||||
* Relevant files inspected:
|
||||
* `src/main/webapp/WEB-INF/jsp/common/header.jspf`
|
||||
* `src/main/webapp/WEB-INF/jsp/dashboard.jsp`
|
||||
* `src/main/webapp/WEB-INF/jsp/books/catalog.jsp`
|
||||
* `src/main/webapp/WEB-INF/jsp/books/manage.jsp`
|
||||
* `src/main/webapp/WEB-INF/jsp/books/categories.jsp`
|
||||
* `src/main/webapp/WEB-INF/jsp/readers/manage.jsp`
|
||||
* `src/main/webapp/WEB-INF/jsp/admin/users/manage.jsp`
|
||||
* `src/main/webapp/static/css/app.css`
|
||||
* Build command from README: `mvn clean package`; fallback path documented as `/home/sjy/.sdkman/candidates/maven/current/bin/mvn clean package` if `mvn` is not on `PATH`.
|
||||
* Verification on 2026-04-28:
|
||||
* `git diff --check` passed.
|
||||
* Search for removed sidebar role/workbench and old active-state patterns returned no matches.
|
||||
* `/home/sjy/.sdkman/candidates/maven/current/bin/mvn clean package` passed with `BUILD SUCCESS`.
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"id": "sidebar-layout-management-ux",
|
||||
"name": "sidebar-layout-management-ux",
|
||||
"title": "修复侧边栏高亮与管理页布局优化",
|
||||
"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,4 @@
|
||||
{"file": ".trellis/spec/backend/index.md", "reason": "Check backend architecture boundaries for login diagnostics."}
|
||||
{"file": ".trellis/spec/backend/logging-guidelines.md", "reason": "Verify logs avoid passwords, hashes, salts, and credentials while remaining useful."}
|
||||
{"file": ".trellis/spec/backend/database-guidelines.md", "reason": "Verify login/authentication and database config contracts remain intact."}
|
||||
{"file": ".trellis/spec/backend/quality-guidelines.md", "reason": "Verify Maven checks and backend quality expectations."}
|
||||
@@ -0,0 +1,4 @@
|
||||
{"file": ".trellis/spec/backend/index.md", "reason": "Backend Servlet/JSP/JDBC architecture context for login diagnostics."}
|
||||
{"file": ".trellis/spec/backend/logging-guidelines.md", "reason": "Logging safety rules, sensitive-data redaction, and diagnostic expectations."}
|
||||
{"file": ".trellis/spec/backend/database-guidelines.md", "reason": "Login/authentication and database configuration contracts."}
|
||||
{"file": ".trellis/spec/backend/quality-guidelines.md", "reason": "Backend quality and Maven verification requirements."}
|
||||
@@ -0,0 +1,82 @@
|
||||
# Add Windows Login Diagnostic Logs
|
||||
|
||||
## Goal
|
||||
|
||||
Add safe server-side diagnostic logs to the login/authentication path so a Windows-built deployment that returns `用户名或密码不正确。` can be diagnosed without exposing passwords, password hashes, or database credentials.
|
||||
|
||||
## What I Already Know
|
||||
|
||||
* The previous frontend rebuild task is solved and has been archived.
|
||||
* On the Windows system build, login now reaches the invalid-credentials path: `用户名或密码不正确。`
|
||||
* The user believes the database connection is probably already working.
|
||||
* Existing login flow is `LoginServlet` -> `AuthServiceImpl` -> `JdbcUserDao.findActiveByUsername` -> `JdbcUtil`.
|
||||
* `AuthServiceImpl` currently logs only generic login failure/success/service-error messages.
|
||||
* Existing backend specs require login failures to keep the same generic user-facing message and to log server-side details for unavailable services.
|
||||
|
||||
## Requirements
|
||||
|
||||
* Add diagnostic logging around login POST handling, authentication lookup, password verification outcome, and database configuration/connection attempts.
|
||||
* Logs must help distinguish:
|
||||
* request reached `LoginServlet`;
|
||||
* username normalization changed the submitted username;
|
||||
* active user row was not found;
|
||||
* user row was found but password verification failed;
|
||||
* database configuration was loaded and which JDBC URL/user key were used, with secrets redacted;
|
||||
* JDBC driver/connection failures if they happen.
|
||||
* Do not log raw passwords, password hashes, salts, database passwords, or full sensitive config values.
|
||||
* Preserve the current user-facing Chinese error message and login behavior.
|
||||
* Keep the implementation in the existing Servlet + service + DAO + JDBC stack.
|
||||
* Prefer `java.util.logging` patterns already used in the project.
|
||||
* Document and seed explicit local/demo initial credentials so new deployments are not blocked by unrecoverable password hashes:
|
||||
* `admin` / `admin123`
|
||||
* `librarian` / `librarian123`
|
||||
* `reader` / `reader123`
|
||||
* Make clear that these demo passwords are for local scaffold verification only and must be changed or removed before non-local/production use.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
* [x] Login failure logs identify whether the username was absent, not found, or found with password mismatch.
|
||||
* [x] Login request logs include safe request diagnostics such as remote address, context path, redirect presence, and submitted username length or sanitized username.
|
||||
* [x] Database logs confirm `db.properties` loading and JDBC connection attempts with password redacted.
|
||||
* [x] No log statement outputs a raw password, password hash, salt, or database password.
|
||||
* [x] Existing login success/failure behavior remains unchanged for users.
|
||||
* [x] `mvn test` or the closest available Maven verification command succeeds.
|
||||
* [x] README lists the local/demo initial login accounts and passwords with an explicit non-production warning.
|
||||
* [x] `schema.sql` seed user hashes verify against the documented demo passwords for new deployments.
|
||||
* [x] Existing deployments have a documented SQL reset path or warning explaining that `INSERT IGNORE` will not overwrite existing user rows.
|
||||
|
||||
## Definition Of Done
|
||||
|
||||
* Diagnostic logging implemented in source.
|
||||
* Maven verification run and results reported.
|
||||
* No database schema changes.
|
||||
* No unrelated frontend/layout changes.
|
||||
|
||||
## Out Of Scope
|
||||
|
||||
* Changing password hashing rules or seed user credentials.
|
||||
* Adding a new logging framework.
|
||||
* Changing database schema or production credentials.
|
||||
* Reworking the login UI.
|
||||
* Committing generated build artifacts.
|
||||
|
||||
## Technical Notes
|
||||
|
||||
* Likely impacted files:
|
||||
* `src/main/java/com/mzh/library/controller/LoginServlet.java`
|
||||
* `src/main/java/com/mzh/library/service/impl/AuthServiceImpl.java`
|
||||
* `src/main/java/com/mzh/library/dao/impl/JdbcUserDao.java`
|
||||
* `src/main/java/com/mzh/library/util/JdbcUtil.java`
|
||||
* Relevant specs:
|
||||
* `.trellis/spec/backend/logging-guidelines.md`
|
||||
* `.trellis/spec/backend/database-guidelines.md`
|
||||
* `.trellis/spec/backend/quality-guidelines.md`
|
||||
* Verification completed at 2026-04-28 18:22 +0800:
|
||||
* `/home/sjy/.sdkman/candidates/maven/current/bin/mvn test` passed with `BUILD SUCCESS`.
|
||||
* `/home/sjy/.sdkman/candidates/maven/current/bin/mvn package` passed with `BUILD SUCCESS` and produced `target/library-management.war`.
|
||||
* `git diff --check` passed.
|
||||
* Sensitive logger scan only found boolean password state fields, `password=<redacted>`, and `password-mismatch` category labels.
|
||||
* Verification completed at 2026-04-28 18:33 +0800:
|
||||
* `PasswordHasher.verify` returned `true` for `admin/admin123`, `librarian/librarian123`, and `reader/reader123` against the updated `schema.sql` PBKDF2 hashes.
|
||||
* `/home/sjy/.sdkman/candidates/maven/current/bin/mvn verify` passed with `BUILD SUCCESS`.
|
||||
* `git diff --check` passed.
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"id": "windows-login-diagnostic-logs",
|
||||
"name": "windows-login-diagnostic-logs",
|
||||
"title": "Add Windows login diagnostic logs",
|
||||
"description": "",
|
||||
"status": "completed",
|
||||
"dev_type": null,
|
||||
"scope": null,
|
||||
"package": null,
|
||||
"priority": "P2",
|
||||
"creator": "Zzzz",
|
||||
"assignee": "Zzzz",
|
||||
"createdAt": "2026-04-28",
|
||||
"completedAt": "2026-04-28",
|
||||
"branch": null,
|
||||
"base_branch": "master",
|
||||
"worktree_path": null,
|
||||
"commit": null,
|
||||
"pr_url": null,
|
||||
"subtasks": [],
|
||||
"children": [],
|
||||
"parent": null,
|
||||
"relatedFiles": [],
|
||||
"notes": "",
|
||||
"meta": {}
|
||||
}
|
||||
@@ -8,8 +8,8 @@
|
||||
|
||||
<!-- @@@auto:current-status -->
|
||||
- **Active File**: `journal-1.md`
|
||||
- **Total Sessions**: 6
|
||||
- **Last Active**: 2026-04-27
|
||||
- **Total Sessions**: 15
|
||||
- **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` | ~573 | Active |
|
||||
<!-- @@@/auto:active-documents -->
|
||||
|
||||
---
|
||||
@@ -29,6 +29,15 @@
|
||||
<!-- @@@auto:session-history -->
|
||||
| # | Date | Title | Commits | Branch |
|
||||
|---|------|-------|---------|--------|
|
||||
| 15 | 2026-04-28 | 登录界面重构 | `8535b4804bc48e6f23d3107f1b34e0a16479e020` | `master` |
|
||||
| 14 | 2026-04-28 | Sidebar layout and management UX cleanup | `d0e71f2` | `master` |
|
||||
| 13 | 2026-04-28 | Frontend workbench display fix | `0a386b8` | `master` |
|
||||
| 12 | 2026-04-28 | Windows login diagnostics and demo credentials | `781ce46` | `master` |
|
||||
| 11 | 2026-04-28 | Frontend Reference Redesign | `89b6dd1` | `master` |
|
||||
| 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` |
|
||||
|
||||
@@ -273,3 +273,301 @@ 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
|
||||
|
||||
|
||||
## Session 11: Frontend Reference Redesign
|
||||
|
||||
**Date**: 2026-04-28
|
||||
**Task**: Frontend Reference Redesign
|
||||
**Branch**: `master`
|
||||
|
||||
### Summary
|
||||
|
||||
Refactored the JSP frontend to match the provided library dashboard reference image, including shared sidebar/topbar layout, dashboard panels, role-aware visibility fixes, Maven verification, and spec context updates.
|
||||
|
||||
### Main Changes
|
||||
|
||||
(Add details)
|
||||
|
||||
### Git Commits
|
||||
|
||||
| Hash | Message |
|
||||
|------|---------|
|
||||
| `89b6dd1` | (see git log) |
|
||||
|
||||
### Testing
|
||||
|
||||
- [OK] (Add test results)
|
||||
|
||||
### Status
|
||||
|
||||
[OK] **Completed**
|
||||
|
||||
### Next Steps
|
||||
|
||||
- None - task complete
|
||||
|
||||
|
||||
## Session 12: Windows login diagnostics and demo credentials
|
||||
|
||||
**Date**: 2026-04-28
|
||||
**Task**: Windows login diagnostics and demo credentials
|
||||
**Branch**: `master`
|
||||
|
||||
### Summary
|
||||
|
||||
Added safe login/database diagnostic logs, documented local demo credentials, updated seed hashes, verified Maven build.
|
||||
|
||||
### Main Changes
|
||||
|
||||
(Add details)
|
||||
|
||||
### Git Commits
|
||||
|
||||
| Hash | Message |
|
||||
|------|---------|
|
||||
| `781ce46` | (see git log) |
|
||||
|
||||
### Testing
|
||||
|
||||
- [OK] (Add test results)
|
||||
|
||||
### Status
|
||||
|
||||
[OK] **Completed**
|
||||
|
||||
### Next Steps
|
||||
|
||||
- None - task complete
|
||||
|
||||
|
||||
## Session 13: Frontend workbench display fix
|
||||
|
||||
**Date**: 2026-04-28
|
||||
**Task**: Frontend workbench display fix
|
||||
**Branch**: `master`
|
||||
|
||||
### Summary
|
||||
|
||||
Replaced hard-coded dashboard data with service-backed workbench data, simplified sidebar/workbench UI, kept sidebar persistent, updated frontend specs, and verified with Maven/service checks.
|
||||
|
||||
### Main Changes
|
||||
|
||||
(Add details)
|
||||
|
||||
### Git Commits
|
||||
|
||||
| Hash | Message |
|
||||
|------|---------|
|
||||
| `0a386b8` | (see git log) |
|
||||
|
||||
### Testing
|
||||
|
||||
- [OK] (Add test results)
|
||||
|
||||
### Status
|
||||
|
||||
[OK] **Completed**
|
||||
|
||||
### Next Steps
|
||||
|
||||
- None - task complete
|
||||
|
||||
|
||||
## Session 14: Sidebar layout and management UX cleanup
|
||||
|
||||
**Date**: 2026-04-28
|
||||
**Task**: Sidebar layout and management UX cleanup
|
||||
**Branch**: `master`
|
||||
|
||||
### Summary
|
||||
|
||||
Fixed sidebar active-state routing and navigation order, corrected management page hero layouts, removed dashboard technical copy, clarified reader profile versus user account UI, updated frontend navigation spec, and verified Maven package build.
|
||||
|
||||
### Main Changes
|
||||
|
||||
(Add details)
|
||||
|
||||
### Git Commits
|
||||
|
||||
| Hash | Message |
|
||||
|------|---------|
|
||||
| `d0e71f2` | (see git log) |
|
||||
|
||||
### Testing
|
||||
|
||||
- [OK] (Add test results)
|
||||
|
||||
### Status
|
||||
|
||||
[OK] **Completed**
|
||||
|
||||
### Next Steps
|
||||
|
||||
- None - task complete
|
||||
|
||||
|
||||
## Session 15: 登录界面重构
|
||||
|
||||
**Date**: 2026-04-28
|
||||
**Task**: 登录界面重构
|
||||
**Branch**: `master`
|
||||
|
||||
### Summary
|
||||
|
||||
按参考截图重构真实可用登录页,保留 /login 认证流程,补充登录辅助控件规范并完成 Trellis 质量检查。
|
||||
|
||||
### Main Changes
|
||||
|
||||
(Add details)
|
||||
|
||||
### Git Commits
|
||||
|
||||
| Hash | Message |
|
||||
|------|---------|
|
||||
| `8535b4804bc48e6f23d3107f1b34e0a16479e020` | (see git log) |
|
||||
|
||||
### Testing
|
||||
|
||||
- [OK] (Add test results)
|
||||
|
||||
### Status
|
||||
|
||||
[OK] **Completed**
|
||||
|
||||
### Next Steps
|
||||
|
||||
- None - task complete
|
||||
|
||||
@@ -1,25 +1,365 @@
|
||||
# MZH Library Management
|
||||
# MZH 图书馆管理系统
|
||||
|
||||
Initial JSP + Servlet + MySQL scaffold for the library-management system.
|
||||
MZH Library Management 是一个基于 Java 11、Maven WAR、JSP、Servlet、JDBC DAO 和 MySQL 的 B/S 图书馆管理系统。项目以 Tomcat 部署为目标,采用传统 Java Web 分层结构,适合用于课程设计、Java Web 实训、图书馆业务原型验证,以及学习 Servlet -> Service -> DAO -> MySQL 的完整数据流。
|
||||
|
||||
## Stack
|
||||
当前仓库已经包含登录认证、角色权限、馆藏检索、图书管理、分类管理、读者档案、借阅流通、读者借阅历史、报表中心、管理员用户管理和系统日志查看等功能切片。
|
||||
|
||||
- Java 11
|
||||
- Maven WAR project layout
|
||||
- JSP + Servlet on Tomcat
|
||||
- MySQL through JDBC DAO classes
|
||||
## 核心功能
|
||||
|
||||
## Local Setup
|
||||
- 登录与会话管理:通过 `/login` 登录,认证成功后进入 `/dashboard`;会话中只保存安全的 `AuthenticatedUser` 快照、角色代码和权限代码集合。
|
||||
- 角色工作台:管理员、馆员、读者分别通过 `/admin/home`、`/librarian/home`、`/reader/home` 进入对应区域。
|
||||
- 馆藏检索:通过 `/catalog` 按图书编号、书名、作者和分类检索图书。
|
||||
- 图书管理:通过 `/books` 维护图书信息,支持新增、编辑、删除和库存状态管理。
|
||||
- 图书分类管理:通过 `/book-categories` 维护图书分类,并防止删除仍被图书引用的分类。
|
||||
- 读者管理:通过 `/readers` 维护读者档案、联系方式、状态和最大借阅数量。
|
||||
- 借阅流通:通过 `/borrowing` 完成借书、还书、续借、逾期筛选和库存联动更新。
|
||||
- 读者借阅历史:读者通过 `/reader/loans` 查看自己的借阅记录。
|
||||
- 报表中心:通过 `/reports` 查看馆藏汇总、借阅汇总、逾期列表和热门图书排行。
|
||||
- 用户管理:管理员通过 `/admin/users` 维护管理员、馆员和读者登录账户。
|
||||
- 系统日志:管理员通过 `/admin/system-logs` 查询关键操作、审计和异常日志。
|
||||
|
||||
1. Create a MySQL database and run `src/main/resources/db/schema.sql`.
|
||||
2. Copy `src/main/resources/db.properties.example` to `src/main/resources/db.properties`.
|
||||
3. Fill in the MySQL URL, username, and password.
|
||||
4. Build with Maven when available:
|
||||
## 技术栈
|
||||
|
||||
| 类别 | 当前配置 |
|
||||
| --- | --- |
|
||||
| Java | Java 11,`maven.compiler.release=11` |
|
||||
| 构建工具 | Maven,WAR 项目 |
|
||||
| Web 技术 | Servlet 4.0、JSP、JSTL |
|
||||
| Servlet API | `javax.servlet:javax.servlet-api:4.0.1`,`provided` |
|
||||
| JSP 标签库 | `javax.servlet:jstl:1.2` |
|
||||
| 数据库 | MySQL,JDBC DAO 访问 |
|
||||
| MySQL 驱动 | `com.mysql:mysql-connector-j:8.0.33`,`runtime` |
|
||||
| 部署产物 | `target/library-management.war` |
|
||||
| Web 配置 | `src/main/webapp/WEB-INF/web.xml`,`web-app` 版本 4.0 |
|
||||
| 编码 | Maven 源码编码 UTF-8;Web 请求通过 `CharacterEncodingFilter` 使用 UTF-8 |
|
||||
|
||||
建议使用兼容 Servlet 4.0 的 Tomcat 9.x 运行该 WAR。数据库脚本使用 InnoDB、`utf8mb4` 字符集和检查约束,建议使用 MySQL 8.x。
|
||||
|
||||
## 项目结构
|
||||
|
||||
```text
|
||||
.
|
||||
├── pom.xml
|
||||
├── README.md
|
||||
├── src
|
||||
│ ├── main
|
||||
│ │ ├── java/com/mzh/library
|
||||
│ │ │ ├── controller/ Servlet 控制器,负责路由、参数读取和 JSP 转发/重定向
|
||||
│ │ │ ├── dao/ DAO 接口
|
||||
│ │ │ ├── dao/impl/ JDBC DAO 实现
|
||||
│ │ │ ├── entity/ JavaBean、枚举、查询条件和报表对象
|
||||
│ │ │ ├── exception/ DAO 异常
|
||||
│ │ │ ├── filter/ 编码、登录认证、权限过滤器
|
||||
│ │ │ ├── service/ Service 接口、权限策略和通用结果对象
|
||||
│ │ │ ├── service/impl/ 业务服务实现
|
||||
│ │ │ └── util/ JDBC、密码哈希、Session 常量等工具
|
||||
│ │ ├── resources
|
||||
│ │ │ ├── db/schema.sql
|
||||
│ │ │ └── db.properties.example
|
||||
│ │ └── webapp
|
||||
│ │ ├── WEB-INF/web.xml
|
||||
│ │ ├── WEB-INF/jsp/ 受保护的 JSP 页面和 JSP 片段
|
||||
│ │ ├── static/css/app.css
|
||||
│ │ ├── static/images/library-login.svg
|
||||
│ │ └── index.jsp
|
||||
│ └── test/java/com/mzh/library/service
|
||||
│ └── *Check.java 服务层和权限策略自检类
|
||||
└── target/ Maven 构建输出,已被 .gitignore 忽略
|
||||
```
|
||||
|
||||
本地数据库配置文件 `src/main/resources/db.properties` 也已被 `.gitignore` 忽略。不要提交真实数据库地址、账号或密码。
|
||||
|
||||
## 分层设计
|
||||
|
||||
项目遵循以下边界:
|
||||
|
||||
```text
|
||||
JSP/CSS 页面 -> Servlet 控制器 -> Service 业务层 -> DAO 数据访问层 -> MySQL
|
||||
```
|
||||
|
||||
- JSP 只负责展示、表单和用户交互,不直接访问数据库,不编写业务流程。
|
||||
- Servlet 负责读取请求参数、做基础格式校验、调用 Service,并决定转发 JSP 或重定向。
|
||||
- Service 负责权限检查、业务规则、事务边界和跨 DAO 工作流,例如借书、还书、续借和库存更新。
|
||||
- DAO 负责 SQL、JDBC 资源访问和 MySQL CRUD,不返回 HTML 或 Servlet 对象。
|
||||
- 过滤器统一处理 UTF-8 编码、登录拦截和基于权限的访问控制。
|
||||
|
||||
## 数据库初始化
|
||||
|
||||
数据库脚本位于:
|
||||
|
||||
```text
|
||||
src/main/resources/db/schema.sql
|
||||
```
|
||||
|
||||
脚本会创建并使用数据库 `mzh_library`,包含以下核心表:
|
||||
|
||||
- `roles`:角色定义。
|
||||
- `permissions`:权限定义。
|
||||
- `role_permissions`:角色与权限的关联。
|
||||
- `users`:登录账户,密码字段保存 PBKDF2 哈希。
|
||||
- `system_logs`:系统操作、审计和异常日志。
|
||||
- `readers`:读者档案、联系方式、状态和借阅上限。
|
||||
- `book_categories`:图书分类。
|
||||
- `books`:图书基础信息、分类、总册数、可借册数和状态。
|
||||
- `borrow_records`:借阅、归还、续借和逾期判断所需记录。
|
||||
|
||||
初始化示例:
|
||||
|
||||
```bash
|
||||
mysql -u root -p < src/main/resources/db/schema.sql
|
||||
```
|
||||
|
||||
脚本内包含本地验证用的演示角色、权限、用户、读者、分类和图书数据。演示账户只用于本地脚手架验证;在非本地数据库中使用前应更换或删除这些数据。
|
||||
|
||||
本地/demo 初始登录账号如下。这些是应用登录账号,不是 MySQL 数据库账号:
|
||||
|
||||
| 角色 | 用户名 | 初始密码 |
|
||||
| --- | --- | --- |
|
||||
| 管理员 | `admin` | `admin123` |
|
||||
| 馆员 | `librarian` | `librarian123` |
|
||||
| 读者 | `reader` | `reader123` |
|
||||
|
||||
这些明文密码只用于新部署本地环境的首次验证。非本地或生产环境上线前,必须通过系统用户管理功能改密,或删除/替换这些演示账号。
|
||||
|
||||
`schema.sql` 使用 `INSERT IGNORE INTO users` 写入演示账号。如果目标数据库里已经存在同名 `admin`、`librarian` 或 `reader` 行,重新执行脚本不会覆盖现有密码哈希。需要重置本地演示账号时,优先在系统用户管理功能中修改密码;如果无法登录,可在确认这是本地/demo 数据库后执行以下 SQL:
|
||||
|
||||
```sql
|
||||
UPDATE users
|
||||
SET password_hash = 'pbkdf2_sha256$60000$Ren1B30RDysysnApRiFVaQ==$1XwzMHaALqC7dKffwjbQkilBedfAuiMOXbR/xTMr5+Y=',
|
||||
active = 1
|
||||
WHERE username = 'admin';
|
||||
|
||||
UPDATE users
|
||||
SET password_hash = 'pbkdf2_sha256$60000$PV/DJwZlMRm8vy0lKMAM4g==$+Aijfop3YoPp6HTePN5r4wG8N3qgxJE+yZHkTfzfbaw=',
|
||||
active = 1
|
||||
WHERE username = 'librarian';
|
||||
|
||||
UPDATE users
|
||||
SET password_hash = 'pbkdf2_sha256$60000$wBzxTIT4ep79hgEzYDV9aQ==$w3oO5iSKRSfG4++b4558yiTHy6Tz9BB2+wuV9UOAKhs=',
|
||||
active = 1
|
||||
WHERE username = 'reader';
|
||||
```
|
||||
|
||||
## 本地配置
|
||||
|
||||
复制配置模板:
|
||||
|
||||
```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`。
|
||||
|
||||
### 重新执行 `schema.sql` 后演示账号密码为什么没变?
|
||||
|
||||
`schema.sql` 使用 `INSERT IGNORE INTO users` 写入本地/demo 账号。已有同名用户时,MySQL 会跳过插入,不会覆盖现有密码哈希。需要重置时,请参考“数据库初始化”里的本地/demo 账号说明;不要在非本地数据库中直接恢复这些演示密码。
|
||||
|
||||
## 维护提示
|
||||
|
||||
当 `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)
|
||||
|
||||
@@ -1,9 +1,37 @@
|
||||
package com.mzh.library.controller;
|
||||
|
||||
import com.mzh.library.dao.impl.JdbcBookDao;
|
||||
import com.mzh.library.dao.impl.JdbcBorrowRecordDao;
|
||||
import com.mzh.library.dao.impl.JdbcReaderDao;
|
||||
import com.mzh.library.dao.impl.JdbcReportDao;
|
||||
import com.mzh.library.entity.AuthenticatedUser;
|
||||
import com.mzh.library.entity.Book;
|
||||
import com.mzh.library.entity.BookCategory;
|
||||
import com.mzh.library.entity.BookSearchCriteria;
|
||||
import com.mzh.library.entity.BookStatus;
|
||||
import com.mzh.library.entity.BorrowRecord;
|
||||
import com.mzh.library.entity.BorrowRecordSearchCriteria;
|
||||
import com.mzh.library.entity.BorrowingSummary;
|
||||
import com.mzh.library.entity.InventorySummary;
|
||||
import com.mzh.library.entity.Reader;
|
||||
import com.mzh.library.entity.ReaderSearchCriteria;
|
||||
import com.mzh.library.entity.ReportCenter;
|
||||
import com.mzh.library.entity.Role;
|
||||
import com.mzh.library.service.BookService;
|
||||
import com.mzh.library.service.BorrowingService;
|
||||
import com.mzh.library.service.ReaderService;
|
||||
import com.mzh.library.service.ReportService;
|
||||
import com.mzh.library.service.ServiceResult;
|
||||
import com.mzh.library.service.impl.BookServiceImpl;
|
||||
import com.mzh.library.service.impl.BorrowingServiceImpl;
|
||||
import com.mzh.library.service.impl.ReaderServiceImpl;
|
||||
import com.mzh.library.service.impl.ReportServiceImpl;
|
||||
import com.mzh.library.util.SessionAttributes;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServlet;
|
||||
@@ -14,13 +42,200 @@ import javax.servlet.http.HttpSession;
|
||||
public class DashboardServlet extends HttpServlet {
|
||||
private static final String DASHBOARD_JSP = "/WEB-INF/jsp/dashboard.jsp";
|
||||
|
||||
private BookService bookService;
|
||||
private BorrowingService borrowingService;
|
||||
private ReaderService readerService;
|
||||
private ReportService reportService;
|
||||
|
||||
@Override
|
||||
public void init() {
|
||||
this.bookService = new BookServiceImpl(new JdbcBookDao());
|
||||
this.borrowingService = new BorrowingServiceImpl(new JdbcBorrowRecordDao());
|
||||
this.readerService = new ReaderServiceImpl(new JdbcReaderDao());
|
||||
this.reportService = new ReportServiceImpl(new JdbcReportDao());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
|
||||
HttpSession session = request.getSession(false);
|
||||
AuthenticatedUser user = session == null
|
||||
? null
|
||||
: (AuthenticatedUser) session.getAttribute(SessionAttributes.AUTHENTICATED_USER);
|
||||
AuthenticatedUser user = currentUser(request);
|
||||
request.setAttribute("currentUser", user);
|
||||
|
||||
ServiceResult<List<BookCategory>> categoryResult = bookService.listCategories();
|
||||
request.setAttribute("categories", categoryResult.isSuccessful()
|
||||
? listOrEmpty(categoryResult.getData())
|
||||
: Collections.emptyList());
|
||||
if (!categoryResult.isSuccessful()) {
|
||||
setErrorMessage(request, categoryResult.getMessage());
|
||||
}
|
||||
|
||||
ServiceResult<List<Book>> bookResult = bookService.searchBooks(new BookSearchCriteria());
|
||||
List<Book> dashboardBooks = bookResult.isSuccessful()
|
||||
? listOrEmpty(bookResult.getData())
|
||||
: Collections.emptyList();
|
||||
request.setAttribute("dashboardBooks", dashboardBooks);
|
||||
if (!bookResult.isSuccessful()) {
|
||||
setErrorMessage(request, bookResult.getMessage());
|
||||
}
|
||||
|
||||
List<DashboardMetric> metrics = Collections.emptyList();
|
||||
if (isStaff(user)) {
|
||||
Integer readerTotal = null;
|
||||
ServiceResult<List<Reader>> readerResult = readerService.searchReaders(new ReaderSearchCriteria());
|
||||
if (readerResult.isSuccessful()) {
|
||||
readerTotal = listOrEmpty(readerResult.getData()).size();
|
||||
} else {
|
||||
setErrorMessage(request, readerResult.getMessage());
|
||||
}
|
||||
|
||||
ServiceResult<ReportCenter> reportResult = reportService.loadReportCenter(user);
|
||||
if (reportResult.isSuccessful()) {
|
||||
ReportCenter reportCenter = reportResult.getData();
|
||||
request.setAttribute("reportCenter", reportCenter);
|
||||
metrics = metricsFromReport(reportCenter, readerTotal);
|
||||
} else {
|
||||
setErrorMessage(request, reportResult.getMessage());
|
||||
}
|
||||
|
||||
ServiceResult<List<BorrowRecord>> borrowResult =
|
||||
borrowingService.searchRecords(user, new BorrowRecordSearchCriteria());
|
||||
request.setAttribute("dashboardBorrowRecords", borrowResult.isSuccessful()
|
||||
? listOrEmpty(borrowResult.getData())
|
||||
: Collections.emptyList());
|
||||
if (!borrowResult.isSuccessful()) {
|
||||
setErrorMessage(request, borrowResult.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
if (metrics.isEmpty() && bookResult.isSuccessful()) {
|
||||
metrics = metricsFromBooks(dashboardBooks);
|
||||
}
|
||||
request.setAttribute("dashboardMetrics", metrics);
|
||||
request.getRequestDispatcher(DASHBOARD_JSP).forward(request, response);
|
||||
}
|
||||
|
||||
private AuthenticatedUser currentUser(HttpServletRequest request) {
|
||||
HttpSession session = request.getSession(false);
|
||||
Object value = session == null ? null : session.getAttribute(SessionAttributes.AUTHENTICATED_USER);
|
||||
return value instanceof AuthenticatedUser ? (AuthenticatedUser) value : null;
|
||||
}
|
||||
|
||||
private boolean isStaff(AuthenticatedUser user) {
|
||||
return user != null && (user.getRole() == Role.ADMINISTRATOR || user.getRole() == Role.LIBRARIAN);
|
||||
}
|
||||
|
||||
private <T> List<T> listOrEmpty(List<T> values) {
|
||||
return values == null ? Collections.emptyList() : values;
|
||||
}
|
||||
|
||||
private List<DashboardMetric> metricsFromReport(ReportCenter reportCenter, Integer readerTotal) {
|
||||
if (reportCenter == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
InventorySummary inventory = reportCenter.getInventorySummary();
|
||||
BorrowingSummary borrowing = reportCenter.getBorrowingSummary();
|
||||
List<DashboardMetric> metrics = new ArrayList<>();
|
||||
metrics.add(new DashboardMetric("馆藏总册", valueOf(inventory, MetricField.TOTAL_COPIES), "册", "来自报表中心"));
|
||||
metrics.add(new DashboardMetric("当前借出", valueOf(borrowing, MetricField.ACTIVE_LOANS), "册", "实时借阅记录"));
|
||||
metrics.add(new DashboardMetric("逾期借阅", valueOf(borrowing, MetricField.OVERDUE_LOANS), "册", "需跟进记录"));
|
||||
if (readerTotal == null) {
|
||||
metrics.add(new DashboardMetric("可借册数", valueOf(inventory, MetricField.AVAILABLE_COPIES),
|
||||
"册", "馆藏可借库存"));
|
||||
} else {
|
||||
metrics.add(new DashboardMetric("读者总数", readerTotal, "人", "实时读者档案"));
|
||||
}
|
||||
return metrics;
|
||||
}
|
||||
|
||||
private List<DashboardMetric> metricsFromBooks(List<Book> books) {
|
||||
int totalTitles = 0;
|
||||
int totalCopies = 0;
|
||||
int availableCopies = 0;
|
||||
int unavailableOrEmptyTitles = 0;
|
||||
for (Book book : books) {
|
||||
totalTitles++;
|
||||
totalCopies += book.getTotalCopies();
|
||||
availableCopies += book.getAvailableCopies();
|
||||
if (book.getStatus() != BookStatus.AVAILABLE || book.getAvailableCopies() <= 0) {
|
||||
unavailableOrEmptyTitles++;
|
||||
}
|
||||
}
|
||||
|
||||
List<DashboardMetric> metrics = new ArrayList<>();
|
||||
metrics.add(new DashboardMetric("图书种类", totalTitles, "种", "来自馆藏检索"));
|
||||
metrics.add(new DashboardMetric("馆藏总册", totalCopies, "册", "来自馆藏检索"));
|
||||
metrics.add(new DashboardMetric("可借册数", availableCopies, "册", "来自馆藏检索"));
|
||||
metrics.add(new DashboardMetric("需关注馆藏", unavailableOrEmptyTitles, "种", "不可借或无库存"));
|
||||
return metrics;
|
||||
}
|
||||
|
||||
private int valueOf(InventorySummary summary, MetricField field) {
|
||||
if (summary == null) {
|
||||
return 0;
|
||||
}
|
||||
switch (field) {
|
||||
case TOTAL_COPIES:
|
||||
return summary.getTotalCopies();
|
||||
case AVAILABLE_COPIES:
|
||||
return summary.getAvailableCopies();
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private int valueOf(BorrowingSummary summary, MetricField field) {
|
||||
if (summary == null) {
|
||||
return 0;
|
||||
}
|
||||
switch (field) {
|
||||
case ACTIVE_LOANS:
|
||||
return summary.getActiveLoans();
|
||||
case OVERDUE_LOANS:
|
||||
return summary.getOverdueLoans();
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private void setErrorMessage(HttpServletRequest request, String message) {
|
||||
if (message != null && !message.isEmpty() && request.getAttribute("errorMessage") == null) {
|
||||
request.setAttribute("errorMessage", message);
|
||||
}
|
||||
}
|
||||
|
||||
private enum MetricField {
|
||||
TOTAL_COPIES,
|
||||
AVAILABLE_COPIES,
|
||||
ACTIVE_LOANS,
|
||||
OVERDUE_LOANS
|
||||
}
|
||||
|
||||
public static final class DashboardMetric {
|
||||
private final String label;
|
||||
private final int value;
|
||||
private final String unit;
|
||||
private final String note;
|
||||
|
||||
private DashboardMetric(String label, int value, String unit, String note) {
|
||||
this.label = label;
|
||||
this.value = value;
|
||||
this.unit = unit;
|
||||
this.note = note;
|
||||
}
|
||||
|
||||
public String getLabel() {
|
||||
return label;
|
||||
}
|
||||
|
||||
public int getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public String getUnit() {
|
||||
return unit;
|
||||
}
|
||||
|
||||
public String getNote() {
|
||||
return note;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import com.mzh.library.service.impl.AuthServiceImpl;
|
||||
import com.mzh.library.util.SessionAttributes;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServlet;
|
||||
@@ -16,6 +17,7 @@ import javax.servlet.http.HttpServletResponse;
|
||||
import javax.servlet.http.HttpSession;
|
||||
|
||||
public class LoginServlet extends HttpServlet {
|
||||
private static final Logger LOGGER = Logger.getLogger(LoginServlet.class.getName());
|
||||
private static final String LOGIN_JSP = "/WEB-INF/jsp/auth/login.jsp";
|
||||
private static final String DASHBOARD_PATH = "/dashboard";
|
||||
private static final int SESSION_TIMEOUT_SECONDS = 30 * 60;
|
||||
@@ -40,9 +42,13 @@ public class LoginServlet extends HttpServlet {
|
||||
|
||||
@Override
|
||||
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
|
||||
String username = trim(request.getParameter("username"));
|
||||
String submittedUsername = request.getParameter("username");
|
||||
String username = trim(submittedUsername);
|
||||
String password = request.getParameter("password");
|
||||
String redirect = safeRedirect(request.getParameter("redirect"));
|
||||
String submittedRedirect = request.getParameter("redirect");
|
||||
String redirect = safeRedirect(submittedRedirect);
|
||||
|
||||
logLoginPost(request, submittedUsername, username, password, submittedRedirect, redirect);
|
||||
|
||||
AuthenticationResult result = authService.authenticate(username, password);
|
||||
if (!result.isAuthenticated()) {
|
||||
@@ -57,6 +63,26 @@ public class LoginServlet extends HttpServlet {
|
||||
response.sendRedirect(resolveRedirect(request, redirect));
|
||||
}
|
||||
|
||||
private void logLoginPost(
|
||||
HttpServletRequest request,
|
||||
String submittedUsername,
|
||||
String username,
|
||||
String password,
|
||||
String submittedRedirect,
|
||||
String redirect
|
||||
) {
|
||||
LOGGER.info("Login POST reached"
|
||||
+ " remoteAddr=" + safeLogValue(request.getRemoteAddr())
|
||||
+ " contextPath=" + safeLogValue(request.getContextPath())
|
||||
+ " redirectSubmitted=" + !trim(submittedRedirect).isEmpty()
|
||||
+ " redirectAccepted=" + !redirect.isEmpty()
|
||||
+ " usernameSubmitted=" + (submittedUsername != null)
|
||||
+ " usernameLength=" + length(submittedUsername)
|
||||
+ " normalizedUsernameLength=" + username.length()
|
||||
+ " usernameNormalizedChanged=" + !username.equals(nullToEmpty(submittedUsername))
|
||||
+ " passwordSubmitted=" + (password != null));
|
||||
}
|
||||
|
||||
private boolean isAuthenticated(HttpServletRequest request) {
|
||||
HttpSession session = request.getSession(false);
|
||||
return session != null && session.getAttribute(SessionAttributes.AUTHENTICATED_USER) != null;
|
||||
@@ -97,4 +123,29 @@ public class LoginServlet extends HttpServlet {
|
||||
private String trim(String value) {
|
||||
return value == null ? "" : value.trim();
|
||||
}
|
||||
|
||||
private int length(String value) {
|
||||
return value == null ? 0 : value.length();
|
||||
}
|
||||
|
||||
private String nullToEmpty(String value) {
|
||||
return value == null ? "" : value;
|
||||
}
|
||||
|
||||
private String safeLogValue(String value) {
|
||||
if (value == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
StringBuilder builder = new StringBuilder();
|
||||
int limit = Math.min(value.length(), 120);
|
||||
for (int i = 0; i < limit; i++) {
|
||||
char current = value.charAt(i);
|
||||
builder.append(Character.isISOControl(current) ? '?' : current);
|
||||
}
|
||||
if (value.length() > limit) {
|
||||
builder.append("...");
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -18,8 +18,11 @@ import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
public class JdbcUserDao implements UserDao, UserAccountDao {
|
||||
private static final Logger LOGGER = Logger.getLogger(JdbcUserDao.class.getName());
|
||||
private static final String USER_COLUMNS = ""
|
||||
+ "id, username, password_hash, display_name, role_code, active, created_at, updated_at ";
|
||||
|
||||
@@ -48,18 +51,27 @@ public class JdbcUserDao implements UserDao, UserAccountDao {
|
||||
|
||||
@Override
|
||||
public Optional<User> findActiveByUsername(String username) {
|
||||
LOGGER.info("Active user lookup start username=" + safeLogValue(username)
|
||||
+ " usernameLength=" + length(username));
|
||||
try (Connection connection = JdbcUtil.getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement(FIND_ACTIVE_BY_USERNAME)) {
|
||||
statement.setString(1, username);
|
||||
|
||||
try (ResultSet resultSet = statement.executeQuery()) {
|
||||
if (!resultSet.next()) {
|
||||
LOGGER.info("Active user lookup result=not-found username=" + safeLogValue(username));
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
return Optional.of(mapUser(resultSet));
|
||||
User user = mapUser(resultSet);
|
||||
LOGGER.info("Active user lookup result=found"
|
||||
+ " userId=" + user.getId()
|
||||
+ " role=" + user.getRole().getCode()
|
||||
+ " username=" + safeLogValue(username));
|
||||
return Optional.of(user);
|
||||
}
|
||||
} catch (SQLException | IllegalArgumentException ex) {
|
||||
LOGGER.log(Level.SEVERE, "Active user lookup failed username=" + safeLogValue(username), ex);
|
||||
throw new DaoException("Unable to load active user by username", ex);
|
||||
}
|
||||
}
|
||||
@@ -205,4 +217,25 @@ public class JdbcUserDao implements UserDao, UserAccountDao {
|
||||
private LocalDateTime toLocalDateTime(Timestamp timestamp) {
|
||||
return timestamp == null ? null : timestamp.toLocalDateTime();
|
||||
}
|
||||
|
||||
private int length(String value) {
|
||||
return value == null ? 0 : value.length();
|
||||
}
|
||||
|
||||
private String safeLogValue(String value) {
|
||||
if (value == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
StringBuilder builder = new StringBuilder();
|
||||
int limit = Math.min(value.length(), 120);
|
||||
for (int i = 0; i < limit; i++) {
|
||||
char current = value.charAt(i);
|
||||
builder.append(Character.isISOControl(current) ? '?' : current);
|
||||
}
|
||||
if (value.length() > limit) {
|
||||
builder.append("...");
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import javax.servlet.FilterConfig;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.ServletRequest;
|
||||
import javax.servlet.ServletResponse;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
public class CharacterEncodingFilter implements Filter {
|
||||
private String encoding = "UTF-8";
|
||||
@@ -25,6 +26,39 @@ public class CharacterEncodingFilter implements Filter {
|
||||
throws IOException, ServletException {
|
||||
request.setCharacterEncoding(encoding);
|
||||
response.setCharacterEncoding(encoding);
|
||||
if (isHtmlRequest(request)) {
|
||||
response.setContentType("text/html;charset=" + encoding);
|
||||
}
|
||||
chain.doFilter(request, response);
|
||||
}
|
||||
|
||||
private boolean isHtmlRequest(ServletRequest request) {
|
||||
if (!(request instanceof HttpServletRequest)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
HttpServletRequest httpRequest = (HttpServletRequest) request;
|
||||
String contextPath = httpRequest.getContextPath();
|
||||
String requestUri = httpRequest.getRequestURI();
|
||||
String path = requestUri.substring(contextPath.length());
|
||||
return !path.startsWith("/static/")
|
||||
&& !path.equals("/favicon.ico")
|
||||
&& !hasStaticAssetExtension(path);
|
||||
}
|
||||
|
||||
private boolean hasStaticAssetExtension(String path) {
|
||||
String normalizedPath = path.toLowerCase();
|
||||
return normalizedPath.endsWith(".css")
|
||||
|| normalizedPath.endsWith(".js")
|
||||
|| normalizedPath.endsWith(".png")
|
||||
|| normalizedPath.endsWith(".jpg")
|
||||
|| normalizedPath.endsWith(".jpeg")
|
||||
|| normalizedPath.endsWith(".gif")
|
||||
|| normalizedPath.endsWith(".svg")
|
||||
|| normalizedPath.endsWith(".ico")
|
||||
|| normalizedPath.endsWith(".woff")
|
||||
|| normalizedPath.endsWith(".woff2")
|
||||
|| normalizedPath.endsWith(".ttf")
|
||||
|| normalizedPath.endsWith(".map");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -36,24 +36,49 @@ public class AuthServiceImpl implements AuthService {
|
||||
@Override
|
||||
public AuthenticationResult authenticate(String username, String password) {
|
||||
String normalizedUsername = normalizeUsername(username);
|
||||
if (normalizedUsername.isEmpty() || password == null || password.trim().isEmpty()) {
|
||||
if (!normalizedUsername.equals(nullToEmpty(username))) {
|
||||
LOGGER.info("Login username normalized"
|
||||
+ " usernameSubmitted=" + (username != null)
|
||||
+ " usernameLength=" + length(username)
|
||||
+ " normalizedUsernameLength=" + normalizedUsername.length()
|
||||
+ " normalizedUsername=" + safeLogValue(normalizedUsername));
|
||||
}
|
||||
|
||||
boolean usernameMissing = normalizedUsername.isEmpty();
|
||||
boolean passwordMissing = password == null || password.trim().isEmpty();
|
||||
if (usernameMissing || passwordMissing) {
|
||||
LOGGER.info("Login rejected reason=missing-required"
|
||||
+ " usernameSubmitted=" + (username != null)
|
||||
+ " usernameMissing=" + usernameMissing
|
||||
+ " passwordSubmitted=" + (password != null)
|
||||
+ " passwordMissing=" + passwordMissing);
|
||||
return AuthenticationResult.failure(REQUIRED_MESSAGE);
|
||||
}
|
||||
|
||||
try {
|
||||
LOGGER.info("Login lookup start username=" + safeLogValue(normalizedUsername));
|
||||
Optional<User> user = userDao.findActiveByUsername(normalizedUsername);
|
||||
if (!user.isPresent() || !PasswordHasher.verify(password, user.get().getPasswordHash())) {
|
||||
LOGGER.info("Login failed for username=" + normalizedUsername);
|
||||
if (!user.isPresent()) {
|
||||
LOGGER.info("Login failed reason=active-user-not-found username=" + safeLogValue(normalizedUsername));
|
||||
return AuthenticationResult.failure(INVALID_MESSAGE);
|
||||
}
|
||||
|
||||
User authenticated = user.get();
|
||||
User candidate = user.get();
|
||||
if (!PasswordHasher.verify(password, candidate.getPasswordHash())) {
|
||||
LOGGER.info("Login failed reason=password-mismatch"
|
||||
+ " userId=" + candidate.getId()
|
||||
+ " role=" + candidate.getRole().getCode()
|
||||
+ " username=" + safeLogValue(normalizedUsername));
|
||||
return AuthenticationResult.failure(INVALID_MESSAGE);
|
||||
}
|
||||
|
||||
User authenticated = candidate;
|
||||
Set<Permission> permissions = permissionPolicy.permissionsFor(authenticated.getRole());
|
||||
AuthenticatedUser sessionUser = AuthenticatedUser.from(authenticated, permissions);
|
||||
LOGGER.info("Login success userId=" + authenticated.getId() + " role=" + authenticated.getRole().getCode());
|
||||
return AuthenticationResult.success(sessionUser);
|
||||
} catch (DaoException | IllegalStateException ex) {
|
||||
LOGGER.log(Level.SEVERE, "Login service error for username=" + normalizedUsername, ex);
|
||||
LOGGER.log(Level.SEVERE, "Login service error for username=" + safeLogValue(normalizedUsername), ex);
|
||||
return AuthenticationResult.failure(UNAVAILABLE_MESSAGE);
|
||||
}
|
||||
}
|
||||
@@ -66,4 +91,29 @@ public class AuthServiceImpl implements AuthService {
|
||||
private String normalizeUsername(String username) {
|
||||
return username == null ? "" : username.trim();
|
||||
}
|
||||
|
||||
private String nullToEmpty(String value) {
|
||||
return value == null ? "" : value;
|
||||
}
|
||||
|
||||
private int length(String value) {
|
||||
return value == null ? 0 : value.length();
|
||||
}
|
||||
|
||||
private String safeLogValue(String value) {
|
||||
if (value == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
StringBuilder builder = new StringBuilder();
|
||||
int limit = Math.min(value.length(), 120);
|
||||
for (int i = 0; i < limit; i++) {
|
||||
char current = value.charAt(i);
|
||||
builder.append(Character.isISOControl(current) ? '?' : current);
|
||||
}
|
||||
if (value.length() > limit) {
|
||||
builder.append("...");
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 + " 个字符。");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,10 +8,17 @@ import java.sql.Connection;
|
||||
import java.sql.DriverManager;
|
||||
import java.sql.SQLException;
|
||||
import java.util.Properties;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
public final class JdbcUtil {
|
||||
private static final Logger LOGGER = Logger.getLogger(JdbcUtil.class.getName());
|
||||
private static final String CONFIG_FILE = "db.properties";
|
||||
private static final String DEFAULT_DRIVER = "com.mysql.cj.jdbc.Driver";
|
||||
private static final String DRIVER_KEY = "db.driver";
|
||||
private static final String URL_KEY = "db.url";
|
||||
private static final String USERNAME_KEY = "db.username";
|
||||
private static final String PASSWORD_KEY = "db.password";
|
||||
|
||||
@FunctionalInterface
|
||||
public interface TransactionCallback<T> {
|
||||
@@ -23,16 +30,42 @@ public final class JdbcUtil {
|
||||
|
||||
public static Connection getConnection() {
|
||||
Properties properties = loadProperties();
|
||||
String driver = properties.getProperty("db.driver", DEFAULT_DRIVER);
|
||||
String url = required(properties, "db.url");
|
||||
String username = required(properties, "db.username");
|
||||
String password = required(properties, "db.password");
|
||||
String driver = properties.getProperty(DRIVER_KEY, DEFAULT_DRIVER);
|
||||
String url = required(properties, URL_KEY);
|
||||
String username = required(properties, USERNAME_KEY);
|
||||
String password = required(properties, PASSWORD_KEY);
|
||||
|
||||
LOGGER.info("Database connection configuration resolved"
|
||||
+ " file=" + CONFIG_FILE
|
||||
+ " driverKey=" + DRIVER_KEY
|
||||
+ " driver=" + safeLogValue(driver)
|
||||
+ " jdbcUrl=" + redactJdbcUrl(url)
|
||||
+ " usernameKey=" + USERNAME_KEY
|
||||
+ " usernameConfigured=" + !username.isEmpty()
|
||||
+ " password=<redacted>");
|
||||
LOGGER.info("Database connection attempt"
|
||||
+ " driverKey=" + DRIVER_KEY
|
||||
+ " driver=" + safeLogValue(driver)
|
||||
+ " jdbcUrl=" + redactJdbcUrl(url)
|
||||
+ " usernameKey=" + USERNAME_KEY);
|
||||
|
||||
try {
|
||||
Class.forName(driver);
|
||||
return DriverManager.getConnection(url, username, password);
|
||||
} catch (ClassNotFoundException | SQLException ex) {
|
||||
Connection connection = DriverManager.getConnection(url, username, password);
|
||||
LOGGER.info("Database connection established jdbcUrl=" + redactJdbcUrl(url)
|
||||
+ " usernameKey=" + USERNAME_KEY);
|
||||
return connection;
|
||||
} catch (ClassNotFoundException ex) {
|
||||
LOGGER.log(Level.SEVERE, "JDBC driver unavailable driver=" + safeLogValue(driver)
|
||||
+ " jdbcUrl=" + redactJdbcUrl(url)
|
||||
+ " usernameKey=" + USERNAME_KEY, ex);
|
||||
throw new DaoException("Unable to open database connection", ex);
|
||||
} catch (SQLException ex) {
|
||||
SQLException safeException = safeSqlException(ex);
|
||||
LOGGER.log(Level.SEVERE, "Database connection failed driver=" + safeLogValue(driver)
|
||||
+ " jdbcUrl=" + redactJdbcUrl(url)
|
||||
+ " usernameKey=" + USERNAME_KEY, safeException);
|
||||
throw new DaoException("Unable to open database connection", safeException);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,13 +101,20 @@ public final class JdbcUtil {
|
||||
.getContextClassLoader()
|
||||
.getResourceAsStream(CONFIG_FILE)) {
|
||||
if (inputStream == null) {
|
||||
LOGGER.severe("Database configuration file not found file=" + CONFIG_FILE);
|
||||
throw new DaoException("Missing database configuration file: " + CONFIG_FILE, null);
|
||||
}
|
||||
|
||||
Properties properties = new Properties();
|
||||
properties.load(inputStream);
|
||||
LOGGER.info("Database configuration loaded file=" + CONFIG_FILE
|
||||
+ " driverConfigured=" + hasText(properties, DRIVER_KEY)
|
||||
+ " urlConfigured=" + hasText(properties, URL_KEY)
|
||||
+ " usernameConfigured=" + hasText(properties, USERNAME_KEY)
|
||||
+ " passwordConfigured=" + hasText(properties, PASSWORD_KEY));
|
||||
return properties;
|
||||
} catch (IOException ex) {
|
||||
LOGGER.log(Level.SEVERE, "Unable to read database configuration file=" + CONFIG_FILE, ex);
|
||||
throw new DaoException("Unable to read database configuration", ex);
|
||||
}
|
||||
}
|
||||
@@ -82,8 +122,55 @@ public final class JdbcUtil {
|
||||
private static String required(Properties properties, String key) {
|
||||
String value = properties.getProperty(key);
|
||||
if (value == null || value.trim().isEmpty()) {
|
||||
LOGGER.severe("Missing database configuration value key=" + key);
|
||||
throw new DaoException("Missing database configuration value: " + key, null);
|
||||
}
|
||||
return value.trim();
|
||||
}
|
||||
|
||||
private static String redactJdbcUrl(String value) {
|
||||
if (value == null) {
|
||||
return "";
|
||||
}
|
||||
return safeLogValue(redactSensitive(value));
|
||||
}
|
||||
|
||||
private static SQLException safeSqlException(SQLException ex) {
|
||||
SQLException safeException = new SQLException(
|
||||
safeLogValue(redactSensitive(ex.getMessage())),
|
||||
ex.getSQLState(),
|
||||
ex.getErrorCode()
|
||||
);
|
||||
safeException.setStackTrace(ex.getStackTrace());
|
||||
return safeException;
|
||||
}
|
||||
|
||||
private static String redactSensitive(String value) {
|
||||
if (value == null) {
|
||||
return "";
|
||||
}
|
||||
return value.replaceAll("(?i)(password|pwd|pass|secret|token)(\\s*[=:]\\s*)([^;&\\s]*)", "$1$2<redacted>");
|
||||
}
|
||||
|
||||
private static boolean hasText(Properties properties, String key) {
|
||||
String value = properties.getProperty(key);
|
||||
return value != null && !value.trim().isEmpty();
|
||||
}
|
||||
|
||||
private static String safeLogValue(String value) {
|
||||
if (value == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
StringBuilder builder = new StringBuilder();
|
||||
int limit = Math.min(value.length(), 240);
|
||||
for (int i = 0; i < limit; i++) {
|
||||
char current = value.charAt(i);
|
||||
builder.append(Character.isISOControl(current) ? '?' : current);
|
||||
}
|
||||
if (value.length() > limit) {
|
||||
builder.append("...");
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,23 +179,33 @@ INSERT IGNORE INTO role_permissions (role_code, permission_code) VALUES
|
||||
('reader', 'view_catalog'),
|
||||
('reader', 'borrow_books');
|
||||
|
||||
-- Demo accounts for local scaffold verification only. Change or remove them
|
||||
-- before using a non-local database.
|
||||
-- Demo accounts for local scaffold verification only:
|
||||
-- admin/admin123, librarian/librarian123, reader/reader123.
|
||||
-- Change or remove them before using a non-local database.
|
||||
INSERT IGNORE INTO users (username, password_hash, display_name, role_code, active) VALUES
|
||||
('admin', 'pbkdf2_sha256$60000$bXpoLWFkbWluLWRlbW8tc2FsdA==$RwBCvhf3Wsc0jemnHlir4mdNZF4ZhHjrfHx/b1Bera0=', 'System Administrator', 'administrator', 1),
|
||||
('librarian', 'pbkdf2_sha256$60000$bXpoLWxpYnJhcmlhbi1kZW1vLXNhbHQ=$StIdJGDRIiF4aCr+qKuwvob5sL3+6j1caF2sQNqFi78=', 'Library Staff', 'librarian', 1),
|
||||
('reader', 'pbkdf2_sha256$60000$bXpoLXJlYWRlci1kZW1vLXNhbHQ=$iaiZPGhaIQ+2R2o9UQRj6wsrmYSJ4efqS3jCzM/XU7g=', 'Demo Reader', 'reader', 1);
|
||||
('admin', 'pbkdf2_sha256$60000$Ren1B30RDysysnApRiFVaQ==$1XwzMHaALqC7dKffwjbQkilBedfAuiMOXbR/xTMr5+Y=', 'System Administrator', 'administrator', 1),
|
||||
('librarian', 'pbkdf2_sha256$60000$PV/DJwZlMRm8vy0lKMAM4g==$+Aijfop3YoPp6HTePN5r4wG8N3qgxJE+yZHkTfzfbaw=', 'Library Staff', 'librarian', 1),
|
||||
('reader', 'pbkdf2_sha256$60000$wBzxTIT4ep79hgEzYDV9aQ==$w3oO5iSKRSfG4++b4558yiTHy6Tz9BB2+wuV9UOAKhs=', 'Demo Reader', 'reader', 1);
|
||||
|
||||
INSERT IGNORE INTO readers (reader_identifier, user_id, full_name, phone, email, status, max_borrow_count) VALUES
|
||||
('RD-0001', (SELECT id FROM users WHERE username = 'reader'), 'Demo Reader', '13800000000',
|
||||
'reader@example.com', 'active', 5),
|
||||
('RD-0002', NULL, 'Suspended Reader', '13900000000', 'suspended.reader@example.com', 'suspended', 3);
|
||||
('RD-0002', NULL, 'Suspended Reader', '13900000000', 'suspended.reader@example.com', 'suspended', 3),
|
||||
('RD-0101', NULL, '张晓雨', '13600010001', 'zhang.xiaoyu@example.com', 'active', 6),
|
||||
('RD-0102', NULL, '李明远', '13600010002', 'li.mingyuan@example.com', 'active', 5),
|
||||
('RD-0103', NULL, '王思涵', '13600010003', 'wang.sihan@example.com', 'active', 4),
|
||||
('RD-0104', NULL, '赵晨', '13600010004', 'zhao.chen@example.com', 'suspended', 3);
|
||||
|
||||
INSERT INTO book_categories (name, description) VALUES
|
||||
('Computer Science', 'Programming, software engineering, and systems books'),
|
||||
('Literature', 'Classic and modern literature'),
|
||||
('History', 'World and regional history'),
|
||||
('Science', 'Natural science and popular science')
|
||||
('Science', 'Natural science and popular science'),
|
||||
('中国文学', '中国现当代文学、经典小说和散文作品'),
|
||||
('计算机技术', '程序设计、软件工程、数据库和信息技术图书'),
|
||||
('历史文化', '中国历史、世界历史和文化研究读物'),
|
||||
('自然科学', '数学、物理、生命科学和科普读物'),
|
||||
('社会科学', '社会学、管理学和公共事务读物')
|
||||
ON DUPLICATE KEY UPDATE
|
||||
description = VALUES(description);
|
||||
|
||||
@@ -207,4 +217,16 @@ INSERT IGNORE INTO books (book_identifier, title, author, category_id, total_cop
|
||||
('BK-0003', 'Pride and Prejudice', 'Jane Austen',
|
||||
(SELECT id FROM book_categories WHERE name = 'Literature'), 3, 3, 'available'),
|
||||
('BK-0004', 'A Brief History of Time', 'Stephen Hawking',
|
||||
(SELECT id FROM book_categories WHERE name = 'Science'), 2, 1, 'available');
|
||||
(SELECT id FROM book_categories WHERE name = 'Science'), 2, 1, 'available'),
|
||||
('BK-0101', '活着', '余华',
|
||||
(SELECT id FROM book_categories WHERE name = '中国文学'), 6, 5, 'available'),
|
||||
('BK-0102', '平凡的世界', '路遥',
|
||||
(SELECT id FROM book_categories WHERE name = '中国文学'), 5, 5, 'available'),
|
||||
('BK-0103', '深入理解Java虚拟机', '周志明',
|
||||
(SELECT id FROM book_categories WHERE name = '计算机技术'), 4, 3, 'available'),
|
||||
('BK-0104', '中国通史', '吕思勉',
|
||||
(SELECT id FROM book_categories WHERE name = '历史文化'), 3, 3, 'available'),
|
||||
('BK-0105', '乡土中国', '费孝通',
|
||||
(SELECT id FROM book_categories WHERE name = '社会科学'), 4, 4, 'available'),
|
||||
('BK-0106', '科学史十五讲', '江晓原',
|
||||
(SELECT id FROM book_categories WHERE name = '自然科学'), 3, 2, 'available');
|
||||
|
||||
@@ -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>
|
||||
<link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/app.css">
|
||||
<title><c:out value="${formTitle}" /> - MZH 图书馆</title>
|
||||
<link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/app.css?v=20260428-visual-shell">
|
||||
</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,23 +2,25 @@
|
||||
<%@ 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>
|
||||
<link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/app.css">
|
||||
<title>用户账户管理 - MZH 图书馆</title>
|
||||
<link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/app.css?v=20260428-visual-shell">
|
||||
</head>
|
||||
<body>
|
||||
<%@ include file="/WEB-INF/jsp/common/header.jspf" %>
|
||||
<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>
|
||||
<div class="hero-actions">
|
||||
<a class="button button-primary" href="${pageContext.request.contextPath}/admin/users/new">新增用户账户</a>
|
||||
</div>
|
||||
<a class="button button-primary" href="${pageContext.request.contextPath}/admin/users/new">New user</a>
|
||||
</section>
|
||||
|
||||
<c:if test="${not empty successMessage}">
|
||||
@@ -32,10 +34,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 +45,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 +60,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 +112,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>
|
||||
|
||||
@@ -2,48 +2,96 @@
|
||||
<%@ 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>
|
||||
<link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/app.css">
|
||||
<title>登录 - 图书管理系统</title>
|
||||
<link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/app.css?v=20260428-login-redesign">
|
||||
</head>
|
||||
<body class="auth-page">
|
||||
<%@ include file="/WEB-INF/jsp/common/header.jspf" %>
|
||||
<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>
|
||||
<div class="login-card-head">
|
||||
<h1 id="login-title">图书管理系统</h1>
|
||||
<p class="login-subtitle">欢迎登录图书管理平台</p>
|
||||
</div>
|
||||
|
||||
<c:if test="${not empty errorMessage}">
|
||||
<div class="message message-error" role="alert">
|
||||
<div class="message message-error login-error" role="alert">
|
||||
<c:out value="${errorMessage}" />
|
||||
</div>
|
||||
</c:if>
|
||||
|
||||
<form class="login-form" action="${pageContext.request.contextPath}/login" method="post" novalidate>
|
||||
<form class="login-form" action="${pageContext.request.contextPath}/login" method="post" novalidate data-login-form>
|
||||
<input type="hidden" name="redirect" value="${fn:escapeXml(redirect)}">
|
||||
<label for="username">Username</label>
|
||||
<input id="username"
|
||||
<div class="login-field">
|
||||
<label class="sr-only" for="username">用户名</label>
|
||||
<div class="login-input-shell">
|
||||
<span class="login-input-icon" aria-hidden="true">
|
||||
<svg viewBox="0 0 24 24" focusable="false">
|
||||
<path d="M20 21a8 8 0 0 0-16 0" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round"/>
|
||||
<circle cx="12" cy="7.5" r="4" fill="none" stroke="currentColor" stroke-width="1.9"/>
|
||||
</svg>
|
||||
</span>
|
||||
<input class="login-control"
|
||||
id="username"
|
||||
name="username"
|
||||
type="text"
|
||||
value="${fn:escapeXml(username)}"
|
||||
autocomplete="username"
|
||||
placeholder="用户名"
|
||||
required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label for="password">Password</label>
|
||||
<input id="password"
|
||||
<div class="login-field">
|
||||
<label class="sr-only" for="password">密码</label>
|
||||
<div class="login-input-shell login-password-shell">
|
||||
<span class="login-input-icon" aria-hidden="true">
|
||||
<svg viewBox="0 0 24 24" focusable="false">
|
||||
<rect x="5" y="10" width="14" height="10" rx="2" fill="none" stroke="currentColor" stroke-width="1.9"/>
|
||||
<path d="M8 10V7.5a4 4 0 0 1 8 0V10M12 14.5v2" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round"/>
|
||||
</svg>
|
||||
</span>
|
||||
<input class="login-control"
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
autocomplete="current-password"
|
||||
placeholder="密码"
|
||||
required>
|
||||
<button class="password-toggle"
|
||||
type="button"
|
||||
aria-label="显示密码"
|
||||
aria-controls="password"
|
||||
aria-pressed="false"
|
||||
data-password-toggle>
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
|
||||
<path d="M2.8 12s3.3-5.5 9.2-5.5 9.2 5.5 9.2 5.5-3.3 5.5-9.2 5.5S2.8 12 2.8 12Z" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linejoin="round"/>
|
||||
<circle cx="12" cy="12" r="2.8" fill="none" stroke="currentColor" stroke-width="1.9"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="button button-primary" type="submit">Sign in</button>
|
||||
<div class="login-options-row">
|
||||
<label class="login-check">
|
||||
<input type="checkbox" name="rememberUsername" value="true" data-remember-username>
|
||||
<span>记住我</span>
|
||||
</label>
|
||||
<button class="forgot-password-link" type="button" data-forgot-password>
|
||||
忘记密码?
|
||||
</button>
|
||||
</div>
|
||||
<p class="login-help-message" id="password-help" tabindex="-1" hidden>
|
||||
请联系系统管理员重置密码。
|
||||
</p>
|
||||
|
||||
<button class="button button-primary login-submit" type="submit">登录</button>
|
||||
</form>
|
||||
</section>
|
||||
</main>
|
||||
<script src="${pageContext.request.contextPath}/static/js/login.js?v=20260428-login-redesign"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -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>
|
||||
<link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/app.css">
|
||||
<title>无权限 - MZH 图书馆</title>
|
||||
<link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/app.css?v=20260428-visual-shell">
|
||||
</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>
|
||||
|
||||
@@ -2,20 +2,22 @@
|
||||
<%@ 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>
|
||||
<link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/app.css">
|
||||
<title>馆藏检索 - MZH 图书馆</title>
|
||||
<link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/app.css?v=20260428-visual-shell">
|
||||
</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>
|
||||
<div>
|
||||
<p class="eyebrow">馆藏</p>
|
||||
<h1 id="catalog-title">馆藏检索</h1>
|
||||
<p>按图书编号、书名、作者或分类检索馆藏。</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<c:if test="${not empty errorMessage}">
|
||||
@@ -24,27 +26,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 +58,28 @@
|
||||
</c:if>
|
||||
</div>
|
||||
|
||||
<button class="button button-primary" type="submit">Search</button>
|
||||
<a class="button button-secondary" href="${pageContext.request.contextPath}/catalog">Clear</a>
|
||||
<c:if test="${canManageBooks}">
|
||||
<a class="button button-secondary" href="${pageContext.request.contextPath}/books">Manage books</a>
|
||||
</c:if>
|
||||
<button class="button button-primary" type="submit">检索</button>
|
||||
<a class="button button-secondary" href="${pageContext.request.contextPath}/catalog">清空</a>
|
||||
</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>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user