Compare commits
4 Commits
3efcb394fb
...
cc32c222a4
| Author | SHA1 | Date | |
|---|---|---|---|
| cc32c222a4 | |||
| dc192e8223 | |||
| fdf0eba506 | |||
| 89b6dd1f85 |
@@ -144,3 +144,74 @@ the server-side exception.
|
|||||||
<c:out value="${log.resultStatusName}" />
|
<c:out value="${log.resultStatusName}" />
|
||||||
</span>
|
</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());
|
||||||
|
```
|
||||||
|
|||||||
@@ -16,8 +16,21 @@ the reusable UI units.
|
|||||||
|
|
||||||
- Use shared fragments for repeated layout pieces such as header, navigation,
|
- Use shared fragments for repeated layout pieces such as header, navigation,
|
||||||
sidebar, footer, pagination, and message banners.
|
sidebar, footer, pagination, and message banners.
|
||||||
- Prefer `.jspf` includes or JSP tag files once the project chooses one
|
- Use `.jspf` includes for the current JSP presentation layer. The authenticated
|
||||||
pattern; document the actual paths after implementation.
|
application frame lives in `src/main/webapp/WEB-INF/jsp/common/header.jspf`
|
||||||
|
and owns the dark sidebar, top utility bar, role workbench links, 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'`.
|
||||||
- Keep fragments presentation-focused. They should not open database
|
- Keep fragments presentation-focused. They should not open database
|
||||||
connections or call DAOs.
|
connections or call DAOs.
|
||||||
|
|
||||||
|
|||||||
@@ -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,70 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
## 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.
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"id": "windows-login-diagnostic-logs",
|
||||||
|
"name": "windows-login-diagnostic-logs",
|
||||||
|
"title": "Add Windows login diagnostic logs",
|
||||||
|
"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,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,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": {}
|
||||||
|
}
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
<!-- @@@auto:current-status -->
|
<!-- @@@auto:current-status -->
|
||||||
- **Active File**: `journal-1.md`
|
- **Active File**: `journal-1.md`
|
||||||
- **Total Sessions**: 10
|
- **Total Sessions**: 11
|
||||||
- **Last Active**: 2026-04-28
|
- **Last Active**: 2026-04-28
|
||||||
<!-- @@@/auto:current-status -->
|
<!-- @@@/auto:current-status -->
|
||||||
|
|
||||||
@@ -19,7 +19,7 @@
|
|||||||
<!-- @@@auto:active-documents -->
|
<!-- @@@auto:active-documents -->
|
||||||
| File | Lines | Status |
|
| File | Lines | Status |
|
||||||
|------|-------|--------|
|
|------|-------|--------|
|
||||||
| `journal-1.md` | ~408 | Active |
|
| `journal-1.md` | ~441 | Active |
|
||||||
<!-- @@@/auto:active-documents -->
|
<!-- @@@/auto:active-documents -->
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -29,6 +29,7 @@
|
|||||||
<!-- @@@auto:session-history -->
|
<!-- @@@auto:session-history -->
|
||||||
| # | Date | Title | Commits | Branch |
|
| # | Date | Title | Commits | Branch |
|
||||||
|---|------|-------|---------|--------|
|
|---|------|-------|---------|--------|
|
||||||
|
| 11 | 2026-04-28 | Frontend Reference Redesign | `89b6dd1` | `master` |
|
||||||
| 10 | 2026-04-28 | 中文详细 README | `2d4a7e2` | `master` |
|
| 10 | 2026-04-28 | 中文详细 README | `2d4a7e2` | `master` |
|
||||||
| 9 | 2026-04-28 | Frontend Chinese UI | `ff044e6` | `master` |
|
| 9 | 2026-04-28 | Frontend Chinese UI | `ff044e6` | `master` |
|
||||||
| 8 | 2026-04-27 | Core Function Gap Check | `d917a62` | `master` |
|
| 8 | 2026-04-27 | Core Function Gap Check | `d917a62` | `master` |
|
||||||
|
|||||||
@@ -406,3 +406,36 @@ Localized JSP frontend UI and displayed backend messages to Simplified Chinese,
|
|||||||
### Next Steps
|
### Next Steps
|
||||||
|
|
||||||
- None - task complete
|
- 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
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import com.mzh.library.service.impl.AuthServiceImpl;
|
|||||||
import com.mzh.library.util.SessionAttributes;
|
import com.mzh.library.util.SessionAttributes;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
import javax.servlet.ServletException;
|
import javax.servlet.ServletException;
|
||||||
import javax.servlet.http.HttpServlet;
|
import javax.servlet.http.HttpServlet;
|
||||||
@@ -16,6 +17,7 @@ import javax.servlet.http.HttpServletResponse;
|
|||||||
import javax.servlet.http.HttpSession;
|
import javax.servlet.http.HttpSession;
|
||||||
|
|
||||||
public class LoginServlet extends HttpServlet {
|
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 LOGIN_JSP = "/WEB-INF/jsp/auth/login.jsp";
|
||||||
private static final String DASHBOARD_PATH = "/dashboard";
|
private static final String DASHBOARD_PATH = "/dashboard";
|
||||||
private static final int SESSION_TIMEOUT_SECONDS = 30 * 60;
|
private static final int SESSION_TIMEOUT_SECONDS = 30 * 60;
|
||||||
@@ -40,9 +42,13 @@ public class LoginServlet extends HttpServlet {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
|
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 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);
|
AuthenticationResult result = authService.authenticate(username, password);
|
||||||
if (!result.isAuthenticated()) {
|
if (!result.isAuthenticated()) {
|
||||||
@@ -57,6 +63,26 @@ public class LoginServlet extends HttpServlet {
|
|||||||
response.sendRedirect(resolveRedirect(request, redirect));
|
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) {
|
private boolean isAuthenticated(HttpServletRequest request) {
|
||||||
HttpSession session = request.getSession(false);
|
HttpSession session = request.getSession(false);
|
||||||
return session != null && session.getAttribute(SessionAttributes.AUTHENTICATED_USER) != null;
|
return session != null && session.getAttribute(SessionAttributes.AUTHENTICATED_USER) != null;
|
||||||
@@ -97,4 +123,29 @@ public class LoginServlet extends HttpServlet {
|
|||||||
private String trim(String value) {
|
private String trim(String value) {
|
||||||
return value == null ? "" : value.trim();
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,8 +18,11 @@ import java.time.LocalDateTime;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
public class JdbcUserDao implements UserDao, UserAccountDao {
|
public class JdbcUserDao implements UserDao, UserAccountDao {
|
||||||
|
private static final Logger LOGGER = Logger.getLogger(JdbcUserDao.class.getName());
|
||||||
private static final String USER_COLUMNS = ""
|
private static final String USER_COLUMNS = ""
|
||||||
+ "id, username, password_hash, display_name, role_code, active, created_at, updated_at ";
|
+ "id, username, password_hash, display_name, role_code, active, created_at, updated_at ";
|
||||||
|
|
||||||
@@ -48,18 +51,27 @@ public class JdbcUserDao implements UserDao, UserAccountDao {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Optional<User> findActiveByUsername(String username) {
|
public Optional<User> findActiveByUsername(String username) {
|
||||||
|
LOGGER.info("Active user lookup start username=" + safeLogValue(username)
|
||||||
|
+ " usernameLength=" + length(username));
|
||||||
try (Connection connection = JdbcUtil.getConnection();
|
try (Connection connection = JdbcUtil.getConnection();
|
||||||
PreparedStatement statement = connection.prepareStatement(FIND_ACTIVE_BY_USERNAME)) {
|
PreparedStatement statement = connection.prepareStatement(FIND_ACTIVE_BY_USERNAME)) {
|
||||||
statement.setString(1, username);
|
statement.setString(1, username);
|
||||||
|
|
||||||
try (ResultSet resultSet = statement.executeQuery()) {
|
try (ResultSet resultSet = statement.executeQuery()) {
|
||||||
if (!resultSet.next()) {
|
if (!resultSet.next()) {
|
||||||
|
LOGGER.info("Active user lookup result=not-found username=" + safeLogValue(username));
|
||||||
return Optional.empty();
|
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) {
|
} 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);
|
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) {
|
private LocalDateTime toLocalDateTime(Timestamp timestamp) {
|
||||||
return timestamp == null ? null : timestamp.toLocalDateTime();
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import javax.servlet.FilterConfig;
|
|||||||
import javax.servlet.ServletException;
|
import javax.servlet.ServletException;
|
||||||
import javax.servlet.ServletRequest;
|
import javax.servlet.ServletRequest;
|
||||||
import javax.servlet.ServletResponse;
|
import javax.servlet.ServletResponse;
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
|
||||||
public class CharacterEncodingFilter implements Filter {
|
public class CharacterEncodingFilter implements Filter {
|
||||||
private String encoding = "UTF-8";
|
private String encoding = "UTF-8";
|
||||||
@@ -25,6 +26,39 @@ public class CharacterEncodingFilter implements Filter {
|
|||||||
throws IOException, ServletException {
|
throws IOException, ServletException {
|
||||||
request.setCharacterEncoding(encoding);
|
request.setCharacterEncoding(encoding);
|
||||||
response.setCharacterEncoding(encoding);
|
response.setCharacterEncoding(encoding);
|
||||||
|
if (isHtmlRequest(request)) {
|
||||||
|
response.setContentType("text/html;charset=" + encoding);
|
||||||
|
}
|
||||||
chain.doFilter(request, response);
|
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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,24 +36,49 @@ public class AuthServiceImpl implements AuthService {
|
|||||||
@Override
|
@Override
|
||||||
public AuthenticationResult authenticate(String username, String password) {
|
public AuthenticationResult authenticate(String username, String password) {
|
||||||
String normalizedUsername = normalizeUsername(username);
|
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);
|
return AuthenticationResult.failure(REQUIRED_MESSAGE);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
LOGGER.info("Login lookup start username=" + safeLogValue(normalizedUsername));
|
||||||
Optional<User> user = userDao.findActiveByUsername(normalizedUsername);
|
Optional<User> user = userDao.findActiveByUsername(normalizedUsername);
|
||||||
if (!user.isPresent() || !PasswordHasher.verify(password, user.get().getPasswordHash())) {
|
if (!user.isPresent()) {
|
||||||
LOGGER.info("Login failed for username=" + normalizedUsername);
|
LOGGER.info("Login failed reason=active-user-not-found username=" + safeLogValue(normalizedUsername));
|
||||||
return AuthenticationResult.failure(INVALID_MESSAGE);
|
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());
|
Set<Permission> permissions = permissionPolicy.permissionsFor(authenticated.getRole());
|
||||||
AuthenticatedUser sessionUser = AuthenticatedUser.from(authenticated, permissions);
|
AuthenticatedUser sessionUser = AuthenticatedUser.from(authenticated, permissions);
|
||||||
LOGGER.info("Login success userId=" + authenticated.getId() + " role=" + authenticated.getRole().getCode());
|
LOGGER.info("Login success userId=" + authenticated.getId() + " role=" + authenticated.getRole().getCode());
|
||||||
return AuthenticationResult.success(sessionUser);
|
return AuthenticationResult.success(sessionUser);
|
||||||
} catch (DaoException | IllegalStateException ex) {
|
} 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);
|
return AuthenticationResult.failure(UNAVAILABLE_MESSAGE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -66,4 +91,29 @@ public class AuthServiceImpl implements AuthService {
|
|||||||
private String normalizeUsername(String username) {
|
private String normalizeUsername(String username) {
|
||||||
return username == null ? "" : username.trim();
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,10 +8,17 @@ import java.sql.Connection;
|
|||||||
import java.sql.DriverManager;
|
import java.sql.DriverManager;
|
||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
import java.util.Properties;
|
import java.util.Properties;
|
||||||
|
import java.util.logging.Level;
|
||||||
|
import java.util.logging.Logger;
|
||||||
|
|
||||||
public final class JdbcUtil {
|
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 CONFIG_FILE = "db.properties";
|
||||||
private static final String DEFAULT_DRIVER = "com.mysql.cj.jdbc.Driver";
|
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
|
@FunctionalInterface
|
||||||
public interface TransactionCallback<T> {
|
public interface TransactionCallback<T> {
|
||||||
@@ -23,16 +30,42 @@ public final class JdbcUtil {
|
|||||||
|
|
||||||
public static Connection getConnection() {
|
public static Connection getConnection() {
|
||||||
Properties properties = loadProperties();
|
Properties properties = loadProperties();
|
||||||
String driver = properties.getProperty("db.driver", DEFAULT_DRIVER);
|
String driver = properties.getProperty(DRIVER_KEY, DEFAULT_DRIVER);
|
||||||
String url = required(properties, "db.url");
|
String url = required(properties, URL_KEY);
|
||||||
String username = required(properties, "db.username");
|
String username = required(properties, USERNAME_KEY);
|
||||||
String password = required(properties, "db.password");
|
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 {
|
try {
|
||||||
Class.forName(driver);
|
Class.forName(driver);
|
||||||
return DriverManager.getConnection(url, username, password);
|
Connection connection = DriverManager.getConnection(url, username, password);
|
||||||
} catch (ClassNotFoundException | SQLException ex) {
|
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);
|
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()
|
.getContextClassLoader()
|
||||||
.getResourceAsStream(CONFIG_FILE)) {
|
.getResourceAsStream(CONFIG_FILE)) {
|
||||||
if (inputStream == null) {
|
if (inputStream == null) {
|
||||||
|
LOGGER.severe("Database configuration file not found file=" + CONFIG_FILE);
|
||||||
throw new DaoException("Missing database configuration file: " + CONFIG_FILE, null);
|
throw new DaoException("Missing database configuration file: " + CONFIG_FILE, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
Properties properties = new Properties();
|
Properties properties = new Properties();
|
||||||
properties.load(inputStream);
|
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;
|
return properties;
|
||||||
} catch (IOException ex) {
|
} 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);
|
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) {
|
private static String required(Properties properties, String key) {
|
||||||
String value = properties.getProperty(key);
|
String value = properties.getProperty(key);
|
||||||
if (value == null || value.trim().isEmpty()) {
|
if (value == null || value.trim().isEmpty()) {
|
||||||
|
LOGGER.severe("Missing database configuration value key=" + key);
|
||||||
throw new DaoException("Missing database configuration value: " + key, null);
|
throw new DaoException("Missing database configuration value: " + key, null);
|
||||||
}
|
}
|
||||||
return value.trim();
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title><c:out value="${formTitle}" /> - MZH 图书馆</title>
|
<title><c:out value="${formTitle}" /> - MZH 图书馆</title>
|
||||||
<link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/app.css">
|
<link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/app.css?v=20260428-visual-shell">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<%@ include file="/WEB-INF/jsp/common/header.jspf" %>
|
<%@ include file="/WEB-INF/jsp/common/header.jspf" %>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>用户管理 - MZH 图书馆</title>
|
<title>用户管理 - MZH 图书馆</title>
|
||||||
<link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/app.css">
|
<link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/app.css?v=20260428-visual-shell">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<%@ include file="/WEB-INF/jsp/common/header.jspf" %>
|
<%@ include file="/WEB-INF/jsp/common/header.jspf" %>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>登录 - MZH 图书馆</title>
|
<title>登录 - MZH 图书馆</title>
|
||||||
<link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/app.css">
|
<link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/app.css?v=20260428-visual-shell">
|
||||||
</head>
|
</head>
|
||||||
<body class="auth-page">
|
<body class="auth-page">
|
||||||
<%@ include file="/WEB-INF/jsp/common/header.jspf" %>
|
<%@ include file="/WEB-INF/jsp/common/header.jspf" %>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>无权限 - MZH 图书馆</title>
|
<title>无权限 - MZH 图书馆</title>
|
||||||
<link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/app.css">
|
<link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/app.css?v=20260428-visual-shell">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<%@ include file="/WEB-INF/jsp/common/header.jspf" %>
|
<%@ include file="/WEB-INF/jsp/common/header.jspf" %>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>馆藏检索 - MZH 图书馆</title>
|
<title>馆藏检索 - MZH 图书馆</title>
|
||||||
<link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/app.css">
|
<link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/app.css?v=20260428-visual-shell">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<%@ include file="/WEB-INF/jsp/common/header.jspf" %>
|
<%@ include file="/WEB-INF/jsp/common/header.jspf" %>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>分类管理 - MZH 图书馆</title>
|
<title>分类管理 - MZH 图书馆</title>
|
||||||
<link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/app.css">
|
<link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/app.css?v=20260428-visual-shell">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<%@ include file="/WEB-INF/jsp/common/header.jspf" %>
|
<%@ include file="/WEB-INF/jsp/common/header.jspf" %>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title><c:out value="${formTitle}" /> - MZH 图书馆</title>
|
<title><c:out value="${formTitle}" /> - MZH 图书馆</title>
|
||||||
<link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/app.css">
|
<link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/app.css?v=20260428-visual-shell">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<%@ include file="/WEB-INF/jsp/common/header.jspf" %>
|
<%@ include file="/WEB-INF/jsp/common/header.jspf" %>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title><c:out value="${formTitle}" /> - MZH 图书馆</title>
|
<title><c:out value="${formTitle}" /> - MZH 图书馆</title>
|
||||||
<link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/app.css">
|
<link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/app.css?v=20260428-visual-shell">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<%@ include file="/WEB-INF/jsp/common/header.jspf" %>
|
<%@ include file="/WEB-INF/jsp/common/header.jspf" %>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>图书管理 - MZH 图书馆</title>
|
<title>图书管理 - MZH 图书馆</title>
|
||||||
<link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/app.css">
|
<link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/app.css?v=20260428-visual-shell">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<%@ include file="/WEB-INF/jsp/common/header.jspf" %>
|
<%@ include file="/WEB-INF/jsp/common/header.jspf" %>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>新增借阅 - MZH 图书馆</title>
|
<title>新增借阅 - MZH 图书馆</title>
|
||||||
<link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/app.css">
|
<link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/app.css?v=20260428-visual-shell">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<%@ include file="/WEB-INF/jsp/common/header.jspf" %>
|
<%@ include file="/WEB-INF/jsp/common/header.jspf" %>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>借阅管理 - MZH 图书馆</title>
|
<title>借阅管理 - MZH 图书馆</title>
|
||||||
<link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/app.css">
|
<link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/app.css?v=20260428-visual-shell">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<%@ include file="/WEB-INF/jsp/common/header.jspf" %>
|
<%@ include file="/WEB-INF/jsp/common/header.jspf" %>
|
||||||
|
|||||||
@@ -1,31 +1,143 @@
|
|||||||
|
<%@ page pageEncoding="UTF-8" %>
|
||||||
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
|
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
|
||||||
<header class="app-header">
|
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
|
||||||
<a class="brand" href="${pageContext.request.contextPath}/dashboard">MZH 图书馆</a>
|
<header class="app-header ${not empty sessionScope.authenticatedUser ? 'app-header-auth' : 'app-header-public'}">
|
||||||
<c:if test="${not empty sessionScope.authenticatedUser}">
|
<c:choose>
|
||||||
<nav class="top-nav" aria-label="主导航">
|
<c:when test="${not empty sessionScope.authenticatedUser}">
|
||||||
<a href="${pageContext.request.contextPath}/dashboard">控制台</a>
|
<c:set var="currentUri" value="${pageContext.request.requestURI}" />
|
||||||
<a href="${pageContext.request.contextPath}/catalog">馆藏检索</a>
|
<aside class="app-sidebar" aria-label="主导航">
|
||||||
<c:if test="${sessionScope.userRole == 'administrator'}">
|
<a class="sidebar-brand" href="${pageContext.request.contextPath}/dashboard">
|
||||||
<a href="${pageContext.request.contextPath}/admin/home">管理</a>
|
<span class="brand-mark" aria-hidden="true">书</span>
|
||||||
<a href="${pageContext.request.contextPath}/admin/users">用户</a>
|
<span class="brand-text">图书管理系统</span>
|
||||||
<a href="${pageContext.request.contextPath}/admin/system-logs">日志</a>
|
</a>
|
||||||
</c:if>
|
|
||||||
<c:if test="${sessionScope.userRole == 'administrator' or sessionScope.userRole == 'librarian'}">
|
<section class="role-workbench" aria-label="角色工作台">
|
||||||
<a href="${pageContext.request.contextPath}/librarian/home">馆员</a>
|
<p class="sidebar-section-title">角色工作台</p>
|
||||||
<a href="${pageContext.request.contextPath}/books">图书</a>
|
<c:if test="${sessionScope.userRole == 'administrator'}">
|
||||||
<a href="${pageContext.request.contextPath}/book-categories">分类</a>
|
<a class="role-chip role-chip-admin" href="${pageContext.request.contextPath}/admin/home">
|
||||||
<a href="${pageContext.request.contextPath}/readers">读者</a>
|
<span class="role-chip-icon" aria-hidden="true">管</span>
|
||||||
<a href="${pageContext.request.contextPath}/borrowing">借阅</a>
|
<span class="role-chip-copy">
|
||||||
<a href="${pageContext.request.contextPath}/reports">报表</a>
|
<strong>管理员</strong>
|
||||||
</c:if>
|
<small>系统管理</small>
|
||||||
<a href="${pageContext.request.contextPath}/reader/home">读者中心</a>
|
</span>
|
||||||
<c:if test="${sessionScope.userRole == 'reader'}">
|
</a>
|
||||||
<a href="${pageContext.request.contextPath}/reader/loans">我的借阅</a>
|
</c:if>
|
||||||
</c:if>
|
<c:if test="${sessionScope.userRole == 'administrator' or sessionScope.userRole == 'librarian'}">
|
||||||
<span class="user-pill">
|
<a class="role-chip role-chip-librarian" href="${pageContext.request.contextPath}/librarian/home">
|
||||||
<c:out value="${sessionScope.authenticatedUser.displayName}" />
|
<span class="role-chip-icon" aria-hidden="true">馆</span>
|
||||||
</span>
|
<span class="role-chip-copy">
|
||||||
<a class="button button-secondary" href="${pageContext.request.contextPath}/logout">退出</a>
|
<strong>馆员</strong>
|
||||||
</nav>
|
<small>流通工作</small>
|
||||||
</c:if>
|
</span>
|
||||||
|
</a>
|
||||||
|
</c:if>
|
||||||
|
<c:if test="${sessionScope.userRole == 'reader'}">
|
||||||
|
<a class="role-chip role-chip-reader" href="${pageContext.request.contextPath}/reader/home">
|
||||||
|
<span class="role-chip-icon" aria-hidden="true">读</span>
|
||||||
|
<span class="role-chip-copy">
|
||||||
|
<strong>读者</strong>
|
||||||
|
<small>自助服务</small>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</c:if>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<nav class="side-nav" aria-label="模块导航">
|
||||||
|
<a class="side-nav-link ${fn:contains(currentUri, '/dashboard') ? 'is-active' : ''}"
|
||||||
|
href="${pageContext.request.contextPath}/dashboard">
|
||||||
|
<span class="nav-icon" aria-hidden="true">台</span>
|
||||||
|
<span class="nav-text">工作台</span>
|
||||||
|
</a>
|
||||||
|
<a class="side-nav-link ${fn:contains(currentUri, '/catalog') ? 'is-active' : ''}"
|
||||||
|
href="${pageContext.request.contextPath}/catalog">
|
||||||
|
<span class="nav-icon" aria-hidden="true">搜</span>
|
||||||
|
<span class="nav-text">馆藏检索</span>
|
||||||
|
</a>
|
||||||
|
<c:if test="${sessionScope.userRole == 'administrator' or sessionScope.userRole == 'librarian'}">
|
||||||
|
<a class="side-nav-link ${fn:contains(currentUri, '/books') ? 'is-active' : ''}"
|
||||||
|
href="${pageContext.request.contextPath}/books">
|
||||||
|
<span class="nav-icon" aria-hidden="true">书</span>
|
||||||
|
<span class="nav-text">图书管理</span>
|
||||||
|
</a>
|
||||||
|
<a class="side-nav-link ${fn:contains(currentUri, '/book-categories') ? 'is-active' : ''}"
|
||||||
|
href="${pageContext.request.contextPath}/book-categories">
|
||||||
|
<span class="nav-icon" aria-hidden="true">类</span>
|
||||||
|
<span class="nav-text">图书分类管理</span>
|
||||||
|
</a>
|
||||||
|
<a class="side-nav-link ${fn:contains(currentUri, '/readers') ? 'is-active' : ''}"
|
||||||
|
href="${pageContext.request.contextPath}/readers">
|
||||||
|
<span class="nav-icon" aria-hidden="true">人</span>
|
||||||
|
<span class="nav-text">读者管理</span>
|
||||||
|
</a>
|
||||||
|
<a class="side-nav-link ${fn:contains(currentUri, '/borrowing') ? 'is-active' : ''}"
|
||||||
|
href="${pageContext.request.contextPath}/borrowing">
|
||||||
|
<span class="nav-icon" aria-hidden="true">借</span>
|
||||||
|
<span class="nav-text">借阅流通</span>
|
||||||
|
</a>
|
||||||
|
<a class="side-nav-link ${fn:contains(currentUri, '/reports') ? 'is-active' : ''}"
|
||||||
|
href="${pageContext.request.contextPath}/reports">
|
||||||
|
<span class="nav-icon" aria-hidden="true">报</span>
|
||||||
|
<span class="nav-text">报表中心</span>
|
||||||
|
</a>
|
||||||
|
</c:if>
|
||||||
|
<c:if test="${sessionScope.userRole == 'reader'}">
|
||||||
|
<a class="side-nav-link ${fn:contains(currentUri, '/reader/loans') ? 'is-active' : ''}"
|
||||||
|
href="${pageContext.request.contextPath}/reader/loans">
|
||||||
|
<span class="nav-icon" aria-hidden="true">历</span>
|
||||||
|
<span class="nav-text">读者借阅历史</span>
|
||||||
|
</a>
|
||||||
|
</c:if>
|
||||||
|
<c:if test="${sessionScope.userRole == 'administrator'}">
|
||||||
|
<a class="side-nav-link ${fn:contains(currentUri, '/admin/users') ? 'is-active' : ''}"
|
||||||
|
href="${pageContext.request.contextPath}/admin/users">
|
||||||
|
<span class="nav-icon" aria-hidden="true">户</span>
|
||||||
|
<span class="nav-text">用户管理</span>
|
||||||
|
</a>
|
||||||
|
<a class="side-nav-link ${fn:contains(currentUri, '/admin/system-logs') ? 'is-active' : ''}"
|
||||||
|
href="${pageContext.request.contextPath}/admin/system-logs">
|
||||||
|
<span class="nav-icon" aria-hidden="true">志</span>
|
||||||
|
<span class="nav-text">系统日志</span>
|
||||||
|
</a>
|
||||||
|
</c:if>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="sidebar-footer">
|
||||||
|
<span class="sidebar-menu-dot" aria-hidden="true">≡</span>
|
||||||
|
<a href="${pageContext.request.contextPath}/logout">退出登录</a>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<div class="app-topbar">
|
||||||
|
<div class="breadcrumb">已登录 <span>/</span> 工作台</div>
|
||||||
|
<form class="global-search" action="${pageContext.request.contextPath}/catalog" method="get">
|
||||||
|
<label class="sr-only" for="globalTitle">搜索图书、读者、功能</label>
|
||||||
|
<input id="globalTitle" name="title" type="search" placeholder="搜索图书、读者、功能...">
|
||||||
|
<button type="submit" aria-label="搜索">搜</button>
|
||||||
|
</form>
|
||||||
|
<div class="topbar-actions">
|
||||||
|
<span class="notification-dot" aria-label="通知">!</span>
|
||||||
|
<span class="user-summary">
|
||||||
|
<span class="avatar" aria-hidden="true">
|
||||||
|
<c:choose>
|
||||||
|
<c:when test="${sessionScope.userRole == 'administrator'}">管</c:when>
|
||||||
|
<c:when test="${sessionScope.userRole == 'librarian'}">馆</c:when>
|
||||||
|
<c:otherwise>读</c:otherwise>
|
||||||
|
</c:choose>
|
||||||
|
</span>
|
||||||
|
<span class="user-meta">
|
||||||
|
<span class="user-pill">
|
||||||
|
<c:out value="${sessionScope.authenticatedUser.displayName}" />
|
||||||
|
</span>
|
||||||
|
<span class="role-label">
|
||||||
|
<c:out value="${sessionScope.authenticatedUser.role.displayName}" />
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</c:when>
|
||||||
|
<c:otherwise>
|
||||||
|
<a class="auth-brand" href="${pageContext.request.contextPath}/dashboard">MZH 图书馆</a>
|
||||||
|
</c:otherwise>
|
||||||
|
</c:choose>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@@ -6,98 +6,304 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>控制台 - MZH 图书馆</title>
|
<title>控制台 - MZH 图书馆</title>
|
||||||
<link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/app.css">
|
<link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/app.css?v=20260428-visual-shell">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<%@ include file="/WEB-INF/jsp/common/header.jspf" %>
|
<%@ include file="/WEB-INF/jsp/common/header.jspf" %>
|
||||||
<main class="page-shell">
|
<main class="page-shell dashboard-shell">
|
||||||
<section class="dashboard-hero" aria-labelledby="dashboard-title">
|
<section class="dashboard-hero dashboard-welcome" aria-labelledby="dashboard-title">
|
||||||
<p class="eyebrow">
|
<div>
|
||||||
<c:out value="${sessionScope.authenticatedUser.role.displayName}" />
|
<p class="eyebrow">
|
||||||
</p>
|
<c:out value="${sessionScope.authenticatedUser.role.displayName}" />
|
||||||
<h1 id="dashboard-title">控制台</h1>
|
</p>
|
||||||
<p>当前登录:<strong><c:out value="${sessionScope.authenticatedUser.displayName}" /></strong></p>
|
<h1 id="dashboard-title">
|
||||||
|
<c:choose>
|
||||||
|
<c:when test="${sessionScope.userRole == 'administrator'}">管理员工作台</c:when>
|
||||||
|
<c:when test="${sessionScope.userRole == 'librarian'}">馆员工作台</c:when>
|
||||||
|
<c:otherwise>读者工作台</c:otherwise>
|
||||||
|
</c:choose>
|
||||||
|
</h1>
|
||||||
|
<p>登录后进入 Dashboard,会话仅保存安全的 AuthenticatedUser 快照、角色代码与权限代码集合。</p>
|
||||||
|
</div>
|
||||||
|
<div class="welcome-user">
|
||||||
|
<span>当前登录</span>
|
||||||
|
<strong><c:out value="${sessionScope.authenticatedUser.displayName}" /></strong>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="card-grid" aria-label="角色工作区">
|
<section class="dashboard-metrics" aria-label="核心指标">
|
||||||
<c:if test="${sessionScope.userRole == 'administrator'}">
|
<article class="metric-card">
|
||||||
<article class="workspace-card">
|
<span class="metric-icon metric-blue" aria-hidden="true">书</span>
|
||||||
<h2>系统管理</h2>
|
<div>
|
||||||
<p>账户、角色、权限和系统维护入口。</p>
|
<h2>馆藏总量</h2>
|
||||||
<a class="button button-secondary" href="${pageContext.request.contextPath}/admin/home">打开</a>
|
<p class="metric-value">12,586 <small>册</small></p>
|
||||||
</article>
|
<p class="metric-trend trend-up">较上月 ↑ 5.2%</p>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
<article class="metric-card">
|
||||||
|
<span class="metric-icon metric-green" aria-hidden="true">借</span>
|
||||||
|
<div>
|
||||||
|
<h2>在借数量</h2>
|
||||||
|
<p class="metric-value">1,258 <small>册</small></p>
|
||||||
|
<p class="metric-trend trend-up">较上月 ↑ 3.1%</p>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
<article class="metric-card">
|
||||||
|
<span class="metric-icon metric-orange" aria-hidden="true">期</span>
|
||||||
|
<div>
|
||||||
|
<h2>逾期数量</h2>
|
||||||
|
<p class="metric-value">87 <small>册</small></p>
|
||||||
|
<p class="metric-trend trend-down">较上月 ↓ 12.4%</p>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
<article class="metric-card">
|
||||||
|
<span class="metric-icon metric-purple" aria-hidden="true">者</span>
|
||||||
|
<div>
|
||||||
|
<h2>读者总数</h2>
|
||||||
|
<p class="metric-value">3,682 <small>人</small></p>
|
||||||
|
<p class="metric-trend trend-up">较上月 ↑ 4.8%</p>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</section>
|
||||||
|
|
||||||
<article class="workspace-card">
|
<section class="dashboard-grid" aria-label="检索与排行">
|
||||||
<h2>用户管理</h2>
|
<article class="dashboard-panel search-panel">
|
||||||
<p>创建、更新、停用和查看登录账户。</p>
|
|
||||||
<a class="button button-secondary" href="${pageContext.request.contextPath}/admin/users">打开</a>
|
|
||||||
</article>
|
|
||||||
|
|
||||||
<article class="workspace-card">
|
|
||||||
<h2>系统日志</h2>
|
|
||||||
<p>查看账户与维护操作的只读审计记录。</p>
|
|
||||||
<a class="button button-secondary" href="${pageContext.request.contextPath}/admin/system-logs">打开</a>
|
|
||||||
</article>
|
|
||||||
</c:if>
|
|
||||||
|
|
||||||
<c:if test="${sessionScope.userRole == 'administrator' or sessionScope.userRole == 'librarian'}">
|
|
||||||
<article class="workspace-card">
|
|
||||||
<h2>馆员工作台</h2>
|
|
||||||
<p>图书、读者、借阅、归还、续借和逾期处理入口。</p>
|
|
||||||
<a class="button button-secondary" href="${pageContext.request.contextPath}/librarian/home">打开</a>
|
|
||||||
</article>
|
|
||||||
|
|
||||||
<article class="workspace-card">
|
|
||||||
<h2>图书管理</h2>
|
|
||||||
<p>创建、更新、删除和查看图书库存记录。</p>
|
|
||||||
<a class="button button-secondary" href="${pageContext.request.contextPath}/books">打开</a>
|
|
||||||
</article>
|
|
||||||
|
|
||||||
<article class="workspace-card">
|
|
||||||
<h2>分类维护</h2>
|
|
||||||
<p>维护图书记录和检索筛选使用的馆藏分类。</p>
|
|
||||||
<a class="button button-secondary" href="${pageContext.request.contextPath}/book-categories">打开</a>
|
|
||||||
</article>
|
|
||||||
|
|
||||||
<article class="workspace-card">
|
|
||||||
<h2>读者管理</h2>
|
|
||||||
<p>创建、更新、停用和查看读者借阅资格记录。</p>
|
|
||||||
<a class="button button-secondary" href="${pageContext.request.contextPath}/readers">打开</a>
|
|
||||||
</article>
|
|
||||||
|
|
||||||
<article class="workspace-card">
|
|
||||||
<h2>借阅管理</h2>
|
|
||||||
<p>创建借阅、处理归还、续借有效记录并查看逾期项目。</p>
|
|
||||||
<a class="button button-secondary" href="${pageContext.request.contextPath}/borrowing">打开</a>
|
|
||||||
</article>
|
|
||||||
|
|
||||||
<article class="workspace-card">
|
|
||||||
<h2>报表中心</h2>
|
|
||||||
<p>查看库存状况、借阅统计、逾期记录和热门图书。</p>
|
|
||||||
<a class="button button-secondary" href="${pageContext.request.contextPath}/reports">打开</a>
|
|
||||||
</article>
|
|
||||||
</c:if>
|
|
||||||
|
|
||||||
<article class="workspace-card">
|
|
||||||
<h2>馆藏检索</h2>
|
<h2>馆藏检索</h2>
|
||||||
<p>按书名、作者、分类或图书编号检索图书。</p>
|
<form class="dashboard-search-form" action="${pageContext.request.contextPath}/catalog" method="get">
|
||||||
<a class="button button-secondary" href="${pageContext.request.contextPath}/catalog">检索</a>
|
<div class="search-field">
|
||||||
|
<label for="dashIdentifier">图书编号</label>
|
||||||
|
<input id="dashIdentifier" name="identifier" type="text" placeholder="请输入图书编号">
|
||||||
|
</div>
|
||||||
|
<div class="search-field">
|
||||||
|
<label for="dashTitle">书名</label>
|
||||||
|
<input id="dashTitle" name="title" type="text" placeholder="请输入书名">
|
||||||
|
</div>
|
||||||
|
<div class="search-field">
|
||||||
|
<label for="dashAuthor">作者</label>
|
||||||
|
<input id="dashAuthor" name="author" type="text" placeholder="请输入作者">
|
||||||
|
</div>
|
||||||
|
<div class="search-field">
|
||||||
|
<label for="dashCategory">分类</label>
|
||||||
|
<select id="dashCategory" name="categoryId">
|
||||||
|
<option value="">请选择分类</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="dashboard-form-actions">
|
||||||
|
<button class="button button-primary" type="submit">搜索</button>
|
||||||
|
<a class="button button-secondary" href="${pageContext.request.contextPath}/catalog">重置</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<article class="workspace-card">
|
<article class="dashboard-panel ranking-panel">
|
||||||
<h2>读者中心</h2>
|
<div class="panel-heading">
|
||||||
<p>读者自助访问馆藏和借阅历史的入口。</p>
|
<h2>热门图书排行</h2>
|
||||||
<a class="button button-secondary" href="${pageContext.request.contextPath}/reader/home">打开</a>
|
<span>借阅次数TOP10</span>
|
||||||
|
</div>
|
||||||
|
<div class="rank-chart" aria-label="热门图书排行柱状图">
|
||||||
|
<div class="rank-item"><span class="rank-value">230</span><span class="rank-bar" style="--bar-height: 92%;"></span><small>活着</small></div>
|
||||||
|
<div class="rank-item"><span class="rank-value">198</span><span class="rank-bar" style="--bar-height: 79%;"></span><small>三体</small></div>
|
||||||
|
<div class="rank-item"><span class="rank-value">175</span><span class="rank-bar" style="--bar-height: 70%;"></span><small>百年孤独</small></div>
|
||||||
|
<div class="rank-item"><span class="rank-value">164</span><span class="rank-bar" style="--bar-height: 66%;"></span><small>围城</small></div>
|
||||||
|
<div class="rank-item"><span class="rank-value">150</span><span class="rank-bar" style="--bar-height: 60%;"></span><small>平凡的世界</small></div>
|
||||||
|
<div class="rank-item"><span class="rank-value">138</span><span class="rank-bar" style="--bar-height: 55%;"></span><small>解忧杂货店</small></div>
|
||||||
|
<div class="rank-item"><span class="rank-value">120</span><span class="rank-bar" style="--bar-height: 48%;"></span><small>红楼梦</small></div>
|
||||||
|
<div class="rank-item"><span class="rank-value">112</span><span class="rank-bar" style="--bar-height: 45%;"></span><small>白夜行</small></div>
|
||||||
|
<div class="rank-item"><span class="rank-value">98</span><span class="rank-bar" style="--bar-height: 39%;"></span><small>追风筝的人</small></div>
|
||||||
|
<div class="rank-item"><span class="rank-value">85</span><span class="rank-bar" style="--bar-height: 34%;"></span><small>小王子</small></div>
|
||||||
|
</div>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<c:if test="${sessionScope.userRole == 'reader'}">
|
|
||||||
<article class="workspace-card">
|
|
||||||
<h2>我的借阅历史</h2>
|
|
||||||
<p>查看您的在借、已还和逾期借阅记录。</p>
|
|
||||||
<a class="button button-secondary" href="${pageContext.request.contextPath}/reader/loans">打开</a>
|
|
||||||
</article>
|
|
||||||
</c:if>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<c:if test="${sessionScope.userRole == 'administrator' or sessionScope.userRole == 'librarian'}">
|
||||||
|
<section class="dashboard-table-grid" aria-label="业务表格">
|
||||||
|
<article class="dashboard-panel table-panel-compact table-panel-wide">
|
||||||
|
<h2>借阅流通 <span>最新记录</span></h2>
|
||||||
|
<div class="table-scroll">
|
||||||
|
<table class="data-table dashboard-data-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col">流水号</th>
|
||||||
|
<th scope="col">读者姓名</th>
|
||||||
|
<th scope="col">图书编号</th>
|
||||||
|
<th scope="col">书名</th>
|
||||||
|
<th scope="col">借阅日期</th>
|
||||||
|
<th scope="col">应还日期</th>
|
||||||
|
<th scope="col">状态</th>
|
||||||
|
<th scope="col">库存联动</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>L20240521001</td>
|
||||||
|
<td>张晓明</td>
|
||||||
|
<td>B001245</td>
|
||||||
|
<td>活着</td>
|
||||||
|
<td>2024-05-21</td>
|
||||||
|
<td>2024-06-04</td>
|
||||||
|
<td><span class="status-pill status-active">在借</span></td>
|
||||||
|
<td><span class="stock-plus">库存-1</span></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>L20240521002</td>
|
||||||
|
<td>李华</td>
|
||||||
|
<td>B001026</td>
|
||||||
|
<td>三体</td>
|
||||||
|
<td>2024-05-20</td>
|
||||||
|
<td>2024-06-03</td>
|
||||||
|
<td><span class="status-pill status-active">在借</span></td>
|
||||||
|
<td><span class="stock-plus">库存-1</span></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>L20240521003</td>
|
||||||
|
<td>王丽</td>
|
||||||
|
<td>B002031</td>
|
||||||
|
<td>百年孤独</td>
|
||||||
|
<td>2024-05-18</td>
|
||||||
|
<td>2024-06-01</td>
|
||||||
|
<td><span class="status-pill status-returned">已归还</span></td>
|
||||||
|
<td><span class="stock-return">库存+1</span></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>L20240521004</td>
|
||||||
|
<td>陈强</td>
|
||||||
|
<td>B001895</td>
|
||||||
|
<td>围城</td>
|
||||||
|
<td>2024-05-10</td>
|
||||||
|
<td>2024-05-24</td>
|
||||||
|
<td><span class="status-pill status-overdue">逾期</span></td>
|
||||||
|
<td><span class="stock-plus">库存-1</span></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>L20240521005</td>
|
||||||
|
<td>刘洋</td>
|
||||||
|
<td>B002119</td>
|
||||||
|
<td>解忧杂货店</td>
|
||||||
|
<td>2024-05-12</td>
|
||||||
|
<td>2024-05-26</td>
|
||||||
|
<td><span class="status-pill status-overdue">逾期</span></td>
|
||||||
|
<td><span class="stock-plus">库存-1</span></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="dashboard-panel table-panel-compact">
|
||||||
|
<h2>逾期列表 <span>待处理</span></h2>
|
||||||
|
<div class="table-scroll">
|
||||||
|
<table class="data-table dashboard-data-table overdue-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col">读者姓名</th>
|
||||||
|
<th scope="col">图书编号</th>
|
||||||
|
<th scope="col">书名</th>
|
||||||
|
<th scope="col">应还日期</th>
|
||||||
|
<th scope="col">逾期天数</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>陈强</td><td>B001895</td><td>围城</td><td>2024-05-24</td><td><span class="overdue-days">7天</span></td></tr>
|
||||||
|
<tr><td>赵敏</td><td>B001122</td><td>平凡的世界</td><td>2024-05-20</td><td><span class="overdue-days">11天</span></td></tr>
|
||||||
|
<tr><td>孙涛</td><td>B002003</td><td>红楼梦</td><td>2024-05-18</td><td><span class="overdue-days">13天</span></td></tr>
|
||||||
|
<tr><td>周雨</td><td>B000987</td><td>追风筝的人</td><td>2024-05-17</td><td><span class="overdue-days">14天</span></td></tr>
|
||||||
|
<tr><td>吴迪</td><td>B001776</td><td>白夜行</td><td>2024-05-15</td><td><span class="overdue-days">16天</span></td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="dashboard-panel table-panel-compact table-panel-wide">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<h2>图书管理 <span>馆藏列表</span></h2>
|
||||||
|
<a href="${pageContext.request.contextPath}/books">进入管理</a>
|
||||||
|
</div>
|
||||||
|
<div class="table-scroll">
|
||||||
|
<table class="data-table dashboard-data-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col">图书编号</th>
|
||||||
|
<th scope="col">书名</th>
|
||||||
|
<th scope="col">作者</th>
|
||||||
|
<th scope="col">分类</th>
|
||||||
|
<th scope="col">出版日期</th>
|
||||||
|
<th scope="col">库存状态</th>
|
||||||
|
<th scope="col">馆藏地</th>
|
||||||
|
<th scope="col">操作</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>B001245</td><td>活着</td><td>余华</td><td>文学 > 小说</td><td>2012-08-01</td>
|
||||||
|
<td><span class="status-pill status-available">可借(15)</span></td><td>二楼文学区</td>
|
||||||
|
<td><a class="text-link" href="${pageContext.request.contextPath}/books">管理</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>B001026</td><td>三体</td><td>刘慈欣</td><td>文学 > 科幻</td><td>2008-01-01</td>
|
||||||
|
<td><span class="status-pill status-available">可借(8)</span></td><td>三楼科幻区</td>
|
||||||
|
<td><a class="text-link" href="${pageContext.request.contextPath}/books">管理</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>B002031</td><td>百年孤独</td><td>加西亚·马尔克斯</td><td>文学 > 外国文学</td><td>2011-06-01</td>
|
||||||
|
<td><span class="status-pill status-available">可借(6)</span></td><td>二楼文学区</td>
|
||||||
|
<td><a class="text-link" href="${pageContext.request.contextPath}/books">管理</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>B001895</td><td>围城</td><td>钱钟书</td><td>文学 > 小说</td><td>2008-05-01</td>
|
||||||
|
<td><span class="status-pill status-available">可借(4)</span></td><td>二楼文学区</td>
|
||||||
|
<td><a class="text-link" href="${pageContext.request.contextPath}/books">管理</a></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>B002119</td><td>解忧杂货店</td><td>东野圭吾</td><td>文学 > 小说</td><td>2014-07-01</td>
|
||||||
|
<td><span class="status-pill status-available">可借(10)</span></td><td>二楼文学区</td>
|
||||||
|
<td><a class="text-link" href="${pageContext.request.contextPath}/books">管理</a></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<aside class="shortcut-grid" aria-label="快捷入口">
|
||||||
|
<a class="shortcut-card" href="${pageContext.request.contextPath}/readers">
|
||||||
|
<span class="shortcut-icon shortcut-blue" aria-hidden="true">者</span>
|
||||||
|
<strong>读者管理</strong>
|
||||||
|
<small>管理读者信息、证件办理与权限设置</small>
|
||||||
|
</a>
|
||||||
|
<a class="shortcut-card" href="${pageContext.request.contextPath}/reports">
|
||||||
|
<span class="shortcut-icon shortcut-green" aria-hidden="true">报</span>
|
||||||
|
<strong>报表中心</strong>
|
||||||
|
<small>生成各类统计报表,支持导出与分析</small>
|
||||||
|
</a>
|
||||||
|
<a class="shortcut-card" href="${pageContext.request.contextPath}/borrowing">
|
||||||
|
<span class="shortcut-icon shortcut-orange" aria-hidden="true">借</span>
|
||||||
|
<strong>借阅流通</strong>
|
||||||
|
<small>借书、还书、续借与逾期处理</small>
|
||||||
|
</a>
|
||||||
|
<c:if test="${sessionScope.userRole == 'administrator'}">
|
||||||
|
<a class="shortcut-card" href="${pageContext.request.contextPath}/admin/system-logs">
|
||||||
|
<span class="shortcut-icon shortcut-purple" aria-hidden="true">志</span>
|
||||||
|
<strong>系统日志</strong>
|
||||||
|
<small>系统操作日志与安全审计记录查询</small>
|
||||||
|
</a>
|
||||||
|
</c:if>
|
||||||
|
</aside>
|
||||||
|
</section>
|
||||||
|
</c:if>
|
||||||
|
|
||||||
|
<c:if test="${sessionScope.userRole == 'reader'}">
|
||||||
|
<section class="shortcut-grid reader-shortcut-grid" aria-label="读者快捷入口">
|
||||||
|
<a class="shortcut-card" href="${pageContext.request.contextPath}/reader/loans">
|
||||||
|
<span class="shortcut-icon shortcut-blue" aria-hidden="true">历</span>
|
||||||
|
<strong>我的借阅</strong>
|
||||||
|
<small>查看在借、已还、续借次数和逾期状态</small>
|
||||||
|
</a>
|
||||||
|
<a class="shortcut-card" href="${pageContext.request.contextPath}/catalog">
|
||||||
|
<span class="shortcut-icon shortcut-green" aria-hidden="true">搜</span>
|
||||||
|
<strong>馆藏检索</strong>
|
||||||
|
<small>按书名、作者、分类或图书编号查找馆藏</small>
|
||||||
|
</a>
|
||||||
|
</section>
|
||||||
|
</c:if>
|
||||||
</main>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>系统日志 - MZH 图书馆</title>
|
<title>系统日志 - MZH 图书馆</title>
|
||||||
<link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/app.css">
|
<link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/app.css?v=20260428-visual-shell">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<%@ include file="/WEB-INF/jsp/common/header.jspf" %>
|
<%@ include file="/WEB-INF/jsp/common/header.jspf" %>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>借阅历史 - MZH 图书馆</title>
|
<title>借阅历史 - MZH 图书馆</title>
|
||||||
<link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/app.css">
|
<link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/app.css?v=20260428-visual-shell">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<%@ include file="/WEB-INF/jsp/common/header.jspf" %>
|
<%@ include file="/WEB-INF/jsp/common/header.jspf" %>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title><c:out value="${formTitle}" /> - MZH 图书馆</title>
|
<title><c:out value="${formTitle}" /> - MZH 图书馆</title>
|
||||||
<link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/app.css">
|
<link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/app.css?v=20260428-visual-shell">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<%@ include file="/WEB-INF/jsp/common/header.jspf" %>
|
<%@ include file="/WEB-INF/jsp/common/header.jspf" %>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>读者管理 - MZH 图书馆</title>
|
<title>读者管理 - MZH 图书馆</title>
|
||||||
<link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/app.css">
|
<link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/app.css?v=20260428-visual-shell">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<%@ include file="/WEB-INF/jsp/common/header.jspf" %>
|
<%@ include file="/WEB-INF/jsp/common/header.jspf" %>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>报表 - MZH 图书馆</title>
|
<title>报表 - MZH 图书馆</title>
|
||||||
<link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/app.css">
|
<link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/app.css?v=20260428-visual-shell">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<%@ include file="/WEB-INF/jsp/common/header.jspf" %>
|
<%@ include file="/WEB-INF/jsp/common/header.jspf" %>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title><c:out value="${areaName}" /> - MZH 图书馆</title>
|
<title><c:out value="${areaName}" /> - MZH 图书馆</title>
|
||||||
<link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/app.css">
|
<link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/app.css?v=20260428-visual-shell">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<%@ include file="/WEB-INF/jsp/common/header.jspf" %>
|
<%@ include file="/WEB-INF/jsp/common/header.jspf" %>
|
||||||
|
|||||||
+1034
-220
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user