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

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,263 @@
package com.mzh.library.service;
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.SystemLogSearchCriteria;
import com.mzh.library.entity.User;
import com.mzh.library.entity.UserSearchCriteria;
import com.mzh.library.exception.DaoException;
import com.mzh.library.service.impl.UserAccountServiceImpl;
import com.mzh.library.util.PasswordHasher;
import java.sql.Connection;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumSet;
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;
import java.util.stream.Collectors;
public final class UserAccountServiceCheck {
private static final String DENIED_MESSAGE = "You do not have permission to manage users.";
private static final String UNAVAILABLE_MESSAGE =
"User management service is temporarily unavailable. Please try again later.";
private UserAccountServiceCheck() {
}
public static void main(String[] args) {
Logger.getLogger(UserAccountServiceImpl.class.getName()).setLevel(Level.OFF);
InMemoryUserAccountDao userDao = new InMemoryUserAccountDao();
InMemorySystemLogDao logDao = new InMemorySystemLogDao();
User adminAccount = user(1L, "admin", "System Administrator", Role.ADMINISTRATOR, true, "admin-password");
User staffAccount = user(2L, "staff", "Library Staff", Role.LIBRARIAN, true, "staff-password");
userDao.put(adminAccount);
userDao.put(staffAccount);
UserAccountService service = new UserAccountServiceImpl(userDao, logDao, new PermissionPolicy(),
new UserAccountServiceImpl.DirectTransactionExecutor());
AuthenticatedUser admin = actor(1L, Role.ADMINISTRATOR);
AuthenticatedUser reader = actor(3L, Role.READER);
requireMessage(service.searchUsers(reader, new UserSearchCriteria()), DENIED_MESSAGE);
User duplicate = user(0L, " admin ", "Duplicate Admin", Role.ADMINISTRATOR, true, "unused");
ServiceResult<Long> duplicateResult = service.createUser(admin, duplicate, "new-password", "127.0.0.1");
require(!duplicateResult.isSuccessful(), "duplicate username should be rejected");
require(duplicateResult.getErrors().containsKey("username"), "duplicate username should target username");
User invalid = new User();
ServiceResult<Long> invalidResult = service.createUser(admin, invalid, "", "127.0.0.1");
require(!invalidResult.isSuccessful(), "invalid user should be rejected");
require(invalidResult.getErrors().containsKey("username"), "missing username should be reported");
require(invalidResult.getErrors().containsKey("displayName"), "missing display name should be reported");
require(invalidResult.getErrors().containsKey("password"), "missing password should be reported");
User newReader = new User();
newReader.setUsername(" new.reader ");
newReader.setDisplayName("New Reader");
newReader.setRole(Role.READER);
newReader.setActive(true);
ServiceResult<Long> created = service.createUser(admin, newReader, "reader-password", "127.0.0.1");
require(created.isSuccessful(), "administrator should create user accounts");
User storedReader = userDao.findById(created.getData()).orElseThrow(AssertionError::new);
require("new.reader".equals(storedReader.getUsername()), "username should be trimmed like login");
require(!"reader-password".equals(storedReader.getPasswordHash()), "password should not be stored in plain text");
require(PasswordHasher.verify("reader-password", storedReader.getPasswordHash()), "stored password should verify");
require(logDao.logs.size() == 1, "user creation should write one system log");
require("user.create".equals(logDao.logs.get(0).getOperationType()), "creation log should use user.create");
User selfRoleChange = user(1L, "admin", "System Administrator", Role.LIBRARIAN, true, "ignored");
ServiceResult<Void> selfRoleResult = service.updateUser(admin, selfRoleChange, "", "127.0.0.1");
require(!selfRoleResult.isSuccessful(), "current administrator role change should be blocked");
require(selfRoleResult.getErrors().containsKey("role"), "self role change should target role");
User selfDeactivate = user(1L, "admin", "System Administrator", Role.ADMINISTRATOR, false, "ignored");
ServiceResult<Void> selfDeactivateResult = service.updateUser(admin, selfDeactivate, "", "127.0.0.1");
require(!selfDeactivateResult.isSuccessful(), "current administrator deactivation should be blocked");
require(selfDeactivateResult.getErrors().containsKey("active"), "self deactivation should target active state");
String originalStaffHash = staffAccount.getPasswordHash();
User updatedStaff = user(2L, "staff", "Lead Librarian", Role.LIBRARIAN, true, "ignored");
ServiceResult<Void> updated = service.updateUser(admin, updatedStaff, "", "127.0.0.1");
require(updated.isSuccessful(), "administrator should update user accounts");
require(originalStaffHash.equals(userDao.findById(2L).orElseThrow(AssertionError::new).getPasswordHash()),
"blank update password should preserve existing hash");
ServiceResult<Void> reset = service.updateUser(admin, updatedStaff, "replacement-password", "127.0.0.1");
require(reset.isSuccessful(), "administrator should reset passwords");
require(PasswordHasher.verify("replacement-password",
userDao.findById(2L).orElseThrow(AssertionError::new).getPasswordHash()),
"replacement password should be hashed");
ServiceResult<Void> deactivated = service.deactivateUser(admin, 2L, "127.0.0.1");
require(deactivated.isSuccessful(), "administrator should deactivate other accounts");
require(!userDao.findById(2L).orElseThrow(AssertionError::new).isActive(),
"deactivate action should mark account inactive");
require(logDao.logs.stream().anyMatch(log -> "user.deactivate".equals(log.getOperationType())),
"deactivate should write a system log");
UserAccountService failingService = new UserAccountServiceImpl(new FailingUserAccountDao(), logDao,
new PermissionPolicy(), new UserAccountServiceImpl.DirectTransactionExecutor());
requireMessage(failingService.searchUsers(admin, new UserSearchCriteria()), UNAVAILABLE_MESSAGE);
}
private static User user(long id, String username, String displayName, Role role, boolean active, String password) {
User user = new User();
user.setId(id);
user.setUsername(username);
user.setDisplayName(displayName);
user.setRole(role);
user.setActive(active);
user.setPasswordHash(PasswordHasher.hash(password));
return user;
}
private static AuthenticatedUser actor(long id, Role role) {
return new AuthenticatedUser(id, role.getCode(), role.getDisplayName(), role,
role == Role.ADMINISTRATOR
? EnumSet.allOf(Permission.class)
: EnumSet.of(Permission.VIEW_CATALOG, Permission.BORROW_BOOKS));
}
private static void requireMessage(ServiceResult<?> result, String message) {
require(!result.isSuccessful(), "result should be a failure");
require(message.equals(result.getMessage()), "expected message: " + message);
}
private static void require(boolean condition, String message) {
if (!condition) {
throw new AssertionError(message);
}
}
private static final class InMemoryUserAccountDao implements UserAccountDao {
private final Map<Long, User> users = new LinkedHashMap<>();
private long nextId = 10L;
private void put(User user) {
users.put(user.getId(), copy(user));
}
@Override
public List<User> search(UserSearchCriteria criteria) {
return users.values().stream()
.filter(user -> criteria.getRoleCode().isEmpty()
|| user.getRole().getCode().equals(criteria.getRoleCode()))
.filter(user -> criteria.getActiveValue() == null
|| user.isActive() == criteria.getActiveValue())
.map(this::copy)
.collect(Collectors.toList());
}
@Override
public Optional<User> findById(long id) {
return Optional.ofNullable(users.get(id)).map(this::copy);
}
@Override
public Optional<User> findByUsername(String username) {
return users.values().stream()
.filter(user -> user.getUsername().equals(username))
.findFirst()
.map(this::copy);
}
@Override
public long create(Connection connection, User user) {
long id = nextId++;
User stored = copy(user);
stored.setId(id);
users.put(id, stored);
return id;
}
@Override
public boolean update(Connection connection, User user, boolean updatePassword) {
User existing = users.get(user.getId());
if (existing == null) {
return false;
}
existing.setDisplayName(user.getDisplayName());
existing.setRole(user.getRole());
existing.setActive(user.isActive());
if (updatePassword) {
existing.setPasswordHash(user.getPasswordHash());
}
return true;
}
private User copy(User source) {
User copy = new User();
copy.setId(source.getId());
copy.setUsername(source.getUsername());
copy.setDisplayName(source.getDisplayName());
copy.setRole(source.getRole());
copy.setActive(source.isActive());
copy.setPasswordHash(source.getPasswordHash());
copy.setCreatedAt(source.getCreatedAt());
copy.setUpdatedAt(source.getUpdatedAt());
return copy;
}
}
private static final class InMemorySystemLogDao implements SystemLogDao {
private final List<SystemLog> logs = new ArrayList<>();
@Override
public List<SystemLog> search(SystemLogSearchCriteria criteria) {
return new ArrayList<>(logs);
}
@Override
public List<String> findOperationTypes() {
return logs.stream()
.map(SystemLog::getOperationType)
.distinct()
.collect(Collectors.toList());
}
@Override
public long create(Connection connection, SystemLog log) {
log.setId(logs.size() + 1L);
logs.add(log);
return log.getId();
}
}
private static final class FailingUserAccountDao implements UserAccountDao {
@Override
public List<User> search(UserSearchCriteria criteria) {
throw new DaoException("Simulated user search failure", null);
}
@Override
public Optional<User> findById(long id) {
throw new DaoException("Simulated user lookup failure", null);
}
@Override
public Optional<User> findByUsername(String username) {
throw new DaoException("Simulated username lookup failure", null);
}
@Override
public long create(Connection connection, User user) {
throw new DaoException("Simulated user create failure", null);
}
@Override
public boolean update(Connection connection, User user, boolean updatePassword) {
throw new DaoException("Simulated user update failure", null);
}
}
}