一. 架构设计
这个 Company 模块的架构复杂度相较之前的情况又上了一筹。
上图估计很难看清楚,还是我去掉了 AppConfig 之类的不关键模块的基础上。
但还是有层次可分。
除了设定好的 Model 类外,类间可大致分为四层。
最底层,数据库接口层,提供七种数据对象与数据库的交互。
然后是 ServiceHelper 和 ShiftHelper 这俩辅助服务类。
特别是 ServiceHelper,可以说是其他 Service 的基础。
而 PermissionService 则是在每个 Controller 类内被调用的服务类,用于判断用户是否属于某数据对象。
再到顶层的 Controller 层,层中类方法与 CompanyClient 接口相关联。
二. 代码分析
因代码量较多,这次只调主要的 Service 层中的关键方法进行分析。
首先是 Service 层的根基 ServiceHelper 类
类中有 onboardWorkerAsync 方法,负责通知雇员的新工作等信息
@Async(AppConfig.ASYNC_EXECUTOR_NAME)
public void onboardWorkerAsync(OnboardWorkerRequest onboardWorkerRequest) {
BaseResponse baseResponse = null;
try {
baseResponse = botClient.onboardWorker(onboardWorkerRequest);
} catch (Exception ex) {
String errMsg = "fail to call onboardWorker through botClient";
handleErrorAndThrowException(logger, ex, errMsg);
}
if (!baseResponse.isSuccess()) {
handleErrorAndThrowException(logger, baseResponse.getMessage());
}
}
基于 bot 模块中的数据类型进行传输
然后是 alertNewShiftAsync 方法,用于提醒新的班次信息
@Async(AppConfig.ASYNC_EXECUTOR_NAME)
public void alertNewShiftAsync(AlertNewShiftRequest alertNewShiftRequest) {
BaseResponse baseResponse = null;
try {
baseResponse = botClient.alertNewShift(alertNewShiftRequest);
} catch (Exception ex) {
String errMsg = "failed to alert worker about new shift";
handleErrorAndThrowException(logger, ex, errMsg);
}
if (!baseResponse.isSuccess()) {
handleErrorAndThrowException(logger, baseResponse.getMessage());
}
}
还有 buildShiftNotificationAsync 方法,以列表显示通知所有 班次 的变跟
@Async(AppConfig.ASYNC_EXECUTOR_NAME)
public void buildShiftNotificationAsync(Map<String, List<ShiftDto>> notifs, boolean published) {
for(Map.Entry<String, List<ShiftDto>> entry : notifs.entrySet()) {
String userId = entry.getKey();
List<ShiftDto> shiftDtos = entry.getValue();
if (published) {
// alert published
AlertNewShiftsRequest alertNewShiftsRequest = AlertNewShiftsRequest.builder()
.userId(userId)
.newShifts(shiftDtos)
.build();
BaseResponse baseResponse = null;
try {
baseResponse = botClient.alertNewShifts(alertNewShiftsRequest);
} catch (Exception ex) {
String errMsg = "failed to alert worker about new shifts";
handleErrorAndThrowException(logger, ex, errMsg);
}
if (!baseResponse.isSuccess()) {
handleErrorAndThrowException(logger, baseResponse.getMessage());
}
} else {
// alert removed
AlertRemovedShiftsRequest alertRemovedShiftsRequest = AlertRemovedShiftsRequest.builder()
.userId(userId)
.oldShifts(shiftDtos)
.build();
BaseResponse baseResponse = null;
try {
baseResponse = botClient.alertRemovedShifts(alertRemovedShiftsRequest);
} catch (Exception ex) {
String errMsg = "failed to alert worker about removed shifts";
handleErrorAndThrowException(logger, ex, errMsg);
}
if (!baseResponse.isSuccess()) {
handleErrorAndThrowException(logger, baseResponse.getMessage());
}
}
}
}
主要是生成 AlertNewShiftsRequest 数据对象,传给 botClient 得到反馈
再到 AdminService 类,方法为增删查改,大同小异,选择增来进行分析。
public DirectoryEntryDto createAdmin(String companyId, String userId) {
Admin existing = adminRepo.findByCompanyIdAndUserId(companyId, userId);
if (existing != null) {
throw new ServiceException("user is already an admin");
}
DirectoryEntryDto directoryEntryDto = directoryService.getDirectoryEntry(companyId, userId);
try {
Admin admin = Admin.builder()
.companyId(companyId)
.userId(userId)
.build();
adminRepo.save(admin);
} catch (Exception ex) {
String errMsg = "could not create the admin";
serviceHelper.handleErrorAndThrowException(logger, ex, errMsg);
}
LogEntry auditLog = LogEntry.builder()
.currentUserId(AuthContext.getUserId())
.authorization(AuthContext.getAuthz())
.targetType("admin")
.targetId(userId)
.companyId(companyId)
.teamId("")
.build();
logger.info("added admin", auditLog);
serviceHelper.trackEventAsync("admin_created");
return directoryEntryDto;
}
逻辑不复杂,通过传入的 companyId 和 userId 判断是否存在了 Admin
有就报错,没有再继续创建。
再根据上面的两个参数找到内部对象 DirectoryEntryDto
创建并存储 Admin 对象后,返回内部对象。
再挑 DirectoryService 的 updateDirectoryEntry 方法
代码量很大,逻辑也不复杂
public DirectoryEntryDto updateDirectoryEntry(DirectoryEntryDto request) {
DirectoryEntryDto orig = this.getDirectoryEntry(request.getCompanyId(), request.getUserId());
GenericAccountResponse genericAccountResponse1 = null;
try {
genericAccountResponse1 = accountClient.getAccount(AuthConstant.AUTHORIZATION_COMPANY_SERVICE, orig.getUserId());
} catch (Exception ex) {
String errMsg = "getting account failed";
serviceHelper.handleErrorAndThrowException(logger, ex, errMsg);
}
if (!genericAccountResponse1.isSuccess()) {
serviceHelper.handleErrorAndThrowException(logger, genericAccountResponse1.getMessage());
}
AccountDto account = genericAccountResponse1.getAccount();
boolean accountUpdateRequested =
!request.getName().equals(orig.getName()) ||
!request.getEmail().equals(orig.getEmail()) ||
!request.getPhoneNumber().equals(orig.getPhoneNumber());
if(account.isConfirmedAndActive() && accountUpdateRequested) {
throw new ServiceException(ResultCode.PARAM_VALID_ERROR, "this user is active, so they cannot be modified");
} else if (account.isSupport() && accountUpdateRequested) {
throw new ServiceException(ResultCode.UN_AUTHORIZED, "you cannot change this account");
}
if (accountUpdateRequested) {
account.setName(request.getName());
account.setPhoneNumber(request.getPhoneNumber());
account.setEmail(request.getEmail());
GenericAccountResponse genericAccountResponse2 = null;
try {
genericAccountResponse2 = accountClient.updateAccount(AuthConstant.AUTHORIZATION_COMPANY_SERVICE, account);
} catch (Exception ex) {
String errMsg = "view updating account";
serviceHelper.handleErrorAndThrowException(logger, ex, errMsg);
}
if (!genericAccountResponse2.isSuccess()) {
serviceHelper.handleErrorAndThrowException(logger, genericAccountResponse2.getMessage());
}
copyAccountToDirectory(account, request);
}
try {
directoryRepo.updateInternalIdByCompanyIdAndUserId(request.getInternalId(), request.getCompanyId(), request.getUserId());
} catch (Exception ex) {
String errMsg = "fail to update directory";
serviceHelper.handleErrorAndThrowException(logger, ex, errMsg);
}
LogEntry auditLog = LogEntry.builder()
.currentUserId(AuthContext.getUserId())
.authorization(AuthContext.getAuthz())
.targetType("directory")
.targetId(account.getId())
.companyId(request.getCompanyId())
.teamId("")
.originalContents(orig.toString())
.updatedContents(request.toString())
.build();
logger.info("updated directory entry for account", auditLog);
if (!request.isConfirmedAndActive() &&
(!orig.getPhoneNumber().equals(request.getPhoneNumber()) || ("".equals(request.getPhoneNumber()) && !orig.getEmail().equals(request.getEmail())))) {
OnboardWorkerRequest onboardWorkerRequest = OnboardWorkerRequest.builder()
.companyId(request.getCompanyId())
.userId(request.getUserId())
.build();
serviceHelper.onboardWorkerAsync(onboardWorkerRequest);
}
serviceHelper.trackEventAsync("directoryentry_updated");
return request;
}
首先,通过封装好的 request 中的新 Directory 对象内的 userId 和 companyId 找到旧对象
再找到 account 对象,确保对象有不同后,修改 account 的属性进行更新。
再更新到内部对象 Directory,更新到底层数据库完成方法。