做读者档案、联系方式、借阅资格功能

This commit is contained in:
Zzzz
2026-04-27 20:32:46 +08:00
parent 78c649b6d1
commit eff118e6fa
25 changed files with 1997 additions and 5 deletions
@@ -0,0 +1,375 @@
package com.mzh.library.controller;
import com.mzh.library.dao.impl.JdbcReaderDao;
import com.mzh.library.entity.AuthenticatedUser;
import com.mzh.library.entity.Reader;
import com.mzh.library.entity.ReaderSearchCriteria;
import com.mzh.library.entity.ReaderStatus;
import com.mzh.library.service.ReaderService;
import com.mzh.library.service.ServiceResult;
import com.mzh.library.service.impl.ReaderServiceImpl;
import com.mzh.library.util.SessionAttributes;
import java.io.IOException;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
public class ReaderManagementServlet extends HttpServlet {
private static final String MANAGE_JSP = "/WEB-INF/jsp/readers/manage.jsp";
private static final String FORM_JSP = "/WEB-INF/jsp/readers/form.jsp";
private static final String UNAUTHORIZED_JSP = "/WEB-INF/jsp/auth/unauthorized.jsp";
private static final String FLASH_SUCCESS = "flashSuccess";
private static final String FLASH_ERROR = "flashError";
private ReaderService readerService;
@Override
public void init() {
this.readerService = new ReaderServiceImpl(new JdbcReaderDao());
}
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String path = request.getServletPath();
if ("/readers/new".equals(path)) {
renderForm(request, response, "Create reader", "/readers", defaultReader(), Collections.emptyMap(),
Collections.emptyMap(), null);
return;
}
if ("/readers/edit".equals(path)) {
showEditForm(request, response);
return;
}
if (!"/readers".equals(path)) {
response.sendError(HttpServletResponse.SC_NOT_FOUND);
return;
}
showManagementList(request, response);
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String path = request.getServletPath();
if ("/readers".equals(path)) {
createReader(request, response);
return;
}
if ("/readers/update".equals(path)) {
updateReader(request, response);
return;
}
if ("/readers/delete".equals(path)) {
deactivateReader(request, response);
return;
}
response.sendError(HttpServletResponse.SC_NOT_FOUND);
}
private void showManagementList(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
ReaderSearchCriteria criteria = searchCriteria(request);
request.setAttribute("criteria", criteria);
request.setAttribute("statuses", ReaderStatus.values());
applyFlash(request);
ServiceResult<List<Reader>> searchResult = readerService.searchReaders(criteria);
request.setAttribute("readers", searchResult.isSuccessful()
? searchResult.getData()
: Collections.emptyList());
if (!searchResult.isSuccessful()) {
request.setAttribute("errorMessage", searchResult.getMessage());
request.setAttribute("errors", searchResult.getErrors());
}
request.getRequestDispatcher(MANAGE_JSP).forward(request, response);
}
private void showEditForm(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
long id = requiredLong(request.getParameter("id"), -1L);
ServiceResult<Optional<Reader>> result = readerService.findReader(id);
if (!result.isSuccessful() || !result.getData().isPresent()) {
flashError(request, result.isSuccessful() ? "Reader profile was not found." : result.getMessage());
response.sendRedirect(request.getContextPath() + "/readers");
return;
}
renderForm(request, response, "Edit reader", "/readers/update", result.getData().get(),
Collections.emptyMap(), Collections.emptyMap(), null);
}
private void createReader(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
ReaderForm form = readReaderForm(request, false);
if (!form.getErrors().isEmpty()) {
renderForm(request, response, "Create reader", "/readers", form.getReader(), form.getValues(),
form.getErrors(), "Please correct the highlighted reader fields.");
return;
}
ServiceResult<Long> result = readerService.createReader(currentUser(request), form.getReader());
if (!result.isSuccessful()) {
handleFormFailure(request, response, "Create reader", "/readers", form, result);
return;
}
flashSuccess(request, result.getMessage());
response.sendRedirect(request.getContextPath() + "/readers");
}
private void updateReader(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
ReaderForm form = readReaderForm(request, true);
if (!form.getErrors().isEmpty()) {
renderForm(request, response, "Edit reader", "/readers/update", form.getReader(), form.getValues(),
form.getErrors(), "Please correct the highlighted reader fields.");
return;
}
ServiceResult<Void> result = readerService.updateReader(currentUser(request), form.getReader());
if (!result.isSuccessful()) {
handleFormFailure(request, response, "Edit reader", "/readers/update", form, result);
return;
}
flashSuccess(request, result.getMessage());
response.sendRedirect(request.getContextPath() + "/readers");
}
private void deactivateReader(HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
long id = requiredLong(request.getParameter("id"), -1L);
ServiceResult<Void> result = readerService.deactivateReader(currentUser(request), id);
if (isPermissionDenied(result)) {
forwardDenied(request, response, result.getMessage());
return;
}
if (result.isSuccessful()) {
flashSuccess(request, result.getMessage());
} else {
flashError(request, result.getMessage());
}
response.sendRedirect(request.getContextPath() + "/readers");
}
private void handleFormFailure(HttpServletRequest request, HttpServletResponse response, String title,
String action, ReaderForm form, ServiceResult<?> result)
throws ServletException, IOException {
if (isPermissionDenied(result)) {
forwardDenied(request, response, result.getMessage());
return;
}
renderForm(request, response, title, action, form.getReader(), form.getValues(), result.getErrors(),
result.getMessage());
}
private void renderForm(HttpServletRequest request, HttpServletResponse response, String title, String action,
Reader reader, Map<String, String> formValues, Map<String, String> errors,
String errorMessage)
throws ServletException, IOException {
request.setAttribute("statuses", ReaderStatus.values());
request.setAttribute("formTitle", title);
request.setAttribute("formAction", action);
request.setAttribute("reader", reader);
request.setAttribute("formValues", formValues);
request.setAttribute("errors", errors);
if (errorMessage != null && !errorMessage.isEmpty()) {
request.setAttribute("errorMessage", errorMessage);
}
request.getRequestDispatcher(FORM_JSP).forward(request, response);
}
private ReaderForm readReaderForm(HttpServletRequest request, boolean requireId) {
Map<String, String> values = formValues(request);
Map<String, String> errors = new LinkedHashMap<>();
Reader reader = new Reader();
if (requireId) {
reader.setId(parseLong(values.get("id"), "id", "Select a valid reader.", errors));
}
reader.setIdentifier(values.get("identifier"));
reader.setUserId(optionalPositiveLong(values.get("userId"), "userId",
"Enter a valid linked account ID.", errors));
reader.setFullName(values.get("fullName"));
reader.setPhone(values.get("phone"));
reader.setEmail(values.get("email"));
reader.setMaxBorrowCount(parseInt(values.get("maxBorrowCount"), "maxBorrowCount",
"Enter a valid max borrow count.", errors));
try {
reader.setStatus(ReaderStatus.fromCode(values.get("status")));
} catch (IllegalArgumentException ex) {
errors.put("status", "Select a status.");
}
return new ReaderForm(reader, values, errors);
}
private Map<String, String> formValues(HttpServletRequest request) {
Map<String, String> values = new LinkedHashMap<>();
values.put("id", trim(request.getParameter("id")));
values.put("identifier", trim(request.getParameter("identifier")));
values.put("userId", trim(request.getParameter("userId")));
values.put("fullName", trim(request.getParameter("fullName")));
values.put("phone", trim(request.getParameter("phone")));
values.put("email", trim(request.getParameter("email")));
values.put("status", trim(request.getParameter("status")));
values.put("maxBorrowCount", trim(request.getParameter("maxBorrowCount")));
return values;
}
private ReaderSearchCriteria searchCriteria(HttpServletRequest request) {
return new ReaderSearchCriteria(
request.getParameter("identifier"),
request.getParameter("name"),
request.getParameter("contact"),
request.getParameter("status")
);
}
private Reader defaultReader() {
Reader reader = new Reader();
reader.setStatus(ReaderStatus.ACTIVE);
reader.setMaxBorrowCount(5);
return reader;
}
private Long optionalPositiveLong(String value, String field, String message, Map<String, String> errors) {
String trimmed = trim(value);
if (trimmed.isEmpty()) {
return null;
}
try {
long parsed = Long.parseLong(trimmed);
if (parsed <= 0) {
errors.put(field, message);
return null;
}
return parsed;
} catch (NumberFormatException ex) {
errors.put(field, message);
return null;
}
}
private long parseLong(String value, String field, String message, Map<String, String> errors) {
String trimmed = trim(value);
if (trimmed.isEmpty()) {
errors.put(field, message);
return 0L;
}
try {
long parsed = Long.parseLong(trimmed);
if (parsed <= 0) {
errors.put(field, message);
}
return parsed;
} catch (NumberFormatException ex) {
errors.put(field, message);
return 0L;
}
}
private int parseInt(String value, String field, String message, Map<String, String> errors) {
String trimmed = trim(value);
if (trimmed.isEmpty()) {
errors.put(field, message);
return -1;
}
try {
return Integer.parseInt(trimmed);
} catch (NumberFormatException ex) {
errors.put(field, message);
return -1;
}
}
private long requiredLong(String value, long fallback) {
try {
long parsed = Long.parseLong(trim(value));
return parsed > 0 ? parsed : fallback;
} catch (NumberFormatException ex) {
return fallback;
}
}
private boolean isPermissionDenied(ServiceResult<?> result) {
return !result.isSuccessful() && "You do not have permission to manage readers.".equals(result.getMessage());
}
private void forwardDenied(HttpServletRequest request, HttpServletResponse response, String message)
throws ServletException, IOException {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
request.setAttribute("errorMessage", message);
request.getRequestDispatcher(UNAUTHORIZED_JSP).forward(request, response);
}
private AuthenticatedUser currentUser(HttpServletRequest request) {
HttpSession session = request.getSession(false);
Object value = session == null ? null : session.getAttribute(SessionAttributes.AUTHENTICATED_USER);
return value instanceof AuthenticatedUser ? (AuthenticatedUser) value : null;
}
private void applyFlash(HttpServletRequest request) {
HttpSession session = request.getSession(false);
if (session == null) {
return;
}
moveFlash(session, request, FLASH_SUCCESS, "successMessage");
moveFlash(session, request, FLASH_ERROR, "errorMessage");
}
private void moveFlash(HttpSession session, HttpServletRequest request, String sessionKey, String requestKey) {
Object value = session.getAttribute(sessionKey);
if (value != null) {
request.setAttribute(requestKey, value);
session.removeAttribute(sessionKey);
}
}
private void flashSuccess(HttpServletRequest request, String message) {
request.getSession().setAttribute(FLASH_SUCCESS, message);
}
private void flashError(HttpServletRequest request, String message) {
request.getSession().setAttribute(FLASH_ERROR, message);
}
private String trim(String value) {
return value == null ? "" : value.trim();
}
private static final class ReaderForm {
private final Reader reader;
private final Map<String, String> values;
private final Map<String, String> errors;
private ReaderForm(Reader reader, Map<String, String> values, Map<String, String> errors) {
this.reader = reader;
this.values = values;
this.errors = errors;
}
private Reader getReader() {
return reader;
}
private Map<String, String> getValues() {
return values;
}
private Map<String, String> getErrors() {
return errors;
}
}
}
@@ -0,0 +1,23 @@
package com.mzh.library.dao;
import com.mzh.library.entity.Reader;
import com.mzh.library.entity.ReaderSearchCriteria;
import java.util.List;
import java.util.Optional;
public interface ReaderDao {
List<Reader> search(ReaderSearchCriteria criteria);
Optional<Reader> findById(long id);
Optional<Reader> findByIdentifier(String identifier);
Optional<Reader> findByUserId(long userId);
long create(Reader reader);
boolean update(Reader reader);
boolean deactivate(long id);
}
@@ -0,0 +1,224 @@
package com.mzh.library.dao.impl;
import com.mzh.library.dao.ReaderDao;
import com.mzh.library.entity.Reader;
import com.mzh.library.entity.ReaderSearchCriteria;
import com.mzh.library.entity.ReaderStatus;
import com.mzh.library.exception.DaoException;
import com.mzh.library.util.JdbcUtil;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.sql.Timestamp;
import java.sql.Types;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
public class JdbcReaderDao implements ReaderDao {
private static final String READER_COLUMNS = ""
+ "r.id, r.reader_identifier, r.user_id, u.username, r.full_name, r.phone, r.email, "
+ "r.status, r.max_borrow_count, r.created_at, r.updated_at ";
private static final String READER_FROM = ""
+ "FROM readers r "
+ "LEFT JOIN users u ON u.id = r.user_id ";
private static final String FIND_BY_ID = "SELECT " + READER_COLUMNS + READER_FROM + "WHERE r.id = ?";
private static final String FIND_BY_IDENTIFIER = "SELECT " + READER_COLUMNS + READER_FROM
+ "WHERE r.reader_identifier = ?";
private static final String FIND_BY_USER_ID = "SELECT " + READER_COLUMNS + READER_FROM
+ "WHERE r.user_id = ?";
private static final String CREATE = ""
+ "INSERT INTO readers "
+ "(reader_identifier, user_id, full_name, phone, email, status, max_borrow_count) "
+ "VALUES (?, ?, ?, ?, ?, ?, ?)";
private static final String UPDATE = ""
+ "UPDATE readers "
+ "SET reader_identifier = ?, user_id = ?, full_name = ?, phone = ?, email = ?, "
+ "status = ?, max_borrow_count = ? "
+ "WHERE id = ?";
private static final String DEACTIVATE = "UPDATE readers SET status = ? WHERE id = ?";
@Override
public List<Reader> search(ReaderSearchCriteria criteria) {
List<Object> parameters = new ArrayList<>();
StringBuilder sql = new StringBuilder("SELECT ")
.append(READER_COLUMNS)
.append(READER_FROM)
.append("WHERE 1 = 1 ");
appendLike(sql, parameters, "r.reader_identifier", criteria.getIdentifier());
appendLike(sql, parameters, "r.full_name", criteria.getName());
appendContact(sql, parameters, criteria.getContact());
if (criteria.getStatusCode() != null && !criteria.getStatusCode().isEmpty()) {
sql.append("AND r.status = ? ");
parameters.add(criteria.getStatusCode());
}
sql.append("ORDER BY r.full_name, r.reader_identifier");
try (Connection connection = JdbcUtil.getConnection();
PreparedStatement statement = connection.prepareStatement(sql.toString())) {
bind(statement, parameters);
try (ResultSet resultSet = statement.executeQuery()) {
List<Reader> readers = new ArrayList<>();
while (resultSet.next()) {
readers.add(mapReader(resultSet));
}
return readers;
}
} catch (SQLException | IllegalArgumentException ex) {
throw new DaoException("Unable to search readers", ex);
}
}
@Override
public Optional<Reader> findById(long id) {
try (Connection connection = JdbcUtil.getConnection();
PreparedStatement statement = connection.prepareStatement(FIND_BY_ID)) {
statement.setLong(1, id);
try (ResultSet resultSet = statement.executeQuery()) {
return resultSet.next() ? Optional.of(mapReader(resultSet)) : Optional.empty();
}
} catch (SQLException | IllegalArgumentException ex) {
throw new DaoException("Unable to load reader by id", ex);
}
}
@Override
public Optional<Reader> findByIdentifier(String identifier) {
try (Connection connection = JdbcUtil.getConnection();
PreparedStatement statement = connection.prepareStatement(FIND_BY_IDENTIFIER)) {
statement.setString(1, identifier);
try (ResultSet resultSet = statement.executeQuery()) {
return resultSet.next() ? Optional.of(mapReader(resultSet)) : Optional.empty();
}
} catch (SQLException | IllegalArgumentException ex) {
throw new DaoException("Unable to load reader by identifier", ex);
}
}
@Override
public Optional<Reader> findByUserId(long userId) {
try (Connection connection = JdbcUtil.getConnection();
PreparedStatement statement = connection.prepareStatement(FIND_BY_USER_ID)) {
statement.setLong(1, userId);
try (ResultSet resultSet = statement.executeQuery()) {
return resultSet.next() ? Optional.of(mapReader(resultSet)) : Optional.empty();
}
} catch (SQLException | IllegalArgumentException ex) {
throw new DaoException("Unable to load reader by user id", ex);
}
}
@Override
public long create(Reader reader) {
try (Connection connection = JdbcUtil.getConnection();
PreparedStatement statement = connection.prepareStatement(CREATE, Statement.RETURN_GENERATED_KEYS)) {
bindReader(statement, reader);
statement.executeUpdate();
try (ResultSet generatedKeys = statement.getGeneratedKeys()) {
if (generatedKeys.next()) {
return generatedKeys.getLong(1);
}
}
throw new DaoException("Unable to read generated reader id", null);
} catch (SQLException ex) {
throw new DaoException("Unable to create reader", ex);
}
}
@Override
public boolean update(Reader reader) {
try (Connection connection = JdbcUtil.getConnection();
PreparedStatement statement = connection.prepareStatement(UPDATE)) {
bindReader(statement, reader);
statement.setLong(8, reader.getId());
return statement.executeUpdate() == 1;
} catch (SQLException ex) {
throw new DaoException("Unable to update reader", ex);
}
}
@Override
public boolean deactivate(long id) {
try (Connection connection = JdbcUtil.getConnection();
PreparedStatement statement = connection.prepareStatement(DEACTIVATE)) {
statement.setString(1, ReaderStatus.INACTIVE.getCode());
statement.setLong(2, id);
return statement.executeUpdate() == 1;
} catch (SQLException ex) {
throw new DaoException("Unable to deactivate reader", ex);
}
}
private void appendLike(StringBuilder sql, List<Object> parameters, String column, String value) {
if (value == null || value.trim().isEmpty()) {
return;
}
sql.append("AND ").append(column).append(" LIKE ? ");
parameters.add("%" + value.trim() + "%");
}
private void appendContact(StringBuilder sql, List<Object> parameters, String value) {
if (value == null || value.trim().isEmpty()) {
return;
}
sql.append("AND (r.phone LIKE ? OR r.email LIKE ?) ");
String filter = "%" + value.trim() + "%";
parameters.add(filter);
parameters.add(filter);
}
private void bind(PreparedStatement statement, List<Object> parameters) throws SQLException {
for (int i = 0; i < parameters.size(); i++) {
Object value = parameters.get(i);
statement.setString(i + 1, value.toString());
}
}
private void bindReader(PreparedStatement statement, Reader reader) throws SQLException {
statement.setString(1, reader.getIdentifier());
if (reader.getUserId() == null) {
statement.setNull(2, Types.BIGINT);
} else {
statement.setLong(2, reader.getUserId());
}
statement.setString(3, reader.getFullName());
statement.setString(4, reader.getPhone());
statement.setString(5, reader.getEmail());
statement.setString(6, reader.getStatus().getCode());
statement.setInt(7, reader.getMaxBorrowCount());
}
private Reader mapReader(ResultSet resultSet) throws SQLException {
Reader reader = new Reader();
reader.setId(resultSet.getLong("id"));
reader.setIdentifier(resultSet.getString("reader_identifier"));
long userId = resultSet.getLong("user_id");
reader.setUserId(resultSet.wasNull() ? null : userId);
reader.setUsername(resultSet.getString("username"));
reader.setFullName(resultSet.getString("full_name"));
reader.setPhone(resultSet.getString("phone"));
reader.setEmail(resultSet.getString("email"));
reader.setStatus(ReaderStatus.fromCode(resultSet.getString("status")));
reader.setMaxBorrowCount(resultSet.getInt("max_borrow_count"));
reader.setCreatedAt(toLocalDateTime(resultSet.getTimestamp("created_at")));
reader.setUpdatedAt(toLocalDateTime(resultSet.getTimestamp("updated_at")));
return reader;
}
private LocalDateTime toLocalDateTime(Timestamp timestamp) {
return timestamp == null ? null : timestamp.toLocalDateTime();
}
}
@@ -0,0 +1,105 @@
package com.mzh.library.entity;
import java.time.LocalDateTime;
public class Reader {
private long id;
private String identifier;
private Long userId;
private String username;
private String fullName;
private String phone;
private String email;
private ReaderStatus status;
private int maxBorrowCount;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getIdentifier() {
return identifier;
}
public void setIdentifier(String identifier) {
this.identifier = identifier;
}
public Long getUserId() {
return userId;
}
public void setUserId(Long userId) {
this.userId = userId;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getFullName() {
return fullName;
}
public void setFullName(String fullName) {
this.fullName = fullName;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public ReaderStatus getStatus() {
return status;
}
public void setStatus(ReaderStatus status) {
this.status = status;
}
public int getMaxBorrowCount() {
return maxBorrowCount;
}
public void setMaxBorrowCount(int maxBorrowCount) {
this.maxBorrowCount = maxBorrowCount;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(LocalDateTime createdAt) {
this.createdAt = createdAt;
}
public LocalDateTime getUpdatedAt() {
return updatedAt;
}
public void setUpdatedAt(LocalDateTime updatedAt) {
this.updatedAt = updatedAt;
}
}
@@ -0,0 +1,54 @@
package com.mzh.library.entity;
public class ReaderSearchCriteria {
private String identifier;
private String name;
private String contact;
private String statusCode;
public ReaderSearchCriteria() {
}
public ReaderSearchCriteria(String identifier, String name, String contact, String statusCode) {
this.identifier = trim(identifier);
this.name = trim(name);
this.contact = trim(contact);
this.statusCode = trim(statusCode);
}
public String getIdentifier() {
return identifier;
}
public void setIdentifier(String identifier) {
this.identifier = trim(identifier);
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = trim(name);
}
public String getContact() {
return contact;
}
public void setContact(String contact) {
this.contact = trim(contact);
}
public String getStatusCode() {
return statusCode;
}
public void setStatusCode(String statusCode) {
this.statusCode = trim(statusCode);
}
private String trim(String value) {
return value == null ? "" : value.trim();
}
}
@@ -0,0 +1,40 @@
package com.mzh.library.entity;
import java.util.Locale;
public enum ReaderStatus {
ACTIVE("active", "Active"),
SUSPENDED("suspended", "Suspended"),
INACTIVE("inactive", "Inactive");
private final String code;
private final String displayName;
ReaderStatus(String code, String displayName) {
this.code = code;
this.displayName = displayName;
}
public String getCode() {
return code;
}
public String getDisplayName() {
return displayName;
}
public static ReaderStatus fromCode(String code) {
if (code == null || code.trim().isEmpty()) {
throw new IllegalArgumentException("Reader status is required");
}
String normalized = code.trim().toLowerCase(Locale.ROOT);
for (ReaderStatus status : values()) {
if (status.code.equals(normalized)) {
return status;
}
}
throw new IllegalArgumentException("Unsupported reader status: " + code);
}
}
@@ -26,6 +26,7 @@ public class AuthorizationFilter implements Filter {
private static final String UNAUTHORIZED_JSP = "/WEB-INF/jsp/auth/unauthorized.jsp";
private static final List<PathRule> RULES = Arrays.asList(
new PathRule("/books", Permission.MANAGE_BOOKS),
new PathRule("/readers", Permission.MANAGE_READERS),
new PathRule("/catalog", Permission.VIEW_CATALOG),
new PathRule("/admin", Permission.MANAGE_USERS),
new PathRule("/librarian", Permission.MANAGE_BORROWING),
@@ -0,0 +1,20 @@
package com.mzh.library.service;
import com.mzh.library.entity.AuthenticatedUser;
import com.mzh.library.entity.Reader;
import com.mzh.library.entity.ReaderSearchCriteria;
import java.util.List;
import java.util.Optional;
public interface ReaderService {
ServiceResult<List<Reader>> searchReaders(ReaderSearchCriteria criteria);
ServiceResult<Optional<Reader>> findReader(long id);
ServiceResult<Long> createReader(AuthenticatedUser actor, Reader reader);
ServiceResult<Void> updateReader(AuthenticatedUser actor, Reader reader);
ServiceResult<Void> deactivateReader(AuthenticatedUser actor, long id);
}
@@ -0,0 +1,245 @@
package com.mzh.library.service.impl;
import com.mzh.library.dao.ReaderDao;
import com.mzh.library.entity.AuthenticatedUser;
import com.mzh.library.entity.Permission;
import com.mzh.library.entity.Reader;
import com.mzh.library.entity.ReaderSearchCriteria;
import com.mzh.library.entity.ReaderStatus;
import com.mzh.library.exception.DaoException;
import com.mzh.library.service.PermissionPolicy;
import com.mzh.library.service.ReaderService;
import com.mzh.library.service.ServiceResult;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.regex.Pattern;
import java.util.logging.Level;
import java.util.logging.Logger;
public class ReaderServiceImpl implements ReaderService {
private static final Logger LOGGER = Logger.getLogger(ReaderServiceImpl.class.getName());
private static final String UNAVAILABLE_MESSAGE =
"Reader service is temporarily unavailable. Please try again later.";
private static final String VALIDATION_MESSAGE = "Please correct the highlighted reader fields.";
private static final String SEARCH_VALIDATION_MESSAGE = "Please correct the reader search filters.";
private static final String DENIED_MESSAGE = "You do not have permission to manage readers.";
private static final int MAX_BORROW_LIMIT = 50;
private static final Pattern PHONE_PATTERN = Pattern.compile("(?=.*\\d)[0-9+()\\-\\s]{6,32}");
private static final Pattern EMAIL_PATTERN = Pattern.compile("^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$");
private final ReaderDao readerDao;
private final PermissionPolicy permissionPolicy;
public ReaderServiceImpl(ReaderDao readerDao) {
this(readerDao, new PermissionPolicy());
}
public ReaderServiceImpl(ReaderDao readerDao, PermissionPolicy permissionPolicy) {
this.readerDao = readerDao;
this.permissionPolicy = permissionPolicy;
}
@Override
public ServiceResult<List<Reader>> searchReaders(ReaderSearchCriteria criteria) {
ReaderSearchCriteria normalized = criteria == null ? new ReaderSearchCriteria() : criteria;
Map<String, String> errors = validateSearch(normalized);
if (!errors.isEmpty()) {
return ServiceResult.validationFailure(SEARCH_VALIDATION_MESSAGE, errors);
}
try {
return ServiceResult.success(readerDao.search(normalized));
} catch (DaoException ex) {
LOGGER.log(Level.SEVERE, "Unable to search readers", ex);
return ServiceResult.failure(UNAVAILABLE_MESSAGE);
}
}
@Override
public ServiceResult<Optional<Reader>> findReader(long id) {
if (id <= 0) {
return ServiceResult.failure("Select a valid reader.");
}
try {
return ServiceResult.success(readerDao.findById(id));
} catch (DaoException ex) {
LOGGER.log(Level.SEVERE, "Unable to load reader id=" + id, ex);
return ServiceResult.failure(UNAVAILABLE_MESSAGE);
}
}
@Override
public ServiceResult<Long> createReader(AuthenticatedUser actor, Reader reader) {
if (!canManageReaders(actor)) {
return ServiceResult.failure(DENIED_MESSAGE);
}
normalize(reader);
Map<String, String> errors = validate(reader, false);
if (!errors.isEmpty()) {
return ServiceResult.validationFailure(VALIDATION_MESSAGE, errors);
}
try {
if (readerDao.findByIdentifier(reader.getIdentifier()).isPresent()) {
errors.put("identifier", "Reader identifier is already in use.");
return ServiceResult.validationFailure(VALIDATION_MESSAGE, errors);
}
if (reader.getUserId() != null && readerDao.findByUserId(reader.getUserId()).isPresent()) {
errors.put("userId", "Linked account is already assigned to a reader profile.");
return ServiceResult.validationFailure(VALIDATION_MESSAGE, errors);
}
long id = readerDao.create(reader);
LOGGER.info("Created reader id=" + id + " actorId=" + actor.getId());
return ServiceResult.success(id, "Reader profile created.");
} catch (DaoException ex) {
LOGGER.log(Level.SEVERE, "Unable to create reader actorId=" + actor.getId(), ex);
return ServiceResult.failure(UNAVAILABLE_MESSAGE);
}
}
@Override
public ServiceResult<Void> updateReader(AuthenticatedUser actor, Reader reader) {
if (!canManageReaders(actor)) {
return ServiceResult.failure(DENIED_MESSAGE);
}
normalize(reader);
Map<String, String> errors = validate(reader, true);
if (!errors.isEmpty()) {
return ServiceResult.validationFailure(VALIDATION_MESSAGE, errors);
}
try {
Optional<Reader> existingWithIdentifier = readerDao.findByIdentifier(reader.getIdentifier());
if (existingWithIdentifier.isPresent() && existingWithIdentifier.get().getId() != reader.getId()) {
errors.put("identifier", "Reader identifier is already in use.");
return ServiceResult.validationFailure(VALIDATION_MESSAGE, errors);
}
if (reader.getUserId() != null) {
Optional<Reader> existingWithUser = readerDao.findByUserId(reader.getUserId());
if (existingWithUser.isPresent() && existingWithUser.get().getId() != reader.getId()) {
errors.put("userId", "Linked account is already assigned to a reader profile.");
return ServiceResult.validationFailure(VALIDATION_MESSAGE, errors);
}
}
if (!readerDao.update(reader)) {
return ServiceResult.failure("Reader profile was not found.");
}
LOGGER.info("Updated reader id=" + reader.getId() + " actorId=" + actor.getId());
return ServiceResult.success(null, "Reader profile updated.");
} catch (DaoException ex) {
LOGGER.log(Level.SEVERE, "Unable to update reader id=" + reader.getId() + " actorId=" + actor.getId(), ex);
return ServiceResult.failure(UNAVAILABLE_MESSAGE);
}
}
@Override
public ServiceResult<Void> deactivateReader(AuthenticatedUser actor, long id) {
if (!canManageReaders(actor)) {
return ServiceResult.failure(DENIED_MESSAGE);
}
if (id <= 0) {
return ServiceResult.failure("Select a valid reader.");
}
try {
if (!readerDao.deactivate(id)) {
return ServiceResult.failure("Reader profile was not found.");
}
LOGGER.info("Deactivated reader id=" + id + " actorId=" + actor.getId());
return ServiceResult.success(null, "Reader profile deactivated.");
} catch (DaoException ex) {
LOGGER.log(Level.SEVERE, "Unable to deactivate reader id=" + id + " actorId=" + actor.getId(), ex);
return ServiceResult.failure(UNAVAILABLE_MESSAGE);
}
}
private boolean canManageReaders(AuthenticatedUser actor) {
return actor != null && permissionPolicy.allows(actor.getRole(), Permission.MANAGE_READERS);
}
private void normalize(Reader reader) {
if (reader == null) {
return;
}
reader.setIdentifier(trim(reader.getIdentifier()));
reader.setFullName(trim(reader.getFullName()));
reader.setPhone(trim(reader.getPhone()));
reader.setEmail(trim(reader.getEmail()));
}
private Map<String, String> validateSearch(ReaderSearchCriteria criteria) {
Map<String, String> errors = new LinkedHashMap<>();
if (criteria.getStatusCode() != null && !criteria.getStatusCode().isEmpty()) {
try {
criteria.setStatusCode(ReaderStatus.fromCode(criteria.getStatusCode()).getCode());
} catch (IllegalArgumentException ex) {
errors.put("status", "Select a valid status.");
}
}
return errors;
}
private Map<String, String> validate(Reader reader, boolean requireId) {
Map<String, String> errors = new LinkedHashMap<>();
if (reader == null) {
errors.put("reader", "Reader details are required.");
return errors;
}
if (requireId && reader.getId() <= 0) {
errors.put("id", "Select a valid reader.");
}
requireLength(errors, "identifier", reader.getIdentifier(), "Reader identifier", 64);
requireLength(errors, "fullName", reader.getFullName(), "Full name", 100);
if (reader.getUserId() != null && reader.getUserId() <= 0) {
errors.put("userId", "Linked account ID must be positive.");
}
validateContact(errors, reader);
if (reader.getStatus() == null) {
errors.put("status", "Select a status.");
}
if (reader.getMaxBorrowCount() < 1 || reader.getMaxBorrowCount() > MAX_BORROW_LIMIT) {
errors.put("maxBorrowCount", "Max borrow count must be between 1 and " + MAX_BORROW_LIMIT + ".");
}
return errors;
}
private void validateContact(Map<String, String> errors, Reader reader) {
String phone = reader.getPhone();
String email = reader.getEmail();
if ((phone == null || phone.isEmpty()) && (email == null || email.isEmpty())) {
errors.put("phone", "Phone or email is required.");
return;
}
if (phone != null && !phone.isEmpty() && !PHONE_PATTERN.matcher(phone).matches()) {
errors.put("phone", "Phone must include a digit and use 6 to 32 digits or common phone symbols.");
}
if (email != null && !email.isEmpty() && !EMAIL_PATTERN.matcher(email).matches()) {
errors.put("email", "Email must be a valid address.");
}
}
private void requireLength(Map<String, String> errors, String field, String value, String label, int maxLength) {
if (value == null || value.isEmpty()) {
errors.put(field, label + " is required.");
return;
}
if (value.length() > maxLength) {
errors.put(field, label + " must be " + maxLength + " characters or fewer.");
}
}
private String trim(String value) {
return value == null ? "" : value.trim();
}
}
+37
View File
@@ -57,6 +57,31 @@ CREATE TABLE IF NOT EXISTS system_logs (
KEY idx_system_logs_created_at (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS readers (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
reader_identifier VARCHAR(64) NOT NULL,
user_id BIGINT NULL,
full_name VARCHAR(100) NOT NULL,
phone VARCHAR(32) NULL,
email VARCHAR(120) NULL,
status VARCHAR(32) NOT NULL DEFAULT 'active',
max_borrow_count INT NOT NULL DEFAULT 5,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uk_readers_identifier (reader_identifier),
UNIQUE KEY uk_readers_user_id (user_id),
KEY idx_readers_full_name (full_name),
KEY idx_readers_phone (phone),
KEY idx_readers_email (email),
KEY idx_readers_status (status),
CONSTRAINT fk_readers_user
FOREIGN KEY (user_id) REFERENCES users (id),
CONSTRAINT chk_readers_status
CHECK (status IN ('active', 'suspended', 'inactive')),
CONSTRAINT chk_readers_max_borrow_count
CHECK (max_borrow_count BETWEEN 1 AND 50)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS book_categories (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(96) NOT NULL,
@@ -137,6 +162,18 @@ INSERT IGNORE INTO users (username, password_hash, display_name, role_code, acti
('librarian', 'pbkdf2_sha256$60000$bXpoLWxpYnJhcmlhbi1kZW1vLXNhbHQ=$StIdJGDRIiF4aCr+qKuwvob5sL3+6j1caF2sQNqFi78=', 'Library Staff', 'librarian', 1),
('reader', 'pbkdf2_sha256$60000$bXpoLXJlYWRlci1kZW1vLXNhbHQ=$iaiZPGhaIQ+2R2o9UQRj6wsrmYSJ4efqS3jCzM/XU7g=', 'Demo Reader', 'reader', 1);
INSERT INTO readers (reader_identifier, user_id, full_name, phone, email, status, max_borrow_count) VALUES
('RD-0001', (SELECT id FROM users WHERE username = 'reader'), 'Demo Reader', '13800000000',
'reader@example.com', 'active', 5),
('RD-0002', NULL, 'Suspended Reader', '13900000000', 'suspended.reader@example.com', 'suspended', 3)
ON DUPLICATE KEY UPDATE
user_id = VALUES(user_id),
full_name = VALUES(full_name),
phone = VALUES(phone),
email = VALUES(email),
status = VALUES(status),
max_borrow_count = VALUES(max_borrow_count);
INSERT INTO book_categories (name, description) VALUES
('Computer Science', 'Programming, software engineering, and systems books'),
('Literature', 'Classic and modern literature'),
@@ -11,6 +11,7 @@
<c:if test="${sessionScope.userRole == 'administrator' or sessionScope.userRole == 'librarian'}">
<a href="${pageContext.request.contextPath}/librarian/home">Librarian</a>
<a href="${pageContext.request.contextPath}/books">Books</a>
<a href="${pageContext.request.contextPath}/readers">Readers</a>
</c:if>
<a href="${pageContext.request.contextPath}/reader/home">Reader</a>
<span class="user-pill">
@@ -40,6 +40,12 @@
<p>Create, update, delete, and review book inventory records.</p>
<a class="button button-secondary" href="${pageContext.request.contextPath}/books">Open</a>
</article>
<article class="workspace-card">
<h2>Reader Management</h2>
<p>Create, update, deactivate, and review reader eligibility records.</p>
<a class="button button-secondary" href="${pageContext.request.contextPath}/readers">Open</a>
</article>
</c:if>
<article class="workspace-card">
@@ -0,0 +1,113 @@
<%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><c:out value="${formTitle}" /> - MZH Library</title>
<link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/app.css">
</head>
<body>
<%@ include file="/WEB-INF/jsp/common/header.jspf" %>
<main class="page-shell">
<section class="form-panel" aria-labelledby="reader-form-title">
<p class="eyebrow">Reader Management</p>
<h1 id="reader-form-title"><c:out value="${formTitle}" /></h1>
<c:if test="${not empty errorMessage}">
<div class="message message-error" role="alert">
<c:out value="${errorMessage}" />
</div>
</c:if>
<c:set var="hasFormValues" value="${not empty formValues}" />
<c:set var="identifierValue" value="${hasFormValues ? formValues.identifier : reader.identifier}" />
<c:set var="userIdValue" value="${hasFormValues ? formValues.userId : reader.userId}" />
<c:set var="fullNameValue" value="${hasFormValues ? formValues.fullName : reader.fullName}" />
<c:set var="phoneValue" value="${hasFormValues ? formValues.phone : reader.phone}" />
<c:set var="emailValue" value="${hasFormValues ? formValues.email : reader.email}" />
<c:set var="statusValue" value="${hasFormValues ? formValues.status : reader.status.code}" />
<c:set var="maxBorrowCountValue" value="${hasFormValues ? formValues.maxBorrowCount : reader.maxBorrowCount}" />
<form class="reader-form" action="${pageContext.request.contextPath}${formAction}" method="post" novalidate>
<c:if test="${reader.id > 0}">
<input type="hidden" name="id" value="${reader.id}">
</c:if>
<div class="form-grid">
<div class="form-field">
<label for="identifier">Reader ID</label>
<input id="identifier" name="identifier" type="text" value="${fn:escapeXml(identifierValue)}" required>
<c:if test="${not empty errors.identifier}">
<span class="field-error"><c:out value="${errors.identifier}" /></span>
</c:if>
</div>
<div class="form-field">
<label for="fullName">Full name</label>
<input id="fullName" name="fullName" type="text" value="${fn:escapeXml(fullNameValue)}" required>
<c:if test="${not empty errors.fullName}">
<span class="field-error"><c:out value="${errors.fullName}" /></span>
</c:if>
</div>
<div class="form-field">
<label for="phone">Phone</label>
<input id="phone" name="phone" type="tel" value="${fn:escapeXml(phoneValue)}">
<c:if test="${not empty errors.phone}">
<span class="field-error"><c:out value="${errors.phone}" /></span>
</c:if>
</div>
<div class="form-field">
<label for="email">Email</label>
<input id="email" name="email" type="email" value="${fn:escapeXml(emailValue)}">
<c:if test="${not empty errors.email}">
<span class="field-error"><c:out value="${errors.email}" /></span>
</c:if>
</div>
<div class="form-field">
<label for="userId">Linked account ID</label>
<input id="userId" name="userId" type="number" min="1" value="${fn:escapeXml(userIdValue)}">
<c:if test="${not empty errors.userId}">
<span class="field-error"><c:out value="${errors.userId}" /></span>
</c:if>
</div>
<div class="form-field">
<label for="maxBorrowCount">Max borrow count</label>
<input id="maxBorrowCount" name="maxBorrowCount" type="number" min="1" max="50"
value="${fn:escapeXml(maxBorrowCountValue)}" required>
<c:if test="${not empty errors.maxBorrowCount}">
<span class="field-error"><c:out value="${errors.maxBorrowCount}" /></span>
</c:if>
</div>
<div class="form-field">
<label for="status">Status</label>
<select id="status" name="status" required>
<option value="">Select status</option>
<c:forEach var="status" items="${statuses}">
<option value="${status.code}" <c:if test="${statusValue == status.code}">selected</c:if>>
<c:out value="${status.displayName}" />
</option>
</c:forEach>
</select>
<c:if test="${not empty errors.status}">
<span class="field-error"><c:out value="${errors.status}" /></span>
</c:if>
</div>
</div>
<div class="form-actions">
<button class="button button-primary" type="submit">Save</button>
<a class="button button-secondary" href="${pageContext.request.contextPath}/readers">Cancel</a>
</div>
</form>
</section>
</main>
</body>
</html>
@@ -0,0 +1,139 @@
<%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Manage Readers - MZH Library</title>
<link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/app.css">
</head>
<body>
<%@ include file="/WEB-INF/jsp/common/header.jspf" %>
<main class="page-shell">
<section class="dashboard-hero catalog-hero" aria-labelledby="manage-readers-title">
<p class="eyebrow">Reader Management</p>
<h1 id="manage-readers-title">Manage readers</h1>
<p>Create, update, and review reader eligibility and contact records.</p>
<a class="button button-primary" href="${pageContext.request.contextPath}/readers/new">New reader</a>
</section>
<c:if test="${not empty successMessage}">
<div class="message message-success" role="status">
<c:out value="${successMessage}" />
</div>
</c:if>
<c:if test="${not empty errorMessage}">
<div class="message message-error" role="alert">
<c:out value="${errorMessage}" />
</div>
</c:if>
<section class="toolbar-panel" aria-label="Reader management search">
<form class="search-form" action="${pageContext.request.contextPath}/readers" method="get">
<div class="search-field">
<label for="identifier">Reader ID</label>
<input id="identifier" name="identifier" type="text" value="${fn:escapeXml(criteria.identifier)}">
</div>
<div class="search-field">
<label for="name">Name</label>
<input id="name" name="name" type="text" value="${fn:escapeXml(criteria.name)}">
</div>
<div class="search-field">
<label for="contact">Phone or email</label>
<input id="contact" name="contact" type="text" value="${fn:escapeXml(criteria.contact)}">
</div>
<div class="search-field">
<label for="status">Status</label>
<select id="status" name="status">
<option value="">All statuses</option>
<c:forEach var="status" items="${statuses}">
<option value="${status.code}" <c:if test="${criteria.statusCode == status.code}">selected</c:if>>
<c:out value="${status.displayName}" />
</option>
</c:forEach>
</select>
<c:if test="${not empty errors.status}">
<span class="field-error"><c:out value="${errors.status}" /></span>
</c:if>
</div>
<button class="button button-primary" type="submit">Search</button>
<a class="button button-secondary" href="${pageContext.request.contextPath}/readers">Clear</a>
</form>
</section>
<section class="table-panel" aria-labelledby="reader-results-title">
<h2 id="reader-results-title">Reader records</h2>
<c:choose>
<c:when test="${empty readers}">
<p class="empty-state">No reader records match the current filters.</p>
</c:when>
<c:otherwise>
<div class="table-scroll">
<table class="data-table">
<thead>
<tr>
<th scope="col">Reader ID</th>
<th scope="col">Name</th>
<th scope="col">Contact</th>
<th scope="col">Account</th>
<th scope="col">Borrow limit</th>
<th scope="col">Status</th>
<th scope="col">Actions</th>
</tr>
</thead>
<tbody>
<c:forEach var="reader" items="${readers}">
<tr>
<td><c:out value="${reader.identifier}" /></td>
<td><c:out value="${reader.fullName}" /></td>
<td>
<c:if test="${not empty reader.phone}">
<div><c:out value="${reader.phone}" /></div>
</c:if>
<c:if test="${not empty reader.email}">
<div><c:out value="${reader.email}" /></div>
</c:if>
</td>
<td>
<c:choose>
<c:when test="${not empty reader.username}">
<c:out value="${reader.username}" />
</c:when>
<c:otherwise>Unlinked</c:otherwise>
</c:choose>
</td>
<td><c:out value="${reader.maxBorrowCount}" /></td>
<td>
<span class="status-pill status-${reader.status.code}">
<c:out value="${reader.status.displayName}" />
</span>
</td>
<td>
<div class="table-actions">
<a class="button button-secondary"
href="${pageContext.request.contextPath}/readers/edit?id=${reader.id}">Edit</a>
<form action="${pageContext.request.contextPath}/readers/delete"
method="post"
onsubmit="return confirm('Deactivate this reader profile?');">
<input type="hidden" name="id" value="${reader.id}">
<button class="button button-danger" type="submit">Deactivate</button>
</form>
</div>
</td>
</tr>
</c:forEach>
</tbody>
</table>
</div>
</c:otherwise>
</c:choose>
</section>
</main>
</body>
</html>
@@ -33,6 +33,12 @@
<p>Create, update, delete, and review inventory fields for book records.</p>
<a class="button button-secondary" href="${pageContext.request.contextPath}/books">Manage books</a>
</article>
<article class="workspace-card">
<h2>Reader Management</h2>
<p>Create, update, deactivate, and review eligibility fields for reader records.</p>
<a class="button button-secondary" href="${pageContext.request.contextPath}/readers">Manage readers</a>
</article>
</c:if>
</section>
</main>
+13
View File
@@ -97,6 +97,19 @@
<url-pattern>/books/delete</url-pattern>
</servlet-mapping>
<servlet>
<servlet-name>ReaderManagementServlet</servlet-name>
<servlet-class>com.mzh.library.controller.ReaderManagementServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>ReaderManagementServlet</servlet-name>
<url-pattern>/readers</url-pattern>
<url-pattern>/readers/new</url-pattern>
<url-pattern>/readers/edit</url-pattern>
<url-pattern>/readers/update</url-pattern>
<url-pattern>/readers/delete</url-pattern>
</servlet-mapping>
<servlet>
<servlet-name>UnauthorizedServlet</servlet-name>
<servlet-class>com.mzh.library.controller.UnauthorizedServlet</servlet-class>
+23 -3
View File
@@ -303,7 +303,9 @@ h2 {
.search-form input,
.search-form select,
.book-form input,
.book-form select {
.book-form select,
.reader-form input,
.reader-form select {
width: 100%;
min-height: 42px;
padding: 9px 11px;
@@ -316,7 +318,9 @@ h2 {
.search-form input:focus,
.search-form select:focus,
.book-form input:focus,
.book-form select:focus {
.book-form select:focus,
.reader-form input:focus,
.reader-form select:focus {
outline: 3px solid rgba(37, 111, 108, 0.18);
border-color: var(--color-primary);
}
@@ -378,6 +382,21 @@ h2 {
background: #eef1f5;
}
.status-active {
color: var(--color-success);
background: #edf8ef;
}
.status-suspended {
color: var(--color-warning);
background: #fff7e5;
}
.status-inactive {
color: var(--color-muted);
background: #eef1f5;
}
.table-actions {
display: flex;
gap: 8px;
@@ -393,7 +412,8 @@ h2 {
max-width: 860px;
}
.book-form {
.book-form,
.reader-form {
display: grid;
gap: 20px;
}