编写UI


我们已经实现了API系统、交易系统、定序系统、行情系统和推送系统,最后就差一个UI系统,让用户可以登录并通过浏览器下订单。UI系统就是一个标准的Web系统,相对比较简单。

UI系统本质上是一个MVC模型的Web系统,我们先引入一个视图的第三方依赖:

  1. <dependency>
  2. <groupId>io.pebbletemplates</groupId>
  3. <artifactId>pebble-spring-boot-starter</artifactId>
  4. <version>${pebble.version}</version>
  5. </dependency>

ui.yml加入最基本的配置:

  1. pebble:
  2. prefix: /templates/
  3. suffix: .html

注意到视图页面都放在src/main/resources/templates/目录下。编写MvcController,实现登录功能:

  1. @Controller
  2. public class MvcController extends LoggerSupport {
  3. // 显示登录页
  4. @GetMapping("/signin")
  5. public ModelAndView signin(HttpServletRequest request) {
  6. if (UserContext.getUserId() != null) {
  7. return redirect("/");
  8. }
  9. return prepareModelAndView("signin");
  10. }
  11. // 登录
  12. @PostMapping("/signin")
  13. public ModelAndView signIn(@RequestParam("email") String email, @RequestParam("password") String password, HttpServletRequest request, HttpServletResponse response) {
  14. try {
  15. UserProfileEntity userProfile = userService.signin(email, password);
  16. // 登录成功后设置Cookie:
  17. AuthToken token = new AuthToken(userProfile.userId, System.currentTimeMillis() + 1000 * cookieService.getExpiresInSeconds());
  18. cookieService.setSessionCookie(request, response, token);
  19. } catch (ApiException e) {
  20. // 登录失败:
  21. return prepareModelAndView("signin", Map.of("email", email, "error", "Invalid email or password."));
  22. } catch (Exception e) {
  23. // 登录失败:
  24. return prepareModelAndView("signin", Map.of("email", email, "error", "Internal server error."));
  25. }
  26. // 登录成功跳转:
  27. return redirect("/");
  28. }
  29. }

登录成功后,设置一个Cookie代表用户身份,以userId:expiresAt:hash表示。由于计算哈希引入了HmacKey,因此,客户端无法伪造Cookie。

继续编写UIFilter,用于验证Cookie并把特定用户的身份绑定到UserContext中:

  1. public class UIFilter {
  2. @Override
  3. public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain)
  4. throws IOException, ServletException {
  5. // 查找Cookie:
  6. AuthToken auth = cookieService.findSessionCookie(req);
  7. Long userId = auth == null ? null : auth.userId();
  8. try (UserContext ctx = new UserContext(userId)) {
  9. chain.doFilter(request, response);
  10. }
  11. }
  12. }

我们再编写一个ProxyFilter,它的目的是将页面JavaScript对API的调用转发给API系统:

  1. public class ProxyFilter {
  2. @Override
  3. public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
  4. throws IOException, ServletException {
  5. Long userId = UserContext.getUserId();
  6. // 构造一次性Token:
  7. String authToken = null;
  8. if (userId != null) {
  9. AuthToken token = new AuthToken(userId, System.currentTimeMillis() + 60_000);
  10. authToken = "Bearer " + token.toSecureString(hmacKey);
  11. }
  12. // 转发到API并读取响应:
  13. String responseJson = null;
  14. try {
  15. if ("GET".equals(request.getMethod())) {
  16. Map<String, String[]> params = request.getParameterMap();
  17. Map<String, String> query = params.isEmpty() ? null : convertParams(params);
  18. responseJson = tradingApiClient.get(String.class, request.getRequestURI(), authToken, query);
  19. } else if ("POST".equals(request.getMethod())) {
  20. responseJson = tradingApiClient.post(String.class, request.getRequestURI(), authToken,
  21. readBody(request));
  22. }
  23. // 写入响应:
  24. response.setContentType("application/json;charset=utf-8");
  25. PrintWriter pw = response.getWriter();
  26. pw.write(responseJson);
  27. pw.flush();
  28. } catch (ApiException e) {
  29. // 写入错误响应:
  30. writeApiException(request, response, e);
  31. } catch (Exception e) {
  32. // 写入错误响应:
  33. writeApiException(request, response,
  34. new ApiException(ApiError.INTERNAL_SERVER_ERROR, null, e.getMessage()));
  35. }
  36. }
  37. }

