用户/账号管理,系统日志

This commit is contained in:
Zzzz
2026-04-27 22:56:27 +08:00
parent f80f2b807f
commit f99002e664
32 changed files with 2801 additions and 2 deletions
@@ -0,0 +1,101 @@
package com.mzh.library.service.impl;
import com.mzh.library.dao.SystemLogDao;
import com.mzh.library.entity.AuthenticatedUser;
import com.mzh.library.entity.Permission;
import com.mzh.library.entity.SystemLogPage;
import com.mzh.library.entity.SystemLogSearchCriteria;
import com.mzh.library.exception.DaoException;
import com.mzh.library.service.PermissionPolicy;
import com.mzh.library.service.ServiceResult;
import com.mzh.library.service.SystemLogService;
import java.time.LocalDate;
import java.time.format.DateTimeParseException;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
public class SystemLogServiceImpl implements SystemLogService {
private static final Logger LOGGER = Logger.getLogger(SystemLogServiceImpl.class.getName());
private static final String UNAVAILABLE_MESSAGE =
"System log service is temporarily unavailable. Please try again later.";
private static final String DENIED_MESSAGE = "You do not have permission to view system logs.";
private static final String VALIDATION_MESSAGE = "Please correct the system log search filters.";
private final SystemLogDao systemLogDao;
private final PermissionPolicy permissionPolicy;
public SystemLogServiceImpl(SystemLogDao systemLogDao) {
this(systemLogDao, new PermissionPolicy());
}
public SystemLogServiceImpl(SystemLogDao systemLogDao, PermissionPolicy permissionPolicy) {
this.systemLogDao = systemLogDao;
this.permissionPolicy = permissionPolicy;
}
@Override
public ServiceResult<SystemLogPage> searchLogs(AuthenticatedUser actor, SystemLogSearchCriteria criteria) {
if (!canViewSystemLogs(actor)) {
return ServiceResult.failure(DENIED_MESSAGE);
}
SystemLogSearchCriteria normalized = criteria == null ? new SystemLogSearchCriteria() : criteria;
Map<String, String> errors = validate(normalized);
if (!errors.isEmpty()) {
return ServiceResult.validationFailure(VALIDATION_MESSAGE, errors);
}
try {
SystemLogPage page = new SystemLogPage();
page.setLogs(systemLogDao.search(normalized));
page.setOperationTypes(systemLogDao.findOperationTypes());
return ServiceResult.success(page);
} catch (DaoException ex) {
LOGGER.log(Level.SEVERE, "Unable to load system logs actorId=" + actor.getId(), ex);
return ServiceResult.failure(UNAVAILABLE_MESSAGE);
}
}
private Map<String, String> validate(SystemLogSearchCriteria criteria) {
Map<String, String> errors = new LinkedHashMap<>();
if (criteria.getOperationType().length() > 64) {
errors.put("operationType", "Operation type must be 64 characters or fewer.");
}
if (criteria.getKeyword().length() > 120) {
errors.put("keyword", "Keyword must be 120 characters or fewer.");
}
parseDate(criteria.getCreatedFromText(), "createdFrom", "Start date", errors, criteria, true);
parseDate(criteria.getCreatedToText(), "createdTo", "End date", errors, criteria, false);
if (criteria.getCreatedFrom() != null
&& criteria.getCreatedTo() != null
&& criteria.getCreatedFrom().isAfter(criteria.getCreatedTo())) {
errors.put("createdTo", "End date must be on or after start date.");
}
return errors;
}
private void parseDate(String value, String field, String label, Map<String, String> errors,
SystemLogSearchCriteria criteria, boolean fromDate) {
if (value == null || value.isEmpty()) {
return;
}
try {
LocalDate parsed = LocalDate.parse(value);
if (fromDate) {
criteria.setCreatedFrom(parsed);
} else {
criteria.setCreatedTo(parsed);
}
} catch (DateTimeParseException ex) {
errors.put(field, label + " must use YYYY-MM-DD.");
}
}
private boolean canViewSystemLogs(AuthenticatedUser actor) {
return actor != null && permissionPolicy.allows(actor.getRole(), Permission.VIEW_SYSTEM_LOGS);
}
}
@@ -0,0 +1,345 @@
package com.mzh.library.service.impl;
import com.mzh.library.dao.SystemLogDao;
import com.mzh.library.dao.UserAccountDao;
import com.mzh.library.entity.AuthenticatedUser;
import com.mzh.library.entity.Permission;
import com.mzh.library.entity.Role;
import com.mzh.library.entity.SystemLog;
import com.mzh.library.entity.User;
import com.mzh.library.entity.UserSearchCriteria;
import com.mzh.library.exception.DaoException;
import com.mzh.library.service.PermissionPolicy;
import com.mzh.library.service.ServiceResult;
import com.mzh.library.service.UserAccountService;
import com.mzh.library.util.JdbcUtil;
import com.mzh.library.util.PasswordHasher;
import java.sql.SQLException;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.logging.Level;
import java.util.logging.Logger;
public class UserAccountServiceImpl implements UserAccountService {
public interface TransactionExecutor {
<T> T execute(JdbcUtil.TransactionCallback<T> callback);
}
private static final Logger LOGGER = Logger.getLogger(UserAccountServiceImpl.class.getName());
private static final String UNAVAILABLE_MESSAGE =
"User management service is temporarily unavailable. Please try again later.";
private static final String VALIDATION_MESSAGE = "Please correct the highlighted account fields.";
private static final String SEARCH_VALIDATION_MESSAGE = "Please correct the account search filters.";
private static final String DENIED_MESSAGE = "You do not have permission to manage users.";
private static final String SELF_DEACTIVATE_MESSAGE = "You cannot deactivate your own administrator account.";
private static final String SELF_ROLE_MESSAGE = "You cannot change your own administrator role.";
private final UserAccountDao userAccountDao;
private final SystemLogDao systemLogDao;
private final PermissionPolicy permissionPolicy;
private final TransactionExecutor transactionExecutor;
public UserAccountServiceImpl(UserAccountDao userAccountDao, SystemLogDao systemLogDao) {
this(userAccountDao, systemLogDao, new PermissionPolicy(), new JdbcTransactionExecutor());
}
public UserAccountServiceImpl(UserAccountDao userAccountDao, SystemLogDao systemLogDao,
PermissionPolicy permissionPolicy, TransactionExecutor transactionExecutor) {
this.userAccountDao = userAccountDao;
this.systemLogDao = systemLogDao;
this.permissionPolicy = permissionPolicy;
this.transactionExecutor = transactionExecutor;
}
@Override
public ServiceResult<List<User>> searchUsers(AuthenticatedUser actor, UserSearchCriteria criteria) {
if (!canManageUsers(actor)) {
return ServiceResult.failure(DENIED_MESSAGE);
}
UserSearchCriteria normalized = criteria == null ? new UserSearchCriteria() : criteria;
Map<String, String> errors = validateSearch(normalized);
if (!errors.isEmpty()) {
return ServiceResult.validationFailure(SEARCH_VALIDATION_MESSAGE, errors);
}
try {
return ServiceResult.success(userAccountDao.search(normalized));
} catch (DaoException ex) {
LOGGER.log(Level.SEVERE, "Unable to search users actorId=" + actor.getId(), ex);
return ServiceResult.failure(UNAVAILABLE_MESSAGE);
}
}
@Override
public ServiceResult<Optional<User>> findUser(AuthenticatedUser actor, long id) {
if (!canManageUsers(actor)) {
return ServiceResult.failure(DENIED_MESSAGE);
}
if (id <= 0) {
return ServiceResult.failure("Select a valid user account.");
}
try {
return ServiceResult.success(userAccountDao.findById(id));
} catch (DaoException ex) {
LOGGER.log(Level.SEVERE, "Unable to load user id=" + id + " actorId=" + actor.getId(), ex);
return ServiceResult.failure(UNAVAILABLE_MESSAGE);
}
}
@Override
public ServiceResult<Long> createUser(AuthenticatedUser actor, User user, String password, String requestIp) {
if (!canManageUsers(actor)) {
return ServiceResult.failure(DENIED_MESSAGE);
}
normalize(user);
Map<String, String> errors = validateUser(user, false, password, true);
if (!errors.isEmpty()) {
return ServiceResult.validationFailure(VALIDATION_MESSAGE, errors);
}
try {
if (userAccountDao.findByUsername(user.getUsername()).isPresent()) {
errors.put("username", "Username is already in use.");
return ServiceResult.validationFailure(VALIDATION_MESSAGE, errors);
}
user.setPasswordHash(PasswordHasher.hash(password));
return transactionExecutor.execute(connection -> {
long id = userAccountDao.create(connection, user);
systemLogDao.create(connection, auditLog(actor, "user.create", id,
"Created account username=" + user.getUsername() + " role=" + user.getRole().getCode(),
requestIp));
LOGGER.info("Created user id=" + id + " actorId=" + actor.getId());
return ServiceResult.success(id, "User account created.");
});
} catch (DaoException | IllegalStateException ex) {
LOGGER.log(Level.SEVERE, "Unable to create user actorId=" + actor.getId()
+ " username=" + safeUsername(user), ex);
return ServiceResult.failure(UNAVAILABLE_MESSAGE);
}
}
@Override
public ServiceResult<Void> updateUser(AuthenticatedUser actor, User user, String password, String requestIp) {
if (!canManageUsers(actor)) {
return ServiceResult.failure(DENIED_MESSAGE);
}
normalize(user);
Map<String, String> errors = validateUser(user, true, password, false);
if (!errors.isEmpty()) {
return ServiceResult.validationFailure(VALIDATION_MESSAGE, errors);
}
try {
Optional<User> existingResult = userAccountDao.findById(user.getId());
if (!existingResult.isPresent()) {
return ServiceResult.failure("User account was not found.");
}
protectCurrentAdministrator(actor, user, errors);
if (!errors.isEmpty()) {
return ServiceResult.validationFailure(VALIDATION_MESSAGE, errors);
}
User existing = existingResult.get();
user.setUsername(existing.getUsername());
boolean updatePassword = password != null && !password.trim().isEmpty();
if (updatePassword) {
user.setPasswordHash(PasswordHasher.hash(password));
}
final boolean passwordChanged = updatePassword;
return transactionExecutor.execute(connection -> {
if (!userAccountDao.update(connection, user, passwordChanged)) {
return ServiceResult.failure("User account was not found.");
}
systemLogDao.create(connection, auditLog(actor, "user.update", user.getId(),
"Updated account username=" + user.getUsername() + " role=" + user.getRole().getCode()
+ " active=" + user.isActive()
+ (passwordChanged ? " passwordReset=true" : ""),
requestIp));
LOGGER.info("Updated user id=" + user.getId() + " actorId=" + actor.getId());
return ServiceResult.success(null, "User account updated.");
});
} catch (DaoException | IllegalStateException ex) {
LOGGER.log(Level.SEVERE, "Unable to update user id=" + user.getId() + " actorId=" + actor.getId(), ex);
return ServiceResult.failure(UNAVAILABLE_MESSAGE);
}
}
@Override
public ServiceResult<Void> deactivateUser(AuthenticatedUser actor, long id, String requestIp) {
if (!canManageUsers(actor)) {
return ServiceResult.failure(DENIED_MESSAGE);
}
if (id <= 0) {
return ServiceResult.failure("Select a valid user account.");
}
if (actor.getId() == id) {
Map<String, String> errors = new LinkedHashMap<>();
errors.put("active", SELF_DEACTIVATE_MESSAGE);
return ServiceResult.validationFailure(VALIDATION_MESSAGE, errors);
}
try {
Optional<User> existingResult = userAccountDao.findById(id);
if (!existingResult.isPresent()) {
return ServiceResult.failure("User account was not found.");
}
User user = existingResult.get();
user.setActive(false);
return transactionExecutor.execute(connection -> {
if (!userAccountDao.update(connection, user, false)) {
return ServiceResult.failure("User account was not found.");
}
systemLogDao.create(connection, auditLog(actor, "user.deactivate", id,
"Deactivated account username=" + user.getUsername(),
requestIp));
LOGGER.info("Deactivated user id=" + id + " actorId=" + actor.getId());
return ServiceResult.success(null, "User account deactivated.");
});
} catch (DaoException ex) {
LOGGER.log(Level.SEVERE, "Unable to deactivate user id=" + id + " actorId=" + actor.getId(), ex);
return ServiceResult.failure(UNAVAILABLE_MESSAGE);
}
}
private Map<String, String> validateSearch(UserSearchCriteria criteria) {
Map<String, String> errors = new LinkedHashMap<>();
if (!criteria.getRoleCode().isEmpty()) {
try {
criteria.setRoleCode(Role.fromCode(criteria.getRoleCode()).getCode());
} catch (IllegalArgumentException ex) {
errors.put("role", "Select a valid role.");
}
}
String activeStatus = criteria.getActiveStatus();
if (!activeStatus.isEmpty()
&& !UserSearchCriteria.ACTIVE_STATUS.equals(activeStatus)
&& !UserSearchCriteria.INACTIVE_STATUS.equals(activeStatus)) {
errors.put("active", "Select a valid active state.");
}
return errors;
}
private Map<String, String> validateUser(User user, boolean requireId, String password, boolean requirePassword) {
Map<String, String> errors = new LinkedHashMap<>();
if (user == null) {
errors.put("user", "User account details are required.");
return errors;
}
if (requireId && user.getId() <= 0) {
errors.put("id", "Select a valid user account.");
}
if (!requireId) {
requireLength(errors, "username", user.getUsername(), "Username", 64);
}
requireLength(errors, "displayName", user.getDisplayName(), "Display name", 100);
if (user.getRole() == null) {
errors.put("role", "Select a role.");
}
validatePassword(errors, password, requirePassword);
return errors;
}
private void validatePassword(Map<String, String> errors, String password, boolean required) {
String trimmed = password == null ? "" : password.trim();
if (trimmed.isEmpty()) {
if (required) {
errors.put("password", "Password is required.");
}
return;
}
if (password.length() > 128) {
errors.put("password", "Password must be 128 characters or fewer.");
}
}
private void protectCurrentAdministrator(AuthenticatedUser actor, User user, Map<String, String> errors) {
if (actor.getId() != user.getId()) {
return;
}
if (!user.isActive()) {
errors.put("active", SELF_DEACTIVATE_MESSAGE);
}
if (user.getRole() != Role.ADMINISTRATOR) {
errors.put("role", SELF_ROLE_MESSAGE);
}
}
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 SystemLog auditLog(AuthenticatedUser actor, String operationType, long userId, String message,
String requestIp) {
SystemLog log = new SystemLog();
log.setOperatorId(actor.getId());
log.setOperatorRole(actor.getRole().getCode());
log.setOperationType(operationType);
log.setTargetTable("users");
log.setTargetId(String.valueOf(userId));
log.setResultStatus("success");
log.setMessage(message);
log.setRequestIp(trim(requestIp));
return log;
}
private boolean canManageUsers(AuthenticatedUser actor) {
return actor != null && permissionPolicy.allows(actor.getRole(), Permission.MANAGE_USERS);
}
private void normalize(User user) {
if (user == null) {
return;
}
user.setUsername(normalizeUsername(user.getUsername()));
user.setDisplayName(trim(user.getDisplayName()));
}
private String normalizeUsername(String username) {
return trim(username);
}
private String safeUsername(User user) {
return user == null ? "" : user.getUsername();
}
private String trim(String value) {
return value == null ? "" : value.trim();
}
private static final class JdbcTransactionExecutor implements TransactionExecutor {
@Override
public <T> T execute(JdbcUtil.TransactionCallback<T> callback) {
return JdbcUtil.executeInTransaction(callback);
}
}
public static final class DirectTransactionExecutor implements TransactionExecutor {
@Override
public <T> T execute(JdbcUtil.TransactionCallback<T> callback) {
try {
return callback.execute(null);
} catch (SQLException ex) {
throw new DaoException("Unable to execute direct transaction", ex);
}
}
}
}