├── gateway ├── src │ └── main │ │ ├── resources │ │ ├── META-INF │ │ │ └── app.properties │ │ ├── static │ │ │ ├── images │ │ │ │ ├── icons.png │ │ │ │ ├── logo.gif │ │ │ │ ├── piggy.gif │ │ │ │ ├── github.gif │ │ │ │ ├── logo@2x.gif │ │ │ │ ├── sprites.png │ │ │ │ ├── userpic.jpg │ │ │ │ ├── icons@2x.png │ │ │ │ ├── logo_large.gif │ │ │ │ ├── logotext.gif │ │ │ │ ├── overview.png │ │ │ │ ├── piggy@2x.gif │ │ │ │ ├── preloader.gif │ │ │ │ ├── sprites@2x.png │ │ │ │ ├── 1pagesprites.png │ │ │ │ ├── logotext@2x.gif │ │ │ │ ├── piggy_large.gif │ │ │ │ ├── 1pagesprites@2x.png │ │ │ │ ├── linesbackground.png │ │ │ │ ├── logo_large@2x.gif │ │ │ │ ├── logotext_large.gif │ │ │ │ ├── piggy_large@2x.gif │ │ │ │ ├── logotext_large@2x.gif │ │ │ │ └── linesbackground@2x.png │ │ │ ├── fonts │ │ │ │ ├── museo-100 │ │ │ │ │ ├── museo-100.eot │ │ │ │ │ ├── museo-100.ttf │ │ │ │ │ └── museo-100.woff │ │ │ │ ├── museo-300 │ │ │ │ │ ├── museo-300.eot │ │ │ │ │ ├── museo-300.ttf │ │ │ │ │ └── museo-300.woff │ │ │ │ └── museo-500 │ │ │ │ │ ├── museo-500.eot │ │ │ │ │ ├── museo-500.ttf │ │ │ │ │ └── museo-500.woff │ │ │ ├── attribution.html │ │ │ └── js │ │ │ │ ├── launch.js │ │ │ │ └── login.js │ │ └── application.properties │ │ └── java │ │ └── io │ │ └── spring2go │ │ ├── cathelper │ │ ├── CatHttpConstants.java │ │ ├── CatContext.java │ │ ├── CatRestInterceptor.java │ │ └── CatServletFilter.java │ │ └── piggymetrics │ │ └── gateway │ │ ├── CatFilterConfigure.java │ │ ├── GatewayApplication.java │ │ └── filter │ │ ├── CatHeaderFilter.java │ │ ├── ValidationConfig.java │ │ └── ValidateTokenFilter.java └── pom.xml ├── registry ├── src │ └── main │ │ ├── resources │ │ ├── META-INF │ │ │ └── app.properties │ │ └── application.properties │ │ └── java │ │ └── io │ │ └── spring2go │ │ ├── cathelper │ │ ├── CatHttpConstants.java │ │ ├── CatContext.java │ │ ├── CatRestInterceptor.java │ │ └── CatServletFilter.java │ │ └── piggymetrics │ │ └── registry │ │ ├── RegistryApplication.java │ │ └── CatFilterConfigure.java └── pom.xml ├── account-service ├── src │ ├── main │ │ ├── resources │ │ │ ├── META-INF │ │ │ │ └── app.properties │ │ │ └── application.properties │ │ └── java │ │ │ └── io │ │ │ └── spring2go │ │ │ ├── piggymetrics │ │ │ └── account │ │ │ │ ├── domain │ │ │ │ ├── TimePeriod.java │ │ │ │ ├── Currency.java │ │ │ │ ├── User.java │ │ │ │ ├── Item.java │ │ │ │ ├── Saving.java │ │ │ │ └── Account.java │ │ │ │ ├── CatAnnotation.java │ │ │ │ ├── repository │ │ │ │ └── AccountRepository.java │ │ │ │ ├── client │ │ │ │ ├── StatisticsServiceClientFallback.java │ │ │ │ ├── UserServiceClient.java │ │ │ │ └── StatisticsServiceClient.java │ │ │ │ ├── AccountApplication.java │ │ │ │ ├── CatFilterConfigure.java │ │ │ │ ├── controller │ │ │ │ ├── ErrorHandler.java │ │ │ │ └── AccountController.java │ │ │ │ ├── service │ │ │ │ ├── AccountService.java │ │ │ │ └── AccountServiceImpl.java │ │ │ │ └── CatAopService.java │ │ │ └── cathelper │ │ │ ├── CatHttpConstants.java │ │ │ ├── CatContext.java │ │ │ ├── CatRestInterceptor.java │ │ │ └── CatServletFilter.java │ └── test │ │ ├── resources │ │ └── application.properties │ │ └── java │ │ └── io │ │ └── spring2go │ │ └── piggymetrics │ │ └── account │ │ ├── AccountServiceApplicationTests.java │ │ ├── client │ │ └── StatisticsServiceClientFallbackTest.java │ │ ├── repository │ │ └── AccountRepositoryTest.java │ │ ├── controller │ │ └── AccountControllerTest.java │ │ └── service │ │ └── AccountServiceTest.java └── pom.xml ├── statistics-service ├── src │ ├── main │ │ ├── resources │ │ │ ├── META-INF │ │ │ │ └── app.properties │ │ │ └── application.properties │ │ └── java │ │ │ └── io │ │ │ └── spring2go │ │ │ ├── piggymetrics │ │ │ └── statistics │ │ │ │ ├── domain │ │ │ │ ├── timeseries │ │ │ │ │ ├── StatisticMetric.java │ │ │ │ │ ├── DataPointId.java │ │ │ │ │ ├── ItemMetric.java │ │ │ │ │ └── DataPoint.java │ │ │ │ ├── Currency.java │ │ │ │ ├── TimePeriod.java │ │ │ │ ├── Item.java │ │ │ │ ├── Account.java │ │ │ │ ├── ExchangeRatesContainer.java │ │ │ │ └── Saving.java │ │ │ │ ├── CatAnnotation.java │ │ │ │ ├── repository │ │ │ │ ├── DataPointRepository.java │ │ │ │ └── converter │ │ │ │ │ ├── DataPointIdReaderConverter.java │ │ │ │ │ └── DataPointIdWriterConverter.java │ │ │ │ ├── client │ │ │ │ ├── ExchangeRatesClientFallback.java │ │ │ │ └── ExchangeRatesClient.java │ │ │ │ ├── CatFilterConfigure.java │ │ │ │ ├── service │ │ │ │ ├── ExchangeRatesService.java │ │ │ │ ├── StatisticsService.java │ │ │ │ ├── ExchangeRatesServiceImpl.java │ │ │ │ └── StatisticsServiceImpl.java │ │ │ │ ├── CatAopService.java │ │ │ │ ├── controller │ │ │ │ └── StatisticsController.java │ │ │ │ └── StatisticsApplication.java │ │ │ └── cathelper │ │ │ ├── CatHttpConstants.java │ │ │ ├── CatContext.java │ │ │ ├── CatRestInterceptor.java │ │ │ └── CatServletFilter.java │ └── test │ │ ├── resources │ │ └── application.properties │ │ └── java │ │ └── io │ │ └── spring2go │ │ └── piggymetrics │ │ └── statistics │ │ ├── StatisticsServiceApplicationTests.java │ │ ├── client │ │ └── ExchangeRatesClientTest.java │ │ ├── service │ │ └── ExchangeRatesServiceImplTest.java │ │ ├── repository │ │ └── DataPointRepositoryTest.java │ │ └── controller │ │ └── StatisticsControllerTest.java └── pom.xml ├── notification-service ├── src │ ├── main │ │ ├── resources │ │ │ ├── META-INF │ │ │ │ └── app.properties │ │ │ └── application.properties │ │ └── java │ │ │ └── io │ │ │ └── spring2go │ │ │ ├── piggymetrics │ │ │ └── notification │ │ │ │ ├── service │ │ │ │ ├── NotificationService.java │ │ │ │ ├── EmailService.java │ │ │ │ ├── RecipientService.java │ │ │ │ ├── EmailServiceImpl.java │ │ │ │ ├── RecipientServiceImpl.java │ │ │ │ └── NotificationServiceImpl.java │ │ │ │ ├── CatAnnotation.java │ │ │ │ ├── repository │ │ │ │ ├── converter │ │ │ │ │ ├── FrequencyReaderConverter.java │ │ │ │ │ └── FrequencyWriterConverter.java │ │ │ │ └── RecipientRepository.java │ │ │ │ ├── domain │ │ │ │ ├── Frequency.java │ │ │ │ ├── NotificationType.java │ │ │ │ ├── NotificationSettings.java │ │ │ │ └── Recipient.java │ │ │ │ ├── client │ │ │ │ └── AccountServiceClient.java │ │ │ │ ├── CatFilterConfigure.java │ │ │ │ ├── CatAopService.java │ │ │ │ ├── controller │ │ │ │ └── RecipientController.java │ │ │ │ └── NotificationServiceApplication.java │ │ │ └── cathelper │ │ │ ├── CatHttpConstants.java │ │ │ ├── CatContext.java │ │ │ ├── CatRestInterceptor.java │ │ │ └── CatServletFilter.java │ └── test │ │ ├── java │ │ └── io │ │ │ └── spring2go │ │ │ └── piggymetrics │ │ │ └── notification │ │ │ ├── NotificationServiceApplicationTests.java │ │ │ ├── service │ │ │ ├── EmailServiceImplTest.java │ │ │ ├── NotificationServiceImplTest.java │ │ │ └── RecipientServiceImplTest.java │ │ │ ├── controller │ │ │ └── RecipientControllerTest.java │ │ │ └── repository │ │ │ └── RecipientRepositoryTest.java │ │ └── resources │ │ └── application.properties └── pom.xml ├── images ├── apicall.png ├── biz_arch.png ├── reglogin.png ├── tech_arch.png ├── custom_arch.png └── piggymetrics.png ├── config ├── registry.properties ├── statistics-service.properties ├── account-service.properties ├── gateway.properties └── notification-service.properties ├── .gitignore ├── LICENSE ├── mongodb └── seed.js ├── pom.xml └── README.md /gateway/src/main/resources/META-INF/app.properties: -------------------------------------------------------------------------------- 1 | app.name=gateway -------------------------------------------------------------------------------- /registry/src/main/resources/META-INF/app.properties: -------------------------------------------------------------------------------- 1 | app.name=registry -------------------------------------------------------------------------------- /account-service/src/main/resources/META-INF/app.properties: -------------------------------------------------------------------------------- 1 | app.name=account-service -------------------------------------------------------------------------------- /statistics-service/src/main/resources/META-INF/app.properties: -------------------------------------------------------------------------------- 1 | app.name=statistics-service -------------------------------------------------------------------------------- /notification-service/src/main/resources/META-INF/app.properties: -------------------------------------------------------------------------------- 1 | app.name=notification-service -------------------------------------------------------------------------------- /images/apicall.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bootcs-cn/piggymetrics/HEAD/images/apicall.png -------------------------------------------------------------------------------- /images/biz_arch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bootcs-cn/piggymetrics/HEAD/images/biz_arch.png -------------------------------------------------------------------------------- /images/reglogin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bootcs-cn/piggymetrics/HEAD/images/reglogin.png -------------------------------------------------------------------------------- /images/tech_arch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bootcs-cn/piggymetrics/HEAD/images/tech_arch.png -------------------------------------------------------------------------------- /images/custom_arch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bootcs-cn/piggymetrics/HEAD/images/custom_arch.png -------------------------------------------------------------------------------- /images/piggymetrics.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bootcs-cn/piggymetrics/HEAD/images/piggymetrics.png -------------------------------------------------------------------------------- /gateway/src/main/resources/static/images/icons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bootcs-cn/piggymetrics/HEAD/gateway/src/main/resources/static/images/icons.png -------------------------------------------------------------------------------- /gateway/src/main/resources/static/images/logo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bootcs-cn/piggymetrics/HEAD/gateway/src/main/resources/static/images/logo.gif -------------------------------------------------------------------------------- /gateway/src/main/resources/static/images/piggy.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bootcs-cn/piggymetrics/HEAD/gateway/src/main/resources/static/images/piggy.gif -------------------------------------------------------------------------------- /gateway/src/main/resources/static/images/github.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bootcs-cn/piggymetrics/HEAD/gateway/src/main/resources/static/images/github.gif -------------------------------------------------------------------------------- /gateway/src/main/resources/static/images/logo@2x.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bootcs-cn/piggymetrics/HEAD/gateway/src/main/resources/static/images/logo@2x.gif -------------------------------------------------------------------------------- /gateway/src/main/resources/static/images/sprites.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bootcs-cn/piggymetrics/HEAD/gateway/src/main/resources/static/images/sprites.png -------------------------------------------------------------------------------- /gateway/src/main/resources/static/images/userpic.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bootcs-cn/piggymetrics/HEAD/gateway/src/main/resources/static/images/userpic.jpg -------------------------------------------------------------------------------- /account-service/src/test/resources/application.properties: -------------------------------------------------------------------------------- 1 | eureka.client.enabled=false 2 | spring.data.mongodb.database=piggymetrics 3 | spring.data.mongodb.port=0 4 | -------------------------------------------------------------------------------- /gateway/src/main/resources/static/images/icons@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bootcs-cn/piggymetrics/HEAD/gateway/src/main/resources/static/images/icons@2x.png -------------------------------------------------------------------------------- /gateway/src/main/resources/static/images/logo_large.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bootcs-cn/piggymetrics/HEAD/gateway/src/main/resources/static/images/logo_large.gif -------------------------------------------------------------------------------- /gateway/src/main/resources/static/images/logotext.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bootcs-cn/piggymetrics/HEAD/gateway/src/main/resources/static/images/logotext.gif -------------------------------------------------------------------------------- /gateway/src/main/resources/static/images/overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bootcs-cn/piggymetrics/HEAD/gateway/src/main/resources/static/images/overview.png -------------------------------------------------------------------------------- /gateway/src/main/resources/static/images/piggy@2x.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bootcs-cn/piggymetrics/HEAD/gateway/src/main/resources/static/images/piggy@2x.gif -------------------------------------------------------------------------------- /gateway/src/main/resources/static/images/preloader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bootcs-cn/piggymetrics/HEAD/gateway/src/main/resources/static/images/preloader.gif -------------------------------------------------------------------------------- /gateway/src/main/resources/static/images/sprites@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bootcs-cn/piggymetrics/HEAD/gateway/src/main/resources/static/images/sprites@2x.png -------------------------------------------------------------------------------- /gateway/src/main/resources/static/images/1pagesprites.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bootcs-cn/piggymetrics/HEAD/gateway/src/main/resources/static/images/1pagesprites.png -------------------------------------------------------------------------------- /gateway/src/main/resources/static/images/logotext@2x.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bootcs-cn/piggymetrics/HEAD/gateway/src/main/resources/static/images/logotext@2x.gif -------------------------------------------------------------------------------- /gateway/src/main/resources/static/images/piggy_large.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bootcs-cn/piggymetrics/HEAD/gateway/src/main/resources/static/images/piggy_large.gif -------------------------------------------------------------------------------- /gateway/src/main/resources/static/images/1pagesprites@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bootcs-cn/piggymetrics/HEAD/gateway/src/main/resources/static/images/1pagesprites@2x.png -------------------------------------------------------------------------------- /gateway/src/main/resources/static/images/linesbackground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bootcs-cn/piggymetrics/HEAD/gateway/src/main/resources/static/images/linesbackground.png -------------------------------------------------------------------------------- /gateway/src/main/resources/static/images/logo_large@2x.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bootcs-cn/piggymetrics/HEAD/gateway/src/main/resources/static/images/logo_large@2x.gif -------------------------------------------------------------------------------- /gateway/src/main/resources/static/images/logotext_large.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bootcs-cn/piggymetrics/HEAD/gateway/src/main/resources/static/images/logotext_large.gif -------------------------------------------------------------------------------- /gateway/src/main/resources/static/images/piggy_large@2x.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bootcs-cn/piggymetrics/HEAD/gateway/src/main/resources/static/images/piggy_large@2x.gif -------------------------------------------------------------------------------- /gateway/src/main/resources/static/images/logotext_large@2x.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bootcs-cn/piggymetrics/HEAD/gateway/src/main/resources/static/images/logotext_large@2x.gif -------------------------------------------------------------------------------- /gateway/src/main/resources/static/fonts/museo-100/museo-100.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bootcs-cn/piggymetrics/HEAD/gateway/src/main/resources/static/fonts/museo-100/museo-100.eot -------------------------------------------------------------------------------- /gateway/src/main/resources/static/fonts/museo-100/museo-100.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bootcs-cn/piggymetrics/HEAD/gateway/src/main/resources/static/fonts/museo-100/museo-100.ttf -------------------------------------------------------------------------------- /gateway/src/main/resources/static/fonts/museo-100/museo-100.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bootcs-cn/piggymetrics/HEAD/gateway/src/main/resources/static/fonts/museo-100/museo-100.woff -------------------------------------------------------------------------------- /gateway/src/main/resources/static/fonts/museo-300/museo-300.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bootcs-cn/piggymetrics/HEAD/gateway/src/main/resources/static/fonts/museo-300/museo-300.eot -------------------------------------------------------------------------------- /gateway/src/main/resources/static/fonts/museo-300/museo-300.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bootcs-cn/piggymetrics/HEAD/gateway/src/main/resources/static/fonts/museo-300/museo-300.ttf -------------------------------------------------------------------------------- /gateway/src/main/resources/static/fonts/museo-300/museo-300.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bootcs-cn/piggymetrics/HEAD/gateway/src/main/resources/static/fonts/museo-300/museo-300.woff -------------------------------------------------------------------------------- /gateway/src/main/resources/static/fonts/museo-500/museo-500.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bootcs-cn/piggymetrics/HEAD/gateway/src/main/resources/static/fonts/museo-500/museo-500.eot -------------------------------------------------------------------------------- /gateway/src/main/resources/static/fonts/museo-500/museo-500.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bootcs-cn/piggymetrics/HEAD/gateway/src/main/resources/static/fonts/museo-500/museo-500.ttf -------------------------------------------------------------------------------- /gateway/src/main/resources/static/fonts/museo-500/museo-500.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bootcs-cn/piggymetrics/HEAD/gateway/src/main/resources/static/fonts/museo-500/museo-500.woff -------------------------------------------------------------------------------- /gateway/src/main/resources/static/images/linesbackground@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bootcs-cn/piggymetrics/HEAD/gateway/src/main/resources/static/images/linesbackground@2x.png -------------------------------------------------------------------------------- /account-service/src/main/java/io/spring2go/piggymetrics/account/domain/TimePeriod.java: -------------------------------------------------------------------------------- 1 | package io.spring2go.piggymetrics.account.domain; 2 | 3 | public enum TimePeriod { 4 | 5 | YEAR, QUARTER, MONTH, DAY, HOUR 6 | 7 | } 8 | -------------------------------------------------------------------------------- /statistics-service/src/main/java/io/spring2go/piggymetrics/statistics/domain/timeseries/StatisticMetric.java: -------------------------------------------------------------------------------- 1 | package io.spring2go.piggymetrics.statistics.domain.timeseries; 2 | 3 | public enum StatisticMetric { 4 | 5 | INCOMES_AMOUNT, EXPENSES_AMOUNT, SAVING_AMOUNT 6 | 7 | } 8 | -------------------------------------------------------------------------------- /account-service/src/main/java/io/spring2go/piggymetrics/account/domain/Currency.java: -------------------------------------------------------------------------------- 1 | package io.spring2go.piggymetrics.account.domain; 2 | 3 | public enum Currency { 4 | 5 | USD, EUR, RUB; 6 | 7 | public static Currency getDefault() { 8 | return USD; 9 | } 10 | } 11 | 12 | -------------------------------------------------------------------------------- /statistics-service/src/main/java/io/spring2go/piggymetrics/statistics/domain/Currency.java: -------------------------------------------------------------------------------- 1 | package io.spring2go.piggymetrics.statistics.domain; 2 | 3 | public enum Currency { 4 | 5 | USD, EUR, RUB; 6 | 7 | public static Currency getBase() { 8 | return USD; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /notification-service/src/main/java/io/spring2go/piggymetrics/notification/service/NotificationService.java: -------------------------------------------------------------------------------- 1 | package io.spring2go.piggymetrics.notification.service; 2 | 3 | public interface NotificationService { 4 | 5 | void sendBackupNotifications(); 6 | 7 | void sendRemindNotifications(); 8 | } 9 | -------------------------------------------------------------------------------- /statistics-service/src/test/resources/application.properties: -------------------------------------------------------------------------------- 1 | eureka.client.enabled=false 2 | 3 | hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=20000 4 | 5 | spring.data.mongodb.database=piggymetrics 6 | spring.data.mongodb.port=0 7 | 8 | rates.url = https://api.exchangeratesapi.io -------------------------------------------------------------------------------- /config/registry.properties: -------------------------------------------------------------------------------- 1 | spring.application.name = registry 2 | logging.level.org.spring.framework.security=INFO 3 | eureka.instance.prefer-ip-address = true 4 | eureka.client.registerWithEureka = false 5 | eureka.client.fetchRegistry = false 6 | eureka.server.waitTimeInMsWhenSyncEmpty = 0 7 | server.port = 8761 8 | -------------------------------------------------------------------------------- /gateway/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | app.id=gateway 2 | # set apollo meta server address, adjust to actual address if necessary 3 | apollo.meta=http://localhost:8080 4 | 5 | # will inject 'application' namespace in bootstrap phase 6 | apollo.bootstrap.enabled = true 7 | 8 | spring.main.allow-bean-definition-overriding=true -------------------------------------------------------------------------------- /registry/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | app.id=registry 2 | # set apollo meta server address, adjust to actual address if necessary 3 | apollo.meta=http://localhost:8080 4 | 5 | # will inject 'application' namespace in bootstrap phase 6 | apollo.bootstrap.enabled = true 7 | 8 | spring.main.allow-bean-definition-overriding=true -------------------------------------------------------------------------------- /account-service/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | app.id=account-service 2 | # set apollo meta server address, adjust to actual address if necessary 3 | apollo.meta=http://localhost:8080 4 | 5 | # will inject 'application' namespace in bootstrap phase 6 | apollo.bootstrap.enabled = true 7 | 8 | spring.main.allow-bean-definition-overriding=true -------------------------------------------------------------------------------- /statistics-service/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | app.id=statistics-service 2 | # set apollo meta server address, adjust to actual address if necessary 3 | apollo.meta=http://localhost:8080 4 | 5 | # will inject 'application' namespace in bootstrap phase 6 | apollo.bootstrap.enabled = true 7 | 8 | spring.main.allow-bean-definition-overriding=true -------------------------------------------------------------------------------- /notification-service/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | app.id=notification-service 2 | # set apollo meta server address, adjust to actual address if necessary 3 | apollo.meta=http://localhost:8080 4 | 5 | # will inject 'application' namespace in bootstrap phase 6 | apollo.bootstrap.enabled = true 7 | 8 | spring.main.allow-bean-definition-overriding=true -------------------------------------------------------------------------------- /gateway/src/main/java/io/spring2go/cathelper/CatHttpConstants.java: -------------------------------------------------------------------------------- 1 | package io.spring2go.cathelper; 2 | 3 | public class CatHttpConstants { 4 | public static final String CAT_HTTP_HEADER_CHILD_MESSAGE_ID = "X-CAT-CHILD-ID"; 5 | public static final String CAT_HTTP_HEADER_PARENT_MESSAGE_ID = "X-CAT-PARENT-ID"; 6 | public static final String CAT_HTTP_HEADER_ROOT_MESSAGE_ID = "X-CAT-ROOT-ID"; 7 | } 8 | -------------------------------------------------------------------------------- /registry/src/main/java/io/spring2go/cathelper/CatHttpConstants.java: -------------------------------------------------------------------------------- 1 | package io.spring2go.cathelper; 2 | 3 | public class CatHttpConstants { 4 | public static final String CAT_HTTP_HEADER_CHILD_MESSAGE_ID = "X-CAT-CHILD-ID"; 5 | public static final String CAT_HTTP_HEADER_PARENT_MESSAGE_ID = "X-CAT-PARENT-ID"; 6 | public static final String CAT_HTTP_HEADER_ROOT_MESSAGE_ID = "X-CAT-ROOT-ID"; 7 | } 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | !.mvn/wrapper/maven-wrapper.jar 3 | 4 | ### STS ### 5 | .apt_generated 6 | .classpath 7 | .factorypath 8 | .project 9 | .settings 10 | .springBeans 11 | .sts4-cache 12 | 13 | ### IntelliJ IDEA ### 14 | .idea 15 | *.iws 16 | *.iml 17 | *.ipr 18 | 19 | ### NetBeans ### 20 | /nbproject/private/ 21 | /build/ 22 | /nbbuild/ 23 | /dist/ 24 | /nbdist/ 25 | /.nb-gradle/ 26 | 27 | -------------------------------------------------------------------------------- /account-service/src/main/java/io/spring2go/cathelper/CatHttpConstants.java: -------------------------------------------------------------------------------- 1 | package io.spring2go.cathelper; 2 | 3 | public class CatHttpConstants { 4 | public static final String CAT_HTTP_HEADER_CHILD_MESSAGE_ID = "X-CAT-CHILD-ID"; 5 | public static final String CAT_HTTP_HEADER_PARENT_MESSAGE_ID = "X-CAT-PARENT-ID"; 6 | public static final String CAT_HTTP_HEADER_ROOT_MESSAGE_ID = "X-CAT-ROOT-ID"; 7 | } 8 | -------------------------------------------------------------------------------- /notification-service/src/main/java/io/spring2go/cathelper/CatHttpConstants.java: -------------------------------------------------------------------------------- 1 | package io.spring2go.cathelper; 2 | 3 | public class CatHttpConstants { 4 | public static final String CAT_HTTP_HEADER_CHILD_MESSAGE_ID = "X-CAT-CHILD-ID"; 5 | public static final String CAT_HTTP_HEADER_PARENT_MESSAGE_ID = "X-CAT-PARENT-ID"; 6 | public static final String CAT_HTTP_HEADER_ROOT_MESSAGE_ID = "X-CAT-ROOT-ID"; 7 | } 8 | -------------------------------------------------------------------------------- /statistics-service/src/main/java/io/spring2go/cathelper/CatHttpConstants.java: -------------------------------------------------------------------------------- 1 | package io.spring2go.cathelper; 2 | 3 | public class CatHttpConstants { 4 | public static final String CAT_HTTP_HEADER_CHILD_MESSAGE_ID = "X-CAT-CHILD-ID"; 5 | public static final String CAT_HTTP_HEADER_PARENT_MESSAGE_ID = "X-CAT-PARENT-ID"; 6 | public static final String CAT_HTTP_HEADER_ROOT_MESSAGE_ID = "X-CAT-ROOT-ID"; 7 | } 8 | -------------------------------------------------------------------------------- /account-service/src/main/java/io/spring2go/piggymetrics/account/CatAnnotation.java: -------------------------------------------------------------------------------- 1 | package io.spring2go.piggymetrics.account; 2 | 3 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 4 | 5 | import java.lang.annotation.ElementType; 6 | import java.lang.annotation.Retention; 7 | import java.lang.annotation.Target; 8 | 9 | @Retention(RUNTIME) 10 | @Target(ElementType.METHOD) 11 | public @interface CatAnnotation { 12 | } -------------------------------------------------------------------------------- /statistics-service/src/main/java/io/spring2go/piggymetrics/statistics/CatAnnotation.java: -------------------------------------------------------------------------------- 1 | package io.spring2go.piggymetrics.statistics; 2 | 3 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 4 | 5 | import java.lang.annotation.ElementType; 6 | import java.lang.annotation.Retention; 7 | import java.lang.annotation.Target; 8 | 9 | @Retention(RUNTIME) 10 | @Target(ElementType.METHOD) 11 | public @interface CatAnnotation { 12 | } -------------------------------------------------------------------------------- /notification-service/src/main/java/io/spring2go/piggymetrics/notification/CatAnnotation.java: -------------------------------------------------------------------------------- 1 | package io.spring2go.piggymetrics.notification; 2 | 3 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 4 | 5 | import java.lang.annotation.ElementType; 6 | import java.lang.annotation.Retention; 7 | import java.lang.annotation.Target; 8 | 9 | @Retention(RUNTIME) 10 | @Target(ElementType.METHOD) 11 | public @interface CatAnnotation { 12 | } -------------------------------------------------------------------------------- /account-service/src/main/java/io/spring2go/piggymetrics/account/repository/AccountRepository.java: -------------------------------------------------------------------------------- 1 | package io.spring2go.piggymetrics.account.repository; 2 | 3 | import io.spring2go.piggymetrics.account.domain.Account; 4 | import org.springframework.data.repository.CrudRepository; 5 | import org.springframework.stereotype.Repository; 6 | 7 | @Repository 8 | public interface AccountRepository extends CrudRepository { 9 | 10 | Account findByName(String name); 11 | 12 | } 13 | -------------------------------------------------------------------------------- /account-service/src/test/java/io/spring2go/piggymetrics/account/AccountServiceApplicationTests.java: -------------------------------------------------------------------------------- 1 | package io.spring2go.piggymetrics.account; 2 | 3 | import org.junit.Test; 4 | import org.junit.runner.RunWith; 5 | import org.springframework.boot.test.context.SpringBootTest; 6 | import org.springframework.test.context.junit4.SpringRunner; 7 | 8 | @RunWith(SpringRunner.class) 9 | @SpringBootTest 10 | public class AccountServiceApplicationTests { 11 | 12 | @Test 13 | public void contextLoads() { 14 | 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /statistics-service/src/test/java/io/spring2go/piggymetrics/statistics/StatisticsServiceApplicationTests.java: -------------------------------------------------------------------------------- 1 | package io.spring2go.piggymetrics.statistics; 2 | 3 | import org.junit.Test; 4 | import org.junit.runner.RunWith; 5 | import org.springframework.boot.test.context.SpringBootTest; 6 | import org.springframework.test.context.junit4.SpringRunner; 7 | 8 | @RunWith(SpringRunner.class) 9 | @SpringBootTest 10 | public class StatisticsServiceApplicationTests { 11 | 12 | @Test 13 | public void contextLoads() { 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /notification-service/src/test/java/io/spring2go/piggymetrics/notification/NotificationServiceApplicationTests.java: -------------------------------------------------------------------------------- 1 | package io.spring2go.piggymetrics.notification; 2 | 3 | import org.junit.Test; 4 | import org.junit.runner.RunWith; 5 | import org.springframework.boot.test.context.SpringBootTest; 6 | import org.springframework.test.context.junit4.SpringRunner; 7 | 8 | @RunWith(SpringRunner.class) 9 | @SpringBootTest 10 | public class NotificationServiceApplicationTests { 11 | 12 | @Test 13 | public void contextLoads() { 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /notification-service/src/main/java/io/spring2go/piggymetrics/notification/service/EmailService.java: -------------------------------------------------------------------------------- 1 | package io.spring2go.piggymetrics.notification.service; 2 | 3 | import io.spring2go.piggymetrics.notification.domain.NotificationType; 4 | import io.spring2go.piggymetrics.notification.domain.Recipient; 5 | 6 | import javax.mail.MessagingException; 7 | import java.io.IOException; 8 | 9 | public interface EmailService { 10 | 11 | void send(NotificationType type, Recipient recipient, String attachment) throws MessagingException, IOException; 12 | 13 | } 14 | -------------------------------------------------------------------------------- /gateway/src/main/java/io/spring2go/cathelper/CatContext.java: -------------------------------------------------------------------------------- 1 | package io.spring2go.cathelper; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | 6 | import com.dianping.cat.Cat; 7 | 8 | public class CatContext implements Cat.Context { 9 | 10 | private Map properties = new HashMap<>(); 11 | 12 | @Override 13 | public void addProperty(String key, String value) { 14 | properties.put(key, value); 15 | } 16 | 17 | @Override 18 | public String getProperty(String key) { 19 | return properties.get(key); 20 | } 21 | } -------------------------------------------------------------------------------- /registry/src/main/java/io/spring2go/cathelper/CatContext.java: -------------------------------------------------------------------------------- 1 | package io.spring2go.cathelper; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | 6 | import com.dianping.cat.Cat; 7 | 8 | public class CatContext implements Cat.Context { 9 | 10 | private Map properties = new HashMap<>(); 11 | 12 | @Override 13 | public void addProperty(String key, String value) { 14 | properties.put(key, value); 15 | } 16 | 17 | @Override 18 | public String getProperty(String key) { 19 | return properties.get(key); 20 | } 21 | } -------------------------------------------------------------------------------- /account-service/src/main/java/io/spring2go/cathelper/CatContext.java: -------------------------------------------------------------------------------- 1 | package io.spring2go.cathelper; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | 6 | import com.dianping.cat.Cat; 7 | 8 | public class CatContext implements Cat.Context { 9 | 10 | private Map properties = new HashMap<>(); 11 | 12 | @Override 13 | public void addProperty(String key, String value) { 14 | properties.put(key, value); 15 | } 16 | 17 | @Override 18 | public String getProperty(String key) { 19 | return properties.get(key); 20 | } 21 | } -------------------------------------------------------------------------------- /notification-service/src/main/java/io/spring2go/cathelper/CatContext.java: -------------------------------------------------------------------------------- 1 | package io.spring2go.cathelper; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | 6 | import com.dianping.cat.Cat; 7 | 8 | public class CatContext implements Cat.Context { 9 | 10 | private Map properties = new HashMap<>(); 11 | 12 | @Override 13 | public void addProperty(String key, String value) { 14 | properties.put(key, value); 15 | } 16 | 17 | @Override 18 | public String getProperty(String key) { 19 | return properties.get(key); 20 | } 21 | } -------------------------------------------------------------------------------- /statistics-service/src/main/java/io/spring2go/cathelper/CatContext.java: -------------------------------------------------------------------------------- 1 | package io.spring2go.cathelper; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | 6 | import com.dianping.cat.Cat; 7 | 8 | public class CatContext implements Cat.Context { 9 | 10 | private Map properties = new HashMap<>(); 11 | 12 | @Override 13 | public void addProperty(String key, String value) { 14 | properties.put(key, value); 15 | } 16 | 17 | @Override 18 | public String getProperty(String key) { 19 | return properties.get(key); 20 | } 21 | } -------------------------------------------------------------------------------- /statistics-service/src/main/java/io/spring2go/piggymetrics/statistics/domain/TimePeriod.java: -------------------------------------------------------------------------------- 1 | package io.spring2go.piggymetrics.statistics.domain; 2 | 3 | import java.math.BigDecimal; 4 | 5 | public enum TimePeriod { 6 | 7 | YEAR(365.2425), QUARTER(91.3106), MONTH(30.4368), DAY(1), HOUR(0.0416); 8 | 9 | private double baseRatio; 10 | 11 | TimePeriod(double baseRatio) { 12 | this.baseRatio = baseRatio; 13 | } 14 | 15 | public BigDecimal getBaseRatio() { 16 | return new BigDecimal(baseRatio); 17 | } 18 | 19 | public static TimePeriod getBase() { 20 | return DAY; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /notification-service/src/main/java/io/spring2go/piggymetrics/notification/repository/converter/FrequencyReaderConverter.java: -------------------------------------------------------------------------------- 1 | package io.spring2go.piggymetrics.notification.repository.converter; 2 | 3 | import io.spring2go.piggymetrics.notification.domain.Frequency; 4 | import org.springframework.core.convert.converter.Converter; 5 | import org.springframework.stereotype.Component; 6 | 7 | @Component 8 | public class FrequencyReaderConverter implements Converter { 9 | 10 | @Override 11 | public Frequency convert(Integer days) { 12 | return Frequency.withDays(days); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /notification-service/src/main/java/io/spring2go/piggymetrics/notification/repository/converter/FrequencyWriterConverter.java: -------------------------------------------------------------------------------- 1 | package io.spring2go.piggymetrics.notification.repository.converter; 2 | 3 | import io.spring2go.piggymetrics.notification.domain.Frequency; 4 | import org.springframework.core.convert.converter.Converter; 5 | import org.springframework.stereotype.Component; 6 | 7 | @Component 8 | public class FrequencyWriterConverter implements Converter { 9 | 10 | @Override 11 | public Integer convert(Frequency frequency) { 12 | return frequency.getDays(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /notification-service/src/main/java/io/spring2go/piggymetrics/notification/domain/Frequency.java: -------------------------------------------------------------------------------- 1 | package io.spring2go.piggymetrics.notification.domain; 2 | 3 | import java.util.stream.Stream; 4 | 5 | public enum Frequency { 6 | 7 | WEEKLY(7), MONTHLY(30), QUARTERLY(90); 8 | 9 | private int days; 10 | 11 | Frequency(int days) { 12 | this.days = days; 13 | } 14 | 15 | public int getDays() { 16 | return days; 17 | } 18 | 19 | public static Frequency withDays(int days) { 20 | return Stream.of(Frequency.values()) 21 | .filter(f -> f.getDays() == days) 22 | .findFirst() 23 | .orElseThrow(IllegalArgumentException::new); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /registry/src/main/java/io/spring2go/piggymetrics/registry/RegistryApplication.java: -------------------------------------------------------------------------------- 1 | package io.spring2go.piggymetrics.registry; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer; 6 | 7 | import com.ctrip.framework.apollo.spring.annotation.EnableApolloConfig; 8 | 9 | @SpringBootApplication 10 | @EnableEurekaServer 11 | @EnableApolloConfig 12 | public class RegistryApplication { 13 | 14 | public static void main(String[] args) { 15 | SpringApplication.run(RegistryApplication.class, args); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /statistics-service/src/main/java/io/spring2go/piggymetrics/statistics/repository/DataPointRepository.java: -------------------------------------------------------------------------------- 1 | package io.spring2go.piggymetrics.statistics.repository; 2 | 3 | import io.spring2go.piggymetrics.statistics.CatAnnotation; 4 | import io.spring2go.piggymetrics.statistics.domain.timeseries.DataPoint; 5 | import io.spring2go.piggymetrics.statistics.domain.timeseries.DataPointId; 6 | import org.springframework.data.repository.CrudRepository; 7 | import org.springframework.stereotype.Repository; 8 | 9 | import java.util.List; 10 | 11 | @Repository 12 | public interface DataPointRepository extends CrudRepository { 13 | 14 | @CatAnnotation 15 | List findByIdAccount(String account); 16 | 17 | } 18 | -------------------------------------------------------------------------------- /account-service/src/main/java/io/spring2go/piggymetrics/account/client/StatisticsServiceClientFallback.java: -------------------------------------------------------------------------------- 1 | package io.spring2go.piggymetrics.account.client; 2 | 3 | import io.spring2go.piggymetrics.account.domain.Account; 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | import org.springframework.stereotype.Component; 7 | 8 | @Component 9 | public class StatisticsServiceClientFallback implements StatisticsServiceClient { 10 | private static final Logger LOGGER = LoggerFactory.getLogger(StatisticsServiceClientFallback.class); 11 | @Override 12 | public void updateStatistics(String accountName, Account account) { 13 | LOGGER.error("Error during update statistics for account: {}", accountName); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /account-service/src/main/java/io/spring2go/piggymetrics/account/domain/User.java: -------------------------------------------------------------------------------- 1 | package io.spring2go.piggymetrics.account.domain; 2 | 3 | import org.hibernate.validator.constraints.Length; 4 | 5 | import javax.validation.constraints.NotNull; 6 | 7 | public class User { 8 | 9 | @NotNull 10 | @Length(min = 3, max = 20) 11 | private String username; 12 | 13 | @NotNull 14 | @Length(min = 6, max = 40) 15 | private String password; 16 | 17 | public String getUsername() { 18 | return username; 19 | } 20 | 21 | public void setUsername(String username) { 22 | this.username = username; 23 | } 24 | 25 | public String getPassword() { 26 | return password; 27 | } 28 | 29 | public void setPassword(String password) { 30 | this.password = password; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /statistics-service/src/main/java/io/spring2go/piggymetrics/statistics/repository/converter/DataPointIdReaderConverter.java: -------------------------------------------------------------------------------- 1 | package io.spring2go.piggymetrics.statistics.repository.converter; 2 | 3 | import com.mongodb.DBObject; 4 | import io.spring2go.piggymetrics.statistics.domain.timeseries.DataPointId; 5 | import org.springframework.core.convert.converter.Converter; 6 | import org.springframework.stereotype.Component; 7 | 8 | import java.util.Date; 9 | 10 | @Component 11 | public class DataPointIdReaderConverter implements Converter { 12 | 13 | @Override 14 | public DataPointId convert(DBObject object) { 15 | 16 | Date date = (Date) object.get("date"); 17 | String account = (String) object.get("account"); 18 | 19 | return new DataPointId(account, date); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /statistics-service/src/main/java/io/spring2go/piggymetrics/statistics/client/ExchangeRatesClientFallback.java: -------------------------------------------------------------------------------- 1 | package io.spring2go.piggymetrics.statistics.client; 2 | 3 | import io.spring2go.piggymetrics.statistics.domain.Currency; 4 | import io.spring2go.piggymetrics.statistics.domain.ExchangeRatesContainer; 5 | import org.springframework.stereotype.Component; 6 | 7 | import java.util.Collections; 8 | 9 | @Component 10 | public class ExchangeRatesClientFallback implements ExchangeRatesClient { 11 | 12 | @Override 13 | public ExchangeRatesContainer getRates(Currency base) { 14 | ExchangeRatesContainer container = new ExchangeRatesContainer(); 15 | container.setBase(Currency.getBase()); 16 | container.setRates(Collections.emptyMap()); 17 | return container; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /account-service/src/main/java/io/spring2go/piggymetrics/account/client/UserServiceClient.java: -------------------------------------------------------------------------------- 1 | package io.spring2go.piggymetrics.account.client; 2 | 3 | import org.springframework.cloud.openfeign.FeignClient; 4 | import org.springframework.web.bind.annotation.RequestMapping; 5 | import org.springframework.web.bind.annotation.RequestMethod; 6 | import org.springframework.web.bind.annotation.RequestParam; 7 | 8 | import io.spring2go.piggymetrics.account.CatAnnotation; 9 | 10 | @FeignClient(url = "${user_service.url}", name = "user-service-client") 11 | public interface UserServiceClient { 12 | 13 | @RequestMapping(method = RequestMethod.POST, value = "/v1/user/create") 14 | @CatAnnotation 15 | void createUser(@RequestParam("username") String username, @RequestParam("password") String password); 16 | 17 | } 18 | -------------------------------------------------------------------------------- /config/statistics-service.properties: -------------------------------------------------------------------------------- 1 | spring.application.name = statistics-service 2 | logging.level.org.spring.framework.security = INFO 3 | hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds = 10000 4 | eureka.instance.instance-id = ${spring.cloud.client.ip-address}:${spring.application.name}:${server.port} 5 | eureka.client.serviceUrl.defaultZone = http://localhost:8761/eureka/ 6 | spring.data.mongodb.host = localhost 7 | spring.data.mongodb.username = user4 8 | spring.data.mongodb.password = test 9 | spring.data.mongodb.database = piggymetrics_statistics_db 10 | spring.data.mongodb.port = 27017 11 | server.servlet.context-path = /statistics 12 | server.port = 7000 13 | rates.url = https://api.exchangeratesapi.io 14 | spring.main.allow-bean-definition-overriding = true 15 | management.endpoints.web.exposure.include = * -------------------------------------------------------------------------------- /notification-service/src/main/java/io/spring2go/piggymetrics/notification/domain/NotificationType.java: -------------------------------------------------------------------------------- 1 | package io.spring2go.piggymetrics.notification.domain; 2 | 3 | public enum NotificationType { 4 | 5 | BACKUP("backup.email.subject", "backup.email.text", "backup.email.attachment"), 6 | REMIND("remind.email.subject", "remind.email.text", null); 7 | 8 | private String subject; 9 | private String text; 10 | private String attachment; 11 | 12 | NotificationType(String subject, String text, String attachment) { 13 | this.subject = subject; 14 | this.text = text; 15 | this.attachment = attachment; 16 | } 17 | 18 | public String getSubject() { 19 | return subject; 20 | } 21 | 22 | public String getText() { 23 | return text; 24 | } 25 | 26 | public String getAttachment() { 27 | return attachment; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /statistics-service/src/main/java/io/spring2go/piggymetrics/statistics/domain/timeseries/DataPointId.java: -------------------------------------------------------------------------------- 1 | package io.spring2go.piggymetrics.statistics.domain.timeseries; 2 | 3 | import java.io.Serializable; 4 | import java.util.Date; 5 | 6 | public class DataPointId implements Serializable { 7 | 8 | private static final long serialVersionUID = 1L; 9 | 10 | private String account; 11 | 12 | private Date date; 13 | 14 | public DataPointId(String account, Date date) { 15 | this.account = account; 16 | this.date = date; 17 | } 18 | 19 | public String getAccount() { 20 | return account; 21 | } 22 | 23 | public Date getDate() { 24 | return date; 25 | } 26 | 27 | @Override 28 | public String toString() { 29 | return "DataPointId{" + 30 | "account='" + account + '\'' + 31 | ", date=" + date + 32 | '}'; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /config/account-service.properties: -------------------------------------------------------------------------------- 1 | spring.application.name = account-service 2 | logging.level.org.spring.framework.security = INFO 3 | hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds = 10000 4 | eureka.instance.instance-id = ${spring.cloud.client.ip-address}:${spring.application.name}:${server.port} 5 | eureka.client.serviceUrl.defaultZone = http://localhost:8761/eureka/ 6 | spring.data.mongodb.host = localhost 7 | spring.data.mongodb.username = user2 8 | spring.data.mongodb.password = test 9 | spring.data.mongodb.database = piggymetrics_account_db 10 | spring.data.mongodb.port = 27017 11 | server.servlet.context-path = /accounts 12 | server.port = 6000 13 | feign.hystrix.enabled = true 14 | user_service.url = http://localhost:5000/ 15 | spring.main.allow-bean-definition-overriding = true 16 | management.endpoints.web.exposure.include = * -------------------------------------------------------------------------------- /account-service/src/main/java/io/spring2go/piggymetrics/account/AccountApplication.java: -------------------------------------------------------------------------------- 1 | package io.spring2go.piggymetrics.account; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker; 6 | import org.springframework.cloud.client.discovery.EnableDiscoveryClient; 7 | import org.springframework.cloud.openfeign.EnableFeignClients; 8 | 9 | import com.ctrip.framework.apollo.spring.annotation.EnableApolloConfig; 10 | 11 | @SpringBootApplication 12 | @EnableDiscoveryClient 13 | @EnableFeignClients 14 | @EnableCircuitBreaker 15 | @EnableApolloConfig 16 | public class AccountApplication { 17 | 18 | public static void main(String[] args) { 19 | SpringApplication.run(AccountApplication.class, args); 20 | } 21 | 22 | } 23 | 24 | -------------------------------------------------------------------------------- /notification-service/src/main/java/io/spring2go/piggymetrics/notification/client/AccountServiceClient.java: -------------------------------------------------------------------------------- 1 | package io.spring2go.piggymetrics.notification.client; 2 | 3 | import org.springframework.cloud.openfeign.FeignClient; 4 | import org.springframework.http.MediaType; 5 | import org.springframework.web.bind.annotation.PathVariable; 6 | import org.springframework.web.bind.annotation.RequestMapping; 7 | import org.springframework.web.bind.annotation.RequestMethod; 8 | 9 | import io.spring2go.piggymetrics.notification.CatAnnotation; 10 | 11 | @FeignClient(name = "account-service") 12 | public interface AccountServiceClient { 13 | 14 | @RequestMapping(method = RequestMethod.GET, value = "/accounts/{accountName}", consumes = MediaType.APPLICATION_JSON_UTF8_VALUE) 15 | @CatAnnotation 16 | String getAccount(@PathVariable("accountName") String accountName); 17 | 18 | } 19 | -------------------------------------------------------------------------------- /statistics-service/src/main/java/io/spring2go/piggymetrics/statistics/repository/converter/DataPointIdWriterConverter.java: -------------------------------------------------------------------------------- 1 | package io.spring2go.piggymetrics.statistics.repository.converter; 2 | 3 | import com.mongodb.BasicDBObject; 4 | import com.mongodb.DBObject; 5 | import io.spring2go.piggymetrics.statistics.domain.timeseries.DataPointId; 6 | import org.springframework.core.convert.converter.Converter; 7 | import org.springframework.stereotype.Component; 8 | 9 | @Component 10 | public class DataPointIdWriterConverter implements Converter { 11 | 12 | private static final int FIELDS = 2; 13 | 14 | @Override 15 | public DBObject convert(DataPointId id) { 16 | 17 | DBObject object = new BasicDBObject(FIELDS); 18 | 19 | object.put("date", id.getDate()); 20 | object.put("account", id.getAccount()); 21 | 22 | return object; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /gateway/src/main/java/io/spring2go/piggymetrics/gateway/CatFilterConfigure.java: -------------------------------------------------------------------------------- 1 | package io.spring2go.piggymetrics.gateway; 2 | 3 | import io.spring2go.cathelper.CatServletFilter; 4 | 5 | import org.springframework.boot.web.servlet.FilterRegistrationBean; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | 9 | 10 | @Configuration 11 | public class CatFilterConfigure { 12 | 13 | @Bean 14 | public FilterRegistrationBean catFilter() { 15 | FilterRegistrationBean registration = new FilterRegistrationBean(); 16 | CatServletFilter filter = new CatServletFilter(); 17 | registration.setFilter(filter); 18 | registration.addUrlPatterns("/*"); 19 | registration.setName("cat-filter"); 20 | registration.setOrder(1); 21 | return registration; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /registry/src/main/java/io/spring2go/piggymetrics/registry/CatFilterConfigure.java: -------------------------------------------------------------------------------- 1 | package io.spring2go.piggymetrics.registry; 2 | 3 | import io.spring2go.cathelper.CatServletFilter; 4 | 5 | import org.springframework.boot.web.servlet.FilterRegistrationBean; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | 9 | 10 | @Configuration 11 | public class CatFilterConfigure { 12 | 13 | @Bean 14 | public FilterRegistrationBean catFilter() { 15 | FilterRegistrationBean registration = new FilterRegistrationBean(); 16 | CatServletFilter filter = new CatServletFilter(); 17 | registration.setFilter(filter); 18 | registration.addUrlPatterns("/*"); 19 | registration.setName("cat-filter"); 20 | registration.setOrder(1); 21 | return registration; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /account-service/src/main/java/io/spring2go/piggymetrics/account/CatFilterConfigure.java: -------------------------------------------------------------------------------- 1 | package io.spring2go.piggymetrics.account; 2 | 3 | import org.springframework.boot.web.servlet.FilterRegistrationBean; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | 7 | import io.spring2go.cathelper.CatServletFilter; 8 | 9 | 10 | @Configuration 11 | public class CatFilterConfigure { 12 | 13 | @Bean 14 | public FilterRegistrationBean catFilter() { 15 | FilterRegistrationBean registration = new FilterRegistrationBean(); 16 | CatServletFilter filter = new CatServletFilter(); 17 | registration.setFilter(filter); 18 | registration.addUrlPatterns("/*"); 19 | registration.setName("cat-filter"); 20 | registration.setOrder(1); 21 | return registration; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /notification-service/src/main/java/io/spring2go/piggymetrics/notification/CatFilterConfigure.java: -------------------------------------------------------------------------------- 1 | package io.spring2go.piggymetrics.notification; 2 | 3 | import org.springframework.boot.web.servlet.FilterRegistrationBean; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | 7 | import io.spring2go.cathelper.CatServletFilter; 8 | 9 | @Configuration 10 | public class CatFilterConfigure { 11 | 12 | @Bean 13 | public FilterRegistrationBean catFilter() { 14 | FilterRegistrationBean registration = new FilterRegistrationBean(); 15 | CatServletFilter filter = new CatServletFilter(); 16 | registration.setFilter(filter); 17 | registration.addUrlPatterns("/*"); 18 | registration.setName("cat-filter"); 19 | registration.setOrder(1); 20 | return registration; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /statistics-service/src/main/java/io/spring2go/piggymetrics/statistics/CatFilterConfigure.java: -------------------------------------------------------------------------------- 1 | package io.spring2go.piggymetrics.statistics; 2 | 3 | import org.springframework.boot.web.servlet.FilterRegistrationBean; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | 7 | import io.spring2go.cathelper.CatServletFilter; 8 | 9 | 10 | @Configuration 11 | public class CatFilterConfigure { 12 | 13 | @Bean 14 | public FilterRegistrationBean catFilter() { 15 | FilterRegistrationBean registration = new FilterRegistrationBean(); 16 | CatServletFilter filter = new CatServletFilter(); 17 | registration.setFilter(filter); 18 | registration.addUrlPatterns("/*"); 19 | registration.setName("cat-filter"); 20 | registration.setOrder(1); 21 | return registration; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /statistics-service/src/main/java/io/spring2go/piggymetrics/statistics/service/ExchangeRatesService.java: -------------------------------------------------------------------------------- 1 | package io.spring2go.piggymetrics.statistics.service; 2 | 3 | import io.spring2go.piggymetrics.statistics.domain.Currency; 4 | 5 | import java.math.BigDecimal; 6 | import java.util.Map; 7 | 8 | public interface ExchangeRatesService { 9 | 10 | /** 11 | * Requests today's foreign exchange rates from a provider 12 | * or reuses values from the last request (if they are still relevant) 13 | * 14 | * @return current date rates 15 | */ 16 | Map getCurrentRates(); 17 | 18 | /** 19 | * Converts given amount to specified currency 20 | * 21 | * @param from {@link Currency} 22 | * @param to {@link Currency} 23 | * @param amount to be converted 24 | * @return converted amount 25 | */ 26 | BigDecimal convert(Currency from, Currency to, BigDecimal amount); 27 | } 28 | -------------------------------------------------------------------------------- /account-service/src/main/java/io/spring2go/piggymetrics/account/controller/ErrorHandler.java: -------------------------------------------------------------------------------- 1 | package io.spring2go.piggymetrics.account.controller; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.http.HttpStatus; 6 | import org.springframework.web.bind.annotation.ControllerAdvice; 7 | import org.springframework.web.bind.annotation.ExceptionHandler; 8 | import org.springframework.web.bind.annotation.ResponseStatus; 9 | 10 | @ControllerAdvice 11 | public class ErrorHandler { 12 | 13 | private final Logger log = LoggerFactory.getLogger(getClass()); 14 | 15 | // TODO add MethodArgumentNotValidException handler 16 | // TODO remove such general handler 17 | @ExceptionHandler(IllegalArgumentException.class) 18 | @ResponseStatus(HttpStatus.BAD_REQUEST) 19 | public void processValidationError(IllegalArgumentException e) { 20 | log.info("Returning HTTP 400 Bad Request", e); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /notification-service/src/main/java/io/spring2go/piggymetrics/notification/domain/NotificationSettings.java: -------------------------------------------------------------------------------- 1 | package io.spring2go.piggymetrics.notification.domain; 2 | 3 | import javax.validation.constraints.NotNull; 4 | import java.util.Date; 5 | 6 | public class NotificationSettings { 7 | 8 | @NotNull 9 | private Boolean active; 10 | 11 | @NotNull 12 | private Frequency frequency; 13 | 14 | private Date lastNotified; 15 | 16 | public Boolean getActive() { 17 | return active; 18 | } 19 | 20 | public void setActive(Boolean active) { 21 | this.active = active; 22 | } 23 | 24 | public Frequency getFrequency() { 25 | return frequency; 26 | } 27 | 28 | public void setFrequency(Frequency frequency) { 29 | this.frequency = frequency; 30 | } 31 | 32 | public Date getLastNotified() { 33 | return lastNotified; 34 | } 35 | 36 | public void setLastNotified(Date lastNotified) { 37 | this.lastNotified = lastNotified; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /statistics-service/src/main/java/io/spring2go/piggymetrics/statistics/service/StatisticsService.java: -------------------------------------------------------------------------------- 1 | package io.spring2go.piggymetrics.statistics.service; 2 | 3 | import io.spring2go.piggymetrics.statistics.domain.Account; 4 | import io.spring2go.piggymetrics.statistics.domain.timeseries.DataPoint; 5 | 6 | import java.util.List; 7 | 8 | public interface StatisticsService { 9 | 10 | /** 11 | * Finds account by given name 12 | * 13 | * @param accountName 14 | * @return found account 15 | */ 16 | List findByAccountName(String accountName); 17 | 18 | /** 19 | * Converts given {@link Account} object to {@link DataPoint} with 20 | * a set of significant statistic metrics. 21 | * 22 | * Compound {@link DataPoint#id} forces to rewrite the object 23 | * for each account within a day. 24 | * 25 | * @param accountName 26 | * @param account 27 | */ 28 | DataPoint save(String accountName, Account account); 29 | 30 | } 31 | -------------------------------------------------------------------------------- /statistics-service/src/main/java/io/spring2go/piggymetrics/statistics/client/ExchangeRatesClient.java: -------------------------------------------------------------------------------- 1 | package io.spring2go.piggymetrics.statistics.client; 2 | 3 | import io.spring2go.piggymetrics.statistics.CatAnnotation; 4 | import io.spring2go.piggymetrics.statistics.domain.Currency; 5 | import io.spring2go.piggymetrics.statistics.domain.ExchangeRatesContainer; 6 | import org.springframework.cloud.openfeign.FeignClient; 7 | import org.springframework.web.bind.annotation.RequestMapping; 8 | import org.springframework.web.bind.annotation.RequestMethod; 9 | import org.springframework.web.bind.annotation.RequestParam; 10 | 11 | @FeignClient(url = "${rates.url}", name = "rates-client", fallback = ExchangeRatesClientFallback.class) 12 | public interface ExchangeRatesClient { 13 | 14 | @RequestMapping(method = RequestMethod.GET, value = "/latest") 15 | @CatAnnotation 16 | ExchangeRatesContainer getRates(@RequestParam("base") Currency base); 17 | 18 | } 19 | -------------------------------------------------------------------------------- /account-service/src/main/java/io/spring2go/piggymetrics/account/client/StatisticsServiceClient.java: -------------------------------------------------------------------------------- 1 | package io.spring2go.piggymetrics.account.client; 2 | 3 | import io.spring2go.piggymetrics.account.CatAnnotation; 4 | import io.spring2go.piggymetrics.account.domain.Account; 5 | import org.springframework.cloud.openfeign.FeignClient; 6 | import org.springframework.http.MediaType; 7 | import org.springframework.web.bind.annotation.PathVariable; 8 | import org.springframework.web.bind.annotation.RequestMapping; 9 | import org.springframework.web.bind.annotation.RequestMethod; 10 | 11 | @FeignClient(name = "statistics-service", fallback = StatisticsServiceClientFallback.class) 12 | public interface StatisticsServiceClient { 13 | 14 | @RequestMapping(method = RequestMethod.PUT, value = "/statistics/{accountName}", consumes = MediaType.APPLICATION_JSON_UTF8_VALUE) 15 | @CatAnnotation 16 | void updateStatistics(@PathVariable("accountName") String accountName, Account account); 17 | 18 | } 19 | -------------------------------------------------------------------------------- /account-service/src/main/java/io/spring2go/piggymetrics/account/service/AccountService.java: -------------------------------------------------------------------------------- 1 | package io.spring2go.piggymetrics.account.service; 2 | 3 | import io.spring2go.piggymetrics.account.domain.Account; 4 | import io.spring2go.piggymetrics.account.domain.User; 5 | 6 | public interface AccountService { 7 | 8 | /** 9 | * Finds account by given name 10 | * 11 | * @param accountName 12 | * @return found account 13 | */ 14 | Account findByName(String accountName); 15 | 16 | /** 17 | * Checks if account with the same name already exists 18 | * Invokes Auth Service user creation 19 | * Creates new account with default parameters 20 | * 21 | * @param user 22 | * @return created account 23 | */ 24 | Account create(User user); 25 | 26 | /** 27 | * Validates and applies incoming account updates 28 | * Invokes Statistics Service update 29 | * 30 | * @param name 31 | * @param update 32 | */ 33 | void saveChanges(String name, Account update); 34 | } 35 | -------------------------------------------------------------------------------- /notification-service/src/test/resources/application.properties: -------------------------------------------------------------------------------- 1 | eureka.client.enabled=false 2 | spring.data.mongodb.database=piggymetrics 3 | spring.data.mongodb.port=0 4 | 5 | remind.cron = 0 0 0 * * * 6 | remind.email.text = Hey, {0}! We''ve missed you here on PiggyMetrics. It''s time to check your budget statistics. Cheers, PiggyMetrics team 7 | remind.email.subject = PiggyMetrics reminder 8 | 9 | backup.cron = 0 0 12 * * * 10 | backup.email.text = Howdy, {0}. Your account backup is ready. Cheers, PiggyMetrics team 11 | backup.email.subject = PiggyMetrics account backup 12 | backup.email.attachment = backup.json 13 | 14 | spring.mail.host = smtp.gmail.com 15 | spring.mail.port = 465 16 | spring.mail.username = test 17 | spring.mail.password = test 18 | spring.mail.properties.mail.smtp.auth = true 19 | spring.mail.properties.mail.smtp.socketFactory.port = 465 20 | spring.mail.properties.mail.smtp.socketFactory.class = javax.net.ssl.SSLSocketFactory 21 | spring.mail.properties.mail.smtp.socketFactory.fallback = false 22 | spring.mail.properties.mail.smtp.ssl.enable = true -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 spring2go.com 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /account-service/src/main/java/io/spring2go/piggymetrics/account/CatAopService.java: -------------------------------------------------------------------------------- 1 | package io.spring2go.piggymetrics.account; 2 | 3 | import java.lang.reflect.Method; 4 | 5 | import org.aspectj.lang.ProceedingJoinPoint; 6 | import org.aspectj.lang.annotation.Around; 7 | import org.aspectj.lang.annotation.Aspect; 8 | import org.aspectj.lang.reflect.MethodSignature; 9 | import org.springframework.stereotype.Component; 10 | 11 | import com.dianping.cat.Cat; 12 | import com.dianping.cat.message.Transaction; 13 | 14 | @Aspect 15 | @Component 16 | public class CatAopService { 17 | 18 | @Around("@annotation(CatAnnotation)") 19 | public Object aroundMethod(ProceedingJoinPoint pjp) { 20 | MethodSignature joinPointObject = (MethodSignature) pjp.getSignature(); 21 | Method method = joinPointObject.getMethod(); 22 | 23 | Transaction t = Cat.newTransaction("method", method.getName()); 24 | 25 | try { 26 | Object obj = pjp.proceed(); 27 | 28 | t.setSuccessStatus(); 29 | return obj; 30 | } catch (Throwable e) { 31 | t.setStatus(e); 32 | Cat.logError(e); 33 | throw new RuntimeException("Exception thrown by CAT aop", e); 34 | } finally { 35 | t.complete(); 36 | } 37 | } 38 | 39 | } -------------------------------------------------------------------------------- /statistics-service/src/main/java/io/spring2go/piggymetrics/statistics/domain/Item.java: -------------------------------------------------------------------------------- 1 | package io.spring2go.piggymetrics.statistics.domain; 2 | 3 | import org.hibernate.validator.constraints.Length; 4 | 5 | import javax.validation.constraints.NotNull; 6 | import java.math.BigDecimal; 7 | 8 | public class Item { 9 | 10 | @NotNull 11 | @Length(min = 1, max = 20) 12 | private String title; 13 | 14 | @NotNull 15 | private BigDecimal amount; 16 | 17 | @NotNull 18 | private Currency currency; 19 | 20 | @NotNull 21 | private TimePeriod period; 22 | 23 | public String getTitle() { 24 | return title; 25 | } 26 | 27 | public void setTitle(String title) { 28 | this.title = title; 29 | } 30 | 31 | public BigDecimal getAmount() { 32 | return amount; 33 | } 34 | 35 | public void setAmount(BigDecimal amount) { 36 | this.amount = amount; 37 | } 38 | 39 | public Currency getCurrency() { 40 | return currency; 41 | } 42 | 43 | public void setCurrency(Currency currency) { 44 | this.currency = currency; 45 | } 46 | 47 | public TimePeriod getPeriod() { 48 | return period; 49 | } 50 | 51 | public void setPeriod(TimePeriod period) { 52 | this.period = period; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /statistics-service/src/main/java/io/spring2go/piggymetrics/statistics/CatAopService.java: -------------------------------------------------------------------------------- 1 | package io.spring2go.piggymetrics.statistics; 2 | 3 | import java.lang.reflect.Method; 4 | 5 | import org.aspectj.lang.ProceedingJoinPoint; 6 | import org.aspectj.lang.annotation.Around; 7 | import org.aspectj.lang.annotation.Aspect; 8 | import org.aspectj.lang.reflect.MethodSignature; 9 | import org.springframework.stereotype.Component; 10 | 11 | import com.dianping.cat.Cat; 12 | import com.dianping.cat.message.Transaction; 13 | 14 | @Aspect 15 | @Component 16 | public class CatAopService { 17 | 18 | @Around("@annotation(CatAnnotation)") 19 | public Object aroundMethod(ProceedingJoinPoint pjp) { 20 | MethodSignature joinPointObject = (MethodSignature) pjp.getSignature(); 21 | Method method = joinPointObject.getMethod(); 22 | 23 | Transaction t = Cat.newTransaction("method", method.getName()); 24 | 25 | try { 26 | Object obj = pjp.proceed(); 27 | 28 | t.setSuccessStatus(); 29 | return obj; 30 | } catch (Throwable e) { 31 | t.setStatus(e); 32 | Cat.logError(e); 33 | throw new RuntimeException("Exception thrown by CAT aop", e); 34 | } finally { 35 | t.complete(); 36 | } 37 | } 38 | 39 | } -------------------------------------------------------------------------------- /notification-service/src/main/java/io/spring2go/piggymetrics/notification/CatAopService.java: -------------------------------------------------------------------------------- 1 | package io.spring2go.piggymetrics.notification; 2 | 3 | import java.lang.reflect.Method; 4 | 5 | import org.aspectj.lang.ProceedingJoinPoint; 6 | import org.aspectj.lang.annotation.Around; 7 | import org.aspectj.lang.annotation.Aspect; 8 | import org.aspectj.lang.reflect.MethodSignature; 9 | import org.springframework.stereotype.Component; 10 | 11 | import com.dianping.cat.Cat; 12 | import com.dianping.cat.message.Transaction; 13 | 14 | @Aspect 15 | @Component 16 | public class CatAopService { 17 | 18 | @Around("@annotation(CatAnnotation)") 19 | public Object aroundMethod(ProceedingJoinPoint pjp) { 20 | MethodSignature joinPointObject = (MethodSignature) pjp.getSignature(); 21 | Method method = joinPointObject.getMethod(); 22 | 23 | Transaction t = Cat.newTransaction("method", method.getName()); 24 | 25 | try { 26 | Object obj = pjp.proceed(); 27 | 28 | t.setSuccessStatus(); 29 | return obj; 30 | } catch (Throwable e) { 31 | t.setStatus(e); 32 | Cat.logError(e); 33 | throw new RuntimeException("Exception thrown by CAT aop", e); 34 | } finally { 35 | t.complete(); 36 | } 37 | } 38 | 39 | } -------------------------------------------------------------------------------- /statistics-service/src/main/java/io/spring2go/piggymetrics/statistics/domain/Account.java: -------------------------------------------------------------------------------- 1 | package io.spring2go.piggymetrics.statistics.domain; 2 | 3 | import org.springframework.data.mongodb.core.mapping.Document; 4 | 5 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 6 | 7 | import javax.validation.Valid; 8 | import javax.validation.constraints.NotNull; 9 | import java.util.List; 10 | 11 | @Document(collection = "accounts") 12 | @JsonIgnoreProperties(ignoreUnknown = true) 13 | public class Account { 14 | 15 | @Valid 16 | @NotNull 17 | private List incomes; 18 | 19 | @Valid 20 | @NotNull 21 | private List expenses; 22 | 23 | @Valid 24 | @NotNull 25 | private Saving saving; 26 | 27 | public List getIncomes() { 28 | return incomes; 29 | } 30 | 31 | public void setIncomes(List incomes) { 32 | this.incomes = incomes; 33 | } 34 | 35 | public List getExpenses() { 36 | return expenses; 37 | } 38 | 39 | public void setExpenses(List expenses) { 40 | this.expenses = expenses; 41 | } 42 | 43 | public Saving getSaving() { 44 | return saving; 45 | } 46 | 47 | public void setSaving(Saving saving) { 48 | this.saving = saving; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /gateway/src/main/java/io/spring2go/piggymetrics/gateway/GatewayApplication.java: -------------------------------------------------------------------------------- 1 | package io.spring2go.piggymetrics.gateway; 2 | 3 | import java.util.Collections; 4 | 5 | import org.springframework.boot.SpringApplication; 6 | import org.springframework.boot.autoconfigure.SpringBootApplication; 7 | import org.springframework.cloud.client.discovery.EnableDiscoveryClient; 8 | import org.springframework.cloud.netflix.zuul.EnableZuulProxy; 9 | import org.springframework.context.annotation.Bean; 10 | import org.springframework.web.client.RestTemplate; 11 | 12 | import com.ctrip.framework.apollo.spring.annotation.EnableApolloConfig; 13 | 14 | import io.spring2go.cathelper.CatRestInterceptor; 15 | 16 | @SpringBootApplication 17 | @EnableDiscoveryClient 18 | @EnableApolloConfig 19 | @EnableZuulProxy 20 | public class GatewayApplication { 21 | 22 | public static void main(String[] args) { 23 | SpringApplication.run(GatewayApplication.class, args); 24 | } 25 | 26 | @Bean 27 | public RestTemplate restTemplate() { 28 | RestTemplate restTemplate = new RestTemplate(); 29 | // for CAT tracing 30 | restTemplate.setInterceptors(Collections.singletonList(new CatRestInterceptor())); 31 | return restTemplate; 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /statistics-service/src/main/java/io/spring2go/piggymetrics/statistics/domain/ExchangeRatesContainer.java: -------------------------------------------------------------------------------- 1 | package io.spring2go.piggymetrics.statistics.domain; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 4 | 5 | import java.math.BigDecimal; 6 | import java.time.LocalDate; 7 | import java.util.Map; 8 | 9 | @JsonIgnoreProperties(ignoreUnknown = true, value = {"date"}) 10 | public class ExchangeRatesContainer { 11 | 12 | private LocalDate date = LocalDate.now(); 13 | 14 | private Currency base; 15 | 16 | private Map rates; 17 | 18 | public LocalDate getDate() { 19 | return date; 20 | } 21 | 22 | public void setDate(LocalDate date) { 23 | this.date = date; 24 | } 25 | 26 | public Currency getBase() { 27 | return base; 28 | } 29 | 30 | public void setBase(Currency base) { 31 | this.base = base; 32 | } 33 | 34 | public Map getRates() { 35 | return rates; 36 | } 37 | 38 | public void setRates(Map rates) { 39 | this.rates = rates; 40 | } 41 | 42 | @Override 43 | public String toString() { 44 | return "RateList{" + 45 | "date=" + date + 46 | ", base=" + base + 47 | ", rates=" + rates + 48 | '}'; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /statistics-service/src/main/java/io/spring2go/piggymetrics/statistics/domain/timeseries/ItemMetric.java: -------------------------------------------------------------------------------- 1 | package io.spring2go.piggymetrics.statistics.domain.timeseries; 2 | 3 | import io.spring2go.piggymetrics.statistics.domain.Currency; 4 | import io.spring2go.piggymetrics.statistics.domain.TimePeriod; 5 | 6 | import java.math.BigDecimal; 7 | 8 | /** 9 | * Represents normalized {@link com.piggymetrics.statistics.domain.Item} object 10 | * with {@link Currency#getBase()} currency and {@link TimePeriod#getBase()} time period 11 | */ 12 | public class ItemMetric { 13 | 14 | private String title; 15 | 16 | private BigDecimal amount; 17 | 18 | public ItemMetric(String title, BigDecimal amount) { 19 | this.title = title; 20 | this.amount = amount; 21 | } 22 | 23 | public String getTitle() { 24 | return title; 25 | } 26 | 27 | public BigDecimal getAmount() { 28 | return amount; 29 | } 30 | 31 | @Override 32 | public boolean equals(Object o) { 33 | if (this == o) return true; 34 | if (o == null || getClass() != o.getClass()) return false; 35 | 36 | ItemMetric that = (ItemMetric) o; 37 | 38 | return title.equalsIgnoreCase(that.title); 39 | 40 | } 41 | 42 | @Override 43 | public int hashCode() { 44 | return title.hashCode(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /notification-service/src/main/java/io/spring2go/piggymetrics/notification/service/RecipientService.java: -------------------------------------------------------------------------------- 1 | package io.spring2go.piggymetrics.notification.service; 2 | 3 | import io.spring2go.piggymetrics.notification.domain.NotificationType; 4 | import io.spring2go.piggymetrics.notification.domain.Recipient; 5 | 6 | import java.util.List; 7 | 8 | public interface RecipientService { 9 | 10 | /** 11 | * Finds recipient by account name 12 | * 13 | * @param accountName 14 | * @return recipient 15 | */ 16 | Recipient findByAccountName(String accountName); 17 | 18 | /** 19 | * Finds recipients, which are ready to be notified 20 | * at the moment 21 | * 22 | * @param type 23 | * @return recipients to notify 24 | */ 25 | List findReadyToNotify(NotificationType type); 26 | 27 | /** 28 | * Creates or updates recipient settings 29 | * 30 | * @param accountName 31 | * @param recipient 32 | * @return updated recipient 33 | */ 34 | Recipient save(String accountName, Recipient recipient); 35 | 36 | /** 37 | * Updates {@link NotificationType} {@code lastNotified} property with current date 38 | * for given recipient. 39 | * 40 | * @param type 41 | * @param recipient 42 | */ 43 | void markNotified(NotificationType type, Recipient recipient); 44 | } 45 | -------------------------------------------------------------------------------- /notification-service/src/main/java/io/spring2go/piggymetrics/notification/repository/RecipientRepository.java: -------------------------------------------------------------------------------- 1 | package io.spring2go.piggymetrics.notification.repository; 2 | 3 | import io.spring2go.piggymetrics.notification.CatAnnotation; 4 | import io.spring2go.piggymetrics.notification.domain.Recipient; 5 | import org.springframework.data.mongodb.repository.Query; 6 | import org.springframework.data.repository.CrudRepository; 7 | import org.springframework.stereotype.Repository; 8 | 9 | import java.util.List; 10 | 11 | @Repository 12 | public interface RecipientRepository extends CrudRepository { 13 | 14 | Recipient findByAccountName(String name); 15 | 16 | @Query("{ $and: [ {'scheduledNotifications.BACKUP.active': true }, { $where: 'this.scheduledNotifications.BACKUP.lastNotified < " + 17 | "new Date(new Date().setDate(new Date().getDate() - this.scheduledNotifications.BACKUP.frequency ))' }] }") 18 | @CatAnnotation 19 | List findReadyForBackup(); 20 | 21 | @Query("{ $and: [ {'scheduledNotifications.REMIND.active': true }, { $where: 'this.scheduledNotifications.REMIND.lastNotified < " + 22 | "new Date(new Date().setDate(new Date().getDate() - this.scheduledNotifications.REMIND.frequency ))' }] }") 23 | @CatAnnotation 24 | List findReadyForRemind(); 25 | 26 | } 27 | -------------------------------------------------------------------------------- /account-service/src/main/java/io/spring2go/piggymetrics/account/domain/Item.java: -------------------------------------------------------------------------------- 1 | package io.spring2go.piggymetrics.account.domain; 2 | 3 | import org.hibernate.validator.constraints.Length; 4 | 5 | import javax.validation.constraints.NotNull; 6 | import java.math.BigDecimal; 7 | 8 | public class Item { 9 | 10 | @NotNull 11 | @Length(min = 1, max = 20) 12 | private String title; 13 | 14 | @NotNull 15 | private BigDecimal amount; 16 | 17 | @NotNull 18 | private Currency currency; 19 | 20 | @NotNull 21 | private TimePeriod period; 22 | 23 | @NotNull 24 | private String icon; 25 | 26 | public String getTitle() { 27 | return title; 28 | } 29 | 30 | public void setTitle(String title) { 31 | this.title = title; 32 | } 33 | 34 | public BigDecimal getAmount() { 35 | return amount; 36 | } 37 | 38 | public void setAmount(BigDecimal amount) { 39 | this.amount = amount; 40 | } 41 | 42 | public Currency getCurrency() { 43 | return currency; 44 | } 45 | 46 | public void setCurrency(Currency currency) { 47 | this.currency = currency; 48 | } 49 | 50 | public TimePeriod getPeriod() { 51 | return period; 52 | } 53 | 54 | public void setPeriod(TimePeriod period) { 55 | this.period = period; 56 | } 57 | 58 | public String getIcon() { 59 | return icon; 60 | } 61 | 62 | public void setIcon(String icon) { 63 | this.icon = icon; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /account-service/src/main/java/io/spring2go/piggymetrics/account/domain/Saving.java: -------------------------------------------------------------------------------- 1 | package io.spring2go.piggymetrics.account.domain; 2 | 3 | import javax.validation.constraints.NotNull; 4 | import java.math.BigDecimal; 5 | 6 | public class Saving { 7 | 8 | @NotNull 9 | private BigDecimal amount; 10 | 11 | @NotNull 12 | private Currency currency; 13 | 14 | @NotNull 15 | private BigDecimal interest; 16 | 17 | @NotNull 18 | private Boolean deposit; 19 | 20 | @NotNull 21 | private Boolean capitalization; 22 | 23 | public BigDecimal getAmount() { 24 | return amount; 25 | } 26 | 27 | public void setAmount(BigDecimal amount) { 28 | this.amount = amount; 29 | } 30 | 31 | public Currency getCurrency() { 32 | return currency; 33 | } 34 | 35 | public void setCurrency(Currency currency) { 36 | this.currency = currency; 37 | } 38 | 39 | public BigDecimal getInterest() { 40 | return interest; 41 | } 42 | 43 | public void setInterest(BigDecimal interest) { 44 | this.interest = interest; 45 | } 46 | 47 | public Boolean getDeposit() { 48 | return deposit; 49 | } 50 | 51 | public void setDeposit(Boolean deposit) { 52 | this.deposit = deposit; 53 | } 54 | 55 | public Boolean getCapitalization() { 56 | return capitalization; 57 | } 58 | 59 | public void setCapitalization(Boolean capitalization) { 60 | this.capitalization = capitalization; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /statistics-service/src/main/java/io/spring2go/piggymetrics/statistics/domain/Saving.java: -------------------------------------------------------------------------------- 1 | package io.spring2go.piggymetrics.statistics.domain; 2 | 3 | import javax.validation.constraints.NotNull; 4 | import java.math.BigDecimal; 5 | 6 | public class Saving { 7 | 8 | @NotNull 9 | private BigDecimal amount; 10 | 11 | @NotNull 12 | private Currency currency; 13 | 14 | @NotNull 15 | private BigDecimal interest; 16 | 17 | @NotNull 18 | private Boolean deposit; 19 | 20 | @NotNull 21 | private Boolean capitalization; 22 | 23 | public BigDecimal getAmount() { 24 | return amount; 25 | } 26 | 27 | public void setAmount(BigDecimal amount) { 28 | this.amount = amount; 29 | } 30 | 31 | public Currency getCurrency() { 32 | return currency; 33 | } 34 | 35 | public void setCurrency(Currency currency) { 36 | this.currency = currency; 37 | } 38 | 39 | public BigDecimal getInterest() { 40 | return interest; 41 | } 42 | 43 | public void setInterest(BigDecimal interest) { 44 | this.interest = interest; 45 | } 46 | 47 | public Boolean getDeposit() { 48 | return deposit; 49 | } 50 | 51 | public void setDeposit(Boolean deposit) { 52 | this.deposit = deposit; 53 | } 54 | 55 | public Boolean getCapitalization() { 56 | return capitalization; 57 | } 58 | 59 | public void setCapitalization(Boolean capitalization) { 60 | this.capitalization = capitalization; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /account-service/src/test/java/io/spring2go/piggymetrics/account/client/StatisticsServiceClientFallbackTest.java: -------------------------------------------------------------------------------- 1 | package io.spring2go.piggymetrics.account.client; 2 | 3 | import io.spring2go.piggymetrics.account.domain.Account; 4 | import org.junit.Before; 5 | import org.junit.Rule; 6 | import org.junit.Test; 7 | import org.junit.runner.RunWith; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.boot.test.context.SpringBootTest; 10 | import org.springframework.boot.test.rule.OutputCapture; 11 | import org.springframework.test.context.junit4.SpringRunner; 12 | 13 | import static org.hamcrest.Matchers.containsString; 14 | 15 | @RunWith(SpringRunner.class) 16 | @SpringBootTest(properties = { 17 | "feign.hystrix.enabled=true" 18 | }) 19 | public class StatisticsServiceClientFallbackTest { 20 | @Autowired 21 | private StatisticsServiceClient statisticsServiceClient; 22 | 23 | @Rule 24 | public final OutputCapture outputCapture = new OutputCapture(); 25 | 26 | @Before 27 | public void setup() { 28 | outputCapture.reset(); 29 | } 30 | 31 | @Test 32 | public void testUpdateStatisticsWithFailFallback(){ 33 | statisticsServiceClient.updateStatistics("test", new Account()); 34 | 35 | outputCapture.expect(containsString("Error during update statistics for account: test")); 36 | 37 | } 38 | 39 | } 40 | 41 | -------------------------------------------------------------------------------- /statistics-service/src/main/java/io/spring2go/piggymetrics/statistics/controller/StatisticsController.java: -------------------------------------------------------------------------------- 1 | package io.spring2go.piggymetrics.statistics.controller; 2 | 3 | import io.spring2go.piggymetrics.statistics.domain.Account; 4 | import io.spring2go.piggymetrics.statistics.domain.timeseries.DataPoint; 5 | import io.spring2go.piggymetrics.statistics.service.StatisticsService; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.web.bind.annotation.*; 8 | 9 | import javax.validation.Valid; 10 | import java.util.List; 11 | 12 | @RestController 13 | public class StatisticsController { 14 | 15 | @Autowired 16 | private StatisticsService statisticsService; 17 | 18 | @RequestMapping(value = "/current", method = RequestMethod.GET) 19 | public List getCurrentAccountStatistics(@RequestHeader("X-S2G-USERNAME") String accountName) { 20 | return statisticsService.findByAccountName(accountName); 21 | } 22 | 23 | @RequestMapping(value = "/{accountName}", method = RequestMethod.GET) 24 | public List getStatisticsByAccountName(@PathVariable String accountName) { 25 | return statisticsService.findByAccountName(accountName); 26 | } 27 | 28 | @RequestMapping(value = "/{accountName}", method = RequestMethod.PUT) 29 | public void saveAccountStatistics(@PathVariable String accountName, @Valid @RequestBody Account account) { 30 | statisticsService.save(accountName, account); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /notification-service/src/main/java/io/spring2go/piggymetrics/notification/controller/RecipientController.java: -------------------------------------------------------------------------------- 1 | package io.spring2go.piggymetrics.notification.controller; 2 | 3 | import io.spring2go.piggymetrics.notification.domain.Recipient; 4 | import io.spring2go.piggymetrics.notification.service.RecipientService; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.web.bind.annotation.RequestBody; 7 | import org.springframework.web.bind.annotation.RequestHeader; 8 | import org.springframework.web.bind.annotation.RequestMapping; 9 | import org.springframework.web.bind.annotation.RequestMethod; 10 | import org.springframework.web.bind.annotation.RestController; 11 | 12 | import javax.validation.Valid; 13 | 14 | @RestController 15 | @RequestMapping("/recipients") 16 | public class RecipientController { 17 | 18 | @Autowired 19 | private RecipientService recipientService; 20 | 21 | @RequestMapping(path = "/current", method = RequestMethod.GET) 22 | public Object getCurrentNotificationsSettings(@RequestHeader("X-S2G-USERNAME") String accountName) { 23 | return recipientService.findByAccountName(accountName); 24 | } 25 | 26 | @RequestMapping(path = "/current", method = RequestMethod.PUT) 27 | public Object saveCurrentNotificationsSettings(@RequestHeader("X-S2G-USERNAME") String accountName, @Valid @RequestBody Recipient recipient) { 28 | return recipientService.save(accountName, recipient); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /registry/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | registry 7 | jar 8 | 9 | registry 10 | 11 | 12 | io.spring2go 13 | piggymetrics 14 | 1.0-SNAPSHOT 15 | 16 | 17 | 18 | 19 | org.springframework.cloud 20 | spring-cloud-starter-netflix-eureka-server 21 | 22 | 23 | org.springframework.boot 24 | spring-boot-starter-actuator 25 | 26 | 27 | org.springframework.boot 28 | spring-boot-starter-test 29 | test 30 | 31 | 32 | 33 | 34 | 35 | 36 | org.springframework.boot 37 | spring-boot-maven-plugin 38 | 39 | ${project.name} 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /statistics-service/src/main/java/io/spring2go/piggymetrics/statistics/StatisticsApplication.java: -------------------------------------------------------------------------------- 1 | package io.spring2go.piggymetrics.statistics; 2 | 3 | import java.util.Arrays; 4 | 5 | import org.springframework.boot.SpringApplication; 6 | import org.springframework.boot.autoconfigure.SpringBootApplication; 7 | import org.springframework.cloud.client.discovery.EnableDiscoveryClient; 8 | import org.springframework.cloud.openfeign.EnableFeignClients; 9 | import org.springframework.context.annotation.Bean; 10 | import org.springframework.context.annotation.Configuration; 11 | import org.springframework.data.mongodb.core.convert.CustomConversions; 12 | 13 | import com.ctrip.framework.apollo.spring.annotation.EnableApolloConfig; 14 | 15 | import io.spring2go.piggymetrics.statistics.repository.converter.DataPointIdReaderConverter; 16 | import io.spring2go.piggymetrics.statistics.repository.converter.DataPointIdWriterConverter; 17 | 18 | @SpringBootApplication 19 | @EnableDiscoveryClient 20 | @EnableFeignClients 21 | @EnableApolloConfig 22 | public class StatisticsApplication { 23 | 24 | public static void main(String[] args) { 25 | SpringApplication.run(StatisticsApplication.class, args); 26 | } 27 | 28 | @Configuration 29 | static class CustomConversionsConfig { 30 | 31 | @Bean 32 | public CustomConversions customConversions() { 33 | return new CustomConversions(Arrays.asList(new DataPointIdReaderConverter(), 34 | new DataPointIdWriterConverter())); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /account-service/src/main/java/io/spring2go/piggymetrics/account/controller/AccountController.java: -------------------------------------------------------------------------------- 1 | package io.spring2go.piggymetrics.account.controller; 2 | 3 | import io.spring2go.piggymetrics.account.domain.Account; 4 | import io.spring2go.piggymetrics.account.domain.User; 5 | import io.spring2go.piggymetrics.account.service.AccountService; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.web.bind.annotation.*; 8 | 9 | import javax.validation.Valid; 10 | 11 | @RestController 12 | public class AccountController { 13 | 14 | @Autowired 15 | private AccountService accountService; 16 | 17 | @RequestMapping(path = "/{name}", method = RequestMethod.GET) 18 | public Account getAccountByName(@PathVariable String name) { 19 | return accountService.findByName(name); 20 | } 21 | 22 | @RequestMapping(path = "/current", method = RequestMethod.GET) 23 | public Account getCurrentAccount(@RequestHeader("X-S2G-USERNAME") String userName) { 24 | return accountService.findByName(userName); 25 | } 26 | 27 | @RequestMapping(path = "/current", method = RequestMethod.PUT) 28 | public void saveCurrentAccount(@RequestHeader("X-S2G-USERNAME") String userName, @Valid @RequestBody Account account) { 29 | accountService.saveChanges(userName, account); 30 | } 31 | 32 | @RequestMapping(path = "/", method = RequestMethod.POST) 33 | public Account createNewAccount(@Valid @RequestBody User user) { 34 | return accountService.create(user); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /gateway/src/main/java/io/spring2go/piggymetrics/gateway/filter/CatHeaderFilter.java: -------------------------------------------------------------------------------- 1 | package io.spring2go.piggymetrics.gateway.filter; 2 | 3 | import org.springframework.stereotype.Component; 4 | 5 | import com.dianping.cat.Cat; 6 | import com.dianping.cat.Cat.Context; 7 | import com.netflix.zuul.ZuulFilter; 8 | import com.netflix.zuul.context.RequestContext; 9 | 10 | import io.spring2go.cathelper.CatContext; 11 | import io.spring2go.cathelper.CatHttpConstants; 12 | 13 | // 借助Zuul Filter以跨进程边界方式传递CAT调用链上下文 14 | @Component 15 | public class CatHeaderFilter extends ZuulFilter { 16 | 17 | @Override 18 | public boolean shouldFilter() { 19 | return true; 20 | } 21 | 22 | @Override 23 | public Object run() { 24 | // 保存和传递CAT调用链上下文 25 | Context ctx = new CatContext(); 26 | Cat.logRemoteCallClient(ctx); 27 | RequestContext requestContext = RequestContext.getCurrentContext(); 28 | requestContext.addZuulRequestHeader(CatHttpConstants.CAT_HTTP_HEADER_ROOT_MESSAGE_ID, ctx.getProperty(Cat.Context.ROOT)); 29 | requestContext.addZuulRequestHeader(CatHttpConstants.CAT_HTTP_HEADER_PARENT_MESSAGE_ID, ctx.getProperty(Cat.Context.PARENT)); 30 | requestContext.addZuulRequestHeader(CatHttpConstants.CAT_HTTP_HEADER_CHILD_MESSAGE_ID, ctx.getProperty(Cat.Context.CHILD)); 31 | return null; 32 | } 33 | 34 | @Override 35 | public String filterType() { 36 | return "pre"; 37 | } 38 | 39 | @Override 40 | public int filterOrder() { 41 | return 10; 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /gateway/src/main/java/io/spring2go/piggymetrics/gateway/filter/ValidationConfig.java: -------------------------------------------------------------------------------- 1 | package io.spring2go.piggymetrics.gateway.filter; 2 | 3 | import javax.annotation.PostConstruct; 4 | import javax.validation.constraints.NotNull; 5 | 6 | import org.apache.commons.codec.binary.Base64; 7 | import org.springframework.boot.context.properties.ConfigurationProperties; 8 | import org.springframework.boot.context.properties.EnableConfigurationProperties; 9 | import org.springframework.stereotype.Component; 10 | 11 | @Component 12 | @EnableConfigurationProperties 13 | @ConfigurationProperties(prefix="piggymetrics") 14 | public class ValidationConfig { 15 | @NotNull private String tokenIntrospectEndpoint; 16 | @NotNull private String clientCredentials; 17 | private String base64Credentials; 18 | 19 | public String getClientCredentials() { 20 | return clientCredentials; 21 | } 22 | 23 | public void setClientCredentials(String creds) { 24 | clientCredentials = creds; 25 | } 26 | 27 | public String getTokenIntrospectEndpoint() { 28 | return tokenIntrospectEndpoint; 29 | } 30 | 31 | public void setTokenIntrospectEndpoint(String endpoint) { 32 | tokenIntrospectEndpoint = endpoint; 33 | } 34 | 35 | public String getBase64Credentials() { 36 | return base64Credentials; 37 | } 38 | 39 | @PostConstruct 40 | public void initBase64Creds() { 41 | byte[] plainCredsBytes = clientCredentials.getBytes(); 42 | byte[] base64CredsBytes = Base64.encodeBase64(plainCredsBytes); 43 | base64Credentials = new String(base64CredsBytes); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /notification-service/src/main/java/io/spring2go/piggymetrics/notification/domain/Recipient.java: -------------------------------------------------------------------------------- 1 | package io.spring2go.piggymetrics.notification.domain; 2 | 3 | import org.hibernate.validator.constraints.Email; 4 | import org.springframework.data.annotation.Id; 5 | import org.springframework.data.mongodb.core.mapping.Document; 6 | 7 | import javax.validation.Valid; 8 | import javax.validation.constraints.NotNull; 9 | import java.util.Map; 10 | 11 | @Document(collection = "recipients") 12 | public class Recipient { 13 | 14 | @Id 15 | private String accountName; 16 | 17 | @NotNull 18 | @Email 19 | private String email; 20 | 21 | @Valid 22 | private Map scheduledNotifications; 23 | 24 | public String getAccountName() { 25 | return accountName; 26 | } 27 | 28 | public void setAccountName(String accountName) { 29 | this.accountName = accountName; 30 | } 31 | 32 | public String getEmail() { 33 | return email; 34 | } 35 | 36 | public void setEmail(String email) { 37 | this.email = email; 38 | } 39 | 40 | public Map getScheduledNotifications() { 41 | return scheduledNotifications; 42 | } 43 | 44 | public void setScheduledNotifications(Map scheduledNotifications) { 45 | this.scheduledNotifications = scheduledNotifications; 46 | } 47 | 48 | @Override 49 | public String toString() { 50 | return "Recipient{" + 51 | "accountName='" + accountName + '\'' + 52 | ", email='" + email + '\'' + 53 | '}'; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /notification-service/src/main/java/io/spring2go/piggymetrics/notification/NotificationServiceApplication.java: -------------------------------------------------------------------------------- 1 | package io.spring2go.piggymetrics.notification; 2 | 3 | import java.util.Arrays; 4 | 5 | import org.springframework.boot.SpringApplication; 6 | import org.springframework.boot.autoconfigure.SpringBootApplication; 7 | import org.springframework.cloud.client.discovery.EnableDiscoveryClient; 8 | import org.springframework.cloud.openfeign.EnableFeignClients; 9 | import org.springframework.context.annotation.Bean; 10 | import org.springframework.context.annotation.Configuration; 11 | import org.springframework.data.mongodb.core.convert.CustomConversions; 12 | import org.springframework.scheduling.annotation.EnableScheduling; 13 | 14 | import com.ctrip.framework.apollo.spring.annotation.EnableApolloConfig; 15 | 16 | import io.spring2go.piggymetrics.notification.repository.converter.FrequencyReaderConverter; 17 | import io.spring2go.piggymetrics.notification.repository.converter.FrequencyWriterConverter; 18 | 19 | @SpringBootApplication 20 | @EnableDiscoveryClient 21 | @EnableFeignClients 22 | @EnableApolloConfig 23 | @EnableScheduling 24 | public class NotificationServiceApplication { 25 | 26 | public static void main(String[] args) { 27 | SpringApplication.run(NotificationServiceApplication.class, args); 28 | } 29 | 30 | @Configuration 31 | static class CustomConversionsConfig { 32 | 33 | @Bean 34 | public CustomConversions customConversions() { 35 | return new CustomConversions(Arrays.asList(new FrequencyReaderConverter(), 36 | new FrequencyWriterConverter())); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /config/gateway.properties: -------------------------------------------------------------------------------- 1 | spring.application.name = gateway 2 | logging.level.org.spring.framework.security = INFO 3 | #hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=20000 4 | eureka.instance.instance-id = ${spring.cloud.client.ip-address}:${spring.application.name}:${server.port} 5 | eureka.client.serviceUrl.defaultZone = http://localhost:8761/eureka/ 6 | ribbon.ReadTimeout = 20000 7 | ribbon.ConnectTimeout = 20000 8 | zuul.ignoredServices = '*' 9 | zuul.host.connect-timeout-millis = 20000 10 | zuul.host.socket-timeout-millis = 20000 11 | zuul.routes.auth-service.path = /v1/oauth/** 12 | zuul.routes.auth-service.url = http://localhost:5000 13 | zuul.routes.auth-service.stripPrefix = false 14 | zuul.routes.auth-service.sensitiveHeaders = 15 | zuul.routes.account-service.path = /accounts/** 16 | zuul.routes.account-service.serviceId = account-service 17 | zuul.routes.account-service.stripPrefix = false 18 | zuul.routes.account-service.sensitiveHeaders = 19 | zuul.routes.statistics-service.path = /statistics/** 20 | zuul.routes.statistics-service.serviceId = statistics-service 21 | zuul.routes.statistics-service.stripPrefix = false 22 | zuul.routes.statistics-service.sensitiveHeaders = 23 | zuul.routes.notification-service.path = /notifications/** 24 | zuul.routes.notification-service.serviceId = notification-service 25 | zuul.routes.notification-service.stripPrefix = false 26 | zuul.routes.notification-service.sensitiveHeaders = 27 | piggymetrics.tokenIntrospectEndpoint = http://localhost:5000/v1/oauth/introspect 28 | piggymetrics.clientCredentials = test_client_2:test_secret 29 | server.port = 4000 30 | management.endpoints.web.exposure.include = * 31 | -------------------------------------------------------------------------------- /config/notification-service.properties: -------------------------------------------------------------------------------- 1 | spring.application.name = notification-service 2 | logging.level.org.spring.framework.security = INFO 3 | hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds = 10000 4 | eureka.instance.instance-id = ${spring.cloud.client.ip-address}:${spring.application.name}:${server.port} 5 | eureka.client.serviceUrl.defaultZone = http://localhost:8761/eureka/ 6 | spring.data.mongodb.host = localhost 7 | spring.data.mongodb.username = user3 8 | spring.data.mongodb.password = test 9 | spring.data.mongodb.database = piggymetrics_notification_db 10 | spring.data.mongodb.port = 27017 11 | server.servlet.context-path = /notifications 12 | server.port = 8000 13 | remind.cron = 0 0 0 * * * 14 | remind.email.text = Hey, {0}! We''ve missed you here on PiggyMetrics. It''s time to check your budget statistics. Cheers, PiggyMetrics team 15 | remind.email.subject = PiggyMetrics reminder 16 | backup.cron = 0 0 12 * * * 17 | backup.email.text = Howdy, {0}. Your account backup is ready. Cheers, PiggyMetrics team 18 | backup.email.subject = PiggyMetrics account backup 19 | backup.email.attachment = backup.json 20 | spring.mail.host = smtp.gmail.com 21 | spring.mail.port = 465 22 | spring.mail.username = dev-user 23 | spring.mail.password = dev-password 24 | spring.mail.properties.mail.smtp.auth = true 25 | spring.mail.properties.mail.smtp.socketFactory.port = 465 26 | spring.mail.properties.mail.smtp.socketFactory.class = javax.net.ssl.SSLSocketFactory 27 | spring.mail.properties.mail.smtp.socketFactory.fallback = false 28 | spring.mail.properties.mail.smtp.ssl.enable = true 29 | management.endpoints.web.exposure.include = * 30 | spring.main.allow-bean-definition-overriding = true 31 | -------------------------------------------------------------------------------- /gateway/src/main/resources/static/attribution.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Piggy Metrics 8 | 9 | 10 | 11 | 12 |
13 |
14 | Creative Commons – Attribution (CC BY 3.0) 15 |
16 | Thanks a lot for icons from The Noun Project collection. 17 |
18 |
19 | Here's the list of all used icons: 20 |
21 | Piggy Bank designed by Jezmael Basilio 22 |
23 | Arrow designed by Jardson A. 24 |
25 | Wallet designed by Luis Prado 26 |
27 | Analytics designed by Aneeque Ahmed 28 |
29 | Piggy Bank designed by Michelle Ann 30 |
31 | Light Bulb designed by Chris Brunskill 32 |
33 | Speech Bubble designed by Cengiz SARI 34 |
35 | Bag designed by Agus Purwanto 36 |
37 | Analytics designed by Luboš Volkov 38 |
39 | College Tuition designed by Rediffusion 40 |
41 | Marijuana designed by Gareth 42 |
43 | Stroller designed by Edward Boatman 44 |
45 | Television designed by Piero Borgo 46 |
47 | Island designed by Bohdan Burmich 48 |
49 | Light Bulb designed by Rémy Médard 50 |
51 | Shirt designed by Megan Sheehan 52 |
53 | Telephone designed by Ian Mawle 54 |
55 | Shopping Cart designed by Megan Sheehan 56 |
57 | Gas designed by Jon Testa 58 |
59 | 60 | 61 | -------------------------------------------------------------------------------- /gateway/src/main/java/io/spring2go/cathelper/CatRestInterceptor.java: -------------------------------------------------------------------------------- 1 | package io.spring2go.cathelper; 2 | 3 | import java.io.IOException; 4 | 5 | import org.springframework.http.HttpHeaders; 6 | import org.springframework.http.HttpRequest; 7 | import org.springframework.http.client.ClientHttpRequestExecution; 8 | import org.springframework.http.client.ClientHttpRequestInterceptor; 9 | import org.springframework.http.client.ClientHttpResponse; 10 | 11 | import com.dianping.cat.Cat; 12 | import com.dianping.cat.Cat.Context; 13 | import com.dianping.cat.CatConstants; 14 | import com.dianping.cat.message.Transaction; 15 | 16 | public class CatRestInterceptor implements ClientHttpRequestInterceptor { 17 | 18 | @Override 19 | public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) 20 | throws IOException { 21 | 22 | Transaction t = Cat.newTransaction(CatConstants.TYPE_REMOTE_CALL, request.getURI().toString()); 23 | 24 | try { 25 | HttpHeaders headers = request.getHeaders(); 26 | 27 | // 保存和传递CAT调用链上下文 28 | Context ctx = new CatContext(); 29 | Cat.logRemoteCallClient(ctx); 30 | headers.add(CatHttpConstants.CAT_HTTP_HEADER_ROOT_MESSAGE_ID, ctx.getProperty(Cat.Context.ROOT)); 31 | headers.add(CatHttpConstants.CAT_HTTP_HEADER_PARENT_MESSAGE_ID, ctx.getProperty(Cat.Context.PARENT)); 32 | headers.add(CatHttpConstants.CAT_HTTP_HEADER_CHILD_MESSAGE_ID, ctx.getProperty(Cat.Context.CHILD)); 33 | 34 | // 保证请求继续被执行 35 | ClientHttpResponse response = execution.execute(request, body); 36 | t.setStatus(Transaction.SUCCESS); 37 | return response; 38 | } catch (Exception e) { 39 | Cat.getProducer().logError(e); 40 | t.setStatus(e); 41 | throw e; 42 | } finally { 43 | t.complete(); 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /registry/src/main/java/io/spring2go/cathelper/CatRestInterceptor.java: -------------------------------------------------------------------------------- 1 | package io.spring2go.cathelper; 2 | 3 | import java.io.IOException; 4 | 5 | import org.springframework.http.HttpHeaders; 6 | import org.springframework.http.HttpRequest; 7 | import org.springframework.http.client.ClientHttpRequestExecution; 8 | import org.springframework.http.client.ClientHttpRequestInterceptor; 9 | import org.springframework.http.client.ClientHttpResponse; 10 | 11 | import com.dianping.cat.Cat; 12 | import com.dianping.cat.Cat.Context; 13 | import com.dianping.cat.CatConstants; 14 | import com.dianping.cat.message.Transaction; 15 | 16 | public class CatRestInterceptor implements ClientHttpRequestInterceptor { 17 | 18 | @Override 19 | public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) 20 | throws IOException { 21 | 22 | Transaction t = Cat.newTransaction(CatConstants.TYPE_REMOTE_CALL, request.getURI().toString()); 23 | 24 | try { 25 | HttpHeaders headers = request.getHeaders(); 26 | 27 | // 保存和传递CAT调用链上下文 28 | Context ctx = new CatContext(); 29 | Cat.logRemoteCallClient(ctx); 30 | headers.add(CatHttpConstants.CAT_HTTP_HEADER_ROOT_MESSAGE_ID, ctx.getProperty(Cat.Context.ROOT)); 31 | headers.add(CatHttpConstants.CAT_HTTP_HEADER_PARENT_MESSAGE_ID, ctx.getProperty(Cat.Context.PARENT)); 32 | headers.add(CatHttpConstants.CAT_HTTP_HEADER_CHILD_MESSAGE_ID, ctx.getProperty(Cat.Context.CHILD)); 33 | 34 | // 保证请求继续被执行 35 | ClientHttpResponse response = execution.execute(request, body); 36 | t.setStatus(Transaction.SUCCESS); 37 | return response; 38 | } catch (Exception e) { 39 | Cat.getProducer().logError(e); 40 | t.setStatus(e); 41 | throw e; 42 | } finally { 43 | t.complete(); 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /statistics-service/src/main/java/io/spring2go/piggymetrics/statistics/domain/timeseries/DataPoint.java: -------------------------------------------------------------------------------- 1 | package io.spring2go.piggymetrics.statistics.domain.timeseries; 2 | 3 | import io.spring2go.piggymetrics.statistics.domain.Currency; 4 | import org.springframework.data.annotation.Id; 5 | import org.springframework.data.mongodb.core.mapping.Document; 6 | 7 | import java.math.BigDecimal; 8 | import java.util.Map; 9 | import java.util.Set; 10 | 11 | /** 12 | * Represents daily time series data point containing 13 | * current account state 14 | */ 15 | @Document(collection = "datapoints") 16 | public class DataPoint { 17 | 18 | @Id 19 | private DataPointId id; 20 | 21 | private Set incomes; 22 | 23 | private Set expenses; 24 | 25 | private Map statistics; 26 | 27 | private Map rates; 28 | 29 | public DataPointId getId() { 30 | return id; 31 | } 32 | 33 | public void setId(DataPointId id) { 34 | this.id = id; 35 | } 36 | 37 | public Set getIncomes() { 38 | return incomes; 39 | } 40 | 41 | public void setIncomes(Set incomes) { 42 | this.incomes = incomes; 43 | } 44 | 45 | public Set getExpenses() { 46 | return expenses; 47 | } 48 | 49 | public void setExpenses(Set expenses) { 50 | this.expenses = expenses; 51 | } 52 | 53 | public Map getStatistics() { 54 | return statistics; 55 | } 56 | 57 | public void setStatistics(Map statistics) { 58 | this.statistics = statistics; 59 | } 60 | 61 | public Map getRates() { 62 | return rates; 63 | } 64 | 65 | public void setRates(Map rates) { 66 | this.rates = rates; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /account-service/src/main/java/io/spring2go/cathelper/CatRestInterceptor.java: -------------------------------------------------------------------------------- 1 | package io.spring2go.cathelper; 2 | 3 | import java.io.IOException; 4 | 5 | import org.springframework.http.HttpHeaders; 6 | import org.springframework.http.HttpRequest; 7 | import org.springframework.http.client.ClientHttpRequestExecution; 8 | import org.springframework.http.client.ClientHttpRequestInterceptor; 9 | import org.springframework.http.client.ClientHttpResponse; 10 | 11 | import com.dianping.cat.Cat; 12 | import com.dianping.cat.Cat.Context; 13 | import com.dianping.cat.CatConstants; 14 | import com.dianping.cat.message.Transaction; 15 | 16 | public class CatRestInterceptor implements ClientHttpRequestInterceptor { 17 | 18 | @Override 19 | public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) 20 | throws IOException { 21 | 22 | Transaction t = Cat.newTransaction(CatConstants.TYPE_REMOTE_CALL, request.getURI().toString()); 23 | 24 | try { 25 | HttpHeaders headers = request.getHeaders(); 26 | 27 | // 保存和传递CAT调用链上下文 28 | Context ctx = new CatContext(); 29 | Cat.logRemoteCallClient(ctx); 30 | headers.add(CatHttpConstants.CAT_HTTP_HEADER_ROOT_MESSAGE_ID, ctx.getProperty(Cat.Context.ROOT)); 31 | headers.add(CatHttpConstants.CAT_HTTP_HEADER_PARENT_MESSAGE_ID, ctx.getProperty(Cat.Context.PARENT)); 32 | headers.add(CatHttpConstants.CAT_HTTP_HEADER_CHILD_MESSAGE_ID, ctx.getProperty(Cat.Context.CHILD)); 33 | 34 | // 保证请求继续被执行 35 | ClientHttpResponse response = execution.execute(request, body); 36 | t.setStatus(Transaction.SUCCESS); 37 | return response; 38 | } catch (Exception e) { 39 | Cat.getProducer().logError(e); 40 | t.setStatus(e); 41 | throw e; 42 | } finally { 43 | t.complete(); 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /statistics-service/src/main/java/io/spring2go/cathelper/CatRestInterceptor.java: -------------------------------------------------------------------------------- 1 | package io.spring2go.cathelper; 2 | 3 | import java.io.IOException; 4 | 5 | import org.springframework.http.HttpHeaders; 6 | import org.springframework.http.HttpRequest; 7 | import org.springframework.http.client.ClientHttpRequestExecution; 8 | import org.springframework.http.client.ClientHttpRequestInterceptor; 9 | import org.springframework.http.client.ClientHttpResponse; 10 | 11 | import com.dianping.cat.Cat; 12 | import com.dianping.cat.Cat.Context; 13 | import com.dianping.cat.CatConstants; 14 | import com.dianping.cat.message.Transaction; 15 | 16 | public class CatRestInterceptor implements ClientHttpRequestInterceptor { 17 | 18 | @Override 19 | public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) 20 | throws IOException { 21 | 22 | Transaction t = Cat.newTransaction(CatConstants.TYPE_REMOTE_CALL, request.getURI().toString()); 23 | 24 | try { 25 | HttpHeaders headers = request.getHeaders(); 26 | 27 | // 保存和传递CAT调用链上下文 28 | Context ctx = new CatContext(); 29 | Cat.logRemoteCallClient(ctx); 30 | headers.add(CatHttpConstants.CAT_HTTP_HEADER_ROOT_MESSAGE_ID, ctx.getProperty(Cat.Context.ROOT)); 31 | headers.add(CatHttpConstants.CAT_HTTP_HEADER_PARENT_MESSAGE_ID, ctx.getProperty(Cat.Context.PARENT)); 32 | headers.add(CatHttpConstants.CAT_HTTP_HEADER_CHILD_MESSAGE_ID, ctx.getProperty(Cat.Context.CHILD)); 33 | 34 | // 保证请求继续被执行 35 | ClientHttpResponse response = execution.execute(request, body); 36 | t.setStatus(Transaction.SUCCESS); 37 | return response; 38 | } catch (Exception e) { 39 | Cat.getProducer().logError(e); 40 | t.setStatus(e); 41 | throw e; 42 | } finally { 43 | t.complete(); 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /notification-service/src/main/java/io/spring2go/cathelper/CatRestInterceptor.java: -------------------------------------------------------------------------------- 1 | package io.spring2go.cathelper; 2 | 3 | import java.io.IOException; 4 | 5 | import org.springframework.http.HttpHeaders; 6 | import org.springframework.http.HttpRequest; 7 | import org.springframework.http.client.ClientHttpRequestExecution; 8 | import org.springframework.http.client.ClientHttpRequestInterceptor; 9 | import org.springframework.http.client.ClientHttpResponse; 10 | 11 | import com.dianping.cat.Cat; 12 | import com.dianping.cat.Cat.Context; 13 | import com.dianping.cat.CatConstants; 14 | import com.dianping.cat.message.Transaction; 15 | 16 | public class CatRestInterceptor implements ClientHttpRequestInterceptor { 17 | 18 | @Override 19 | public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) 20 | throws IOException { 21 | 22 | Transaction t = Cat.newTransaction(CatConstants.TYPE_REMOTE_CALL, request.getURI().toString()); 23 | 24 | try { 25 | HttpHeaders headers = request.getHeaders(); 26 | 27 | // 保存和传递CAT调用链上下文 28 | Context ctx = new CatContext(); 29 | Cat.logRemoteCallClient(ctx); 30 | headers.add(CatHttpConstants.CAT_HTTP_HEADER_ROOT_MESSAGE_ID, ctx.getProperty(Cat.Context.ROOT)); 31 | headers.add(CatHttpConstants.CAT_HTTP_HEADER_PARENT_MESSAGE_ID, ctx.getProperty(Cat.Context.PARENT)); 32 | headers.add(CatHttpConstants.CAT_HTTP_HEADER_CHILD_MESSAGE_ID, ctx.getProperty(Cat.Context.CHILD)); 33 | 34 | // 保证请求继续被执行 35 | ClientHttpResponse response = execution.execute(request, body); 36 | t.setStatus(Transaction.SUCCESS); 37 | return response; 38 | } catch (Exception e) { 39 | Cat.getProducer().logError(e); 40 | t.setStatus(e); 41 | throw e; 42 | } finally { 43 | t.complete(); 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /statistics-service/src/test/java/io/spring2go/piggymetrics/statistics/client/ExchangeRatesClientTest.java: -------------------------------------------------------------------------------- 1 | package io.spring2go.piggymetrics.statistics.client; 2 | 3 | import io.spring2go.piggymetrics.statistics.domain.Currency; 4 | import io.spring2go.piggymetrics.statistics.domain.ExchangeRatesContainer; 5 | import org.junit.Test; 6 | import org.junit.runner.RunWith; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.boot.test.context.SpringBootTest; 9 | import org.springframework.test.context.junit4.SpringRunner; 10 | 11 | import java.time.LocalDate; 12 | 13 | import static org.junit.Assert.assertEquals; 14 | import static org.junit.Assert.assertNotNull; 15 | 16 | @RunWith(SpringRunner.class) 17 | @SpringBootTest 18 | public class ExchangeRatesClientTest { 19 | 20 | @Autowired 21 | private ExchangeRatesClient client; 22 | 23 | @Test 24 | public void shouldRetrieveExchangeRates() { 25 | 26 | ExchangeRatesContainer container = client.getRates(Currency.getBase()); 27 | 28 | assertEquals(container.getDate(), LocalDate.now()); 29 | assertEquals(container.getBase(), Currency.getBase()); 30 | 31 | assertNotNull(container.getRates()); 32 | assertNotNull(container.getRates().get(Currency.USD.name())); 33 | assertNotNull(container.getRates().get(Currency.EUR.name())); 34 | assertNotNull(container.getRates().get(Currency.RUB.name())); 35 | } 36 | 37 | @Test 38 | public void shouldRetrieveExchangeRatesForSpecifiedCurrency() { 39 | 40 | Currency requestedCurrency = Currency.EUR; 41 | ExchangeRatesContainer container = client.getRates(Currency.getBase()); 42 | 43 | assertEquals(container.getDate(), LocalDate.now()); 44 | assertEquals(container.getBase(), Currency.getBase()); 45 | 46 | assertNotNull(container.getRates()); 47 | assertNotNull(container.getRates().get(requestedCurrency.name())); 48 | } 49 | } -------------------------------------------------------------------------------- /account-service/src/main/java/io/spring2go/piggymetrics/account/domain/Account.java: -------------------------------------------------------------------------------- 1 | package io.spring2go.piggymetrics.account.domain; 2 | 3 | import org.hibernate.validator.constraints.Length; 4 | import org.springframework.data.annotation.Id; 5 | import org.springframework.data.mongodb.core.mapping.Document; 6 | 7 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 8 | 9 | import javax.validation.Valid; 10 | import javax.validation.constraints.NotNull; 11 | import java.util.Date; 12 | import java.util.List; 13 | 14 | @Document(collection = "accounts") 15 | @JsonIgnoreProperties(ignoreUnknown = true) 16 | public class Account { 17 | 18 | @Id 19 | private String name; 20 | 21 | private Date lastSeen; 22 | 23 | @Valid 24 | private List incomes; 25 | 26 | @Valid 27 | private List expenses; 28 | 29 | @Valid 30 | @NotNull 31 | private Saving saving; 32 | 33 | @Length(min = 0, max = 20_000) 34 | private String note; 35 | 36 | public String getName() { 37 | return name; 38 | } 39 | 40 | public void setName(String name) { 41 | this.name = name; 42 | } 43 | 44 | public Date getLastSeen() { 45 | return lastSeen; 46 | } 47 | 48 | public void setLastSeen(Date lastSeen) { 49 | this.lastSeen = lastSeen; 50 | } 51 | 52 | public List getIncomes() { 53 | return incomes; 54 | } 55 | 56 | public void setIncomes(List incomes) { 57 | this.incomes = incomes; 58 | } 59 | 60 | public List getExpenses() { 61 | return expenses; 62 | } 63 | 64 | public void setExpenses(List expenses) { 65 | this.expenses = expenses; 66 | } 67 | 68 | public Saving getSaving() { 69 | return saving; 70 | } 71 | 72 | public void setSaving(Saving saving) { 73 | this.saving = saving; 74 | } 75 | 76 | public String getNote() { 77 | return note; 78 | } 79 | 80 | public void setNote(String note) { 81 | this.note = note; 82 | } 83 | } 84 | 85 | -------------------------------------------------------------------------------- /gateway/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | gateway 7 | jar 8 | 9 | gateway 10 | 11 | 12 | io.spring2go 13 | piggymetrics 14 | 1.0-SNAPSHOT 15 | 16 | 17 | 18 | 19 | org.springframework.cloud 20 | spring-cloud-starter-netflix-zuul 21 | 22 | 23 | org.springframework.cloud 24 | spring-cloud-starter 25 | 26 | 27 | org.springframework.cloud 28 | spring-cloud-starter-netflix-eureka-client 29 | 30 | 31 | org.springframework.boot 32 | spring-boot-starter-actuator 33 | 34 | 35 | 36 | io.micrometer 37 | micrometer-registry-prometheus 38 | 39 | 40 | org.springframework.boot 41 | spring-boot-starter-test 42 | test 43 | 44 | 45 | 46 | 47 | 48 | 49 | org.springframework.boot 50 | spring-boot-maven-plugin 51 | 52 | ${project.name} 53 | 54 | 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /notification-service/src/main/java/io/spring2go/piggymetrics/notification/service/EmailServiceImpl.java: -------------------------------------------------------------------------------- 1 | package io.spring2go.piggymetrics.notification.service; 2 | 3 | import java.io.IOException; 4 | import java.text.MessageFormat; 5 | 6 | import javax.mail.MessagingException; 7 | import javax.mail.internet.MimeMessage; 8 | 9 | import org.slf4j.Logger; 10 | import org.slf4j.LoggerFactory; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.cloud.context.config.annotation.RefreshScope; 13 | import org.springframework.core.env.Environment; 14 | import org.springframework.core.io.ByteArrayResource; 15 | import org.springframework.mail.javamail.JavaMailSender; 16 | import org.springframework.mail.javamail.MimeMessageHelper; 17 | import org.springframework.stereotype.Service; 18 | import org.springframework.util.StringUtils; 19 | 20 | import io.spring2go.piggymetrics.notification.CatAnnotation; 21 | import io.spring2go.piggymetrics.notification.domain.NotificationType; 22 | import io.spring2go.piggymetrics.notification.domain.Recipient; 23 | 24 | @Service 25 | @RefreshScope 26 | public class EmailServiceImpl implements EmailService { 27 | 28 | private final Logger log = LoggerFactory.getLogger(getClass()); 29 | 30 | @Autowired 31 | private JavaMailSender mailSender; 32 | 33 | @Autowired 34 | private Environment env; 35 | 36 | @Override 37 | @CatAnnotation 38 | public void send(NotificationType type, Recipient recipient, String attachment) throws MessagingException, IOException { 39 | 40 | final String subject = env.getProperty(type.getSubject()); 41 | final String text = MessageFormat.format(env.getProperty(type.getText()), recipient.getAccountName()); 42 | 43 | MimeMessage message = mailSender.createMimeMessage(); 44 | 45 | MimeMessageHelper helper = new MimeMessageHelper(message, true); 46 | helper.setTo(recipient.getEmail()); 47 | helper.setSubject(subject); 48 | helper.setText(text); 49 | 50 | if (StringUtils.hasLength(attachment)) { 51 | helper.addAttachment(env.getProperty(type.getAttachment()), new ByteArrayResource(attachment.getBytes())); 52 | } 53 | 54 | mailSender.send(message); 55 | log.info("{} email notification has been send to {}", type, recipient.getEmail()); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /mongodb/seed.js: -------------------------------------------------------------------------------- 1 | print('dump start'); 2 | 3 | db.accounts.update( 4 | { "_id": "demo" }, 5 | { 6 | "_id": "demo", 7 | "lastSeen": new Date(), 8 | "note": "demo note", 9 | "expenses": [ 10 | { 11 | "amount": 1300, 12 | "currency": "USD", 13 | "icon": "home", 14 | "period": "MONTH", 15 | "title": "Rent" 16 | }, 17 | { 18 | "amount": 120, 19 | "currency": "USD", 20 | "icon": "utilities", 21 | "period": "MONTH", 22 | "title": "Utilities" 23 | }, 24 | { 25 | "amount": 20, 26 | "currency": "USD", 27 | "icon": "meal", 28 | "period": "DAY", 29 | "title": "Meal" 30 | }, 31 | { 32 | "amount": 240, 33 | "currency": "USD", 34 | "icon": "gas", 35 | "period": "MONTH", 36 | "title": "Gas" 37 | }, 38 | { 39 | "amount": 3500, 40 | "currency": "EUR", 41 | "icon": "island", 42 | "period": "YEAR", 43 | "title": "Vacation" 44 | }, 45 | { 46 | "amount": 30, 47 | "currency": "EUR", 48 | "icon": "phone", 49 | "period": "MONTH", 50 | "title": "Phone" 51 | }, 52 | { 53 | "amount": 700, 54 | "currency": "USD", 55 | "icon": "sport", 56 | "period": "YEAR", 57 | "title": "Gym" 58 | } 59 | ], 60 | "incomes": [ 61 | { 62 | "amount": 42000, 63 | "currency": "USD", 64 | "icon": "wallet", 65 | "period": "YEAR", 66 | "title": "Salary" 67 | }, 68 | { 69 | "amount": 500, 70 | "currency": "USD", 71 | "icon": "edu", 72 | "period": "MONTH", 73 | "title": "Scholarship" 74 | } 75 | ], 76 | "saving": { 77 | "amount": 5900, 78 | "capitalization": false, 79 | "currency": "USD", 80 | "deposit": true, 81 | "interest": 3.32 82 | } 83 | }, 84 | { upsert: true } 85 | ); 86 | 87 | print('dump complete'); -------------------------------------------------------------------------------- /notification-service/src/main/java/io/spring2go/piggymetrics/notification/service/RecipientServiceImpl.java: -------------------------------------------------------------------------------- 1 | package io.spring2go.piggymetrics.notification.service; 2 | 3 | import io.spring2go.piggymetrics.notification.CatAnnotation; 4 | import io.spring2go.piggymetrics.notification.domain.NotificationType; 5 | import io.spring2go.piggymetrics.notification.domain.Recipient; 6 | import io.spring2go.piggymetrics.notification.repository.RecipientRepository; 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.stereotype.Service; 11 | import org.springframework.util.Assert; 12 | 13 | import java.util.Date; 14 | import java.util.List; 15 | 16 | @Service 17 | public class RecipientServiceImpl implements RecipientService { 18 | 19 | private final Logger log = LoggerFactory.getLogger(getClass()); 20 | 21 | @Autowired 22 | private RecipientRepository repository; 23 | 24 | @Override 25 | @CatAnnotation 26 | public Recipient findByAccountName(String accountName) { 27 | Assert.hasLength(accountName); 28 | 29 | return repository.findByAccountName(accountName); 30 | } 31 | 32 | /** 33 | * {@inheritDoc} 34 | */ 35 | @Override 36 | @CatAnnotation 37 | public Recipient save(String accountName, Recipient recipient) { 38 | 39 | recipient.setAccountName(accountName); 40 | recipient.getScheduledNotifications().values() 41 | .forEach(settings -> { 42 | if (settings.getLastNotified() == null) { 43 | settings.setLastNotified(new Date()); 44 | } 45 | }); 46 | 47 | repository.save(recipient); 48 | 49 | log.info("recipient {} settings has been updated", recipient); 50 | 51 | return recipient; 52 | } 53 | 54 | /** 55 | * {@inheritDoc} 56 | */ 57 | @Override 58 | @CatAnnotation 59 | public List findReadyToNotify(NotificationType type) { 60 | switch (type) { 61 | case BACKUP: 62 | 63 | return repository.findReadyForBackup(); 64 | 65 | case REMIND: 66 | 67 | return repository.findReadyForRemind(); 68 | 69 | default: 70 | throw new IllegalArgumentException(); 71 | } 72 | } 73 | 74 | /** 75 | * {@inheritDoc} 76 | */ 77 | @Override 78 | @CatAnnotation 79 | public void markNotified(NotificationType type, Recipient recipient) { 80 | recipient.getScheduledNotifications().get(type).setLastNotified(new Date()); 81 | 82 | repository.save(recipient); 83 | 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /notification-service/src/main/java/io/spring2go/piggymetrics/notification/service/NotificationServiceImpl.java: -------------------------------------------------------------------------------- 1 | package io.spring2go.piggymetrics.notification.service; 2 | 3 | import io.spring2go.piggymetrics.notification.CatAnnotation; 4 | import io.spring2go.piggymetrics.notification.client.AccountServiceClient; 5 | import io.spring2go.piggymetrics.notification.domain.NotificationType; 6 | import io.spring2go.piggymetrics.notification.domain.Recipient; 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.scheduling.annotation.Scheduled; 11 | import org.springframework.stereotype.Service; 12 | 13 | import java.util.List; 14 | import java.util.concurrent.CompletableFuture; 15 | 16 | @Service 17 | public class NotificationServiceImpl implements NotificationService { 18 | 19 | private final Logger log = LoggerFactory.getLogger(getClass()); 20 | 21 | @Autowired 22 | private AccountServiceClient client; 23 | 24 | @Autowired 25 | private RecipientService recipientService; 26 | 27 | @Autowired 28 | private EmailService emailService; 29 | 30 | @Override 31 | @Scheduled(cron = "${backup.cron}") 32 | @CatAnnotation 33 | public void sendBackupNotifications() { 34 | 35 | final NotificationType type = NotificationType.BACKUP; 36 | 37 | List recipients = recipientService.findReadyToNotify(type); 38 | log.info("found {} recipients for backup notification", recipients.size()); 39 | 40 | recipients.forEach(recipient -> CompletableFuture.runAsync(() -> { 41 | try { 42 | String attachment = client.getAccount(recipient.getAccountName()); 43 | emailService.send(type, recipient, attachment); 44 | recipientService.markNotified(type, recipient); 45 | } catch (Throwable t) { 46 | log.error("an error during backup notification for {}", recipient, t); 47 | } 48 | })); 49 | } 50 | 51 | @Override 52 | @Scheduled(cron = "${remind.cron}") 53 | @CatAnnotation 54 | public void sendRemindNotifications() { 55 | 56 | final NotificationType type = NotificationType.REMIND; 57 | 58 | List recipients = recipientService.findReadyToNotify(type); 59 | log.info("found {} recipients for remind notification", recipients.size()); 60 | 61 | recipients.forEach(recipient -> CompletableFuture.runAsync(() -> { 62 | try { 63 | emailService.send(type, recipient, null); 64 | recipientService.markNotified(type, recipient); 65 | } catch (Throwable t) { 66 | log.error("an error during remind notification for {}", recipient, t); 67 | } 68 | })); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /statistics-service/src/main/java/io/spring2go/piggymetrics/statistics/service/ExchangeRatesServiceImpl.java: -------------------------------------------------------------------------------- 1 | package io.spring2go.piggymetrics.statistics.service; 2 | 3 | import com.dianping.cat.Cat; 4 | import com.dianping.cat.CatConstants; 5 | import com.dianping.cat.message.Transaction; 6 | import com.google.common.collect.ImmutableMap; 7 | 8 | import io.spring2go.piggymetrics.statistics.CatAnnotation; 9 | import io.spring2go.piggymetrics.statistics.client.ExchangeRatesClient; 10 | import io.spring2go.piggymetrics.statistics.domain.Currency; 11 | import io.spring2go.piggymetrics.statistics.domain.ExchangeRatesContainer; 12 | import org.slf4j.Logger; 13 | import org.slf4j.LoggerFactory; 14 | import org.springframework.beans.factory.annotation.Autowired; 15 | import org.springframework.stereotype.Service; 16 | import org.springframework.util.Assert; 17 | 18 | import java.math.BigDecimal; 19 | import java.math.RoundingMode; 20 | import java.time.LocalDate; 21 | import java.util.Map; 22 | 23 | @Service 24 | public class ExchangeRatesServiceImpl implements ExchangeRatesService { 25 | 26 | private static final Logger log = LoggerFactory.getLogger(ExchangeRatesServiceImpl.class); 27 | 28 | private ExchangeRatesContainer container; 29 | 30 | @Autowired 31 | private ExchangeRatesClient client; 32 | 33 | /** 34 | * {@inheritDoc} 35 | */ 36 | @Override 37 | @CatAnnotation 38 | public Map getCurrentRates() { 39 | 40 | if (container == null || !container.getDate().equals(LocalDate.now())) { 41 | 42 | Transaction dbTransaction = Cat.newTransaction(CatConstants.TYPE_REMOTE_CALL, "get_current_rates"); 43 | 44 | try { 45 | container = client.getRates(Currency.getBase()); 46 | log.info("exchange rates has been updated: {}", container); 47 | dbTransaction.setStatus(Transaction.SUCCESS); 48 | } catch (Exception e) { 49 | Cat.getProducer().logError(e); 50 | dbTransaction.setStatus(e); 51 | throw e; 52 | } finally { 53 | dbTransaction.complete(); 54 | } 55 | } 56 | 57 | return ImmutableMap.of( 58 | Currency.EUR, container.getRates().get(Currency.EUR.name()), 59 | Currency.RUB, container.getRates().get(Currency.RUB.name()), 60 | Currency.USD, BigDecimal.ONE 61 | ); 62 | } 63 | 64 | /** 65 | * {@inheritDoc} 66 | */ 67 | @Override 68 | @CatAnnotation 69 | public BigDecimal convert(Currency from, Currency to, BigDecimal amount) { 70 | 71 | Assert.notNull(amount); 72 | 73 | Map rates = getCurrentRates(); 74 | BigDecimal ratio = rates.get(to).divide(rates.get(from), 4, RoundingMode.HALF_UP); 75 | 76 | return amount.multiply(ratio); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /gateway/src/main/java/io/spring2go/cathelper/CatServletFilter.java: -------------------------------------------------------------------------------- 1 | package io.spring2go.cathelper; 2 | 3 | import com.dianping.cat.Cat; 4 | import com.dianping.cat.CatConstants; 5 | import com.dianping.cat.message.Message; 6 | import com.dianping.cat.message.Transaction; 7 | 8 | import javax.servlet.*; 9 | import javax.servlet.http.HttpServletRequest; 10 | import java.io.IOException; 11 | 12 | public class CatServletFilter implements Filter { 13 | 14 | private String[] urlPatterns = new String[0]; 15 | 16 | @Override 17 | public void init(FilterConfig filterConfig) throws ServletException { 18 | String patterns = filterConfig.getInitParameter("CatHttpModuleUrlPatterns"); 19 | if (patterns != null) { 20 | patterns = patterns.trim(); 21 | urlPatterns = patterns.split(","); 22 | for (int i = 0; i < urlPatterns.length; i++) { 23 | urlPatterns[i] = urlPatterns[i].trim(); 24 | } 25 | } 26 | } 27 | 28 | @Override 29 | public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { 30 | 31 | HttpServletRequest request = (HttpServletRequest) servletRequest; 32 | 33 | String url = request.getRequestURL().toString(); 34 | for (String urlPattern : urlPatterns) { 35 | if (url.startsWith(urlPattern)) { 36 | url = urlPattern; 37 | } 38 | } 39 | 40 | CatContext catContext = new CatContext(); 41 | catContext.addProperty(Cat.Context.ROOT, request.getHeader(CatHttpConstants.CAT_HTTP_HEADER_ROOT_MESSAGE_ID)); 42 | catContext.addProperty(Cat.Context.PARENT, request.getHeader(CatHttpConstants.CAT_HTTP_HEADER_PARENT_MESSAGE_ID)); 43 | catContext.addProperty(Cat.Context.CHILD, request.getHeader(CatHttpConstants.CAT_HTTP_HEADER_CHILD_MESSAGE_ID)); 44 | Cat.logRemoteCallServer(catContext); 45 | 46 | Transaction t = Cat.newTransaction("Service", url); 47 | 48 | try { 49 | 50 | Cat.logEvent("Service.method", request.getMethod(), Message.SUCCESS, request.getRequestURL().toString()); 51 | Cat.logEvent("Service.client", request.getRemoteHost()); 52 | 53 | filterChain.doFilter(servletRequest, servletResponse); 54 | 55 | t.setStatus(Transaction.SUCCESS); 56 | } catch (Exception ex) { 57 | t.setStatus(ex); 58 | Cat.logError(ex); 59 | throw ex; 60 | } finally { 61 | t.complete(); 62 | } 63 | } 64 | 65 | @Override 66 | public void destroy() { 67 | 68 | } 69 | } -------------------------------------------------------------------------------- /registry/src/main/java/io/spring2go/cathelper/CatServletFilter.java: -------------------------------------------------------------------------------- 1 | package io.spring2go.cathelper; 2 | 3 | import com.dianping.cat.Cat; 4 | import com.dianping.cat.CatConstants; 5 | import com.dianping.cat.message.Message; 6 | import com.dianping.cat.message.Transaction; 7 | 8 | import javax.servlet.*; 9 | import javax.servlet.http.HttpServletRequest; 10 | import java.io.IOException; 11 | 12 | public class CatServletFilter implements Filter { 13 | 14 | private String[] urlPatterns = new String[0]; 15 | 16 | @Override 17 | public void init(FilterConfig filterConfig) throws ServletException { 18 | String patterns = filterConfig.getInitParameter("CatHttpModuleUrlPatterns"); 19 | if (patterns != null) { 20 | patterns = patterns.trim(); 21 | urlPatterns = patterns.split(","); 22 | for (int i = 0; i < urlPatterns.length; i++) { 23 | urlPatterns[i] = urlPatterns[i].trim(); 24 | } 25 | } 26 | } 27 | 28 | @Override 29 | public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { 30 | 31 | HttpServletRequest request = (HttpServletRequest) servletRequest; 32 | 33 | String url = request.getRequestURL().toString(); 34 | for (String urlPattern : urlPatterns) { 35 | if (url.startsWith(urlPattern)) { 36 | url = urlPattern; 37 | } 38 | } 39 | 40 | CatContext catContext = new CatContext(); 41 | catContext.addProperty(Cat.Context.ROOT, request.getHeader(CatHttpConstants.CAT_HTTP_HEADER_ROOT_MESSAGE_ID)); 42 | catContext.addProperty(Cat.Context.PARENT, request.getHeader(CatHttpConstants.CAT_HTTP_HEADER_PARENT_MESSAGE_ID)); 43 | catContext.addProperty(Cat.Context.CHILD, request.getHeader(CatHttpConstants.CAT_HTTP_HEADER_CHILD_MESSAGE_ID)); 44 | Cat.logRemoteCallServer(catContext); 45 | 46 | Transaction t = Cat.newTransaction("Service", url); 47 | 48 | try { 49 | 50 | Cat.logEvent("Service.method", request.getMethod(), Message.SUCCESS, request.getRequestURL().toString()); 51 | Cat.logEvent("Service.client", request.getRemoteHost()); 52 | 53 | filterChain.doFilter(servletRequest, servletResponse); 54 | 55 | t.setStatus(Transaction.SUCCESS); 56 | } catch (Exception ex) { 57 | t.setStatus(ex); 58 | Cat.logError(ex); 59 | throw ex; 60 | } finally { 61 | t.complete(); 62 | } 63 | } 64 | 65 | @Override 66 | public void destroy() { 67 | 68 | } 69 | } -------------------------------------------------------------------------------- /account-service/src/main/java/io/spring2go/cathelper/CatServletFilter.java: -------------------------------------------------------------------------------- 1 | package io.spring2go.cathelper; 2 | 3 | import com.dianping.cat.Cat; 4 | import com.dianping.cat.CatConstants; 5 | import com.dianping.cat.message.Message; 6 | import com.dianping.cat.message.Transaction; 7 | 8 | import javax.servlet.*; 9 | import javax.servlet.http.HttpServletRequest; 10 | import java.io.IOException; 11 | 12 | public class CatServletFilter implements Filter { 13 | 14 | private String[] urlPatterns = new String[0]; 15 | 16 | @Override 17 | public void init(FilterConfig filterConfig) throws ServletException { 18 | String patterns = filterConfig.getInitParameter("CatHttpModuleUrlPatterns"); 19 | if (patterns != null) { 20 | patterns = patterns.trim(); 21 | urlPatterns = patterns.split(","); 22 | for (int i = 0; i < urlPatterns.length; i++) { 23 | urlPatterns[i] = urlPatterns[i].trim(); 24 | } 25 | } 26 | } 27 | 28 | @Override 29 | public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { 30 | 31 | HttpServletRequest request = (HttpServletRequest) servletRequest; 32 | 33 | String url = request.getRequestURL().toString(); 34 | for (String urlPattern : urlPatterns) { 35 | if (url.startsWith(urlPattern)) { 36 | url = urlPattern; 37 | } 38 | } 39 | 40 | CatContext catContext = new CatContext(); 41 | catContext.addProperty(Cat.Context.ROOT, request.getHeader(CatHttpConstants.CAT_HTTP_HEADER_ROOT_MESSAGE_ID)); 42 | catContext.addProperty(Cat.Context.PARENT, request.getHeader(CatHttpConstants.CAT_HTTP_HEADER_PARENT_MESSAGE_ID)); 43 | catContext.addProperty(Cat.Context.CHILD, request.getHeader(CatHttpConstants.CAT_HTTP_HEADER_CHILD_MESSAGE_ID)); 44 | Cat.logRemoteCallServer(catContext); 45 | 46 | Transaction t = Cat.newTransaction("Service", url); 47 | 48 | try { 49 | 50 | Cat.logEvent("Service.method", request.getMethod(), Message.SUCCESS, request.getRequestURL().toString()); 51 | Cat.logEvent("Service.client", request.getRemoteHost()); 52 | 53 | filterChain.doFilter(servletRequest, servletResponse); 54 | 55 | t.setStatus(Transaction.SUCCESS); 56 | } catch (Exception ex) { 57 | t.setStatus(ex); 58 | Cat.logError(ex); 59 | throw ex; 60 | } finally { 61 | t.complete(); 62 | } 63 | } 64 | 65 | @Override 66 | public void destroy() { 67 | 68 | } 69 | } -------------------------------------------------------------------------------- /gateway/src/main/resources/static/js/launch.js: -------------------------------------------------------------------------------- 1 | var global = { 2 | mobileClient: false, 3 | savePermit: true, 4 | usd: 0, 5 | eur: 0 6 | }; 7 | 8 | /** 9 | * Oauth2 10 | */ 11 | function requestOauthToken(username, password) { 12 | 13 | var success = false; 14 | 15 | $.ajax({ 16 | url: 'v1/oauth/tokens', 17 | datatype: 'json', 18 | type: 'post', 19 | headers: {'Authorization': 'Basic dGVzdF9jbGllbnRfMjp0ZXN0X3NlY3JldA=='}, 20 | async: false, 21 | data: { 22 | scope: 'read_write', 23 | username: username, 24 | password: password, 25 | grant_type: 'password' 26 | }, 27 | success: function (data) { 28 | localStorage.setItem('token', data.access_token); 29 | success = true; 30 | }, 31 | error: function () { 32 | removeOauthTokenFromStorage(); 33 | } 34 | }); 35 | 36 | return success; 37 | } 38 | 39 | function getOauthTokenFromStorage() { 40 | return localStorage.getItem('token'); 41 | } 42 | 43 | function removeOauthTokenFromStorage() { 44 | return localStorage.removeItem('token'); 45 | } 46 | 47 | /** 48 | * Current account 49 | */ 50 | 51 | function getCurrentAccount() { 52 | 53 | var token = getOauthTokenFromStorage(); 54 | var account = null; 55 | 56 | if (token) { 57 | $.ajax({ 58 | url: 'accounts/current', 59 | datatype: 'json', 60 | type: 'get', 61 | headers: {'Authorization': 'Bearer ' + token}, 62 | async: false, 63 | success: function (data) { 64 | account = data; 65 | }, 66 | error: function () { 67 | removeOauthTokenFromStorage(); 68 | } 69 | }); 70 | } 71 | 72 | return account; 73 | } 74 | 75 | $(window).load(function(){ 76 | 77 | if(/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) ) { 78 | FastClick.attach(document.body); 79 | global.mobileClient = true; 80 | } 81 | 82 | $.getJSON("https://api.exchangeratesapi.io/latest?base=RUB&symbols=EUR,USD", function( data ) { 83 | global.eur = 1 / data.rates.EUR; 84 | global.usd = 1 / data.rates.USD; 85 | }); 86 | 87 | var account = getCurrentAccount(); 88 | 89 | if (account) { 90 | showGreetingPage(account); 91 | } else { 92 | showLoginForm(); 93 | } 94 | }); 95 | 96 | function showGreetingPage(account) { 97 | initAccount(account); 98 | var userAvatar = $("").attr("src","images/userpic.jpg"); 99 | $(userAvatar).load(function() { 100 | setTimeout(initGreetingPage, 500); 101 | }); 102 | } 103 | 104 | function showLoginForm() { 105 | $("#loginpage").show(); 106 | $("#frontloginform").focus(); 107 | setTimeout(initialShaking, 700); 108 | } -------------------------------------------------------------------------------- /notification-service/src/main/java/io/spring2go/cathelper/CatServletFilter.java: -------------------------------------------------------------------------------- 1 | package io.spring2go.cathelper; 2 | 3 | import com.dianping.cat.Cat; 4 | import com.dianping.cat.CatConstants; 5 | import com.dianping.cat.message.Message; 6 | import com.dianping.cat.message.Transaction; 7 | 8 | import javax.servlet.*; 9 | import javax.servlet.http.HttpServletRequest; 10 | import java.io.IOException; 11 | 12 | public class CatServletFilter implements Filter { 13 | 14 | private String[] urlPatterns = new String[0]; 15 | 16 | @Override 17 | public void init(FilterConfig filterConfig) throws ServletException { 18 | String patterns = filterConfig.getInitParameter("CatHttpModuleUrlPatterns"); 19 | if (patterns != null) { 20 | patterns = patterns.trim(); 21 | urlPatterns = patterns.split(","); 22 | for (int i = 0; i < urlPatterns.length; i++) { 23 | urlPatterns[i] = urlPatterns[i].trim(); 24 | } 25 | } 26 | } 27 | 28 | @Override 29 | public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { 30 | 31 | HttpServletRequest request = (HttpServletRequest) servletRequest; 32 | 33 | String url = request.getRequestURL().toString(); 34 | for (String urlPattern : urlPatterns) { 35 | if (url.startsWith(urlPattern)) { 36 | url = urlPattern; 37 | } 38 | } 39 | 40 | CatContext catContext = new CatContext(); 41 | catContext.addProperty(Cat.Context.ROOT, request.getHeader(CatHttpConstants.CAT_HTTP_HEADER_ROOT_MESSAGE_ID)); 42 | catContext.addProperty(Cat.Context.PARENT, request.getHeader(CatHttpConstants.CAT_HTTP_HEADER_PARENT_MESSAGE_ID)); 43 | catContext.addProperty(Cat.Context.CHILD, request.getHeader(CatHttpConstants.CAT_HTTP_HEADER_CHILD_MESSAGE_ID)); 44 | Cat.logRemoteCallServer(catContext); 45 | 46 | Transaction t = Cat.newTransaction("Service", url); 47 | 48 | try { 49 | 50 | Cat.logEvent("Service.method", request.getMethod(), Message.SUCCESS, request.getRequestURL().toString()); 51 | Cat.logEvent("Service.client", request.getRemoteHost()); 52 | 53 | filterChain.doFilter(servletRequest, servletResponse); 54 | 55 | t.setStatus(Transaction.SUCCESS); 56 | } catch (Exception ex) { 57 | t.setStatus(ex); 58 | Cat.logError(ex); 59 | throw ex; 60 | } finally { 61 | t.complete(); 62 | } 63 | } 64 | 65 | @Override 66 | public void destroy() { 67 | 68 | } 69 | } -------------------------------------------------------------------------------- /statistics-service/src/main/java/io/spring2go/cathelper/CatServletFilter.java: -------------------------------------------------------------------------------- 1 | package io.spring2go.cathelper; 2 | 3 | import com.dianping.cat.Cat; 4 | import com.dianping.cat.CatConstants; 5 | import com.dianping.cat.message.Message; 6 | import com.dianping.cat.message.Transaction; 7 | 8 | import javax.servlet.*; 9 | import javax.servlet.http.HttpServletRequest; 10 | import java.io.IOException; 11 | 12 | public class CatServletFilter implements Filter { 13 | 14 | private String[] urlPatterns = new String[0]; 15 | 16 | @Override 17 | public void init(FilterConfig filterConfig) throws ServletException { 18 | String patterns = filterConfig.getInitParameter("CatHttpModuleUrlPatterns"); 19 | if (patterns != null) { 20 | patterns = patterns.trim(); 21 | urlPatterns = patterns.split(","); 22 | for (int i = 0; i < urlPatterns.length; i++) { 23 | urlPatterns[i] = urlPatterns[i].trim(); 24 | } 25 | } 26 | } 27 | 28 | @Override 29 | public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { 30 | 31 | HttpServletRequest request = (HttpServletRequest) servletRequest; 32 | 33 | String url = request.getRequestURL().toString(); 34 | for (String urlPattern : urlPatterns) { 35 | if (url.startsWith(urlPattern)) { 36 | url = urlPattern; 37 | } 38 | } 39 | 40 | CatContext catContext = new CatContext(); 41 | catContext.addProperty(Cat.Context.ROOT, request.getHeader(CatHttpConstants.CAT_HTTP_HEADER_ROOT_MESSAGE_ID)); 42 | catContext.addProperty(Cat.Context.PARENT, request.getHeader(CatHttpConstants.CAT_HTTP_HEADER_PARENT_MESSAGE_ID)); 43 | catContext.addProperty(Cat.Context.CHILD, request.getHeader(CatHttpConstants.CAT_HTTP_HEADER_CHILD_MESSAGE_ID)); 44 | Cat.logRemoteCallServer(catContext); 45 | 46 | Transaction t = Cat.newTransaction("Service", url); 47 | 48 | try { 49 | 50 | Cat.logEvent("Service.method", request.getMethod(), Message.SUCCESS, request.getRequestURL().toString()); 51 | Cat.logEvent("Service.client", request.getRemoteHost()); 52 | 53 | filterChain.doFilter(servletRequest, servletResponse); 54 | 55 | t.setStatus(Transaction.SUCCESS); 56 | } catch (Exception ex) { 57 | t.setStatus(ex); 58 | Cat.logError(ex); 59 | throw ex; 60 | } finally { 61 | t.complete(); 62 | } 63 | } 64 | 65 | @Override 66 | public void destroy() { 67 | 68 | } 69 | } -------------------------------------------------------------------------------- /account-service/src/test/java/io/spring2go/piggymetrics/account/repository/AccountRepositoryTest.java: -------------------------------------------------------------------------------- 1 | package io.spring2go.piggymetrics.account.repository; 2 | 3 | import io.spring2go.piggymetrics.account.domain.Account; 4 | import io.spring2go.piggymetrics.account.domain.Currency; 5 | import io.spring2go.piggymetrics.account.domain.Item; 6 | import io.spring2go.piggymetrics.account.domain.Saving; 7 | import io.spring2go.piggymetrics.account.domain.TimePeriod; 8 | import org.junit.Test; 9 | import org.junit.runner.RunWith; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.boot.test.autoconfigure.data.mongo.DataMongoTest; 12 | import org.springframework.test.context.junit4.SpringRunner; 13 | 14 | import java.math.BigDecimal; 15 | import java.util.Arrays; 16 | import java.util.Date; 17 | 18 | import static org.junit.Assert.assertEquals; 19 | 20 | @RunWith(SpringRunner.class) 21 | @DataMongoTest 22 | public class AccountRepositoryTest { 23 | 24 | @Autowired 25 | private AccountRepository repository; 26 | 27 | @Test 28 | public void shouldFindAccountByName() { 29 | 30 | Account stub = getStubAccount(); 31 | repository.save(stub); 32 | 33 | Account found = repository.findByName(stub.getName()); 34 | assertEquals(stub.getLastSeen(), found.getLastSeen()); 35 | assertEquals(stub.getNote(), found.getNote()); 36 | assertEquals(stub.getIncomes().size(), found.getIncomes().size()); 37 | assertEquals(stub.getExpenses().size(), found.getExpenses().size()); 38 | } 39 | 40 | private Account getStubAccount() { 41 | 42 | Saving saving = new Saving(); 43 | saving.setAmount(new BigDecimal(1500)); 44 | saving.setCurrency(Currency.USD); 45 | saving.setInterest(new BigDecimal("3.32")); 46 | saving.setDeposit(true); 47 | saving.setCapitalization(false); 48 | 49 | Item vacation = new Item(); 50 | vacation.setTitle("Vacation"); 51 | vacation.setAmount(new BigDecimal(3400)); 52 | vacation.setCurrency(Currency.EUR); 53 | vacation.setPeriod(TimePeriod.YEAR); 54 | vacation.setIcon("tourism"); 55 | 56 | Item grocery = new Item(); 57 | grocery.setTitle("Grocery"); 58 | grocery.setAmount(new BigDecimal(10)); 59 | grocery.setCurrency(Currency.USD); 60 | grocery.setPeriod(TimePeriod.DAY); 61 | grocery.setIcon("meal"); 62 | 63 | Item salary = new Item(); 64 | salary.setTitle("Salary"); 65 | salary.setAmount(new BigDecimal(9100)); 66 | salary.setCurrency(Currency.USD); 67 | salary.setPeriod(TimePeriod.MONTH); 68 | salary.setIcon("wallet"); 69 | 70 | Account account = new Account(); 71 | account.setName("test"); 72 | account.setNote("test note"); 73 | account.setLastSeen(new Date()); 74 | account.setSaving(saving); 75 | account.setExpenses(Arrays.asList(grocery, vacation)); 76 | account.setIncomes(Arrays.asList(salary)); 77 | 78 | return account; 79 | } 80 | } 81 | 82 | -------------------------------------------------------------------------------- /account-service/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | account-service 7 | jar 8 | 9 | account-service 10 | 11 | 12 | io.spring2go 13 | piggymetrics 14 | 1.0-SNAPSHOT 15 | 16 | 17 | 18 | 19 | org.springframework.boot 20 | spring-boot-starter-web 21 | 22 | 23 | org.springframework.cloud 24 | spring-cloud-starter-openfeign 25 | 26 | 27 | org.springframework.cloud 28 | spring-cloud-starter-netflix-eureka-client 29 | 30 | 31 | org.springframework.boot 32 | spring-boot-starter-data-mongodb 33 | 34 | 35 | org.springframework.boot 36 | spring-boot-starter-actuator 37 | 38 | 39 | org.springframework.cloud 40 | spring-cloud-starter-netflix-hystrix 41 | 42 | 43 | org.springframework.boot 44 | spring-boot-starter-aop 45 | 46 | 47 | org.springframework.boot 48 | spring-boot-starter-test 49 | test 50 | 51 | 52 | de.flapdoodle.embed 53 | de.flapdoodle.embed.mongo 54 | test 55 | 56 | 57 | com.jayway.jsonpath 58 | json-path 59 | test 60 | 61 | 62 | 63 | 64 | 65 | 66 | org.springframework.boot 67 | spring-boot-maven-plugin 68 | 69 | account-service 70 | 71 | 72 | 73 | org.jacoco 74 | jacoco-maven-plugin 75 | 0.7.6.201602180812 76 | 77 | 78 | 79 | prepare-agent 80 | 81 | 82 | 83 | report 84 | test 85 | 86 | report 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /notification-service/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | notification-service 7 | jar 8 | 9 | notification-service 10 | 11 | 12 | io.spring2go 13 | piggymetrics 14 | 1.0-SNAPSHOT 15 | 16 | 17 | 18 | 19 | org.springframework.boot 20 | spring-boot-starter-web 21 | 22 | 23 | org.springframework.cloud 24 | spring-cloud-starter-openfeign 25 | 26 | 27 | org.springframework.cloud 28 | spring-cloud-starter-netflix-eureka-client 29 | 30 | 31 | org.springframework.boot 32 | spring-boot-starter-data-mongodb 33 | 34 | 35 | org.springframework.boot 36 | spring-boot-starter-actuator 37 | 38 | 39 | org.springframework.boot 40 | spring-boot-starter-mail 41 | 42 | 43 | org.springframework.boot 44 | spring-boot-starter-aop 45 | 46 | 47 | de.flapdoodle.embed 48 | de.flapdoodle.embed.mongo 49 | test 50 | 51 | 52 | com.jayway.jsonpath 53 | json-path 54 | test 55 | 56 | 57 | org.springframework.boot 58 | spring-boot-starter-test 59 | test 60 | 61 | 62 | 63 | 64 | 65 | 66 | org.springframework.boot 67 | spring-boot-maven-plugin 68 | 69 | notification-service 70 | 71 | 72 | 73 | org.jacoco 74 | jacoco-maven-plugin 75 | 0.7.6.201602180812 76 | 77 | 78 | 79 | prepare-agent 80 | 81 | 82 | 83 | report 84 | test 85 | 86 | report 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /statistics-service/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | statistics-service 7 | jar 8 | 9 | statistics-service 10 | 11 | 12 | io.spring2go 13 | piggymetrics 14 | 1.0-SNAPSHOT 15 | 16 | 17 | 18 | 19 | org.springframework.boot 20 | spring-boot-starter-web 21 | 22 | 23 | org.springframework.cloud 24 | spring-cloud-starter-openfeign 25 | 26 | 27 | org.springframework.cloud 28 | spring-cloud-starter-netflix-eureka-client 29 | 30 | 31 | org.springframework.boot 32 | spring-boot-starter-data-mongodb 33 | 34 | 35 | org.springframework.boot 36 | spring-boot-starter-actuator 37 | 38 | 39 | org.springframework.boot 40 | spring-boot-starter-aop 41 | 42 | 43 | com.google.guava 44 | guava 45 | 19.0 46 | 47 | 48 | 49 | org.springframework.boot 50 | spring-boot-starter-test 51 | test 52 | 53 | 54 | de.flapdoodle.embed 55 | de.flapdoodle.embed.mongo 56 | test 57 | 58 | 59 | com.jayway.jsonpath 60 | json-path 61 | test 62 | 63 | 64 | 65 | 66 | 67 | 68 | org.springframework.boot 69 | spring-boot-maven-plugin 70 | 71 | statistics-service 72 | 73 | 74 | 75 | org.jacoco 76 | jacoco-maven-plugin 77 | 0.7.6.201602180812 78 | 79 | 80 | 81 | prepare-agent 82 | 83 | 84 | 85 | report 86 | test 87 | 88 | report 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | 5 | io.spring2go 6 | piggymetrics 7 | 1.0-SNAPSHOT 8 | pom 9 | piggymetrics 10 | 11 | 12 | org.springframework.boot 13 | spring-boot-starter-parent 14 | 2.1.0.RELEASE 15 | 16 | 17 | 18 | 19 | UTF-8 20 | UTF-8 21 | 1.8 22 | Greenwich.M1 23 | 1.0.0 24 | 3.0.0 25 | 26 | 27 | 28 | 29 | 30 | org.springframework.cloud 31 | spring-cloud-dependencies 32 | ${spring-cloud.version} 33 | pom 34 | import 35 | 36 | 37 | 38 | com.ctrip.framework.apollo 39 | apollo-client 40 | ${apollo.version} 41 | 42 | 43 | 44 | com.dianping.cat 45 | cat-client 46 | ${cat.version} 47 | 48 | 49 | 50 | 51 | 52 | 53 | com.ctrip.framework.apollo 54 | apollo-client 55 | 56 | 57 | com.dianping.cat 58 | cat-client 59 | 60 | 61 | 62 | 63 | 64 | 65 | org.springframework.boot 66 | spring-boot-maven-plugin 67 | 68 | 69 | 70 | 71 | 72 | 73 | spring-milestones 74 | Spring Milestones 75 | https://repo.spring.io/milestone 76 | 77 | false 78 | 79 | 80 | 81 | unidal.releases 82 | http://unidal.org/nexus/content/repositories/releases/ 83 | 84 | 85 | 86 | 87 | gateway 88 | registry 89 | account-service 90 | statistics-service 91 | notification-service 92 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /notification-service/src/test/java/io/spring2go/piggymetrics/notification/service/EmailServiceImplTest.java: -------------------------------------------------------------------------------- 1 | package io.spring2go.piggymetrics.notification.service; 2 | 3 | import io.spring2go.piggymetrics.notification.domain.NotificationType; 4 | import io.spring2go.piggymetrics.notification.domain.Recipient; 5 | import org.junit.Before; 6 | import org.junit.Test; 7 | import org.mockito.ArgumentCaptor; 8 | import org.mockito.Captor; 9 | import org.mockito.InjectMocks; 10 | import org.mockito.Mock; 11 | import org.springframework.core.env.Environment; 12 | import org.springframework.mail.javamail.JavaMailSender; 13 | 14 | import javax.mail.MessagingException; 15 | import javax.mail.Session; 16 | import javax.mail.internet.MimeMessage; 17 | import java.io.IOException; 18 | import java.util.Properties; 19 | 20 | import static org.junit.Assert.assertEquals; 21 | import static org.mockito.Mockito.verify; 22 | import static org.mockito.Mockito.when; 23 | import static org.mockito.MockitoAnnotations.initMocks; 24 | 25 | public class EmailServiceImplTest { 26 | 27 | @InjectMocks 28 | private EmailServiceImpl emailService; 29 | 30 | @Mock 31 | private JavaMailSender mailSender; 32 | 33 | @Mock 34 | private Environment env; 35 | 36 | @Captor 37 | private ArgumentCaptor captor; 38 | 39 | @Before 40 | public void setup() { 41 | initMocks(this); 42 | when(mailSender.createMimeMessage()) 43 | .thenReturn(new MimeMessage(Session.getDefaultInstance(new Properties()))); 44 | } 45 | 46 | @Test 47 | public void shouldSendBackupEmail() throws MessagingException, IOException { 48 | 49 | final String subject = "subject"; 50 | final String text = "text"; 51 | final String attachment = "attachment.json"; 52 | 53 | Recipient recipient = new Recipient(); 54 | recipient.setAccountName("test"); 55 | recipient.setEmail("test@test.com"); 56 | 57 | when(env.getProperty(NotificationType.BACKUP.getSubject())).thenReturn(subject); 58 | when(env.getProperty(NotificationType.BACKUP.getText())).thenReturn(text); 59 | when(env.getProperty(NotificationType.BACKUP.getAttachment())).thenReturn(attachment); 60 | 61 | emailService.send(NotificationType.BACKUP, recipient, "{\"name\":\"test\""); 62 | 63 | verify(mailSender).send(captor.capture()); 64 | 65 | MimeMessage message = captor.getValue(); 66 | assertEquals(subject, message.getSubject()); 67 | // TODO check other fields 68 | } 69 | 70 | @Test 71 | public void shouldSendRemindEmail() throws MessagingException, IOException { 72 | 73 | final String subject = "subject"; 74 | final String text = "text"; 75 | 76 | Recipient recipient = new Recipient(); 77 | recipient.setAccountName("test"); 78 | recipient.setEmail("test@test.com"); 79 | 80 | when(env.getProperty(NotificationType.REMIND.getSubject())).thenReturn(subject); 81 | when(env.getProperty(NotificationType.REMIND.getText())).thenReturn(text); 82 | 83 | emailService.send(NotificationType.REMIND, recipient, null); 84 | 85 | verify(mailSender).send(captor.capture()); 86 | 87 | MimeMessage message = captor.getValue(); 88 | assertEquals(subject, message.getSubject()); 89 | // TODO check other fields 90 | } 91 | } -------------------------------------------------------------------------------- /account-service/src/main/java/io/spring2go/piggymetrics/account/service/AccountServiceImpl.java: -------------------------------------------------------------------------------- 1 | package io.spring2go.piggymetrics.account.service; 2 | 3 | import io.spring2go.piggymetrics.account.CatAnnotation; 4 | import io.spring2go.piggymetrics.account.client.StatisticsServiceClient; 5 | import io.spring2go.piggymetrics.account.client.UserServiceClient; 6 | import io.spring2go.piggymetrics.account.domain.Account; 7 | import io.spring2go.piggymetrics.account.domain.Currency; 8 | import io.spring2go.piggymetrics.account.domain.Saving; 9 | import io.spring2go.piggymetrics.account.domain.User; 10 | import io.spring2go.piggymetrics.account.repository.AccountRepository; 11 | import org.slf4j.Logger; 12 | import org.slf4j.LoggerFactory; 13 | import org.springframework.beans.factory.annotation.Autowired; 14 | import org.springframework.stereotype.Service; 15 | import org.springframework.util.Assert; 16 | 17 | import java.math.BigDecimal; 18 | import java.util.Date; 19 | 20 | @Service 21 | public class AccountServiceImpl implements AccountService { 22 | 23 | private final Logger log = LoggerFactory.getLogger(getClass()); 24 | 25 | @Autowired 26 | private StatisticsServiceClient statisticsClient; 27 | 28 | @Autowired 29 | private UserServiceClient userClient; 30 | 31 | @Autowired 32 | private AccountRepository repository; 33 | 34 | /** 35 | * {@inheritDoc} 36 | */ 37 | @Override 38 | @CatAnnotation 39 | public Account findByName(String accountName) { 40 | Assert.hasLength(accountName); 41 | 42 | Account account = repository.findByName(accountName); 43 | return account; 44 | } 45 | 46 | /** 47 | * {@inheritDoc} 48 | */ 49 | @Override 50 | @CatAnnotation 51 | public Account create(User user) { 52 | 53 | Account existing = repository.findByName(user.getUsername()); 54 | Assert.isNull(existing, "account already exists: " + user.getUsername()); 55 | 56 | userClient.createUser(user.getUsername(), user.getPassword()); 57 | 58 | Saving saving = new Saving(); 59 | saving.setAmount(new BigDecimal(0)); 60 | saving.setCurrency(Currency.getDefault()); 61 | saving.setInterest(new BigDecimal(0)); 62 | saving.setDeposit(false); 63 | saving.setCapitalization(false); 64 | 65 | Account account = new Account(); 66 | account.setName(user.getUsername()); 67 | account.setLastSeen(new Date()); 68 | account.setSaving(saving); 69 | 70 | repository.save(account); 71 | 72 | log.info("new account has been created: " + account.getName()); 73 | 74 | return account; 75 | } 76 | 77 | /** 78 | * {@inheritDoc} 79 | */ 80 | @Override 81 | @CatAnnotation 82 | public void saveChanges(String name, Account update) { 83 | 84 | Account account = repository.findByName(name); 85 | Assert.notNull(account, "can't find account with name " + name); 86 | 87 | account.setIncomes(update.getIncomes()); 88 | account.setExpenses(update.getExpenses()); 89 | account.setSaving(update.getSaving()); 90 | account.setNote(update.getNote()); 91 | account.setLastSeen(new Date()); 92 | 93 | repository.save(account); 94 | 95 | log.debug("account {} changes has been saved", name); 96 | 97 | statisticsClient.updateStatistics(name, account); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /notification-service/src/test/java/io/spring2go/piggymetrics/notification/service/NotificationServiceImplTest.java: -------------------------------------------------------------------------------- 1 | package io.spring2go.piggymetrics.notification.service; 2 | 3 | import com.google.common.collect.ImmutableList; 4 | import io.spring2go.piggymetrics.notification.client.AccountServiceClient; 5 | import io.spring2go.piggymetrics.notification.domain.NotificationType; 6 | import io.spring2go.piggymetrics.notification.domain.Recipient; 7 | import org.junit.Before; 8 | import org.junit.Test; 9 | import org.mockito.InjectMocks; 10 | import org.mockito.Mock; 11 | 12 | import javax.mail.MessagingException; 13 | import java.io.IOException; 14 | 15 | import static org.mockito.Mockito.*; 16 | import static org.mockito.MockitoAnnotations.initMocks; 17 | 18 | public class NotificationServiceImplTest { 19 | 20 | @InjectMocks 21 | private NotificationServiceImpl notificationService; 22 | 23 | @Mock 24 | private RecipientService recipientService; 25 | 26 | @Mock 27 | private AccountServiceClient client; 28 | 29 | @Mock 30 | private EmailService emailService; 31 | 32 | @Before 33 | public void setup() { 34 | initMocks(this); 35 | } 36 | 37 | @Test 38 | public void shouldSendBackupNotificationsEvenWhenErrorsOccursForSomeRecipients() throws IOException, MessagingException, InterruptedException { 39 | 40 | final String attachment = "json"; 41 | 42 | Recipient withError = new Recipient(); 43 | withError.setAccountName("with-error"); 44 | 45 | Recipient withNoError = new Recipient(); 46 | withNoError.setAccountName("with-no-error"); 47 | 48 | when(client.getAccount(withError.getAccountName())).thenThrow(new RuntimeException()); 49 | when(client.getAccount(withNoError.getAccountName())).thenReturn(attachment); 50 | 51 | when(recipientService.findReadyToNotify(NotificationType.BACKUP)).thenReturn(ImmutableList.of(withNoError, withError)); 52 | 53 | notificationService.sendBackupNotifications(); 54 | 55 | // TODO test concurrent code in a right way 56 | 57 | verify(emailService, timeout(100)).send(NotificationType.BACKUP, withNoError, attachment); 58 | verify(recipientService, timeout(100)).markNotified(NotificationType.BACKUP, withNoError); 59 | 60 | verify(recipientService, never()).markNotified(NotificationType.BACKUP, withError); 61 | } 62 | 63 | @Test 64 | public void shouldSendRemindNotificationsEvenWhenErrorsOccursForSomeRecipients() throws IOException, MessagingException, InterruptedException { 65 | 66 | final String attachment = "json"; 67 | 68 | Recipient withError = new Recipient(); 69 | withError.setAccountName("with-error"); 70 | 71 | Recipient withNoError = new Recipient(); 72 | withNoError.setAccountName("with-no-error"); 73 | 74 | when(recipientService.findReadyToNotify(NotificationType.REMIND)).thenReturn(ImmutableList.of(withNoError, withError)); 75 | doThrow(new RuntimeException()).when(emailService).send(NotificationType.REMIND, withError, null); 76 | 77 | notificationService.sendRemindNotifications(); 78 | 79 | // TODO test concurrent code in a right way 80 | 81 | verify(emailService, timeout(100)).send(NotificationType.REMIND, withNoError, null); 82 | verify(recipientService, timeout(100)).markNotified(NotificationType.REMIND, withNoError); 83 | 84 | verify(recipientService, never()).markNotified(NotificationType.REMIND, withError); 85 | } 86 | } -------------------------------------------------------------------------------- /statistics-service/src/test/java/io/spring2go/piggymetrics/statistics/service/ExchangeRatesServiceImplTest.java: -------------------------------------------------------------------------------- 1 | package io.spring2go.piggymetrics.statistics.service; 2 | 3 | import com.google.common.collect.ImmutableMap; 4 | import io.spring2go.piggymetrics.statistics.client.ExchangeRatesClient; 5 | import io.spring2go.piggymetrics.statistics.domain.Currency; 6 | import io.spring2go.piggymetrics.statistics.domain.ExchangeRatesContainer; 7 | import org.junit.Before; 8 | import org.junit.Test; 9 | import org.mockito.InjectMocks; 10 | import org.mockito.Mock; 11 | 12 | import java.math.BigDecimal; 13 | import java.util.Map; 14 | 15 | import static org.junit.Assert.assertEquals; 16 | import static org.junit.Assert.assertTrue; 17 | import static org.mockito.Mockito.*; 18 | import static org.mockito.MockitoAnnotations.initMocks; 19 | 20 | public class ExchangeRatesServiceImplTest { 21 | 22 | @InjectMocks 23 | private ExchangeRatesServiceImpl ratesService; 24 | 25 | @Mock 26 | private ExchangeRatesClient client; 27 | 28 | @Before 29 | public void setup() { 30 | initMocks(this); 31 | } 32 | 33 | @Test 34 | public void shouldReturnCurrentRatesWhenContainerIsEmptySoFar() { 35 | 36 | ExchangeRatesContainer container = new ExchangeRatesContainer(); 37 | container.setRates(ImmutableMap.of( 38 | Currency.EUR.name(), new BigDecimal("0.8"), 39 | Currency.RUB.name(), new BigDecimal("80") 40 | )); 41 | 42 | when(client.getRates(Currency.getBase())).thenReturn(container); 43 | 44 | Map result = ratesService.getCurrentRates(); 45 | verify(client, times(1)).getRates(Currency.getBase()); 46 | 47 | assertEquals(container.getRates().get(Currency.EUR.name()), result.get(Currency.EUR)); 48 | assertEquals(container.getRates().get(Currency.RUB.name()), result.get(Currency.RUB)); 49 | assertEquals(BigDecimal.ONE, result.get(Currency.USD)); 50 | } 51 | 52 | @Test 53 | public void shouldNotRequestRatesWhenTodaysContainerAlreadyExists() { 54 | 55 | ExchangeRatesContainer container = new ExchangeRatesContainer(); 56 | container.setRates(ImmutableMap.of( 57 | Currency.EUR.name(), new BigDecimal("0.8"), 58 | Currency.RUB.name(), new BigDecimal("80") 59 | )); 60 | 61 | when(client.getRates(Currency.getBase())).thenReturn(container); 62 | 63 | // initialize container 64 | ratesService.getCurrentRates(); 65 | 66 | // use existing container 67 | ratesService.getCurrentRates(); 68 | 69 | verify(client, times(1)).getRates(Currency.getBase()); 70 | } 71 | 72 | @Test 73 | public void shouldConvertCurrency() { 74 | 75 | ExchangeRatesContainer container = new ExchangeRatesContainer(); 76 | container.setRates(ImmutableMap.of( 77 | Currency.EUR.name(), new BigDecimal("0.8"), 78 | Currency.RUB.name(), new BigDecimal("80") 79 | )); 80 | 81 | when(client.getRates(Currency.getBase())).thenReturn(container); 82 | 83 | final BigDecimal amount = new BigDecimal(100); 84 | final BigDecimal expectedConvertionResult = new BigDecimal("1.25"); 85 | 86 | BigDecimal result = ratesService.convert(Currency.RUB, Currency.USD, amount); 87 | 88 | assertTrue(expectedConvertionResult.compareTo(result) == 0); 89 | } 90 | 91 | @Test(expected = IllegalArgumentException.class) 92 | public void shouldFailToConvertWhenAmountIsNull() { 93 | ratesService.convert(Currency.EUR, Currency.RUB, null); 94 | } 95 | } -------------------------------------------------------------------------------- /statistics-service/src/test/java/io/spring2go/piggymetrics/statistics/repository/DataPointRepositoryTest.java: -------------------------------------------------------------------------------- 1 | package io.spring2go.piggymetrics.statistics.repository; 2 | 3 | import com.google.common.collect.ImmutableMap; 4 | import com.google.common.collect.Sets; 5 | import io.spring2go.piggymetrics.statistics.domain.timeseries.DataPoint; 6 | import io.spring2go.piggymetrics.statistics.domain.timeseries.DataPointId; 7 | import io.spring2go.piggymetrics.statistics.domain.timeseries.ItemMetric; 8 | import io.spring2go.piggymetrics.statistics.domain.timeseries.StatisticMetric; 9 | import org.junit.Test; 10 | import org.junit.runner.RunWith; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.boot.test.autoconfigure.data.mongo.DataMongoTest; 13 | import org.springframework.test.context.junit4.SpringRunner; 14 | 15 | import java.math.BigDecimal; 16 | import java.util.Date; 17 | import java.util.List; 18 | 19 | import static org.junit.Assert.assertEquals; 20 | 21 | @RunWith(SpringRunner.class) 22 | @DataMongoTest 23 | public class DataPointRepositoryTest { 24 | 25 | @Autowired 26 | private DataPointRepository repository; 27 | 28 | @Test 29 | public void shouldSaveDataPoint() { 30 | 31 | ItemMetric salary = new ItemMetric("salary", new BigDecimal(20_000)); 32 | 33 | ItemMetric grocery = new ItemMetric("grocery", new BigDecimal(1_000)); 34 | ItemMetric vacation = new ItemMetric("vacation", new BigDecimal(2_000)); 35 | 36 | DataPointId pointId = new DataPointId("test-account", new Date(0)); 37 | 38 | DataPoint point = new DataPoint(); 39 | point.setId(pointId); 40 | point.setIncomes(Sets.newHashSet(salary)); 41 | point.setExpenses(Sets.newHashSet(grocery, vacation)); 42 | point.setStatistics(ImmutableMap.of( 43 | StatisticMetric.SAVING_AMOUNT, new BigDecimal(400_000), 44 | StatisticMetric.INCOMES_AMOUNT, new BigDecimal(20_000), 45 | StatisticMetric.EXPENSES_AMOUNT, new BigDecimal(3_000) 46 | )); 47 | 48 | repository.save(point); 49 | 50 | List points = repository.findByIdAccount(pointId.getAccount()); 51 | assertEquals(1, points.size()); 52 | assertEquals(pointId.getDate(), points.get(0).getId().getDate()); 53 | assertEquals(point.getStatistics().size(), points.get(0).getStatistics().size()); 54 | assertEquals(point.getIncomes().size(), points.get(0).getIncomes().size()); 55 | assertEquals(point.getExpenses().size(), points.get(0).getExpenses().size()); 56 | } 57 | 58 | @Test 59 | public void shouldRewriteDataPointWithinADay() { 60 | 61 | final BigDecimal earlyAmount = new BigDecimal(100); 62 | final BigDecimal lateAmount = new BigDecimal(200); 63 | 64 | DataPointId pointId = new DataPointId("test-account", new Date(0)); 65 | 66 | DataPoint earlier = new DataPoint(); 67 | earlier.setId(pointId); 68 | earlier.setStatistics(ImmutableMap.of( 69 | StatisticMetric.SAVING_AMOUNT, earlyAmount 70 | )); 71 | 72 | repository.save(earlier); 73 | 74 | DataPoint later = new DataPoint(); 75 | later.setId(pointId); 76 | later.setStatistics(ImmutableMap.of( 77 | StatisticMetric.SAVING_AMOUNT, lateAmount 78 | )); 79 | 80 | repository.save(later); 81 | 82 | List points = repository.findByIdAccount(pointId.getAccount()); 83 | 84 | assertEquals(1, points.size()); 85 | assertEquals(lateAmount, points.get(0).getStatistics().get(StatisticMetric.SAVING_AMOUNT)); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /notification-service/src/test/java/io/spring2go/piggymetrics/notification/controller/RecipientControllerTest.java: -------------------------------------------------------------------------------- 1 | package io.spring2go.piggymetrics.notification.controller; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import com.google.common.collect.ImmutableMap; 5 | import io.spring2go.piggymetrics.notification.domain.Frequency; 6 | import io.spring2go.piggymetrics.notification.domain.NotificationSettings; 7 | import io.spring2go.piggymetrics.notification.domain.NotificationType; 8 | import io.spring2go.piggymetrics.notification.domain.Recipient; 9 | import io.spring2go.piggymetrics.notification.service.RecipientService; 10 | import com.sun.security.auth.UserPrincipal; 11 | import org.junit.Before; 12 | import org.junit.Test; 13 | import org.junit.runner.RunWith; 14 | import org.mockito.InjectMocks; 15 | import org.mockito.Mock; 16 | import org.springframework.boot.test.context.SpringBootTest; 17 | import org.springframework.http.MediaType; 18 | import org.springframework.test.context.junit4.SpringRunner; 19 | import org.springframework.test.web.servlet.MockMvc; 20 | import org.springframework.test.web.servlet.setup.MockMvcBuilders; 21 | 22 | import static org.mockito.Mockito.when; 23 | import static org.mockito.MockitoAnnotations.initMocks; 24 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 25 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; 26 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; 27 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 28 | 29 | @RunWith(SpringRunner.class) 30 | @SpringBootTest 31 | public class RecipientControllerTest { 32 | 33 | private static final ObjectMapper mapper = new ObjectMapper(); 34 | 35 | @InjectMocks 36 | private RecipientController recipientController; 37 | 38 | @Mock 39 | private RecipientService recipientService; 40 | 41 | private MockMvc mockMvc; 42 | 43 | @Before 44 | public void setup() { 45 | initMocks(this); 46 | this.mockMvc = MockMvcBuilders.standaloneSetup(recipientController).build(); 47 | } 48 | 49 | @Test 50 | public void shouldSaveCurrentRecipientSettings() throws Exception { 51 | 52 | Recipient recipient = getStubRecipient(); 53 | String json = mapper.writeValueAsString(recipient); 54 | 55 | mockMvc.perform(put("/recipients/current").principal(new UserPrincipal(recipient.getAccountName())).contentType(MediaType.APPLICATION_JSON).content(json)) 56 | .andExpect(status().isOk()); 57 | } 58 | 59 | @Test 60 | public void shouldGetCurrentRecipientSettings() throws Exception { 61 | 62 | Recipient recipient = getStubRecipient(); 63 | when(recipientService.findByAccountName(recipient.getAccountName())).thenReturn(recipient); 64 | 65 | mockMvc.perform(get("/recipients/current").principal(new UserPrincipal(recipient.getAccountName()))) 66 | .andExpect(jsonPath("$.accountName").value(recipient.getAccountName())) 67 | .andExpect(status().isOk()); 68 | } 69 | 70 | private Recipient getStubRecipient() { 71 | 72 | NotificationSettings remind = new NotificationSettings(); 73 | remind.setActive(true); 74 | remind.setFrequency(Frequency.WEEKLY); 75 | remind.setLastNotified(null); 76 | 77 | NotificationSettings backup = new NotificationSettings(); 78 | backup.setActive(false); 79 | backup.setFrequency(Frequency.MONTHLY); 80 | backup.setLastNotified(null); 81 | 82 | Recipient recipient = new Recipient(); 83 | recipient.setAccountName("test"); 84 | recipient.setEmail("test@test.com"); 85 | recipient.setScheduledNotifications(ImmutableMap.of( 86 | NotificationType.BACKUP, backup, 87 | NotificationType.REMIND, remind 88 | )); 89 | 90 | return recipient; 91 | } 92 | } -------------------------------------------------------------------------------- /gateway/src/main/java/io/spring2go/piggymetrics/gateway/filter/ValidateTokenFilter.java: -------------------------------------------------------------------------------- 1 | package io.spring2go.piggymetrics.gateway.filter; 2 | 3 | import java.util.Map; 4 | 5 | import javax.servlet.http.HttpServletRequest; 6 | 7 | import org.apache.http.HttpStatus; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.boot.context.properties.ConfigurationProperties; 10 | import org.springframework.boot.context.properties.EnableConfigurationProperties; 11 | import org.springframework.http.HttpEntity; 12 | import org.springframework.http.HttpHeaders; 13 | import org.springframework.stereotype.Component; 14 | import org.springframework.util.LinkedMultiValueMap; 15 | import org.springframework.util.MultiValueMap; 16 | import org.springframework.web.client.RestTemplate; 17 | 18 | import com.netflix.zuul.ZuulFilter; 19 | import com.netflix.zuul.context.RequestContext; 20 | import com.netflix.zuul.exception.ZuulException; 21 | 22 | import io.netty.util.internal.StringUtil; 23 | 24 | // 令牌集中校验过滤器 25 | @Component 26 | @EnableConfigurationProperties 27 | @ConfigurationProperties(prefix = "piggymetrics") 28 | public class ValidateTokenFilter extends ZuulFilter { 29 | 30 | @Autowired 31 | private RestTemplate restTemplate; 32 | 33 | @Autowired 34 | private ValidationConfig config; 35 | 36 | @Override 37 | public boolean shouldFilter() { 38 | RequestContext requestContext = RequestContext.getCurrentContext(); 39 | 40 | HttpServletRequest request = requestContext.getRequest(); 41 | String url = request.getRequestURI(); 42 | String requestMethod = requestContext.getRequest().getMethod(); 43 | 44 | // check if token validation should be enabled 45 | if (url.startsWith("/accounts")) { 46 | // no auth for demo account 47 | if (url.startsWith("/accounts/demo")) 48 | return false; 49 | // no auth for new account registration 50 | if ("POST".equals(requestMethod)) 51 | return false; 52 | return true; 53 | } 54 | 55 | if (url.startsWith("/statistics") || url.startsWith("/notifications")) { 56 | return true; 57 | } 58 | 59 | return false; 60 | 61 | } 62 | 63 | @Override 64 | public Object run() throws ZuulException { 65 | RequestContext requestContext = RequestContext.getCurrentContext(); 66 | 67 | String token = requestContext.getRequest().getHeader("Authorization"); 68 | if (StringUtil.isNullOrEmpty(token)) { 69 | throw new ZuulException("no token found", HttpStatus.SC_UNAUTHORIZED, "no token found"); 70 | } 71 | 72 | token = token.replace("Bearer ", ""); // remove prefix 73 | 74 | HttpHeaders headers = new HttpHeaders(); 75 | headers.add("Authorization", "Basic " + config.getBase64Credentials()); 76 | 77 | MultiValueMap map = new LinkedMultiValueMap<>(); 78 | map.add("token", token); 79 | map.add("token_type_hint", "access_token"); 80 | 81 | HttpEntity> request = new HttpEntity<>(map, headers); 82 | 83 | String url = config.getTokenIntrospectEndpoint(); 84 | @SuppressWarnings("unchecked") 85 | Map resultMap = restTemplate.postForEntity(url, request, Map.class) 86 | .getBody(); 87 | 88 | Boolean active = (Boolean) resultMap.get("active"); 89 | 90 | if (active == null || !active) { 91 | throw new ZuulException("token inactive", HttpStatus.SC_UNAUTHORIZED, "token inactive"); 92 | } 93 | 94 | String username = (String) resultMap.get("username"); 95 | if (StringUtil.isNullOrEmpty(username)) { 96 | throw new ZuulException("username empty", HttpStatus.SC_UNAUTHORIZED, "username empty"); 97 | } 98 | 99 | requestContext.addZuulRequestHeader("X-S2G-USERNAME", username); 100 | 101 | return null; 102 | } 103 | 104 | @Override 105 | public String filterType() { 106 | return "pre"; 107 | } 108 | 109 | @Override 110 | public int filterOrder() { 111 | return 5; 112 | } 113 | 114 | } 115 | -------------------------------------------------------------------------------- /statistics-service/src/main/java/io/spring2go/piggymetrics/statistics/service/StatisticsServiceImpl.java: -------------------------------------------------------------------------------- 1 | package io.spring2go.piggymetrics.statistics.service; 2 | 3 | import com.google.common.collect.ImmutableMap; 4 | 5 | import io.spring2go.piggymetrics.statistics.CatAnnotation; 6 | import io.spring2go.piggymetrics.statistics.domain.*; 7 | import io.spring2go.piggymetrics.statistics.domain.timeseries.DataPoint; 8 | import io.spring2go.piggymetrics.statistics.domain.timeseries.DataPointId; 9 | import io.spring2go.piggymetrics.statistics.domain.timeseries.ItemMetric; 10 | import io.spring2go.piggymetrics.statistics.domain.timeseries.StatisticMetric; 11 | import io.spring2go.piggymetrics.statistics.repository.DataPointRepository; 12 | import org.slf4j.Logger; 13 | import org.slf4j.LoggerFactory; 14 | import org.springframework.beans.factory.annotation.Autowired; 15 | import org.springframework.stereotype.Service; 16 | import org.springframework.util.Assert; 17 | 18 | import java.math.BigDecimal; 19 | import java.math.RoundingMode; 20 | import java.time.Instant; 21 | import java.time.LocalDate; 22 | import java.time.ZoneId; 23 | import java.util.Date; 24 | import java.util.List; 25 | import java.util.Map; 26 | import java.util.Set; 27 | import java.util.stream.Collectors; 28 | 29 | @Service 30 | public class StatisticsServiceImpl implements StatisticsService { 31 | 32 | private final Logger log = LoggerFactory.getLogger(getClass()); 33 | 34 | @Autowired 35 | private DataPointRepository repository; 36 | 37 | @Autowired 38 | private ExchangeRatesService ratesService; 39 | 40 | /** 41 | * {@inheritDoc} 42 | */ 43 | @Override 44 | @CatAnnotation 45 | public List findByAccountName(String accountName) { 46 | Assert.hasLength(accountName); 47 | 48 | return repository.findByIdAccount(accountName); 49 | } 50 | 51 | /** 52 | * {@inheritDoc} 53 | */ 54 | @Override 55 | @CatAnnotation 56 | public DataPoint save(String accountName, Account account) { 57 | 58 | Instant instant = LocalDate.now().atStartOfDay() 59 | .atZone(ZoneId.systemDefault()).toInstant(); 60 | 61 | DataPointId pointId = new DataPointId(accountName, Date.from(instant)); 62 | 63 | Set incomes = account.getIncomes().stream() 64 | .map(this::createItemMetric) 65 | .collect(Collectors.toSet()); 66 | 67 | Set expenses = account.getExpenses().stream() 68 | .map(this::createItemMetric) 69 | .collect(Collectors.toSet()); 70 | 71 | Map statistics = createStatisticMetrics(incomes, expenses, account.getSaving()); 72 | 73 | DataPoint dataPoint = new DataPoint(); 74 | dataPoint.setId(pointId); 75 | dataPoint.setIncomes(incomes); 76 | dataPoint.setExpenses(expenses); 77 | dataPoint.setStatistics(statistics); 78 | dataPoint.setRates(ratesService.getCurrentRates()); 79 | 80 | log.debug("new datapoint has been created: {}", pointId); 81 | 82 | return repository.save(dataPoint); 83 | } 84 | 85 | private Map createStatisticMetrics(Set incomes, Set expenses, Saving saving) { 86 | 87 | BigDecimal savingAmount = ratesService.convert(saving.getCurrency(), Currency.getBase(), saving.getAmount()); 88 | 89 | BigDecimal expensesAmount = expenses.stream() 90 | .map(ItemMetric::getAmount) 91 | .reduce(BigDecimal.ZERO, BigDecimal::add); 92 | 93 | BigDecimal incomesAmount = incomes.stream() 94 | .map(ItemMetric::getAmount) 95 | .reduce(BigDecimal.ZERO, BigDecimal::add); 96 | 97 | return ImmutableMap.of( 98 | StatisticMetric.EXPENSES_AMOUNT, expensesAmount, 99 | StatisticMetric.INCOMES_AMOUNT, incomesAmount, 100 | StatisticMetric.SAVING_AMOUNT, savingAmount 101 | ); 102 | } 103 | 104 | /** 105 | * Normalizes given item amount to {@link Currency#getBase()} currency with 106 | * {@link TimePeriod#getBase()} time period 107 | */ 108 | private ItemMetric createItemMetric(Item item) { 109 | 110 | BigDecimal amount = ratesService 111 | .convert(item.getCurrency(), Currency.getBase(), item.getAmount()) 112 | .divide(item.getPeriod().getBaseRatio(), 4, RoundingMode.HALF_UP); 113 | 114 | return new ItemMetric(item.getTitle(), amount); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /notification-service/src/test/java/io/spring2go/piggymetrics/notification/service/RecipientServiceImplTest.java: -------------------------------------------------------------------------------- 1 | package io.spring2go.piggymetrics.notification.service; 2 | 3 | import com.google.common.collect.ImmutableList; 4 | import com.google.common.collect.ImmutableMap; 5 | import io.spring2go.piggymetrics.notification.domain.Frequency; 6 | import io.spring2go.piggymetrics.notification.domain.NotificationSettings; 7 | import io.spring2go.piggymetrics.notification.domain.NotificationType; 8 | import io.spring2go.piggymetrics.notification.domain.Recipient; 9 | import io.spring2go.piggymetrics.notification.repository.RecipientRepository; 10 | import org.junit.Before; 11 | import org.junit.Test; 12 | import org.mockito.InjectMocks; 13 | import org.mockito.Mock; 14 | 15 | import java.util.Date; 16 | import java.util.List; 17 | 18 | import static org.junit.Assert.assertEquals; 19 | import static org.junit.Assert.assertNotNull; 20 | import static org.mockito.Mockito.verify; 21 | import static org.mockito.Mockito.when; 22 | import static org.mockito.MockitoAnnotations.initMocks; 23 | 24 | public class RecipientServiceImplTest { 25 | 26 | @InjectMocks 27 | private RecipientServiceImpl recipientService; 28 | 29 | @Mock 30 | private RecipientRepository repository; 31 | 32 | @Before 33 | public void setup() { 34 | initMocks(this); 35 | } 36 | 37 | @Test 38 | public void shouldFindByAccountName() { 39 | Recipient recipient = new Recipient(); 40 | recipient.setAccountName("test"); 41 | 42 | when(repository.findByAccountName(recipient.getAccountName())).thenReturn(recipient); 43 | Recipient found = recipientService.findByAccountName(recipient.getAccountName()); 44 | 45 | assertEquals(recipient, found); 46 | } 47 | 48 | @Test(expected = IllegalArgumentException.class) 49 | public void shouldFailToFindRecipientWhenAccountNameIsEmpty() { 50 | recipientService.findByAccountName(""); 51 | } 52 | 53 | @Test 54 | public void shouldSaveRecipient() { 55 | 56 | NotificationSettings remind = new NotificationSettings(); 57 | remind.setActive(true); 58 | remind.setFrequency(Frequency.WEEKLY); 59 | remind.setLastNotified(null); 60 | 61 | NotificationSettings backup = new NotificationSettings(); 62 | backup.setActive(false); 63 | backup.setFrequency(Frequency.MONTHLY); 64 | backup.setLastNotified(new Date()); 65 | 66 | Recipient recipient = new Recipient(); 67 | recipient.setEmail("test@test.com"); 68 | recipient.setScheduledNotifications(ImmutableMap.of( 69 | NotificationType.BACKUP, backup, 70 | NotificationType.REMIND, remind 71 | )); 72 | 73 | Recipient saved = recipientService.save("test", recipient); 74 | 75 | verify(repository).save(recipient); 76 | assertNotNull(saved.getScheduledNotifications().get(NotificationType.REMIND).getLastNotified()); 77 | assertEquals("test", saved.getAccountName()); 78 | } 79 | 80 | @Test 81 | public void shouldFindReadyToNotifyWhenNotificationTypeIsBackup() { 82 | final List recipients = ImmutableList.of(new Recipient()); 83 | when(repository.findReadyForBackup()).thenReturn(recipients); 84 | 85 | List found = recipientService.findReadyToNotify(NotificationType.BACKUP); 86 | assertEquals(recipients, found); 87 | } 88 | 89 | @Test 90 | public void shouldFindReadyToNotifyWhenNotificationTypeIsRemind() { 91 | final List recipients = ImmutableList.of(new Recipient()); 92 | when(repository.findReadyForRemind()).thenReturn(recipients); 93 | 94 | List found = recipientService.findReadyToNotify(NotificationType.REMIND); 95 | assertEquals(recipients, found); 96 | } 97 | 98 | @Test 99 | public void shouldMarkAsNotified() { 100 | 101 | NotificationSettings remind = new NotificationSettings(); 102 | remind.setActive(true); 103 | remind.setFrequency(Frequency.WEEKLY); 104 | remind.setLastNotified(null); 105 | 106 | Recipient recipient = new Recipient(); 107 | recipient.setAccountName("test"); 108 | recipient.setEmail("test@test.com"); 109 | recipient.setScheduledNotifications(ImmutableMap.of( 110 | NotificationType.REMIND, remind 111 | )); 112 | 113 | recipientService.markNotified(NotificationType.REMIND, recipient); 114 | assertNotNull(recipient.getScheduledNotifications().get(NotificationType.REMIND).getLastNotified()); 115 | verify(repository).save(recipient); 116 | } 117 | } -------------------------------------------------------------------------------- /statistics-service/src/test/java/io/spring2go/piggymetrics/statistics/controller/StatisticsControllerTest.java: -------------------------------------------------------------------------------- 1 | package io.spring2go.piggymetrics.statistics.controller; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import com.google.common.collect.ImmutableList; 5 | import io.spring2go.piggymetrics.statistics.domain.Account; 6 | import io.spring2go.piggymetrics.statistics.domain.Currency; 7 | import io.spring2go.piggymetrics.statistics.domain.Item; 8 | import io.spring2go.piggymetrics.statistics.domain.Saving; 9 | import io.spring2go.piggymetrics.statistics.domain.TimePeriod; 10 | import io.spring2go.piggymetrics.statistics.domain.timeseries.DataPoint; 11 | import io.spring2go.piggymetrics.statistics.domain.timeseries.DataPointId; 12 | import io.spring2go.piggymetrics.statistics.service.StatisticsService; 13 | import com.sun.security.auth.UserPrincipal; 14 | import org.junit.Before; 15 | import org.junit.Test; 16 | import org.junit.runner.RunWith; 17 | import org.mockito.InjectMocks; 18 | import org.mockito.Mock; 19 | import org.springframework.boot.test.context.SpringBootTest; 20 | import org.springframework.http.MediaType; 21 | import org.springframework.test.context.junit4.SpringRunner; 22 | import org.springframework.test.web.servlet.MockMvc; 23 | import org.springframework.test.web.servlet.setup.MockMvcBuilders; 24 | 25 | import java.math.BigDecimal; 26 | import java.util.Date; 27 | 28 | import static org.mockito.ArgumentMatchers.any; 29 | import static org.mockito.ArgumentMatchers.anyString; 30 | import static org.mockito.Mockito.verify; 31 | import static org.mockito.Mockito.when; 32 | import static org.mockito.MockitoAnnotations.initMocks; 33 | import static org.mockito.internal.verification.VerificationModeFactory.times; 34 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 35 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; 36 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; 37 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 38 | 39 | @RunWith(SpringRunner.class) 40 | @SpringBootTest 41 | public class StatisticsControllerTest { 42 | 43 | private static final ObjectMapper mapper = new ObjectMapper(); 44 | 45 | @InjectMocks 46 | private StatisticsController statisticsController; 47 | 48 | @Mock 49 | private StatisticsService statisticsService; 50 | 51 | private MockMvc mockMvc; 52 | 53 | @Before 54 | public void setup() { 55 | initMocks(this); 56 | this.mockMvc = MockMvcBuilders.standaloneSetup(statisticsController).build(); 57 | } 58 | 59 | @Test 60 | public void shouldGetStatisticsByAccountName() throws Exception { 61 | 62 | final DataPoint dataPoint = new DataPoint(); 63 | dataPoint.setId(new DataPointId("test", new Date())); 64 | 65 | when(statisticsService.findByAccountName(dataPoint.getId().getAccount())) 66 | .thenReturn(ImmutableList.of(dataPoint)); 67 | 68 | mockMvc.perform(get("/test").principal(new UserPrincipal(dataPoint.getId().getAccount()))) 69 | .andExpect(jsonPath("$[0].id.account").value(dataPoint.getId().getAccount())) 70 | .andExpect(status().isOk()); 71 | } 72 | 73 | @Test 74 | public void shouldGetCurrentAccountStatistics() throws Exception { 75 | 76 | final DataPoint dataPoint = new DataPoint(); 77 | dataPoint.setId(new DataPointId("test", new Date())); 78 | 79 | when(statisticsService.findByAccountName(dataPoint.getId().getAccount())) 80 | .thenReturn(ImmutableList.of(dataPoint)); 81 | 82 | mockMvc.perform(get("/current").principal(new UserPrincipal(dataPoint.getId().getAccount()))) 83 | .andExpect(jsonPath("$[0].id.account").value(dataPoint.getId().getAccount())) 84 | .andExpect(status().isOk()); 85 | } 86 | 87 | @Test 88 | public void shouldSaveAccountStatistics() throws Exception { 89 | 90 | Saving saving = new Saving(); 91 | saving.setAmount(new BigDecimal(1500)); 92 | saving.setCurrency(Currency.USD); 93 | saving.setInterest(new BigDecimal("3.32")); 94 | saving.setDeposit(true); 95 | saving.setCapitalization(false); 96 | 97 | Item grocery = new Item(); 98 | grocery.setTitle("Grocery"); 99 | grocery.setAmount(new BigDecimal(10)); 100 | grocery.setCurrency(Currency.USD); 101 | grocery.setPeriod(TimePeriod.DAY); 102 | 103 | Item salary = new Item(); 104 | salary.setTitle("Salary"); 105 | salary.setAmount(new BigDecimal(9100)); 106 | salary.setCurrency(Currency.USD); 107 | salary.setPeriod(TimePeriod.MONTH); 108 | 109 | final Account account = new Account(); 110 | account.setSaving(saving); 111 | account.setExpenses(ImmutableList.of(grocery)); 112 | account.setIncomes(ImmutableList.of(salary)); 113 | 114 | String json = mapper.writeValueAsString(account); 115 | 116 | mockMvc.perform(put("/test").contentType(MediaType.APPLICATION_JSON).content(json)) 117 | .andExpect(status().isOk()); 118 | 119 | verify(statisticsService, times(1)).save(anyString(), any(Account.class)); 120 | } 121 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 介绍 2 | 3 | 本项目是课程《微服务架构和实践160讲》的综合案例分析项目,将会通过一个简单的模拟业务案例,将之前课程的各个组件集成起来,包括: 4 | 5 | * 统一授权认证中心Gravitee OAuth2 6 | * 集中配置Apollo 7 | * 基础服务Zuul/Eureka/Ribbon/Hystrix 8 | * 监控反馈CAT/Prometheus 9 | 10 | 这些组件既包括Spring Cloud技术栈的部分组件(Zuul/Eureka/Ribbon/Hystrix),也包含国内一线互联网公司落地的一些组件(如大众点评CAT和携程Apollo),也包括为课程开发的组件[Gravitee OAuth2](https://github.com/spring2go/gravitee)(非生产级),所以本案例可以称为是一个中国式微服务技术栈综合演示案例,可供学习参考。 11 | 12 | 另外,课程开播以来陆续收到一些学员的提问,比较典型的有: 13 | 14 | * 如何使用Apollo集中管理Spring应用的配置? 15 | * 网关集中验证令牌怎么做? 16 | * 基于OAuth2的注册登录和API调用具体如何实现? 17 | * CAT非侵入式埋点怎么做,如何尽量减少业务研发直接使用CAT进行埋点? 18 | 19 | 在课程中,通过案例演示,我也会统一回复这些问题。 20 | 21 | ## 案例背景 22 | 23 | 我本人并不打算完全自己开发一个演示案例,而是会重用比较流行的开源项目,在它基础上做定制扩展,所以本案例是基于github上的一个开源项目[PiggyMetrics](https://github.com/sqshq/PiggyMetrics)改造而来。PiggyMetrics是一个模拟的个人记账理财的应用,原作者称其为一个端到端的微服务PoC(Proof of Concept),也就是说他开发这个是为了验证微服务架构和Spring Cloud技术栈。PiggyMetrics目前在github上有超过4.6k星,是学习微服务架构和Spring Cloud技术栈的一个不错参考。 24 | 25 | ![piggy metrics](images/piggymetrics.png) 26 | 27 | PiggMetrics采用前后分离架构,前端是单页SPA,后端采用基于Spring Cloud技术栈的微服务架构。 28 | 29 | ## 架构设计 30 | 31 | ### 业务服务架构 32 | 33 | ![biz arch](images/biz_arch.png) 34 | 35 | 上图是PiggyMetrics的业务服务架构,包括: 36 | 37 | 1. **CLIENT**,一个纯JS/HTML/CSS单页应用,实现注册登录和前端展示逻辑 38 | 2. **ACCOUNT SERVICE**,账户服务,存储用户账户和记账信息 39 | 3. **NOTIFICATION SERVICE**,通知服务,存储通知和备份等相关配置 40 | 4. **STATISTICS SERVICE**,统计服务,计算用户财务状况和统计信息 41 | 42 | 每个服务有一个独立的MongoDB数据存储(表示微服务独立数据源思想)。客户端可调用后台服务,例如前端调用账户服务去注册账户。服务之间也会相互调用,例如账户更新时,账户服务会同时调统计服务去更新用户统计信息。另外,统计服务还会调用第三方汇率服务获取汇率信息。 43 | 44 | ### 原基础服务架构 45 | 46 | ![tech arch](images/tech_arch.png) 47 | 48 | 上图是PiggyMetrics的原基础服务架构,包括: 49 | 50 | 1. **API网关**:基于**Spring Cloud Zuul**的网关,是调用后台API的聚合入口,实现反向路由和负载均衡(Eureka+Ribbon)、限流熔断(Hystrix)等功能。CLIENT单页应用和ZUUL网关暂住在一起,简化部署。 51 | 2. **服务注册和发现**:基于**Spring Cloud Eureka**的服务注册中心。业务服务启动时通过Eureka注册,网关调用后台服务通过Eureka做服务发现,服务之间调用也通过Eureka做服务发现。 52 | 3. **授权认证服务**:基于**Spring Security OAuth2**的授权认证中心。客户端登录时通过AUTHSERVICE获取访问令牌(走用户名密码模式)。服务之间调用也通过AUTHSERVICE获取访问令牌(走客户端模式)。令牌校验方式~各资源服务器去AUTHSERVICE集中校验令牌。 53 | 4. **配置服务**:基于**Spring Cloud Config**的配置中心,集中管理所有Spring服务的配置文件。 54 | 5. **分布式调用链**:基于**Spring Cloud Sleuth**的调用链监控。网关调用后台服务,服务之间调用,都采用Zipkin进行埋点和跟踪。 55 | 6. **软负载和限流熔断**:基于**Spring Cloud Ribbon&Hystrix**,Zuul调用后台服务,服务之间相互调用,都通过Ribbon实现软负载,也通过Hystrix实现熔断限流保护。 56 | 7. **METRICS & DASHBOARD**:基于**Spring Cloud Turbine + Hystrix Dashboard**,对所有Hystrix产生的Metrics流进行聚合,并展示在Hystrix Dashboard上。 57 | 8. **日志监控**:采用**ELK**栈集中收集和分析应用日志。 58 | 59 | ### 改造后的基础服务架构 60 | 61 | ![customized arch](images/custom_arch.png) 62 | 63 | 上图是经过我改造后的架构,浅蓝色标注的都属于基础服务,主要替换的组件如下: 64 | 65 | 1. **授权认证服务**:替换为使用第8模块为课程定制开发的**Gravitee OAuth2**服务器。 66 | 2. **配置服务**:替换为使用携程**Apollo**做统一配置中心,集中管理所有Spring微服务的配置。 67 | 3. **分布式调用链**:替换为使用大众点评开源的**CAT**做调用链监控,从网关调后台服务,服务之间相互调用,都采用CAT客户端进行埋点监控。CAT埋点既演示使用拦截器(interceptor)方式,也演示使用AOP非侵入方式。 68 | 4. **METRICS&ALERTING**:网关和微服务都启用Prometheus Metrics端点,便于集成**Prometheus**监控和告警。 69 | 70 | 其它组件,比如**Zuul**网关、**Eureka**服务发现、**Ribbon**软负载、**Hystrix**限流熔断,以及**ELK**集中日志都同原架构,没有太大变化。 71 | 72 | ## 注册登录和服务调用流程 73 | 74 | ### 注册登录流程 75 | ![register & login](images/reglogin.png) 76 | 77 | 上图展示PiggyMetrics的登录注册流程,简化流程如下: 78 | 79 | 1. 客户端应用向后台发起注册请求。 80 | 2. 请求通过网关反向路由到账户服务(Account Svc)。 81 | 3. 账户服务先去授权认证服务(Gravitee OAuth2)创建一个用户(包括用户和密码,这样后续才可以登录获取访问令牌)。账户服务再保存新账户信息到本地MongoDB数据库。 82 | 4. 注册成功以后,客户应用向授权认证服务请求访问令牌(走用户名密码模式),拿到令牌以后缓存本地localstorage。 83 | 84 | ### 服务调用流程 85 | 86 | ![api call](images/apicall.png) 87 | 88 | 上图展示PiggyMetrics的API调用流程,简化流程如下: 89 | 90 | 1. 客户端向后台服务发起API调用,调用时在HTTP授权头上带上访问令牌 91 | 2. 网关截获API请求,根据安全需求判断是否需要验令牌,如果需要,则向授权服务器发起令牌校验请求。授权服务器校验令牌并返回有效型性信息,如果令牌有效,同时返回用户名等相关信息。网关再判断校验是否通过,如果通过,则**将用户名以HTTP HEADER方式向后台服务传递**,如果不通过,则直接报授权错到客户端。 92 | 3. 资源服务器从HTTP HEADER获取用户名等信息,可通过用户名进一步查询用户相关信息,实现业务逻辑。 93 | 94 | 客户端调用后台服务,经过改造为**网关集中校验令牌**方式,这样可以简化安全架构,即在企业内网,资源服务器端可直接获取用户名信息,不需要再到授权服务器做集中令牌校验。另外,服务之间的调用也改造为可以直接调用,不需要授权认证和令牌,这种做法也是很多一线企业实际落地的做法,即在生产环境中,内部服务之间调用不授权认证,这样可以简化服务的开发和部署,但是对于安全敏感的服务要求做好生产网段隔离(需运维配合)。 95 | 96 | ## 生产扩展 97 | 98 | **注意!!!**,我扩展的PiggyMetrics仅供学习参考,如果要参考这个架构进行生产化,仍需做生产化扩展,下面是一些可能的扩展点: 99 | 100 | 1. **安全**,采用网关集中令牌校验后,内部服务可以直接调用,不需要授权认证,但在生产环境中,特别是对于安全敏感的服务,需要考虑安全增强,例如生产网段隔离和IP白名单等机制。 101 | 2. **CAT客户端进一步封装**,案例演示中为了简化,使用一些手工埋点,但在实际生产中,一般需要有独立框架团队对CAT客户端进行进一步封装,对常用基础组件(服务框架,数据访问层,MVC框架,消息系统,缓存系统等)进行集中埋点,并提供封装好的客户端(最好做到无侵入,可参考Spring Cloud Sleuth Starter埋点方式),方便业务研发团队接入。基本上,框架层集中埋点以后,业务应用只需引入依赖即可,一般不需要再手工埋点。 102 | 3. **用户服务解耦**,演示案例中,用户服务(包括用户数据库)和Gravitee OAuth2集成在一起,但实际企业中用户服务可能是独立不耦合的,Gravitee OAuth2可以扩展集成独立用户服务,账户服务也可以集成对接独立用户服务。 103 | 4. **前后分离部署**,演示案例中,为简化部署,前端应用和网关住在一起,但在实际生产中,根据企业业务和团队规模,前端应用和后端微服务可能是完全分离部署的,具体做法可参考波波的视频课程。 104 | 5. **Gravitee OAuth2**,另外Gravitee OAuth2本身也需要扩展,具体可参考[其站点文档说明](https://github.com/spring2go/gravitee) 105 | 106 | ## 总结复盘 107 | 108 | 近年,国外一线互联网公司(如Netflix)在成功落地微服务架构的基础上,陆续开源了其中的一些核心组件,如Zuul/Eureka/Hystrix等,推动了社区技术进步。Pivotal则将这些组件和Spring集成起来,推出Spring Cloud技术栈,在社区产生较大影响,但整个体系可以认为是一个纯国外技术文化的技术栈。同样在近年,我们国内一线互联网公司在实践中也落地了不少基础组件,例如大众点评的CAT,携程的Apollo等,这些组件同样经过大流量考验,使用上更具中国文化特色,也更接地气。我们架构师在做技术选型的时候,不可盲信国外技术栈,更好的做法是兼收并蓄,在吸收借鉴Spring Cloud技术栈的基础上,替换融入一些中国特色的微服务组件,构建中国特色的微服务基础架构,通过实践走出自己的道路。 109 | 110 | <<微服务架构和实践160讲>>课程,包括本次的综合案例分析,其实就是这样一种博采众长、融合提炼思想的尝试,希望对国内架构师带来一些新的参考和启发。 111 | 112 | ## 进一步参考 113 | 114 | [综合案例实验步骤](https://github.com/spring2go/case_study_lab)和视频课程 115 | 116 | ![微服务架构实战160讲](https://github.com/spring2go/oauth2lab/blob/master/image/course_ad.jpg) 117 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /account-service/src/test/java/io/spring2go/piggymetrics/account/controller/AccountControllerTest.java: -------------------------------------------------------------------------------- 1 | package io.spring2go.piggymetrics.account.controller; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import com.google.common.collect.ImmutableList; 5 | import io.spring2go.piggymetrics.account.domain.*; 6 | import io.spring2go.piggymetrics.account.service.AccountService; 7 | import com.sun.security.auth.UserPrincipal; 8 | import org.junit.Before; 9 | import org.junit.Test; 10 | import org.junit.runner.RunWith; 11 | import org.mockito.InjectMocks; 12 | import org.mockito.Mock; 13 | import org.springframework.boot.test.context.SpringBootTest; 14 | import org.springframework.http.MediaType; 15 | import org.springframework.test.context.junit4.SpringRunner; 16 | import org.springframework.test.web.servlet.MockMvc; 17 | import org.springframework.test.web.servlet.setup.MockMvcBuilders; 18 | 19 | import java.math.BigDecimal; 20 | import java.util.Date; 21 | 22 | import static org.mockito.Mockito.when; 23 | import static org.mockito.MockitoAnnotations.initMocks; 24 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; 25 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; 26 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 27 | 28 | @RunWith(SpringRunner.class) 29 | @SpringBootTest 30 | public class AccountControllerTest { 31 | 32 | private static final ObjectMapper mapper = new ObjectMapper(); 33 | 34 | @InjectMocks 35 | private AccountController accountController; 36 | 37 | @Mock 38 | private AccountService accountService; 39 | 40 | private MockMvc mockMvc; 41 | 42 | @Before 43 | public void setup() { 44 | initMocks(this); 45 | this.mockMvc = MockMvcBuilders.standaloneSetup(accountController).build(); 46 | } 47 | 48 | @Test 49 | public void shouldGetAccountByName() throws Exception { 50 | 51 | final Account account = new Account(); 52 | account.setName("test"); 53 | 54 | when(accountService.findByName(account.getName())).thenReturn(account); 55 | 56 | mockMvc.perform(get("/" + account.getName())) 57 | .andExpect(jsonPath("$.name").value(account.getName())) 58 | .andExpect(status().isOk()); 59 | } 60 | 61 | @Test 62 | public void shouldGetCurrentAccount() throws Exception { 63 | 64 | final Account account = new Account(); 65 | account.setName("test"); 66 | 67 | when(accountService.findByName(account.getName())).thenReturn(account); 68 | 69 | mockMvc.perform(get("/current").principal(new UserPrincipal(account.getName()))) 70 | .andExpect(jsonPath("$.name").value(account.getName())) 71 | .andExpect(status().isOk()); 72 | } 73 | 74 | @Test 75 | public void shouldSaveCurrentAccount() throws Exception { 76 | 77 | Saving saving = new Saving(); 78 | saving.setAmount(new BigDecimal(1500)); 79 | saving.setCurrency(Currency.USD); 80 | saving.setInterest(new BigDecimal("3.32")); 81 | saving.setDeposit(true); 82 | saving.setCapitalization(false); 83 | 84 | Item grocery = new Item(); 85 | grocery.setTitle("Grocery"); 86 | grocery.setAmount(new BigDecimal(10)); 87 | grocery.setCurrency(Currency.USD); 88 | grocery.setPeriod(TimePeriod.DAY); 89 | grocery.setIcon("meal"); 90 | 91 | Item salary = new Item(); 92 | salary.setTitle("Salary"); 93 | salary.setAmount(new BigDecimal(9100)); 94 | salary.setCurrency(Currency.USD); 95 | salary.setPeriod(TimePeriod.MONTH); 96 | salary.setIcon("wallet"); 97 | 98 | final Account account = new Account(); 99 | account.setName("test"); 100 | account.setNote("test note"); 101 | account.setLastSeen(new Date()); 102 | account.setSaving(saving); 103 | account.setExpenses(ImmutableList.of(grocery)); 104 | account.setIncomes(ImmutableList.of(salary)); 105 | 106 | String json = mapper.writeValueAsString(account); 107 | 108 | mockMvc.perform(put("/current").principal(new UserPrincipal(account.getName())).contentType(MediaType.APPLICATION_JSON).content(json)) 109 | .andExpect(status().isOk()); 110 | } 111 | 112 | @Test 113 | public void shouldFailOnValidationTryingToSaveCurrentAccount() throws Exception { 114 | 115 | final Account account = new Account(); 116 | account.setName("test"); 117 | 118 | String json = mapper.writeValueAsString(account); 119 | 120 | mockMvc.perform(put("/current").principal(new UserPrincipal(account.getName())).contentType(MediaType.APPLICATION_JSON).content(json)) 121 | .andExpect(status().isBadRequest()); 122 | } 123 | 124 | @Test 125 | public void shouldRegisterNewAccount() throws Exception { 126 | 127 | final User user = new User(); 128 | user.setUsername("test"); 129 | user.setPassword("password"); 130 | 131 | String json = mapper.writeValueAsString(user); 132 | System.out.println(json); 133 | mockMvc.perform(post("/").principal(new UserPrincipal("test")).contentType(MediaType.APPLICATION_JSON).content(json)) 134 | .andExpect(status().isOk()); 135 | } 136 | 137 | @Test 138 | public void shouldFailOnValidationTryingToRegisterNewAccount() throws Exception { 139 | 140 | final User user = new User(); 141 | user.setUsername("t"); 142 | 143 | String json = mapper.writeValueAsString(user); 144 | 145 | mockMvc.perform(post("/").principal(new UserPrincipal("test")).contentType(MediaType.APPLICATION_JSON).content(json)) 146 | .andExpect(status().isBadRequest()); 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /gateway/src/main/resources/static/js/login.js: -------------------------------------------------------------------------------- 1 | /** * Registration form */ $('#signup').submit(function(e) { e.preventDefault(); var username = $("input[id='backloginform']").val(); var password = $("input[id='backpasswordform']").val(); if (username.length < 3 || password.length < 6) { alert("Username must be at least 3 characters and password - at least 6. Be tricky!"); return; } if (username && password) { $.ajax({ url: 'accounts/', datatype: 'json', type: "post", contentType: "application/json", data: JSON.stringify({ username: username, password: password }), success: function (data) { requestOauthToken(username, password); initAccount(getCurrentAccount()); $('#registrationforms, .fliptext, #createaccount').fadeOut(300); $('#mailform').fadeIn(500); setTimeout(function(){ $("#backmailform").focus() }, 10); }, error: function (xhr, ajaxOptions, thrownError) { if (xhr.status == 400) { alert("Sorry, account with the same name already exists."); } else { alert("An error during account creation. Please, try again."); } } }); } else { alert("Please, fill all the fields."); } }); /** * E-mail form */ $('#mail').submit(function(e) { e.preventDefault(); var email = $("input[name='usermail']").val(); if (email) { $.ajax({ url: 'notifications/recipients/current', datatype: 'json', type: 'put', contentType: "application/json", data: JSON.stringify({ email: email, scheduledNotifications: { "REMIND": { "active": true, "frequency": "MONTHLY" } } }), headers: {'Authorization': 'Bearer ' + getOauthTokenFromStorage()}, async: true, success: function () { setTimeout(initGreetingPage, 200); setTimeout(function(){ $('#backmailform').val(''); $("#lastlogo").show(); }, 300); }, error: function (xhr, ajaxOptions, thrownError) { if (xhr.status == 400) { alert("Sorry, it seems your email address is invalid."); } else { alert("An error during saving notifications options"); } } }); } }); /** * Login */ function login() { $("#piggy").toggleClass("loadingspin"); $("#secondenter").hide(); $("#preloader, #lastlogo").show(); var username = $("input[id='frontloginform']").val(); var password = $("input[id='frontpasswordform']").val(); if (requestOauthToken(username, password)) { initAccount(getCurrentAccount()); var userAvatar = $("").attr("src","images/userpic.jpg"); $(userAvatar).load(function() { setTimeout(initGreetingPage, 500); }); } else { $("#preloader, #enter, #secondenter").hide(); flipForm(); $('.frontforms').val(''); $("#frontloginform").focus(); alert("Something went wrong. Please, check your credentials"); } } /** * Logout */ function logout() { removeOauthTokenFromStorage(); location.reload(); } /** * Demo */ $(".demobutton").bind("click", function(){ $.ajax({ url: 'accounts/demo', datatype: 'json', type: 'get', async: false, success: function (data) { global.savePermit = false; initAccount(data); var userAvatar = $("").attr("src","images/userpic.jpg"); $(userAvatar).load(function() { setTimeout(initGreetingPage, 500); }); }, error: function () { alert("Something went wrong. Please, try again"); } }); }); $("#skipmail").bind("click", function(){ $("#lastlogo").show(); setTimeout(initGreetingPage, 300); }); /** * Login form effects */ function initialShaking(){ autoShake(); setTimeout(autoShake, 1900); } function autoShake() { $("#piggy").toggleClass("auto-shake"); } function OnHoverShaking() { hoverShake(); setTimeout(hoverShake, 1700); } function hoverShake() { $("#piggy").toggleClass("hover-shake"); } function toggleInfo() { $("#infopage").toggle(); } function flipForm() { $("#cube").toggleClass("flippedform"); $("#frontpasswordform").focus(); } $("#piggy").on("click mouseover", function(){ if ($(this).hasClass("skakelogo") === false && $(this).hasClass("hover-shake") === false) { OnHoverShaking(); } }); $(".fliptext").bind("click", function(){ setTimeout( function() { $("#plusavatar").addClass("avataranimation"); } , 1000); $("#flipper").toggleClass("flippedcard"); }); $(".flipinfo").on("click", function() { $("#flipper").toggleClass("flippedcardinfo"); toggleInfo(); }); $(".frominfo, #infotitle, #infosubtitle").on("click", function() { $("#flipper").toggleClass("flippedcardinfo"); setTimeout(toggleInfo, 400); }); $("#enter").on("click", function() {flipForm()}); $("#secondenter").on("click", function() {login()}); $("#frontloginform").keyup(function (e) { if( $(this).val().length >= 3 ) { $("#enter").show(); if (e.which == 13) { flipForm(); $("#enter").hide(); } return; } else { $("#enter").hide(); } }); $("#frontpasswordform").keyup(function(e) { if ( $(this).val().length >= 6) { $("#secondenter").show(); if(e.which == 13) { $(this).blur(); login(); } return; } else { $("#secondenter").hide(); } }); -------------------------------------------------------------------------------- /account-service/src/test/java/io/spring2go/piggymetrics/account/service/AccountServiceTest.java: -------------------------------------------------------------------------------- 1 | package io.spring2go.piggymetrics.account.service; 2 | 3 | import io.spring2go.piggymetrics.account.client.StatisticsServiceClient; 4 | import io.spring2go.piggymetrics.account.domain.*; 5 | import io.spring2go.piggymetrics.account.repository.AccountRepository; 6 | import org.junit.Before; 7 | import org.junit.Test; 8 | import org.mockito.InjectMocks; 9 | import org.mockito.Mock; 10 | 11 | import java.math.BigDecimal; 12 | import java.util.Arrays; 13 | 14 | import static org.junit.Assert.assertEquals; 15 | import static org.junit.Assert.assertNotNull; 16 | import static org.mockito.Mockito.*; 17 | import static org.mockito.MockitoAnnotations.initMocks; 18 | 19 | public class AccountServiceTest { 20 | 21 | @InjectMocks 22 | private AccountServiceImpl accountService; 23 | 24 | @Mock 25 | private StatisticsServiceClient statisticsClient; 26 | 27 | 28 | @Mock 29 | private AccountRepository repository; 30 | 31 | @Before 32 | public void setup() { 33 | initMocks(this); 34 | } 35 | 36 | @Test 37 | public void shouldFindByName() { 38 | 39 | final Account account = new Account(); 40 | account.setName("test"); 41 | 42 | when(accountService.findByName(account.getName())).thenReturn(account); 43 | Account found = accountService.findByName(account.getName()); 44 | 45 | assertEquals(account, found); 46 | } 47 | 48 | @Test(expected = IllegalArgumentException.class) 49 | public void shouldFailWhenNameIsEmpty() { 50 | accountService.findByName(""); 51 | } 52 | 53 | @Test 54 | public void shouldCreateAccountWithGivenUser() { 55 | 56 | User user = new User(); 57 | user.setUsername("test"); 58 | 59 | Account account = accountService.create(user); 60 | 61 | assertEquals(user.getUsername(), account.getName()); 62 | assertEquals(0, account.getSaving().getAmount().intValue()); 63 | assertEquals(Currency.getDefault(), account.getSaving().getCurrency()); 64 | assertEquals(0, account.getSaving().getInterest().intValue()); 65 | assertEquals(false, account.getSaving().getDeposit()); 66 | assertEquals(false, account.getSaving().getCapitalization()); 67 | assertNotNull(account.getLastSeen()); 68 | 69 | verify(repository, times(1)).save(account); 70 | } 71 | 72 | @Test 73 | public void shouldSaveChangesWhenUpdatedAccountGiven() { 74 | 75 | Item grocery = new Item(); 76 | grocery.setTitle("Grocery"); 77 | grocery.setAmount(new BigDecimal(10)); 78 | grocery.setCurrency(Currency.USD); 79 | grocery.setPeriod(TimePeriod.DAY); 80 | grocery.setIcon("meal"); 81 | 82 | Item salary = new Item(); 83 | salary.setTitle("Salary"); 84 | salary.setAmount(new BigDecimal(9100)); 85 | salary.setCurrency(Currency.USD); 86 | salary.setPeriod(TimePeriod.MONTH); 87 | salary.setIcon("wallet"); 88 | 89 | Saving saving = new Saving(); 90 | saving.setAmount(new BigDecimal(1500)); 91 | saving.setCurrency(Currency.USD); 92 | saving.setInterest(new BigDecimal("3.32")); 93 | saving.setDeposit(true); 94 | saving.setCapitalization(false); 95 | 96 | final Account update = new Account(); 97 | update.setName("test"); 98 | update.setNote("test note"); 99 | update.setIncomes(Arrays.asList(salary)); 100 | update.setExpenses(Arrays.asList(grocery)); 101 | update.setSaving(saving); 102 | 103 | final Account account = new Account(); 104 | 105 | when(accountService.findByName("test")).thenReturn(account); 106 | accountService.saveChanges("test", update); 107 | 108 | assertEquals(update.getNote(), account.getNote()); 109 | assertNotNull(account.getLastSeen()); 110 | 111 | assertEquals(update.getSaving().getAmount(), account.getSaving().getAmount()); 112 | assertEquals(update.getSaving().getCurrency(), account.getSaving().getCurrency()); 113 | assertEquals(update.getSaving().getInterest(), account.getSaving().getInterest()); 114 | assertEquals(update.getSaving().getDeposit(), account.getSaving().getDeposit()); 115 | assertEquals(update.getSaving().getCapitalization(), account.getSaving().getCapitalization()); 116 | 117 | assertEquals(update.getExpenses().size(), account.getExpenses().size()); 118 | assertEquals(update.getIncomes().size(), account.getIncomes().size()); 119 | 120 | assertEquals(update.getExpenses().get(0).getTitle(), account.getExpenses().get(0).getTitle()); 121 | assertEquals(0, update.getExpenses().get(0).getAmount().compareTo(account.getExpenses().get(0).getAmount())); 122 | assertEquals(update.getExpenses().get(0).getCurrency(), account.getExpenses().get(0).getCurrency()); 123 | assertEquals(update.getExpenses().get(0).getPeriod(), account.getExpenses().get(0).getPeriod()); 124 | assertEquals(update.getExpenses().get(0).getIcon(), account.getExpenses().get(0).getIcon()); 125 | 126 | assertEquals(update.getIncomes().get(0).getTitle(), account.getIncomes().get(0).getTitle()); 127 | assertEquals(0, update.getIncomes().get(0).getAmount().compareTo(account.getIncomes().get(0).getAmount())); 128 | assertEquals(update.getIncomes().get(0).getCurrency(), account.getIncomes().get(0).getCurrency()); 129 | assertEquals(update.getIncomes().get(0).getPeriod(), account.getIncomes().get(0).getPeriod()); 130 | assertEquals(update.getIncomes().get(0).getIcon(), account.getIncomes().get(0).getIcon()); 131 | 132 | verify(repository, times(1)).save(account); 133 | verify(statisticsClient, times(1)).updateStatistics("test", account); 134 | } 135 | 136 | @Test(expected = IllegalArgumentException.class) 137 | public void shouldFailWhenNoAccountsExistedWithGivenName() { 138 | final Account update = new Account(); 139 | update.setIncomes(Arrays.asList(new Item())); 140 | update.setExpenses(Arrays.asList(new Item())); 141 | 142 | when(accountService.findByName("test")).thenReturn(null); 143 | accountService.saveChanges("test", update); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /notification-service/src/test/java/io/spring2go/piggymetrics/notification/repository/RecipientRepositoryTest.java: -------------------------------------------------------------------------------- 1 | package io.spring2go.piggymetrics.notification.repository; 2 | 3 | import com.google.common.collect.ImmutableMap; 4 | import io.spring2go.piggymetrics.notification.domain.Frequency; 5 | import io.spring2go.piggymetrics.notification.domain.NotificationSettings; 6 | import io.spring2go.piggymetrics.notification.domain.NotificationType; 7 | import io.spring2go.piggymetrics.notification.domain.Recipient; 8 | import org.apache.commons.lang.time.DateUtils; 9 | import org.junit.Test; 10 | import org.junit.runner.RunWith; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.boot.test.autoconfigure.data.mongo.DataMongoTest; 13 | import org.springframework.test.context.junit4.SpringRunner; 14 | 15 | import java.util.Date; 16 | import java.util.List; 17 | 18 | import static org.junit.Assert.assertEquals; 19 | import static org.junit.Assert.assertFalse; 20 | import static org.junit.Assert.assertTrue; 21 | 22 | @RunWith(SpringRunner.class) 23 | @DataMongoTest 24 | public class RecipientRepositoryTest { 25 | 26 | @Autowired 27 | private RecipientRepository repository; 28 | 29 | @Test 30 | public void shouldFindByAccountName() { 31 | 32 | NotificationSettings remind = new NotificationSettings(); 33 | remind.setActive(true); 34 | remind.setFrequency(Frequency.WEEKLY); 35 | remind.setLastNotified(new Date(0)); 36 | 37 | NotificationSettings backup = new NotificationSettings(); 38 | backup.setActive(false); 39 | backup.setFrequency(Frequency.MONTHLY); 40 | backup.setLastNotified(new Date()); 41 | 42 | Recipient recipient = new Recipient(); 43 | recipient.setAccountName("test"); 44 | recipient.setEmail("test@test.com"); 45 | recipient.setScheduledNotifications(ImmutableMap.of( 46 | NotificationType.BACKUP, backup, 47 | NotificationType.REMIND, remind 48 | )); 49 | 50 | repository.save(recipient); 51 | 52 | Recipient found = repository.findByAccountName(recipient.getAccountName()); 53 | assertEquals(recipient.getAccountName(), found.getAccountName()); 54 | assertEquals(recipient.getEmail(), found.getEmail()); 55 | 56 | assertEquals(recipient.getScheduledNotifications().get(NotificationType.BACKUP).getActive(), 57 | found.getScheduledNotifications().get(NotificationType.BACKUP).getActive()); 58 | assertEquals(recipient.getScheduledNotifications().get(NotificationType.BACKUP).getFrequency(), 59 | found.getScheduledNotifications().get(NotificationType.BACKUP).getFrequency()); 60 | assertEquals(recipient.getScheduledNotifications().get(NotificationType.BACKUP).getLastNotified(), 61 | found.getScheduledNotifications().get(NotificationType.BACKUP).getLastNotified()); 62 | 63 | assertEquals(recipient.getScheduledNotifications().get(NotificationType.REMIND).getActive(), 64 | found.getScheduledNotifications().get(NotificationType.REMIND).getActive()); 65 | assertEquals(recipient.getScheduledNotifications().get(NotificationType.REMIND).getFrequency(), 66 | found.getScheduledNotifications().get(NotificationType.REMIND).getFrequency()); 67 | assertEquals(recipient.getScheduledNotifications().get(NotificationType.REMIND).getLastNotified(), 68 | found.getScheduledNotifications().get(NotificationType.REMIND).getLastNotified()); 69 | } 70 | 71 | @Test 72 | public void shouldFindReadyForRemindWhenFrequencyIsWeeklyAndLastNotifiedWas8DaysAgo() { 73 | 74 | NotificationSettings remind = new NotificationSettings(); 75 | remind.setActive(true); 76 | remind.setFrequency(Frequency.WEEKLY); 77 | remind.setLastNotified(DateUtils.addDays(new Date(), -8)); 78 | 79 | Recipient recipient = new Recipient(); 80 | recipient.setAccountName("test"); 81 | recipient.setEmail("test@test.com"); 82 | recipient.setScheduledNotifications(ImmutableMap.of( 83 | NotificationType.REMIND, remind 84 | )); 85 | 86 | repository.save(recipient); 87 | 88 | List found = repository.findReadyForRemind(); 89 | assertFalse(found.isEmpty()); 90 | } 91 | 92 | @Test 93 | public void shouldNotFindReadyForRemindWhenFrequencyIsWeeklyAndLastNotifiedWasYesterday() { 94 | 95 | NotificationSettings remind = new NotificationSettings(); 96 | remind.setActive(true); 97 | remind.setFrequency(Frequency.WEEKLY); 98 | remind.setLastNotified(DateUtils.addDays(new Date(), -1)); 99 | 100 | Recipient recipient = new Recipient(); 101 | recipient.setAccountName("test"); 102 | recipient.setEmail("test@test.com"); 103 | recipient.setScheduledNotifications(ImmutableMap.of( 104 | NotificationType.REMIND, remind 105 | )); 106 | 107 | repository.save(recipient); 108 | 109 | List found = repository.findReadyForRemind(); 110 | assertTrue(found.isEmpty()); 111 | } 112 | 113 | @Test 114 | public void shouldNotFindReadyForRemindWhenNotificationIsNotActive() { 115 | 116 | NotificationSettings remind = new NotificationSettings(); 117 | remind.setActive(false); 118 | remind.setFrequency(Frequency.WEEKLY); 119 | remind.setLastNotified(DateUtils.addDays(new Date(), -30)); 120 | 121 | Recipient recipient = new Recipient(); 122 | recipient.setAccountName("test"); 123 | recipient.setEmail("test@test.com"); 124 | recipient.setScheduledNotifications(ImmutableMap.of( 125 | NotificationType.REMIND, remind 126 | )); 127 | 128 | repository.save(recipient); 129 | 130 | List found = repository.findReadyForRemind(); 131 | assertTrue(found.isEmpty()); 132 | } 133 | 134 | @Test 135 | public void shouldNotFindReadyForBackupWhenFrequencyIsQuaterly() { 136 | 137 | NotificationSettings remind = new NotificationSettings(); 138 | remind.setActive(true); 139 | remind.setFrequency(Frequency.QUARTERLY); 140 | remind.setLastNotified(DateUtils.addDays(new Date(), -91)); 141 | 142 | Recipient recipient = new Recipient(); 143 | recipient.setAccountName("test"); 144 | recipient.setEmail("test@test.com"); 145 | recipient.setScheduledNotifications(ImmutableMap.of( 146 | NotificationType.BACKUP, remind 147 | )); 148 | 149 | repository.save(recipient); 150 | 151 | List found = repository.findReadyForBackup(); 152 | assertFalse(found.isEmpty()); 153 | } 154 | } --------------------------------------------------------------------------------