Since December of 2004, Google has provided an annual Christmas-themed site which allows users to track Santa during Christmas Eve. Additionally, NORAD has been tracking Santa since 1955. While no official API exists, there is an unofficial API which can be used to track Santa’s whereabouts.
In celebration of the Christmas season, I wanted to create a Spring Boot application that could be used to get updates on Santa’s location via SMS. The full code for this Java application can be found on GitHub.
首先,我想解释一下应用程序如何在更高层次上工作。 然后,我们可以研究一些更具挑战性的问题,以及解决这些问题的方法。
See it in Action
当收到我的Nexmo号码上的SMS消息时,有效负载将发送到我注册的Webhook。 然后,使用诸如发件人的电话号码和消息内容之类的信息来确定如何响应消息。
实际效果如下:
The Initial Message
第一次收到消息时,总是向用户显示圣诞老人的当前位置。 还询问他们是否要提供邮政编码以获取距离信息。
Location Lookup
位置查找不是一件容易的事。 邮政编码在全球范围内不一致,因此有助于了解用户与哪个国家/地区联系,以缩小搜索范围。
If the user elects to provide postal code information, I use Nexmo Number 一世nsight to lookup the country that they are sending messages from. They are then asked to provide their postal code. From this, I use a service called GeoNames to lookup the latitude and longitude for that postal code.
此信息连同其电话号码一起保存在数据库中。 纬度和经度用于计算圣诞老人离他们有多远:
Getting More Technical
从表面上看,该应用程序似乎并不那么复杂。 但是,在开发过程中我遇到了很多挑战。 我想强调一下代码的更多技术方面。
Routing Incoming Messages
我的Nexmo号码已配置为发送开机自检请求到Webhook URL。 我有一个传入消息控制器用于处理传入消息的设置:
@PostMapping
public void post(@RequestParam("msisdn") String from,
@RequestParam("to") String nexmoNumber,
@RequestParam("keyword") String keyword,
@RequestParam("text") String text
) {
Phone phone = findOrCreatePhone(from, nexmoNumber);
findHandler(phone, keyword).handle(phone, text);
}
的findOrCreate电话方法用于保留新的电话实体,或查找现有实体。 电话实体的外观如下:
@Entity
public class Phone {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String number;
private String nexmoNumber;
private String countryCode;
@Enumerated(EnumType.STRING)
private Stage stage;
@OneToOne
private Location location;
// Getters and Setters
}
该实体包含电话号码,Nexmo号码,国家/地区代码和用户所在的当前阶段。我将讨论阶段在后面的部分中枚举。
的findHandler方法用于查找适当的关键字处理程序处理消息:
public interface KeywordHandler {
void handle(Phone phone, String text);
}
每个处理程序负责处理一个特定的关键字。 应用程序知道的关键字是:
- 救命提供上下文信息以帮助用户。取消这取消了当前的一系列问题。去掉从数据库中删除用户。是用户对问题的回答是肯定的。没有用户对问题的回答是否定的。哪里并以圣诞老人的当前位置作为回应。位置允许用户更新其位置。
每关键字处理程序被注册为Spring Managed Bean。 的Help关键字处理程序,例如,如下所示:
@Component
public class HelpKeywordHandler implements KeywordHandler {
@Override
public void handle(Phone phone, String text) {
// Logic here
}
}
全部关键字处理程序实现被注入到keywordHandler地图上传入消息控制器:
private final Map<String, KeywordHandler> keywordHandlers;
@Autowired
public IncomingMessageController(Map<String, KeywordHandler> keywordHandlers) {
this.keywordHandlers = keywordHandlers;
}
注入地图时,Spring将使用骆驼香烟盒类名称为键,实例化的类为值。 例如,HelpKeywordHandler与密钥一起存储helpKeywordHandler:
HelpKeywordHandler handler = keywordHandlers.get("helpKeywordHandler");
The appropriate KeywordHandler
is then picked using a variant of the Strategy Pattern. If no valid KeywordHandler
is found, then the DefaultKeywordHandler
is used to respond.
private KeywordHandler findHandler(Phone phone, String keyword) {
// New users should always go to the default handler
if (phone.getStage() == null) {
return keywordHandlers.get("defaultKeywordHandler");
}
KeywordHandler handler = keywordHandlers.get(keywordToHandlerName(keyword));
return (handler != null) ? handler : keywordHandlers.get("defaultKeywordHandler");
}
private String keywordToHandlerName(String keyword) {
return keyword.toLowerCase() + "KeywordHandler";
}
这样路由消息可以在需要添加新关键字时提供灵活性。
Handling the Various Stages
每个处理程序负责以其关键字开头的消息。 但是,如何YesKeywordHandler知道用户在回答哪个问题? 这是阶段里面的枚举电话类进来了。
的电话实体可以存在于以下中介阶段:
- 无舞台(空值)对于刚刚创建但尚未被问到问题的用户。初始对于已收到第一条消息的用户,其中包含圣诞老人的位置,并提示他们是否要提供邮政编码。COUNTRY_PROMPT适用于被询问其国家/地区代码是否正确的用户。POSTAL_PROMPT适用于被要求提供邮政编码的用户。
提出问题后,它们将进入以下最后阶段:
- 注册适用于已提供邮政编码并会收到更多详细信息的用户。客人适用于不希望提供邮政编码信息的用户,他们只会收到圣诞老人的当前位置,而不会计算距离。
这就是YesKeywordHandler使用阶段:
@Override
public void handle(Phone phone, String text) {
if (phone.getStage() == Phone.Stage.INITIAL) {
handlePromptForCountryCode(phone);
} else if (phone.getStage() == Phone.Stage.COUNTRY_PROMPT) {
handlePromptForPostalCode(phone);
} else {
outgoingMessageService.sendUnknown(phone);
}
}
在初始阶段必须回答以下问题:“您想提供邮政编码信息吗?”
用户在COUNTRY_PROMPT阶段必须回答以下问题:“我看到您是从美国发来的邮件。请回答YES(2个字符的国家/地区代码,如果您改变了主意,请取消”。)
Getting the Country Code
为了更准确地查找邮政编码,它有助于了解用户所来自的国家。 我创建了一个PhoneLocationLookupService它使用Nexmo Basic Number Insight查找电话号码的国家/地区信息:
@Service
public class PhoneLocationLookupService {
private final InsightClient insightClient;
@Autowired
public PhoneLocationLookupService(NexmoClient nexmoClient) {
this.insightClient = nexmoClient.getInsightClient();
}
public String lookupCountryCode(String number) {
try {
return this.insightClient.getBasicNumberInsight(number).getCountryCode();
} catch (IOException | NexmoClientException e) {
return null;
}
}
}
该应用程序将要求用户确认自己的国家,以防万一他们正在旅行并且想要将自己设置在其他国家。
Lookup for Postal Codes
事实证明,查找邮政编码的纬度和经度信息并不是一件容易的事。 我选择使用GeoNames,因为该服务是免费的。
我创建了一个PostCodeLookupService处理查找邮政编码信息。 的getLocation方法首先查看是否已经知道数据库中的邮政编码。 这是一个好习惯,因为它有助于限制打给第三方服务的电话数量。
如果不存在位置实体,将调用第三方服务,并保留一个新实体。 如果找不到与该邮政编码匹配的位置,则返回一个空白可选的以便应用程序知道要求用户重试:
private Optional<Location> getLocation(String country, String postalCode) {
Optional<Location> locationOptional = locationRepository.findByPostalCode(postalCode);
if (locationOptional.isPresent()) {
return locationOptional;
}
LocationResponse response = getLocationResponse(country, postalCode);
if (response.getPostalCodes().isEmpty()) {
return Optional.empty();
}
Location newLocation = buildLocation(response, postalCode);
return Optional.of(locationRepository.save(newLocation));
}
Calculating Distance
有了用户的纬度和经度后,我们可以查找圣诞老人的当前位置,并使用公式确定用户与圣诞老人的距离。 这是在DistanceCalculationService:
public double getDistanceInMiles(double lat1, double lng1, double lat2, double lng2) {
double theta = lng1 - lng2;
double dist = Math.sin(Math.toRadians(lat1))
* Math.sin(Math.toRadians(lat2))
+ Math.cos(Math.toRadians(lat1))
* Math.cos(Math.toRadians(lat2))
* Math.cos(Math.toRadians(theta));
dist = Math.acos(dist);
dist = Math.toDegrees(dist);
dist = dist * 60 * 1.1515;
return dist;
}
Conclusion
这是对如何创建一个允许您通过SMS获取Santa位置更新的应用程序的一种研究。 我们介绍了基于关键字的路由消息,使用多个阶段来确定用户正在响应的问题,并使用Nexmo Number Insight获取电话号码的国家/地区代码。
For more detailed information, I would recommend looking at the code on GitHub. The README
file contains all the information you need to get up and running with the application itself.
Be sure to check out Google's Santa Tracker or NORAD Santa Tracker around Christmas Eve as an additional way to keep tabs on Santa.