ProxyFilter挂载到/api/*,通过UI转发请求的目的是简化页面JavaScript调用API,一是不再需要跨域,二是UI已经经过了登录认证,转发过程中自动生成一次性Token来调用API,这样JavaScript不再关心如何生成Authorization头。

下面我们就可以开始编写页面了:

  • signin.html:登录页;
  • signup.html:注册页;
  • index.html:交易页。

页面功能主要由JavaScript实现,我们选择Vue前端框架,最终实现效果如下:

warpexchange

最后,在后台注册时,如果检测到本地开发环境,就自动调用内部API给用户添加一些资产,否则新注册用户无法交易。

参考源码

可以从GitHubGitee下载源码。

GitHubmichaelliaowarpexchange/

▸ build)

▸ bot)

▤ bot.py)

▤ README.md)

▤ sdk.py)

▸ sql)

▤ schema.sql)

▤ docker-compose.yml)

▤ pom.xml)

▸ common)

▸ src/main)

▸ java/com/itranswarp/exchange)

▸ bean)

▤ AuthToken.java)

▤ OrderBookBean.java)

▤ OrderBookItemBean.java)

▤ OrderRequestBean.java)

▤ SimpleMatchDetailRecord.java)

▤ TransferRequestBean.java)

▤ ValidatableBean.java)

▸ client)

▤ RestClient.java)

▸ config)

▤ ExchangeConfiguration.java)

▸ ctx)

▤ UserContext.java)

▸ db)

▤ AccessibleProperty.java)

▤ Criteria.java)

▤ CriteriaQuery.java)

▤ DbTemplate.java)

▤ From.java)

▤ Limit.java)

▤ Mapper.java)

▤ OrderBy.java)

▤ Select.java)

▤ Where.java)

▸ enums)

▤ AssetEnum.java)

▤ BarType.java)

▤ ClearingType.java)

▤ Direction.java)

▤ MatchType.java)

▤ OrderStatus.java)

▤ UserType.java)

▸ message)

▸ event)

▤ AbstractEvent.java)

▤ OrderCancelEvent.java)

▤ OrderRequestEvent.java)

▤ TransferEvent.java)

▤ AbstractMessage.java)

▤ ApiResultMessage.java)

▤ NotificationMessage.java)

▤ TickMessage.java)

▸ messaging)

▤ BatchMessageHandler.java)

▤ MessageConsumer.java)

▤ MessageProducer.java)

▤ MessageTypes.java)

▤ Messaging.java)

▤ MessagingConfiguration.java)

▤ MessagingFactory.java)

▸ model)

▸ quotation)

▤ DayBarEntity.java)

▤ HourBarEntity.java)

▤ MinBarEntity.java)

▤ SecBarEntity.java)

▤ TickEntity.java)

▸ support)

▤ AbstractBarEntity.java)

▤ EntitySupport.java)

▸ trade)

▤ ClearingEntity.java)

▤ EventEntity.java)

▤ MatchDetailEntity.java)

▤ OrderEntity.java)

▤ TransferLogEntity.java)

▤ UniqueEventEntity.java)

▸ ui)

▤ ApiKeyAuthEntity.java)

▤ PasswordAuthEntity.java)

▤ UserEntity.java)

▤ UserProfileEntity.java)

▸ redis)

▤ RedisCache.java)

▤ RedisConfiguration.java)

▤ RedisService.java)

▤ SyncCommandCallback.java)

▸ support)

▤ AbstractApiController.java)

▤ AbstractDbService.java)

▤ AbstractFilter.java)

▤ LoggerSupport.java)

▸ user)

▤ UserService.java)

▸ util)

▤ ByteUtil.java)

▤ ClassPathUtil.java)

▤ HashUtil.java)

▤ HttpUtil.java)

▤ IdUtil.java)

▤ IpUtil.java)

▤ JsonUtil.java)

▤ RandomUtil.java)

▤ ApiError.java)

▤ ApiErrorResponse.java)

▤ ApiException.java)

▤ SchemaExporter.java)

▸ resources)

▸ redis)

▤ update-bar.lua)

▤ update-orderbook.lua)

▤ update-recent-ticks.lua)

▤ logback-spring.xml)

▤ pom.xml)

▸ config)

▸ src/main)

▸ java/com/itranswarp/exchange/config)

▤ ConfigApplication.java)

▸ resources)

▤ application.yml)

▤ pom.xml)

▸ config-repo)

▤ application-default.yml)

▤ application-test.yml)

▤ application.yml)

▤ push.yml)

▤ quotation.yml)

▤ trading-api.yml)

▤ trading-engine.yml)

▤ trading-sequencer.yml)

▤ ui-default.yml)

▤ ui.yml)

▸ parent)

▤ pom.xml)

▸ push)

▸ src/main)

▸ java/com/itranswarp/exchange/push)

▤ PushApplication.java)

▤ PushService.java)

▤ PushVerticle.java)

▸ resources)

▤ application.yml)

▤ pom.xml)

▸ quotation)

▸ src/main)

▸ java/com/itranswarp/exchange)

▸ quotation)

▤ QuotationDbService.java)

▤ QuotationService.java)

▤ QuotationApplication.java)

▸ resources)

▤ application.yml)

▤ pom.xml)

▸ trading-api)

▸ src/main)

▸ java/com/itranswarp/exchange)

▸ service)

▤ HistoryService.java)

▤ SendEventService.java)

▤ TradingEngineApiProxyService.java)

▸ web)

▸ api)

▤ TradingApiController.java)

▤ TradingInternalApiController.java)

▤ ApiFilterRegistrationBean.java)

▤ TradingApiApplication.java)

▸ resources)

▤ application.yml)

▤ pom.xml)

▸ trading-engine)

▸ src)

▸ main)

▸ java/com/itranswarp/exchange)

▸ assets)

▤ Asset.java)

▤ AssetService.java)

▤ Transfer.java)

▸ clearing)

▤ ClearingService.java)

▸ match)

▤ MatchDetailRecord.java)

▤ MatchEngine.java)

▤ MatchResult.java)

▤ OrderBook.java)

▤ OrderKey.java)

▸ order)

▤ OrderService.java)

▸ store)

▤ StoreService.java)

▸ web/api)

▤ InternalTradingEngineApiController.java)

▤ TradingEngineApplication.java)

▤ TradingEngineService.java)

▸ resources)

▤ application.yml)

▸ test/java/com/itranswarp/exchange)

▸ assets)

▤ AssetServiceTest.java)

▸ match)

▤ MatchEngineTest.java)

▤ TradingEngineServiceTest.java)

▤ pom.xml)

▸ trading-sequencer)

▸ src/main)

▸ java/com/itranswarp/exchange)

▸ sequencer)

▤ SequenceHandler.java)

▤ SequenceService.java)

▤ TradingSequencerApplication.java)

▸ resources)

▤ application.yml)

▤ pom.xml)

▸ ui)

▸ src/main)

▸ java/com/itranswarp/exchange)

▸ ui/web)

▤ CookieService.java)

▤ MvcController.java)

▤ ProxyFilterRegistrationBean.java)

▤ UIFilterRegistrationBean.java)

▤ UIApplication.java)

▸ resources)

▸ static)

▤ favicon.ico)

▸ templates)

▤ _base.html)

▤ _inc_highlights.html)

▤ index.html)

▤ signin.html)

▤ signup.html)

▤ application.yml)

▤ pom.xml)

▤ .gitignore)

▤ LICENSE)

▤ README.md)

小结

UI系统是标准的Web系统,除了注册、登录外,主要交易功能均由页面JavaScript实现。UI系统本身不是交易入口,它通过转发JavaScript请求至真正的API入口。

读后有收获可以支付宝请作者喝咖啡:

编写UI - 图2