一. 架构设计
上面的图片在 API 处有省略数个不同 数据模型,具体请自行查看源码
二. 代码分析
首先,是 BotClient 这个唯一的接口。
接口将三个 Controller 的方法综合防在了一起,对外提供接口调用。
相同的 Controller 的方法大同小异,各举一个例子
@PostMapping(path="sms_greeting")
BaseResponse sendSmsGreeting(@RequestBody @Validated GreetingRequest request);
@PostMapping(path="onboard_worker")
BaseResponse onboardWorker(@RequestBody @Validated OnboardWorkerRequest request);
@PostMapping(path="alert_new_shift")
BaseResponse alertNewShift(@RequestBody @Validated AlertNewShiftRequest request);
可以看出。都没有头部的验证,只是将数据模型作为参数传递。
数据模型方面,每个数据模型都较为细致且基础。
主要是 userId ,shift(ShiftDto 封装对象),companyId 等
不展示了。
然后是老规矩,代码自底向上分析。
Bot 模块的代码可以被认为分成四层。具体可以在上面我展示的架构图中可以看出
AppProps 提供一个布尔值,判断是发送 email 还是 sms
private boolean forceEmailPreference;
AppConfig 也不复杂,就提供一个异步线程池,在 HelperService 中被调用
@Bean(name=ASYNC_EXECUTOR_NAME)
public Executor asyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setTaskDecorator(new ContextCopyingDecorator());
executor.setCorePoolSize(3);
executor.setMaxPoolSize(5);
executor.setQueueCapacity(100);
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setThreadNamePrefix("AsyncThread-");
executor.initialize();
return executor;
}
然后是 DispatchPreference,他含三个标签值,判断时 SMS,Email,还是发不出去
enum DispatchPreference {
DISPATCH_SMS,
DISPATCH_EMAIL,
DISPATCH_UNAVAILABLE
}
接着是重头戏 HelperService。它可以说是整个模块的根基。主要的功能实现代码就在此处。
比如,异步发送短信
@Async(AppConfig.ASYNC_EXECUTOR_NAME)
void smsGreetingAsync(String phoneNumber) {
String templateCode = BotConstant.GREETING_SMS_TEMPLATE_CODE;
String templateParam = "";
this.sendSms(phoneNumber, templateCode, templateParam);
}
void sendSms(String phoneNumber, String templateCode, String templateParam) {
SmsRequest smsRequest = SmsRequest.builder()
.to(phoneNumber)
.templateCode(templateCode)
.templateParam(templateParam)
.build();
BaseResponse baseResponse = null;
try {
baseResponse = smsClient.send(AuthConstant.AUTHORIZATION_BOT_SERVICE, smsRequest);
} catch (Exception ex) {
String errMsg = "could not send sms";
logger.error(errMsg, ex);
sentryClient.sendException(ex);
throw new ServiceException(errMsg, ex);
}
if (!baseResponse.isSuccess()) {
logger.error(baseResponse.getMessage());
sentryClient.sendMessage(baseResponse.getMessage());
throw new ServiceException(baseResponse.getMessage());
}
}
发送 email 的方法大同小异,所以只对该方法进行详细分析。
方法和之前的相同点在于。都会生成一个封装好的数据对象。
将这个对象当做参数传递,得到反馈(以 BaseResponse 类型)。
但有些不同的是,本方法使用的 Sms 模块 中封装的 SmsRequest 对象
且,使用 smsClient 接口就行代码调用。从而实现了不同模块间的通信。
Email 相关方法也是如此。
再到具体方法的执行。
首先是最多的 AlertService 类。
里面的方法是对 shift 这一班次的增删查改进行处理。
方法大同小异,择一分析。
public void alertNewShift(AlertNewShiftRequest req) {
String companyId = req.getNewShift().getCompanyId();
String teamId = req.getNewShift().getTeamId();
AccountDto account = helperService.getAccountById(req.getUserId());
DispatchPreference dispatchPreference = helperService.getPreferredDispatch(account);
if (dispatchPreference == DispatchPreference.DISPATCH_UNAVAILABLE) {
return;
}
CompanyDto companyDto = helperService.getCompanyById(companyId);
TeamDto teamDto = this.getTeamByCompanyIdAndTeamId(companyId, teamId);
String newShiftMsg = this.printShiftSmsMsg(req.getNewShift(), teamDto.getTimezone());
String jobName = this.getJobName(companyId, teamId, req.getNewShift().getJobId());
// Format name with leading space
if (!StringUtils.isEmpty(jobName)) {
jobName = " " + jobName;
}
String greet = HelperService.getGreet(account.getName());
String companyName = companyDto.getName();
if (dispatchPreference == DispatchPreference.DISPATCH_EMAIL) {
String htmlBody = String.format(BotConstant.ALERT_NEW_SHIFT_EMAIL_TEMPLATE,
greet, companyName, jobName, newShiftMsg);
String subject = "New Shift Alert";
String email = account.getEmail();
String name = account.getName();
helperService.sendMail(email, name, subject, htmlBody);
} else { // sms
String templateParam = Json.createObjectBuilder()
.add("greet", greet)
.add("company_name", companyName)
.add("job_name", jobName)
.add("shift_msg", newShiftMsg)
.build()
.toString();
String phoneNumber = account.getPhoneNumber();
// TODO crate sms template on aliyun then update constant
// String msg = String.format("%s Your %s manager just published a new%s shift for you: \n%s",
greet, company.getName(), jobName, newShiftMsg);
helperService.sendSms(phoneNumber, BotConstant.ALERT_NEW_SHIFT_SMS_TEMPLATE_CODE, templateParam);
}
}
代码量不低,但逻辑并不复杂。
通过封装好传过来的 Request 对象取出其中的 companyId 和 teamId
在根据对象中的 userId 找到对应的 account 对象
通过这个对象,判断他支持发送邮件还是短信
然后通过之前取出的俩 id 找到对应的 company 和 team
在进一步得到新的 班次信息(newShift)和工作信息(jobName)
接着通过 helperService.getGreet 通知用户。
是发送短信还是邮件,根据权限(DispatchPreference)处理。
然后是俩 Controller,都只有一个方法
GreetingController 中的 sendSmsGreeting 方法在 account 模块中被运用到通知激活等信息
public void greeting(String userId) {
AccountDto account = helperService.getAccountById(userId);
DispatchPreference dispatchPreference = helperService.getPreferredDispatch(account);
switch (dispatchPreference) {
case DISPATCH_SMS:
helperService.smsGreetingAsync(account.getPhoneNumber());
break;
case DISPATCH_EMAIL:
helperService.mailGreetingAsync(account);
break;
default:
logger.info("Unable to send greeting to user %s - no comm method found", userId);
}
}
传入参数 userId 即可,便会择情处理。
另一边的 OnBoardingService 中的 onboardWorker 则在 company 模块中被用来通知班次的创建和修改
public void onboardWorker(OnboardWorkerRequest req) {
AccountDto account = helperService.getAccountById(req.getUserId());
CompanyDto companyDto = helperService.getCompanyById(req.getCompanyId());
DispatchPreference dispatchPreference = helperService.getPreferredDispatch(account);
switch (dispatchPreference) {
case DISPATCH_SMS:
helperService.smsOnboardAsync(account, companyDto);
break;
case DISPATCH_EMAIL:
helperService.mailOnBoardAsync(account, companyDto);
break;
default:
logger.info("Unable to onboard user %s - no comm method found", req.getUserId());
}
}
参数相对复杂
因为不仅需要 userId 还要 companyId ,从而同时找到用户和公司
然后进行处理。
Controller 层就中规中矩,没什么特别的,各展示一个范例,收工。
@PostMapping(value = "/sms_greeting")
BaseResponse sendSmsGreeting(@RequestBody @Validated GreetingRequest request) {
greetingService.greeting(request.getUserId());
return BaseResponse.builder().message("greeting sent").build();
}
@PostMapping(value = "alert_changed_shifts")
public BaseResponse alertChangedShifts(@RequestBody @Validated AlertChangedShiftRequest request) {
alertService.alertChangedShift(request);
return BaseResponse.builder().message("changed shifts alerted").build();
}
@PostMapping(value = "/onboard_worker")
public BaseResponse onboardWorker(@RequestBody @Validated OnboardWorkerRequest request) {
onBoardingService.onboardWorker(request);
return BaseResponse.builder().message("onboarded worker").build();
}
三. 小关键点
使用 lombok 插件生成后的构建方法,可以清晰且方便的构建封装数据对象。
SmsRequest smsRequest = SmsRequest.builder()
.to(phoneNumber)
.templateCode(templateCode)
.templateParam(templateParam)
.build();