├── .editorconfig ├── .gitignore ├── README.md ├── assets ├── docker │ ├── compose │ │ └── database.yml │ └── mysql │ │ ├── .gitignore │ │ ├── db │ │ └── init_mysql_user.sql │ │ └── master │ │ └── conf.d │ │ └── mysqld.cnf └── image │ ├── account_fsm.png │ ├── component.png │ ├── container.png │ ├── context.png │ ├── order_fsm.png │ ├── order_swagger.png │ └── product_fsm.png ├── pom.xml ├── rest-tcc-commons ├── .gitignore ├── pom.xml └── src │ └── main │ └── java │ └── com │ └── github │ └── prontera │ ├── ChainHandler.java │ ├── GenericChainHandler.java │ ├── Pipeline.java │ ├── Shifts.java │ ├── annotation │ ├── MyBatisRepository.java │ └── marker │ │ └── NonBehavior.java │ ├── concurrent │ ├── LoggableAbortPolicy.java │ ├── LoggableCallerRunsPolicy.java │ ├── LoggableDiscardOldestPolicy.java │ ├── LoggableDiscardPolicy.java │ └── LoggablePolicy.java │ ├── domain │ └── IdenticalDomain.java │ ├── exception │ ├── ApplicationException.java │ ├── InvalidModelException.java │ └── ResolvableStatusException.java │ ├── persistence │ ├── CrudMapper.java │ └── GenericTypeHandler.java │ ├── service │ ├── CrudService.java │ └── IdenticalCrudService.java │ └── util │ ├── HibernateValidators.java │ └── Jacksons.java ├── rest-tcc-contract ├── pom.xml └── src │ └── main │ └── java │ └── com │ └── github │ └── prontera │ ├── enums │ ├── EnumFunction.java │ └── NumericStatusCode.java │ ├── model │ └── response │ │ └── ResolvableResponse.java │ └── util │ └── Capacity.java └── rest-tcc-projects ├── pom.xml ├── rest-tcc-account-client ├── pom.xml └── src │ └── main │ └── java │ └── com │ └── github │ └── prontera │ └── account │ ├── enums │ ├── ReservingState.java │ └── StatusCode.java │ └── model │ ├── request │ ├── BalanceReservingRequest.java │ ├── ConfirmAccountTxnRequest.java │ ├── QueryAccountRequest.java │ ├── QueryAccountTxnRequest.java │ └── SignUpRequest.java │ └── response │ ├── BalanceReservingResponse.java │ ├── ConfirmAccountTxnResponse.java │ ├── QueryAccountResponse.java │ ├── QueryAccountTxnResponse.java │ └── SignUpResponse.java ├── rest-tcc-account ├── .gitignore ├── pom.xml └── src │ └── main │ ├── java │ └── com │ │ └── github │ │ └── prontera │ │ ├── AccountApplication.java │ │ ├── ContextStartedListener.java │ │ ├── annotation │ │ └── FaultBarrier.java │ │ ├── aspect │ │ └── FaultBarrierAspect.java │ │ ├── config │ │ ├── AopConfig.java │ │ ├── BeanConfig.java │ │ └── SwaggerConfig.java │ │ ├── domain │ │ ├── Account.java │ │ └── AccountTransaction.java │ │ ├── http │ │ └── AccountController.java │ │ ├── persistence │ │ ├── AccountMapper.java │ │ ├── AccountTransactionMapper.java │ │ └── handler │ │ │ └── ReservingStateHandler.java │ │ ├── service │ │ └── AccountService.java │ │ └── util │ │ └── Responses.java │ └── resources │ ├── application.properties │ ├── generatorConfig.xml │ ├── log4j2.xml │ ├── mapper │ ├── AccountMapper.xml │ └── AccountTransactionMapper.xml │ └── puml │ └── participant-state.puml ├── rest-tcc-order ├── .gitignore ├── pom.xml └── src │ ├── main │ ├── java │ │ └── com │ │ │ └── github │ │ │ └── prontera │ │ │ ├── OrderApplication.java │ │ │ ├── annotation │ │ │ └── FaultBarrier.java │ │ │ ├── aspect │ │ │ └── FaultBarrierAspect.java │ │ │ ├── concurrent │ │ │ └── Pools.java │ │ │ ├── config │ │ │ ├── AopConfig.java │ │ │ ├── BeanConfig.java │ │ │ └── SwaggerConfig.java │ │ │ ├── domain │ │ │ └── Order.java │ │ │ ├── enums │ │ │ ├── OrderState.java │ │ │ └── StatusCode.java │ │ │ ├── http │ │ │ ├── OrderController.java │ │ │ └── client │ │ │ │ ├── AccountClient.java │ │ │ │ └── ProductClient.java │ │ │ ├── model │ │ │ ├── request │ │ │ │ ├── CheckoutRequest.java │ │ │ │ └── DiagnoseRequest.java │ │ │ └── response │ │ │ │ ├── CheckoutResponse.java │ │ │ │ └── DiagnoseResponse.java │ │ │ ├── persistence │ │ │ ├── OrderMapper.java │ │ │ └── handler │ │ │ │ └── OrderStateHandler.java │ │ │ ├── service │ │ │ └── OrderService.java │ │ │ └── util │ │ │ └── Responses.java │ └── resources │ │ ├── application.properties │ │ ├── generatorConfig.xml │ │ ├── log4j2.xml │ │ ├── mapper │ │ └── OrderMapper.xml │ │ └── puml │ │ └── order-state.puml │ └── test │ └── java │ └── com │ └── github │ └── prontera │ └── DatetimeTest.java ├── rest-tcc-product-client ├── pom.xml └── src │ └── main │ └── java │ └── com │ └── github │ └── prontera │ └── product │ ├── enums │ ├── ReservingState.java │ └── StatusCode.java │ └── model │ ├── request │ ├── AddProductRequest.java │ ├── ConfirmProductTxnRequest.java │ ├── InventoryReservingRequest.java │ ├── QueryProductRequest.java │ └── QueryProductTxnRequest.java │ └── response │ ├── AddProductResponse.java │ ├── ConfirmProductTxnResponse.java │ ├── InventoryReservingResponse.java │ ├── QueryProductResponse.java │ └── QueryProductTxnResponse.java ├── rest-tcc-product ├── .gitignore ├── pom.xml └── src │ └── main │ ├── java │ └── com │ │ └── github │ │ └── prontera │ │ ├── ContextStartedListener.java │ │ ├── ProductApplication.java │ │ ├── annotation │ │ └── FaultBarrier.java │ │ ├── aspect │ │ └── FaultBarrierAspect.java │ │ ├── config │ │ ├── AopConfig.java │ │ ├── BeanConfig.java │ │ └── SwaggerConfig.java │ │ ├── domain │ │ ├── Product.java │ │ └── ProductTransaction.java │ │ ├── http │ │ └── ProductController.java │ │ ├── persistence │ │ ├── ProductMapper.java │ │ ├── ProductTransactionMapper.java │ │ └── handler │ │ │ └── ReservingStateHandler.java │ │ ├── service │ │ └── ProductService.java │ │ └── util │ │ └── Responses.java │ └── resources │ ├── application.properties │ ├── generatorConfig.xml │ ├── log4j2.xml │ ├── mapper │ ├── ProductMapper.xml │ └── ProductTransactionMapper.xml │ └── puml │ └── participant-state.puml └── rest-tcc-service-discovery ├── pom.xml └── src └── main ├── java └── com │ └── github │ └── prontera │ └── ServiceDiscoveryApplication.java └── resources └── application.properties /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | # Unix-style newlines with a newline ending every file 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | 10 | [*.{groovy, java, kt, xml}] 11 | indent_style = space 12 | indent_size = 4 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | .mvn/ 3 | **/.DS_Store 4 | 5 | ### IntelliJ IDEA ### 6 | .idea 7 | *.iws 8 | *.iml 9 | *.ipr 10 | -------------------------------------------------------------------------------- /assets/docker/compose/database.yml: -------------------------------------------------------------------------------- 1 | version: '3.1' 2 | 3 | services: 4 | solar_mysql: 5 | image: mysql:8.0.19 6 | command: --default-authentication-plugin=mysql_native_password 7 | restart: always 8 | volumes: 9 | - "../mysql/share/:/share/" 10 | - "../mysql/db/:/docker-entrypoint-initdb.d/:ro" 11 | - "../mysql/master/lib/:/var/lib/mysql/" 12 | - "../mysql/master/log/:/var/log/mysql/" 13 | - "../mysql/master/conf.d/:/etc/mysql/mysql.conf.d/" 14 | environment: 15 | TZ: Asia/Shanghai 16 | MYSQL_ROOT_PASSWORD: Paint1%Journey%direct 17 | ports: 18 | - "3306:3306" 19 | -------------------------------------------------------------------------------- /assets/docker/mysql/.gitignore: -------------------------------------------------------------------------------- 1 | !build/ 2 | # Created by .ignore support plugin (hsz.mobi) 3 | ### JetBrains template 4 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio 5 | *.iml 6 | 7 | ## Directory-based project format: 8 | .idea/ 9 | # if you remove the above rule, at least ignore the following: 10 | 11 | # MHA Docker example 12 | master/lib/ 13 | master/log/ 14 | 15 | # User-specific stuff: 16 | # .idea/workspace.xml 17 | # .idea/tasks.xml 18 | # .idea/dictionaries 19 | 20 | # Sensitive or high-churn files: 21 | # .idea/dataSources.ids 22 | # .idea/dataSources.xml 23 | # .idea/sqlDataSources.xml 24 | # .idea/dynamic.xml 25 | # .idea/uiDesigner.xml 26 | 27 | # Gradle: 28 | # .idea/gradle.xml 29 | # .idea/libraries 30 | 31 | # Mongo Explorer plugin: 32 | # .idea/mongoSettings.xml 33 | 34 | ## File-based project format: 35 | *.ipr 36 | *.iws 37 | 38 | ## Plugin-specific files: 39 | 40 | # IntelliJ 41 | /out/ 42 | **/target/ 43 | **/generatorConfig.xml 44 | **/rebel.xml 45 | **/rebel-remote.xml 46 | 47 | # mpeltonen/sbt-idea plugin 48 | .idea_modules/ 49 | 50 | # JIRA plugin 51 | atlassian-ide-plugin.xml 52 | 53 | # Crashlytics plugin (for Android Studio and IntelliJ) 54 | com_crashlytics_export_strings.xml 55 | crashlytics.properties 56 | crashlytics-build.properties 57 | ### TortoiseGit template 58 | # Project-level settings 59 | /.tgitconfig 60 | ### CVS template 61 | /CVS/* 62 | */CVS/* 63 | .cvsignore 64 | */.cvsignore 65 | ### MicrosoftOffice template 66 | *.tmp 67 | 68 | # Word temporary 69 | ~$*.doc* 70 | 71 | # Excel temporary 72 | ~$*.xls* 73 | 74 | # Excel Backup File 75 | *.xlk 76 | ### Java template 77 | *.class 78 | 79 | # Mobile Tools for Java (J2ME) 80 | .mtj.tmp/ 81 | 82 | # Package Files # 83 | #*.jar 84 | *.war 85 | *.ear 86 | 87 | # domain machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 88 | hs_err_pid* 89 | ### SublimeText template 90 | # cache files for sublime text 91 | *.tmlanguage.cache 92 | *.tmPreferences.cache 93 | *.stTheme.cache 94 | 95 | # workspace files are user-specific 96 | *.sublime-workspace 97 | 98 | # project files should be checked into the repository, unless a significant 99 | # proportion of contributors will probably not be using SublimeText 100 | # *.sublime-project 101 | 102 | # sftp configuration file 103 | sftp-config.json 104 | ### Eclipse template 105 | *.pydevproject 106 | .metadata 107 | .gradle 108 | bin/ 109 | tmp/ 110 | *.bak 111 | *.swp 112 | *~.nib 113 | local.properties 114 | .settings/ 115 | .loadpath 116 | 117 | # Eclipse Core 118 | .project 119 | 120 | # External tool builders 121 | .externalToolBuilders/ 122 | 123 | # Locally stored "Eclipse launch configurations" 124 | *.launch 125 | 126 | # CDT-specific 127 | .cproject 128 | 129 | # JDT-specific (Eclipse Java Development Tools) 130 | .classpath 131 | 132 | # Java annotation processor (APT) 133 | .factorypath 134 | 135 | # PDT-specific 136 | .buildpath 137 | 138 | # sbteclipse plugin 139 | .target 140 | 141 | # TeXlipse plugin 142 | .texlipse 143 | ### Linux template 144 | *~ 145 | 146 | # KDE directory preferences 147 | .directory 148 | 149 | # Linux trash folder which might appear on any partition or disk 150 | .Trash-* 151 | ### Vim template 152 | [._]*.s[a-w][a-z] 153 | [._]s[a-w][a-z] 154 | *.un~ 155 | Session.vim 156 | .netrwhist 157 | ### SVN template 158 | .svn/ 159 | ### Windows template 160 | # Windows image file caches 161 | Thumbs.db 162 | ehthumbs.db 163 | 164 | # Folder config file 165 | Desktop.ini 166 | 167 | # Recycle Bin used on file shares 168 | $RECYCLE.BIN/ 169 | 170 | # Windows Installer files 171 | *.cab 172 | *.msi 173 | *.msm 174 | *.msp 175 | 176 | # Windows shortcuts 177 | *.lnk 178 | ### Maven template 179 | target/ 180 | pom.xml.tag 181 | pom.xml.releaseBackup 182 | pom.xml.versionsBackup 183 | pom.xml.next 184 | release.properties 185 | dependency-reduced-pom.xml 186 | buildNumber.properties 187 | .mvn/timing.properties 188 | 189 | -------------------------------------------------------------------------------- /assets/docker/mysql/db/init_mysql_user.sql: -------------------------------------------------------------------------------- 1 | CREATE USER 'chris' 2 | IDENTIFIED BY '123123'; 3 | 4 | -- ----------------------------------------------------- 5 | -- Schema order 6 | -- ----------------------------------------------------- 7 | CREATE SCHEMA IF NOT EXISTS `order` 8 | DEFAULT CHARACTER SET utf8mb4; 9 | GRANT ALL ON `order`.* TO 'chris'; 10 | USE `order`; 11 | 12 | -- ----------------------------------------------------- 13 | -- Table `order`.`t_order` 14 | -- ----------------------------------------------------- 15 | CREATE TABLE IF NOT EXISTS `order`.`t_order` 16 | ( 17 | `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, 18 | `user_id` BIGINT UNSIGNED NOT NULL COMMENT '下单用户ID', 19 | `product_id` BIGINT UNSIGNED NOT NULL COMMENT '产品ID', 20 | `price` INT UNSIGNED NOT NULL COMMENT '实际支付金额', 21 | `quantity` INT UNSIGNED NOT NULL COMMENT '下单数量', 22 | `state` TINYINT UNSIGNED NOT NULL COMMENT '订单状态', 23 | `guid` BIGINT UNSIGNED NOT NULL COMMENT '幂等GUID', 24 | `create_at` DATETIME NOT NULL COMMENT '创建时间', 25 | `update_at` DATETIME NOT NULL COMMENT '修改时间', 26 | `expire_at` DATETIME NOT NULL COMMENT '失效时间', 27 | `done_at` DATETIME NOT NULL DEFAULT '2000-01-01 00:00:00' COMMENT '完成时间', 28 | PRIMARY KEY (`id`), 29 | INDEX `idx_order_user_id` (`user_id` ASC), 30 | INDEX `idx_order_guid` (`guid` ASC) 31 | ); 32 | 33 | -- ----------------------------------------------------- 34 | -- Schema account 35 | -- ----------------------------------------------------- 36 | CREATE SCHEMA IF NOT EXISTS `account` 37 | DEFAULT CHARACTER SET utf8mb4; 38 | GRANT ALL ON `account`.* TO 'chris'; 39 | USE `account`; 40 | 41 | -- ----------------------------------------------------- 42 | -- Table `account`.`t_account` 43 | -- ----------------------------------------------------- 44 | CREATE TABLE IF NOT EXISTS `account`.`t_account` 45 | ( 46 | `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, 47 | `name` VARCHAR(20) NOT NULL COMMENT '用户名', 48 | `balance` BIGINT UNSIGNED NOT NULL DEFAULT 100000000 COMMENT '余额, 单位元', 49 | `create_at` DATETIME NOT NULL COMMENT '创建时间', 50 | `update_at` DATETIME NOT NULL COMMENT '修改时间', 51 | PRIMARY KEY (`id`), 52 | UNIQUE INDEX `uni_account_name` (`name` ASC) 53 | ); 54 | 55 | -- ----------------------------------------------------- 56 | -- Table `account`.`t_account_transaction` 57 | -- ----------------------------------------------------- 58 | CREATE TABLE IF NOT EXISTS `account`.`t_account_transaction` 59 | ( 60 | `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, 61 | `user_id` BIGINT UNSIGNED NOT NULL COMMENT '用户ID', 62 | `order_id` BIGINT UNSIGNED NOT NULL COMMENT '订单ID', 63 | `amount` BIGINT NOT NULL COMMENT '预留资源金额', 64 | `state` TINYINT NOT NULL COMMENT '预留资源状态', 65 | `create_at` DATETIME NOT NULL COMMENT '创建时间', 66 | `update_at` DATETIME NOT NULL COMMENT '修改时间', 67 | `expire_at` DATETIME NOT NULL COMMENT '失效时间', 68 | `done_at` DATETIME NOT NULL DEFAULT '2000-01-01 00:00:00' COMMENT '事务完成时间', 69 | PRIMARY KEY (`id`), 70 | UNIQUE INDEX `uni_account_txn_order` (`order_id` ASC) 71 | ); 72 | 73 | INSERT INTO `account`.`t_account` (`name`, `create_at`, `update_at`) 74 | VALUES ('chris', now(), now()); 75 | 76 | INSERT INTO `account`.`t_account` (`name`, `create_at`, `update_at`) 77 | VALUES ('scott', now(), now()); 78 | 79 | INSERT INTO `account`.`t_account` (`name`, `create_at`, `update_at`) 80 | VALUES ('ryan', now(), now()); 81 | 82 | -- ----------------------------------------------------- 83 | -- Schema product 84 | -- ----------------------------------------------------- 85 | CREATE SCHEMA IF NOT EXISTS `product` 86 | DEFAULT CHARACTER SET utf8mb4; 87 | GRANT ALL ON `product`.* TO 'chris'; 88 | USE `product`; 89 | 90 | -- ----------------------------------------------------- 91 | -- Table `product`.`t_product` 92 | -- ----------------------------------------------------- 93 | CREATE TABLE IF NOT EXISTS `product`.`t_product` 94 | ( 95 | `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, 96 | `name` VARCHAR(20) NOT NULL COMMENT '产品名', 97 | `inventory` BIGINT UNSIGNED NOT NULL DEFAULT 9999 COMMENT '库存余量, 单位个', 98 | `create_at` DATETIME NOT NULL COMMENT '创建时间', 99 | `update_at` DATETIME NOT NULL COMMENT '修改时间', 100 | PRIMARY KEY (`id`), 101 | UNIQUE INDEX `uni_product_name` (`name` ASC) 102 | ); 103 | 104 | -- ----------------------------------------------------- 105 | -- Table `product`.`t_product_transaction` 106 | -- ----------------------------------------------------- 107 | CREATE TABLE IF NOT EXISTS `product`.`t_product_transaction` 108 | ( 109 | `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, 110 | `product_id` BIGINT UNSIGNED NOT NULL COMMENT '产品ID', 111 | `order_id` BIGINT UNSIGNED NOT NULL COMMENT '订单ID', 112 | `amount` BIGINT NOT NULL COMMENT '预留资源数量', 113 | `state` TINYINT NOT NULL COMMENT '预留资源状态', 114 | `create_at` DATETIME NOT NULL COMMENT '创建时间', 115 | `update_at` DATETIME NOT NULL COMMENT '修改时间', 116 | `expire_at` DATETIME NOT NULL COMMENT '失效时间', 117 | `done_at` DATETIME NOT NULL DEFAULT '2000-01-01 00:00:00' COMMENT '事务完成时间', 118 | PRIMARY KEY (`id`), 119 | UNIQUE INDEX `uni_product_txn_order` (`order_id` ASC) 120 | ); 121 | 122 | INSERT INTO `t_product` (`name`, `create_at`, `update_at`) 123 | VALUES ('gba', now(), now()); 124 | 125 | INSERT INTO `t_product` (`name`, `create_at`, `update_at`) 126 | VALUES ('ps4', now(), now()); 127 | 128 | INSERT INTO `t_product` (`name`, `create_at`, `update_at`) 129 | VALUES ('fc', now(), now()); 130 | -------------------------------------------------------------------------------- /assets/docker/mysql/master/conf.d/mysqld.cnf: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014, 2016, Oracle and/or its affiliates. All rights reserved. 2 | # 3 | # This program is free software; you can redistribute it and/or modify 4 | # it under the terms of the GNU General Public License as published by 5 | # the Free Software Foundation; version 2 of the License. 6 | # 7 | # This program is distributed in the hope that it will be useful, 8 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | # GNU General Public License for more details. 11 | # 12 | # You should have received a copy of the GNU General Public License 13 | # along with this program; if not, write to the Free Software 14 | # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 15 | 16 | # 17 | # The MySQL Server configuration file. 18 | # 19 | # For explanations see 20 | # http://dev.mysql.com/doc/mysql/en/server-system-variables.html 21 | 22 | [mysql] 23 | default-character-set = utf8mb4 24 | 25 | [mysqld] 26 | default-time-zone = '+8:00' 27 | character_set_server = utf8mb4 28 | collation_server = utf8mb4_unicode_ci 29 | pid-file = /var/run/mysqld/mysqld.pid 30 | socket = /var/run/mysqld/mysqld.sock 31 | datadir = /var/lib/mysql 32 | log-error = /var/log/mysql/error.log 33 | # By default we only accept connections from localhost 34 | #bind-address = 127.0.0.1 35 | # Disabling symbolic-links is recommended to prevent assorted security risks 36 | symbolic-links=0 37 | # replication 38 | log_bin = /var/log/mysql/mysql-bin 39 | log_bin_index = /var/log/mysql/mysql-bin.index 40 | binlog_format = row 41 | server_id = 115 42 | gtid_mode = ON 43 | enforce_gtid_consistency = ON 44 | query_cache_size = 0 45 | -------------------------------------------------------------------------------- /assets/image/account_fsm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prontera/spring-cloud-rest-tcc/20eda7129afe257242d0c29c80837fd42aa516d7/assets/image/account_fsm.png -------------------------------------------------------------------------------- /assets/image/component.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prontera/spring-cloud-rest-tcc/20eda7129afe257242d0c29c80837fd42aa516d7/assets/image/component.png -------------------------------------------------------------------------------- /assets/image/container.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prontera/spring-cloud-rest-tcc/20eda7129afe257242d0c29c80837fd42aa516d7/assets/image/container.png -------------------------------------------------------------------------------- /assets/image/context.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prontera/spring-cloud-rest-tcc/20eda7129afe257242d0c29c80837fd42aa516d7/assets/image/context.png -------------------------------------------------------------------------------- /assets/image/order_fsm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prontera/spring-cloud-rest-tcc/20eda7129afe257242d0c29c80837fd42aa516d7/assets/image/order_fsm.png -------------------------------------------------------------------------------- /assets/image/order_swagger.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prontera/spring-cloud-rest-tcc/20eda7129afe257242d0c29c80837fd42aa516d7/assets/image/order_swagger.png -------------------------------------------------------------------------------- /assets/image/product_fsm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prontera/spring-cloud-rest-tcc/20eda7129afe257242d0c29c80837fd42aa516d7/assets/image/product_fsm.png -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | com.github.prontera 7 | spring-cloud-rest-tcc 8 | 0.0.1-SNAPSHOT 9 | pom 10 | 11 | spring-cloud-rest-tcc 12 | spring cloud netflix samples 13 | 14 | 15 | rest-tcc-commons 16 | rest-tcc-contract 17 | rest-tcc-projects 18 | 19 | 20 | 21 | org.springframework.boot 22 | spring-boot-starter-parent 23 | 2.2.2.RELEASE 24 | 25 | 26 | 27 | 28 | 29 | 1.8 30 | UTF-8 31 | UTF-8 32 | 33 | Hoxton.SR1 34 | 35 | 28.2-jre 36 | 3.0.0-SNAPSHOT 37 | 38 | 39 | 40 | 41 | 42 | org.springframework.cloud 43 | spring-cloud-dependencies 44 | ${spring-cloud.version} 45 | pom 46 | import 47 | 48 | 49 | 50 | com.google.guava 51 | guava 52 | ${guava.version} 53 | 54 | 55 | 56 | io.springfox 57 | springfox-swagger2 58 | ${springfox-swagger2.version} 59 | 60 | 61 | io.springfox 62 | springfox-bean-validators 63 | ${springfox-swagger2.version} 64 | 65 | 66 | io.springfox 67 | springfox-swagger-ui 68 | ${springfox-swagger2.version} 69 | 70 | 71 | io.springfox 72 | springfox-spring-webflux 73 | ${springfox-swagger2.version} 74 | 75 | 76 | 77 | 78 | 79 | 80 | alimaven 81 | aliyun maven 82 | http://maven.aliyun.com/nexus/content/groups/public/ 83 | 84 | 85 | jcenter-snapshots 86 | jcenter 87 | http://oss.jfrog.org/artifactory/oss-snapshot-local/ 88 | 89 | 90 | spring-snapshots 91 | Spring Snapshots 92 | https://repo.spring.io/libs-snapshot 93 | 94 | true 95 | 96 | 97 | 98 | spring-milestones 99 | spring-milestones 100 | https://repo.spring.io/milestone 101 | 102 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /rest-tcc-commons/.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 | 12 | ### IntelliJ IDEA ### 13 | .idea 14 | *.iws 15 | *.iml 16 | *.ipr 17 | 18 | ### NetBeans ### 19 | nbproject/private/ 20 | build/ 21 | nbbuild/ 22 | dist/ 23 | nbdist/ 24 | .nb-gradle/ -------------------------------------------------------------------------------- /rest-tcc-commons/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | rest-tcc-commons 7 | 0.0.1-SNAPSHOT 8 | jar 9 | 10 | rest-tcc-commons 11 | 12 | 13 | 本module在实际应用中应该为逻辑概念, 本模块为三个演示模块的父pom, 用于管理业务依赖. 14 | 如不能作为client的一员, 但业务上有共有关系的mybatis组件, log4j2组件和swagger组件等. 15 | 16 | 17 | 18 | com.github.prontera 19 | spring-cloud-rest-tcc 20 | 0.0.1-SNAPSHOT 21 | 22 | 23 | 24 | 25 | 2.1.1 26 | 27 | 1.5.4 28 | 1.13 29 | 3.4.2 30 | 3.0.1 31 | 3.9 32 | 33 | 34 | 35 | 36 | 37 | com.github.prontera 38 | rest-tcc-contract 39 | 0.0.1-SNAPSHOT 40 | 41 | 42 | 43 | org.springframework.boot 44 | spring-boot-starter-webflux 45 | 46 | 47 | org.springframework.boot 48 | spring-boot-starter-logging 49 | 50 | 51 | 52 | 53 | org.springframework.boot 54 | spring-boot-starter-log4j2 55 | 56 | 57 | org.springframework.boot 58 | spring-boot-starter-aop 59 | 60 | 61 | org.mybatis.spring.boot 62 | mybatis-spring-boot-starter 63 | ${mybatis-spring-boot-starter.version} 64 | 65 | 66 | 67 | com.google.guava 68 | guava 69 | 70 | 71 | mysql 72 | mysql-connector-java 73 | runtime 74 | 75 | 76 | org.projectlombok 77 | lombok 78 | 79 | 80 | com.lmax 81 | disruptor 82 | ${disruptor.version} 83 | 84 | 85 | org.apache.commons 86 | commons-lang3 87 | ${commons-lang3.version} 88 | 89 | 90 | commons-codec 91 | commons-codec 92 | ${commons-codec.version} 93 | 94 | 95 | ma.glasnost.orika 96 | orika-core 97 | ${orika.version} 98 | 99 | 100 | org.objenesis 101 | objenesis 102 | ${objenesis.version} 103 | 104 | 105 | 106 | io.springfox 107 | springfox-swagger2 108 | 109 | 110 | io.springfox 111 | springfox-bean-validators 112 | 113 | 114 | io.springfox 115 | springfox-swagger-ui 116 | 117 | 118 | io.springfox 119 | springfox-spring-webflux 120 | 121 | 122 | 123 | 124 | -------------------------------------------------------------------------------- /rest-tcc-commons/src/main/java/com/github/prontera/ChainHandler.java: -------------------------------------------------------------------------------- 1 | package com.github.prontera; 2 | 3 | import javax.annotation.Nonnull; 4 | import java.util.concurrent.atomic.AtomicReference; 5 | 6 | /** 7 | * @author Zhao Junjian 8 | * @date 2020/02/02 9 | */ 10 | public interface ChainHandler { 11 | 12 | /** 13 | * @param in 从中读取数据的类型为{@link I}的对象 14 | * @param out 经转换后的结果 15 | */ 16 | void invoke(@Nonnull I in, @Nonnull AtomicReference out); 17 | 18 | /** 19 | * 经由{@link #invoke(Object, AtomicReference)}内部判断和处理后, 标记为当前数据已经被该handler处理 20 | */ 21 | void complete(); 22 | 23 | /** 24 | * 当类型为{@link Type#EXCLUSIVE}时, 实现者不应该继续迭代 25 | * 26 | * @return 当前处理器的类型 27 | */ 28 | Type getType(); 29 | 30 | enum Type { 31 | /** 32 | * exclusive类型{@link ChainHandler}, 当{@link #complete()}被调用后不应该继续迭代责任链 33 | */ 34 | EXCLUSIVE, 35 | /** 36 | * shared类型{@link ChainHandler}, 也是默认类型 37 | */ 38 | SHARED, 39 | /** LINE SEPARATOR */ 40 | ; 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /rest-tcc-commons/src/main/java/com/github/prontera/GenericChainHandler.java: -------------------------------------------------------------------------------- 1 | package com.github.prontera; 2 | 3 | /** 4 | * @author Zhao Junjian 5 | * @date 2020/02/02 6 | */ 7 | public abstract class GenericChainHandler implements ChainHandler { 8 | 9 | private volatile boolean isRequestCompletion; 10 | 11 | @Override 12 | public void complete() { 13 | isRequestCompletion = true; 14 | } 15 | 16 | public boolean isRequestCompletion() { 17 | return isRequestCompletion; 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /rest-tcc-commons/src/main/java/com/github/prontera/Pipeline.java: -------------------------------------------------------------------------------- 1 | package com.github.prontera; 2 | 3 | import javax.annotation.Nonnull; 4 | import java.util.LinkedList; 5 | import java.util.List; 6 | import java.util.Objects; 7 | import java.util.concurrent.atomic.AtomicReference; 8 | 9 | /** 10 | * @author Zhao Junjian 11 | * @date 2020/02/02 12 | */ 13 | public final class Pipeline { 14 | 15 | private final Object lock = new Object(); 16 | 17 | private final List> handlers = new LinkedList<>(); 18 | 19 | public void addLast(@Nonnull GenericChainHandler handler) { 20 | Objects.requireNonNull(handler); 21 | synchronized (lock) { 22 | handlers.add(handler); 23 | } 24 | } 25 | 26 | public O fire(@Nonnull I in) { 27 | Objects.requireNonNull(in); 28 | final AtomicReference out = new AtomicReference<>(); 29 | for (GenericChainHandler handler : handlers) { 30 | handler.invoke(in, out); 31 | if (handler.isRequestCompletion() && handler.getType() == ChainHandler.Type.EXCLUSIVE) { 32 | break; 33 | } 34 | } 35 | return out.get(); 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /rest-tcc-commons/src/main/java/com/github/prontera/Shifts.java: -------------------------------------------------------------------------------- 1 | package com.github.prontera; 2 | 3 | import com.github.prontera.enums.NumericStatusCode; 4 | import com.github.prontera.exception.ResolvableStatusException; 5 | 6 | import javax.annotation.Nonnull; 7 | 8 | /** 9 | * @author Zhao Junjian 10 | * @date 2020/01/22 11 | */ 12 | public final class Shifts { 13 | 14 | private Shifts() { 15 | } 16 | 17 | public static void fatal(@Nonnull NumericStatusCode element) { 18 | throw new ResolvableStatusException(element); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /rest-tcc-commons/src/main/java/com/github/prontera/annotation/MyBatisRepository.java: -------------------------------------------------------------------------------- 1 | package com.github.prontera.annotation; 2 | 3 | import org.springframework.stereotype.Repository; 4 | 5 | import java.lang.annotation.Documented; 6 | import java.lang.annotation.ElementType; 7 | import java.lang.annotation.Retention; 8 | import java.lang.annotation.RetentionPolicy; 9 | import java.lang.annotation.Target; 10 | 11 | /** 12 | * @author Zhao Junjian 13 | */ 14 | @Target({ElementType.TYPE}) 15 | @Retention(RetentionPolicy.RUNTIME) 16 | @Documented 17 | @Repository 18 | public @interface MyBatisRepository { 19 | } 20 | -------------------------------------------------------------------------------- /rest-tcc-commons/src/main/java/com/github/prontera/annotation/marker/NonBehavior.java: -------------------------------------------------------------------------------- 1 | package com.github.prontera.annotation.marker; 2 | 3 | import java.lang.annotation.Documented; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | import static java.lang.annotation.ElementType.FIELD; 9 | 10 | /** 11 | * 用于标记日志型字段, 不会参与到其他逻辑当中 12 | * 13 | * @author Zhao Junjian 14 | * @date 2020/02/01 15 | */ 16 | @Target({FIELD}) 17 | @Retention(RetentionPolicy.RUNTIME) 18 | @Documented 19 | public @interface NonBehavior { 20 | } 21 | -------------------------------------------------------------------------------- /rest-tcc-commons/src/main/java/com/github/prontera/concurrent/LoggableAbortPolicy.java: -------------------------------------------------------------------------------- 1 | package com.github.prontera.concurrent; 2 | 3 | import java.util.Objects; 4 | import java.util.concurrent.ThreadPoolExecutor; 5 | 6 | /** 7 | * @author Zhao Junjian 8 | * @date 2020/01/29 9 | */ 10 | public class LoggableAbortPolicy extends ThreadPoolExecutor.AbortPolicy implements LoggablePolicy { 11 | private final String recordName; 12 | 13 | public LoggableAbortPolicy(String recordName) { 14 | this.recordName = Objects.requireNonNull(recordName, "recordName"); 15 | } 16 | 17 | @Override 18 | public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { 19 | // report here 20 | super.rejectedExecution(r, e); 21 | } 22 | 23 | @Override 24 | public String getRecordName() { 25 | return recordName; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /rest-tcc-commons/src/main/java/com/github/prontera/concurrent/LoggableCallerRunsPolicy.java: -------------------------------------------------------------------------------- 1 | package com.github.prontera.concurrent; 2 | 3 | import java.util.Objects; 4 | import java.util.concurrent.ThreadPoolExecutor; 5 | 6 | /** 7 | * @author Zhao Junjian 8 | * @date 2020/01/29 9 | */ 10 | public class LoggableCallerRunsPolicy extends ThreadPoolExecutor.CallerRunsPolicy implements LoggablePolicy { 11 | private final String recordName; 12 | 13 | public LoggableCallerRunsPolicy(String recordName) { 14 | this.recordName = Objects.requireNonNull(recordName, "recordName"); 15 | } 16 | 17 | @Override 18 | public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { 19 | // report here 20 | super.rejectedExecution(r, e); 21 | } 22 | 23 | @Override 24 | public String getRecordName() { 25 | return recordName; 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /rest-tcc-commons/src/main/java/com/github/prontera/concurrent/LoggableDiscardOldestPolicy.java: -------------------------------------------------------------------------------- 1 | package com.github.prontera.concurrent; 2 | 3 | import java.util.Objects; 4 | import java.util.concurrent.ThreadPoolExecutor; 5 | 6 | /** 7 | * @author Zhao Junjian 8 | * @date 2020/01/29 9 | */ 10 | public class LoggableDiscardOldestPolicy extends ThreadPoolExecutor.DiscardOldestPolicy implements LoggablePolicy { 11 | private final String recordName; 12 | 13 | public LoggableDiscardOldestPolicy(String recordName) { 14 | this.recordName = Objects.requireNonNull(recordName, "recordName"); 15 | } 16 | 17 | @Override 18 | public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { 19 | // report here 20 | super.rejectedExecution(r, e); 21 | } 22 | 23 | @Override 24 | public String getRecordName() { 25 | return recordName; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /rest-tcc-commons/src/main/java/com/github/prontera/concurrent/LoggableDiscardPolicy.java: -------------------------------------------------------------------------------- 1 | package com.github.prontera.concurrent; 2 | 3 | import java.util.Objects; 4 | import java.util.concurrent.ThreadPoolExecutor; 5 | 6 | /** 7 | * @author Zhao Junjian 8 | * @date 2020/01/29 9 | */ 10 | public class LoggableDiscardPolicy extends ThreadPoolExecutor.DiscardPolicy implements LoggablePolicy { 11 | private final String recordName; 12 | 13 | public LoggableDiscardPolicy(String recordName) { 14 | this.recordName = Objects.requireNonNull(recordName, "recordName"); 15 | } 16 | 17 | @Override 18 | public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { 19 | // report here 20 | super.rejectedExecution(r, e); 21 | } 22 | 23 | @Override 24 | public String getRecordName() { 25 | return recordName; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /rest-tcc-commons/src/main/java/com/github/prontera/concurrent/LoggablePolicy.java: -------------------------------------------------------------------------------- 1 | package com.github.prontera.concurrent; 2 | 3 | /** 4 | * @author Zhao Junjian 5 | * @date 2020/01/29 6 | */ 7 | public interface LoggablePolicy { 8 | 9 | String getRecordName(); 10 | 11 | } 12 | -------------------------------------------------------------------------------- /rest-tcc-commons/src/main/java/com/github/prontera/domain/IdenticalDomain.java: -------------------------------------------------------------------------------- 1 | package com.github.prontera.domain; 2 | 3 | import lombok.EqualsAndHashCode; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | import lombok.Setter; 7 | import lombok.ToString; 8 | 9 | import java.io.Serializable; 10 | import java.time.LocalDateTime; 11 | 12 | /** 13 | * @author Zhao Junjian 14 | * @date 2020/01/21 15 | */ 16 | @Getter 17 | @Setter 18 | @NoArgsConstructor 19 | @ToString(callSuper = true) 20 | @EqualsAndHashCode 21 | public class IdenticalDomain implements Serializable { 22 | 23 | private static final long serialVersionUID = -3893569795599411412L; 24 | 25 | private Long id; 26 | 27 | private LocalDateTime createAt; 28 | 29 | private LocalDateTime updateAt; 30 | 31 | } 32 | -------------------------------------------------------------------------------- /rest-tcc-commons/src/main/java/com/github/prontera/exception/ApplicationException.java: -------------------------------------------------------------------------------- 1 | package com.github.prontera.exception; 2 | 3 | /** 4 | * @author Zhao Junjian 5 | * @date 2020/01/29 6 | */ 7 | public class ApplicationException extends RuntimeException { 8 | private static final long serialVersionUID = 6476602059021589106L; 9 | 10 | public ApplicationException() { 11 | super(); 12 | } 13 | 14 | public ApplicationException(String message) { 15 | super(message); 16 | } 17 | 18 | public ApplicationException(String message, Throwable cause) { 19 | super(message, cause); 20 | } 21 | 22 | public ApplicationException(Throwable cause) { 23 | super(cause); 24 | } 25 | 26 | protected ApplicationException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { 27 | super(message, cause, enableSuppression, writableStackTrace); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /rest-tcc-commons/src/main/java/com/github/prontera/exception/InvalidModelException.java: -------------------------------------------------------------------------------- 1 | package com.github.prontera.exception; 2 | 3 | import javax.annotation.Nonnull; 4 | import java.util.Map; 5 | 6 | /** 7 | * 用于Hibernate Validator的校验异常 8 | * 9 | * @author Zhao Junjian 10 | */ 11 | public class InvalidModelException extends ApplicationException { 12 | private static final long serialVersionUID = 8096590956382108583L; 13 | 14 | public InvalidModelException(@Nonnull Map info) { 15 | super(info.toString()); 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /rest-tcc-commons/src/main/java/com/github/prontera/exception/ResolvableStatusException.java: -------------------------------------------------------------------------------- 1 | package com.github.prontera.exception; 2 | 3 | import com.github.prontera.enums.NumericStatusCode; 4 | 5 | import java.util.Objects; 6 | 7 | /** 8 | * 用于在业务上, 表示为可解析的异常, 如用户不存在这类的业务异常 9 | * 10 | * @author Zhao Junjian 11 | * @date 2020/01/20 12 | */ 13 | public class ResolvableStatusException extends ApplicationException { 14 | 15 | private static final long serialVersionUID = -7620210836307698986L; 16 | 17 | private final NumericStatusCode statusCode; 18 | 19 | private final String injectMessage; 20 | 21 | public ResolvableStatusException(NumericStatusCode statusCode) { 22 | this.statusCode = Objects.requireNonNull(statusCode); 23 | this.injectMessage = null; 24 | } 25 | 26 | public ResolvableStatusException(NumericStatusCode statusCode, String injectMessage) { 27 | this.statusCode = Objects.requireNonNull(statusCode); 28 | this.injectMessage = injectMessage; 29 | } 30 | 31 | public ResolvableStatusException(String message, NumericStatusCode statusCode, String injectMessage) { 32 | super(message); 33 | this.statusCode = Objects.requireNonNull(statusCode); 34 | this.injectMessage = injectMessage; 35 | } 36 | 37 | public ResolvableStatusException(String message, Throwable cause, NumericStatusCode statusCode, String injectMessage) { 38 | super(message, cause); 39 | this.statusCode = Objects.requireNonNull(statusCode); 40 | this.injectMessage = injectMessage; 41 | } 42 | 43 | public ResolvableStatusException(Throwable cause, NumericStatusCode statusCode, String injectMessage) { 44 | super(cause); 45 | this.statusCode = Objects.requireNonNull(statusCode); 46 | this.injectMessage = injectMessage; 47 | } 48 | 49 | public NumericStatusCode getStatusCode() { 50 | return statusCode; 51 | } 52 | 53 | public String getInjectMessage() { 54 | return injectMessage; 55 | } 56 | 57 | @Override 58 | public String toString() { 59 | return "ResolvableStatusException{" + 60 | "statusCode=" + statusCode + 61 | ", injectMessage='" + injectMessage + '\'' + 62 | '}'; 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /rest-tcc-commons/src/main/java/com/github/prontera/persistence/CrudMapper.java: -------------------------------------------------------------------------------- 1 | package com.github.prontera.persistence; 2 | 3 | import com.github.prontera.annotation.MyBatisRepository; 4 | 5 | /** 6 | * @author Zhao Junjian 7 | * @date 2020/01/20 8 | */ 9 | @SuppressWarnings({"InterfaceNeverImplemented", "MybatisMapperMethodInspection"}) 10 | @MyBatisRepository 11 | public interface CrudMapper { 12 | 13 | int deleteByPrimaryKey(Long id); 14 | 15 | int insert(T record); 16 | 17 | int insertSelective(T record); 18 | 19 | T selectByPrimaryKey(Long id); 20 | 21 | int updateByPrimaryKeySelective(T record); 22 | 23 | int updateByPrimaryKey(T record); 24 | 25 | } 26 | -------------------------------------------------------------------------------- /rest-tcc-commons/src/main/java/com/github/prontera/persistence/GenericTypeHandler.java: -------------------------------------------------------------------------------- 1 | package com.github.prontera.persistence; 2 | 3 | import org.apache.ibatis.type.BaseTypeHandler; 4 | import org.apache.ibatis.type.JdbcType; 5 | 6 | import java.lang.reflect.ParameterizedType; 7 | import java.sql.CallableStatement; 8 | import java.sql.PreparedStatement; 9 | import java.sql.ResultSet; 10 | import java.sql.SQLException; 11 | import java.util.Objects; 12 | 13 | /** 14 | * @author Zhao Junjian 15 | */ 16 | public abstract class GenericTypeHandler> extends BaseTypeHandler { 17 | private final Class type; 18 | 19 | private final T[] enums; 20 | 21 | protected GenericTypeHandler() { 22 | this.type = (Class) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0]; 23 | enums = type.getEnumConstants(); 24 | if (enums == null) { 25 | throw new IllegalArgumentException(type.getSimpleName() + " does not represent an enum type."); 26 | } 27 | } 28 | 29 | public abstract int getEnumIntegerValue(T parameter); 30 | 31 | @Override 32 | public void setNonNullParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException { 33 | ps.setInt(i, getEnumIntegerValue(parameter)); 34 | } 35 | 36 | @Override 37 | public T getNullableResult(ResultSet rs, String columnName) throws SQLException { 38 | int i = rs.getInt(columnName); 39 | if (rs.wasNull()) { 40 | return null; 41 | } else { 42 | return locateEnumIntegerValue(i); 43 | } 44 | } 45 | 46 | @Override 47 | public T getNullableResult(ResultSet rs, int columnIndex) throws SQLException { 48 | int i = rs.getInt(columnIndex); 49 | if (rs.wasNull()) { 50 | return null; 51 | } else { 52 | return locateEnumIntegerValue(i); 53 | } 54 | } 55 | 56 | @Override 57 | public T getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { 58 | int i = cs.getInt(columnIndex); 59 | if (cs.wasNull()) { 60 | return null; 61 | } else { 62 | return locateEnumIntegerValue(i); 63 | } 64 | } 65 | 66 | private T locateEnumIntegerValue(int code) { 67 | for (T status : enums) { 68 | if (Objects.equals(getEnumIntegerValue(status), code)) { 69 | return status; 70 | } 71 | } 72 | throw new IllegalArgumentException("unknown enum integer code:" + code + ", type name is: " + type.getSimpleName()); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /rest-tcc-commons/src/main/java/com/github/prontera/service/CrudService.java: -------------------------------------------------------------------------------- 1 | package com.github.prontera.service; 2 | 3 | /** 4 | * @author Zhao Junjian 5 | */ 6 | public interface CrudService { 7 | 8 | T find(Long id); 9 | 10 | int persistNonNullProperties(T entity); 11 | 12 | int persist(T entity); 13 | 14 | int updateNonNullProperties(T entity); 15 | 16 | int update(T entity); 17 | 18 | int delete(Long id); 19 | 20 | } 21 | -------------------------------------------------------------------------------- /rest-tcc-commons/src/main/java/com/github/prontera/service/IdenticalCrudService.java: -------------------------------------------------------------------------------- 1 | package com.github.prontera.service; 2 | 3 | import com.github.prontera.domain.IdenticalDomain; 4 | import com.github.prontera.persistence.CrudMapper; 5 | import com.google.common.base.Preconditions; 6 | 7 | import java.time.LocalDateTime; 8 | 9 | /** 10 | * @author Zhao Junjian 11 | */ 12 | public class IdenticalCrudService implements CrudService { 13 | 14 | private final CrudMapper mapper; 15 | 16 | public IdenticalCrudService(CrudMapper mapper) { 17 | this.mapper = mapper; 18 | } 19 | 20 | @Override 21 | public T find(Long id) { 22 | Preconditions.checkNotNull(id, "type of id should not be NULL"); 23 | return getMapper().selectByPrimaryKey(id); 24 | } 25 | 26 | @Override 27 | public int persistNonNullProperties(T entity) { 28 | Preconditions.checkNotNull(entity, "persisting entity should not be NULL"); 29 | initializeDateTime(entity); 30 | return getMapper().insertSelective(entity); 31 | } 32 | 33 | @Override 34 | public int persist(T entity) { 35 | Preconditions.checkNotNull(entity, "persisting entity should not be NULL"); 36 | initializeDateTime(entity); 37 | return getMapper().insert(entity); 38 | } 39 | 40 | @Override 41 | public int update(T entity) { 42 | Preconditions.checkNotNull(entity, "entity in updating should not be NULL"); 43 | entity.setUpdateAt(LocalDateTime.now()); 44 | return getMapper().updateByPrimaryKey(entity); 45 | } 46 | 47 | @Override 48 | public int updateNonNullProperties(T entity) { 49 | Preconditions.checkNotNull(entity, "entity in updating should not be NULL"); 50 | entity.setUpdateAt(LocalDateTime.now()); 51 | return getMapper().updateByPrimaryKeySelective(entity); 52 | } 53 | 54 | @Override 55 | public int delete(Long id) { 56 | Preconditions.checkNotNull(id, "type of id should not be NULL"); 57 | return getMapper().deleteByPrimaryKey(id); 58 | } 59 | 60 | private CrudMapper getMapper() { 61 | return mapper; 62 | } 63 | 64 | private void initializeDateTime(T entity) { 65 | entity.setCreateAt(LocalDateTime.now()); 66 | entity.setUpdateAt(LocalDateTime.now()); 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /rest-tcc-commons/src/main/java/com/github/prontera/util/HibernateValidators.java: -------------------------------------------------------------------------------- 1 | package com.github.prontera.util; 2 | 3 | import com.github.prontera.exception.InvalidModelException; 4 | import org.hibernate.validator.HibernateValidator; 5 | 6 | import javax.annotation.Nonnull; 7 | import javax.validation.ConstraintViolation; 8 | import javax.validation.Validation; 9 | import javax.validation.Validator; 10 | import javax.validation.ValidatorFactory; 11 | import java.util.Collections; 12 | import java.util.HashMap; 13 | import java.util.Map; 14 | import java.util.Objects; 15 | import java.util.Set; 16 | 17 | /** 18 | * @author Zhao Junjian 19 | * @date 2020/01/20 20 | */ 21 | public final class HibernateValidators { 22 | 23 | private static final Validator VALIDATOR; 24 | 25 | static { 26 | ValidatorFactory factory = Validation.byProvider(HibernateValidator.class) 27 | .configure() 28 | .failFast(true) 29 | .buildValidatorFactory(); 30 | VALIDATOR = factory.getValidator(); 31 | } 32 | 33 | private HibernateValidators() { 34 | } 35 | 36 | /** 37 | * 当校验有错误的时候聚合成Map集合, 否则返回空集合 38 | */ 39 | public static Map asInvalidMap(@Nonnull T object, Class... groups) { 40 | Objects.requireNonNull(object); 41 | final Set> constraintViolations = VALIDATOR.validate(object, groups); 42 | Map errorMap = Collections.emptyMap(); 43 | if (!constraintViolations.isEmpty()) { 44 | errorMap = new HashMap<>(Capacity.toMapExpectedSize(constraintViolations.size())); 45 | for (ConstraintViolation violation : constraintViolations) { 46 | errorMap.put(violation.getPropertyPath().toString(), violation.getMessage()); 47 | } 48 | } 49 | return errorMap; 50 | } 51 | 52 | /** 53 | * @throws InvalidModelException 当校验有错误的时候抛出异常 54 | */ 55 | public static void throwsIfInvalid(@Nonnull T object, Class... groups) { 56 | final Map invalidMap = asInvalidMap(object, groups); 57 | if (!invalidMap.isEmpty()) { 58 | throw new InvalidModelException(invalidMap); 59 | } 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /rest-tcc-commons/src/main/java/com/github/prontera/util/Jacksons.java: -------------------------------------------------------------------------------- 1 | package com.github.prontera.util; 2 | 3 | import com.fasterxml.jackson.core.JsonProcessingException; 4 | import com.fasterxml.jackson.core.type.TypeReference; 5 | import com.fasterxml.jackson.databind.DeserializationFeature; 6 | import com.fasterxml.jackson.databind.ObjectMapper; 7 | import com.google.common.base.Strings; 8 | 9 | import java.io.IOException; 10 | 11 | /** 12 | * @author Zhao Junjian 13 | */ 14 | public final class Jacksons { 15 | 16 | public static final String EMPTY = "{}"; 17 | 18 | private static final ObjectMapper MAPPER = new ObjectMapper(); 19 | 20 | static { 21 | MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); 22 | } 23 | 24 | private Jacksons() { 25 | 26 | } 27 | 28 | public static String parse(T obj) { 29 | try { 30 | String json; 31 | if (obj != null) { 32 | json = MAPPER.writeValueAsString(obj); 33 | } else { 34 | json = EMPTY; 35 | } 36 | return json; 37 | } catch (JsonProcessingException e) { 38 | throw new IllegalArgumentException(e); 39 | } 40 | } 41 | 42 | public static String parseInPrettyMode(T obj) { 43 | try { 44 | String json; 45 | if (obj != null) { 46 | json = MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(obj); 47 | } else { 48 | json = EMPTY; 49 | } 50 | return json; 51 | } catch (JsonProcessingException e) { 52 | throw new IllegalArgumentException(e); 53 | } 54 | } 55 | 56 | public static T convert(String json) { 57 | try { 58 | return MAPPER.readValue(json, new TypeReference() { 59 | }); 60 | } catch (IOException e) { 61 | throw new IllegalArgumentException(e); 62 | } 63 | } 64 | 65 | public static T convert(String json, Class clazz) { 66 | try { 67 | return MAPPER.readValue(json, clazz); 68 | } catch (IOException e) { 69 | throw new IllegalArgumentException(e); 70 | } 71 | } 72 | 73 | public static ObjectMapper getMapper() { 74 | return MAPPER; 75 | } 76 | 77 | public static boolean isNotEmpty(String json) { 78 | return !Strings.isNullOrEmpty(json) && !EMPTY.equals(json); 79 | } 80 | 81 | public static boolean isEmpty(String json) { 82 | return !isNotEmpty(json); 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /rest-tcc-contract/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 4.0.0 7 | 8 | rest-tcc-contract 9 | 0.0.1-SNAPSHOT 10 | jar 11 | 12 | rest-tcc-contract 13 | client contract 14 | 15 | 16 | spring-cloud-rest-tcc 17 | com.github.prontera 18 | 0.0.1-SNAPSHOT 19 | 20 | 21 | 22 | 1.5.20 23 | 3.0.2 24 | 25 | 26 | 27 | 28 | org.projectlombok 29 | lombok 30 | 31 | 32 | javax.validation 33 | validation-api 34 | 35 | 36 | com.google.code.findbugs 37 | jsr305 38 | ${jsr305.version} 39 | 40 | 41 | io.swagger 42 | swagger-annotations 43 | ${swagger-annotations.version} 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /rest-tcc-contract/src/main/java/com/github/prontera/enums/EnumFunction.java: -------------------------------------------------------------------------------- 1 | package com.github.prontera.enums; 2 | 3 | import javax.annotation.Nonnull; 4 | 5 | /** 6 | * 使用入参转换为指定的Enumeration类型, 类似Java 8中的{@link java.util.function.Function}, 7 | * 但在JDK的版本兼容上有更好的体验, 所以也没标记为{@link FunctionalInterface} 8 | * 9 | * @author Zhao Junjian 10 | * @date 2020/01/18 11 | */ 12 | public interface EnumFunction> { 13 | 14 | /** 15 | * 将给定的入参应用在转换函数中 16 | * 17 | * @param source function入参 18 | * @return function转化后的结果 19 | */ 20 | R apply(@Nonnull T source); 21 | 22 | } 23 | -------------------------------------------------------------------------------- /rest-tcc-contract/src/main/java/com/github/prontera/enums/NumericStatusCode.java: -------------------------------------------------------------------------------- 1 | package com.github.prontera.enums; 2 | 3 | import javax.annotation.Nonnull; 4 | import java.util.Objects; 5 | 6 | /** 7 | * @author Zhao Junjian 8 | * @date 2020/01/22 9 | */ 10 | public interface NumericStatusCode { 11 | 12 | /** 13 | * Returns {@code true} if the given element representing successful. 14 | * 15 | * @return {@code true} if the given element representing successful. 16 | */ 17 | static boolean isSuccessful(@Nonnull NumericStatusCode element) { 18 | Objects.requireNonNull(element); 19 | return isSuccessful(element.code()); 20 | } 21 | 22 | /** 23 | * Returns {@code true} if the given element representing successful. 24 | * 25 | * @return {@code true} if the given element representing successful. 26 | */ 27 | static boolean isSuccessful(int code) { 28 | return code >= 20000 && code <= 30000; 29 | } 30 | 31 | /** 32 | * the status codes of per restful request. 33 | * 34 | * @return 20xxx if succeed, 40xxx if client error, 50xxx if server side crash. 35 | */ 36 | int code(); 37 | 38 | /** 39 | * @return status enum name 40 | */ 41 | String name(); 42 | 43 | /** 44 | * @return message summary 45 | */ 46 | String message(); 47 | 48 | } 49 | -------------------------------------------------------------------------------- /rest-tcc-contract/src/main/java/com/github/prontera/model/response/ResolvableResponse.java: -------------------------------------------------------------------------------- 1 | package com.github.prontera.model.response; 2 | 3 | import io.swagger.annotations.ApiModelProperty; 4 | import lombok.EqualsAndHashCode; 5 | import lombok.Getter; 6 | import lombok.NoArgsConstructor; 7 | import lombok.Setter; 8 | import lombok.ToString; 9 | 10 | import javax.validation.constraints.NotNull; 11 | import java.io.Serializable; 12 | 13 | /** 14 | * @author Zhao Junjian 15 | * @date 2020/01/20 16 | */ 17 | @Getter 18 | @Setter 19 | @NoArgsConstructor 20 | @ToString 21 | @EqualsAndHashCode 22 | public class ResolvableResponse implements Serializable { 23 | private static final long serialVersionUID = 2589811349478861719L; 24 | 25 | @ApiModelProperty(value = "响应标志位", required = true, example = "true") 26 | private boolean successful; 27 | 28 | @ApiModelProperty(value = "响应码", required = true, example = "20000") 29 | private int code; 30 | 31 | @ApiModelProperty(value = "响应信息", required = true, example = "true") 32 | private @NotNull String message; 33 | 34 | } 35 | -------------------------------------------------------------------------------- /rest-tcc-contract/src/main/java/com/github/prontera/util/Capacity.java: -------------------------------------------------------------------------------- 1 | package com.github.prontera.util; 2 | 3 | /** 4 | * @author Zhao Junjian 5 | * @date 2019/06/20 6 | */ 7 | public final class Capacity { 8 | 9 | private Capacity() { 10 | } 11 | 12 | /** 13 | * @throws IllegalArgumentException throws if less than 0 or larger than the largest power of 14 | * two that can be represented as an {@code int} 15 | */ 16 | public static int toMapExpectedSize(int expectedSize) { 17 | if (expectedSize >= 0 && expectedSize < 1 << Integer.SIZE - 2) { 18 | return (int) (expectedSize / 0.75F + 1.0F); 19 | } 20 | throw new IllegalArgumentException("expectedSize '" + expectedSize + "' is invalid."); 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /rest-tcc-projects/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | com.github.prontera 7 | spring-cloud-rest-tcc 8 | 0.0.1-SNAPSHOT 9 | 10 | 4.0.0 11 | 12 | pom 13 | 14 | rest-tcc-projects 15 | 16 | 17 | rest-tcc-account 18 | rest-tcc-order 19 | rest-tcc-product 20 | rest-tcc-service-discovery 21 | rest-tcc-account-client 22 | rest-tcc-product-client 23 | 24 | 25 | 26 | 1.3.7 27 | 8.0.18 28 | 1.3.1.Final 29 | 1.3.1.Final 30 | 31 | 32 | 33 | 34 | org.springframework.boot 35 | spring-boot-starter-actuator 36 | 37 | 38 | org.mapstruct 39 | mapstruct-jdk8 40 | ${mapstruct-jdk8.version} 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | org.mybatis.generator 49 | mybatis-generator-maven-plugin 50 | ${mybatis-generator-maven-plugin.version} 51 | 52 | 53 | mysql 54 | mysql-connector-java 55 | ${mysql-connector-java.version} 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | maven-compiler-plugin 64 | 65 | ${java.version} 66 | ${java.version} 67 | UTF-8 68 | 69 | 70 | org.projectlombok 71 | lombok 72 | ${lombok.version} 73 | 74 | 75 | org.mapstruct 76 | mapstruct-processor 77 | ${org.mapstruct.version} 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /rest-tcc-projects/rest-tcc-account-client/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | com.github.prontera 7 | rest-tcc-projects 8 | 0.0.1-SNAPSHOT 9 | 10 | 4.0.0 11 | 12 | rest-tcc-account-client 13 | 14 | 15 | 0.0.1-SNAPSHOT 16 | 17 | 18 | 19 | 20 | com.github.prontera 21 | rest-tcc-contract 22 | ${rest-tcc-contract.version} 23 | 24 | 25 | com.fasterxml.jackson.core 26 | jackson-annotations 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /rest-tcc-projects/rest-tcc-account-client/src/main/java/com/github/prontera/account/enums/ReservingState.java: -------------------------------------------------------------------------------- 1 | package com.github.prontera.account.enums; 2 | 3 | import com.github.prontera.enums.EnumFunction; 4 | import com.github.prontera.util.Capacity; 5 | 6 | import javax.annotation.Nonnull; 7 | import java.util.Collections; 8 | import java.util.HashMap; 9 | import java.util.Map; 10 | import java.util.NoSuchElementException; 11 | import java.util.Objects; 12 | 13 | /** 14 | * @author Zhao Junjian 15 | * @date 2020/01/20 16 | */ 17 | public enum ReservingState { 18 | /** 19 | * invalid value 20 | */ 21 | INVALID(Integer.MAX_VALUE), 22 | /** 23 | * 处理中 - intermediate state 24 | */ 25 | TRYING(0), 26 | /** 27 | * 交易成功, 依据fsm的设计可以一个虚状态出现, 即可直接删除存储介质上的数据 - final state 28 | */ 29 | CONFIRMED(1), 30 | /** 31 | * 交易超时 - final state 32 | */ 33 | CANCELLED(2), 34 | /** LINE SEPARATOR */ 35 | ; 36 | 37 | private static final Map CACHE; 38 | 39 | private static final DefaultParser PARSER; 40 | 41 | private static final DefaultParser GENTLE_PARSER; 42 | 43 | static { 44 | final int expectedSize = Capacity.toMapExpectedSize(values().length); 45 | final Map builder = new HashMap<>(expectedSize); 46 | for (ReservingState element : values()) { 47 | builder.put(element.val(), element); 48 | } 49 | CACHE = Collections.unmodifiableMap(builder); 50 | PARSER = new DefaultParser(true); 51 | GENTLE_PARSER = new DefaultParser(false); 52 | } 53 | 54 | private final int val; 55 | 56 | ReservingState(int val) { 57 | this.val = val; 58 | } 59 | 60 | public static ReservingState parse(int val) { 61 | return PARSER.apply(val); 62 | } 63 | 64 | public static ReservingState parseQuietly(int val) { 65 | return GENTLE_PARSER.apply(val); 66 | } 67 | 68 | public static ReservingState parse(@Nonnull T val, @Nonnull EnumFunction function) { 69 | Objects.requireNonNull(val); 70 | Objects.requireNonNull(function); 71 | return function.apply(val); 72 | } 73 | 74 | public int val() { 75 | return val; 76 | } 77 | 78 | private static final class DefaultParser implements EnumFunction { 79 | private final boolean throwsIfInvalid; 80 | 81 | private DefaultParser(boolean throwsIfInvalid) { 82 | this.throwsIfInvalid = throwsIfInvalid; 83 | } 84 | 85 | @Override 86 | public ReservingState apply(@Nonnull Integer source) { 87 | Objects.requireNonNull(source); 88 | ReservingState element = CACHE.get(source); 89 | if (element == null) { 90 | if (throwsIfInvalid) { 91 | throw new NoSuchElementException("No matching constant for [" + source + "]"); 92 | } else { 93 | element = INVALID; 94 | } 95 | } 96 | return element; 97 | } 98 | } 99 | 100 | } 101 | 102 | -------------------------------------------------------------------------------- /rest-tcc-projects/rest-tcc-account-client/src/main/java/com/github/prontera/account/enums/StatusCode.java: -------------------------------------------------------------------------------- 1 | package com.github.prontera.account.enums; 2 | 3 | import com.github.prontera.enums.NumericStatusCode; 4 | import com.github.prontera.util.Capacity; 5 | 6 | import java.util.Collections; 7 | import java.util.HashMap; 8 | import java.util.Map; 9 | 10 | /** 11 | * @author Zhao Junjian 12 | * @date 2020/01/22 13 | */ 14 | public enum StatusCode implements NumericStatusCode { 15 | 16 | // 20xxx 客户端请求成功 17 | OK(20000, "请求成功"), 18 | 19 | /** 20 | * 接受请求, 将会异步处理 21 | */ 22 | ACCEPTED(20001, "请求已受理"), 23 | 24 | /** 25 | * 重复对同一guid进行预留操作, 已保证幂等性 26 | */ 27 | IDEMPOTENT_RESERVING(20002, "幂等预留"), 28 | 29 | // 40xxx 客户端不合法的请求 30 | /** 31 | * 不被允许的访问 32 | */ 33 | BAD_REQUEST(40000, "访问被拒绝"), 34 | 35 | /** 36 | * 字段校验错误 37 | */ 38 | INVALID_MODEL_FIELDS(40001, "字段校验非法"), 39 | 40 | /** 41 | * 用户不存在 42 | */ 43 | USER_NOT_EXISTS(40003, "用户名不存在"), 44 | 45 | /** 46 | * 无该订单信息 47 | */ 48 | ORDER_NOT_EXISTS(40004, "无相关订单信息"), 49 | 50 | // 成功接收请求, 但是处理失败 51 | 52 | /** 53 | * 触发限流 54 | */ 55 | RATE_LIMITED(42000, "触发限流规则, 请确认当前操作所允许的QPS"), 56 | 57 | /** 58 | * Duplicate Key 59 | */ 60 | DUPLICATE_KEY(42001, "操作过快, 请稍后再试"), 61 | 62 | /** 63 | * 余额不足 64 | */ 65 | INSUFFICIENT_BALANCE(42002, "余额不足"), 66 | 67 | /** 68 | * 超时并取消预留资源 69 | */ 70 | TIMEOUT_AND_CANCELLED(42003, "超时并取消预留资源"), 71 | 72 | /** 73 | * {@link ReservingState}已经处于final state 74 | */ 75 | RESERVING_FINAL_STATE(42004, "资源已处于final state"), 76 | 77 | // 50xxx 服务端异常 78 | /** 79 | * 用于处理未知的服务端错误 80 | */ 81 | SERVER_UNKNOWN_ERROR(50001, "服务端异常, 请稍后再试"), 82 | 83 | /** 84 | * 用于远程调用时的系统出错 85 | */ 86 | SERVER_IS_BUSY_NOW(50002, "系统繁忙, 请稍后再试"), 87 | 88 | /** 89 | * 一般常见于DB连接抖动 90 | */ 91 | DB_LINK_FAILURE(50003, "DB链接失败, 请稍后再试"), 92 | 93 | /** 94 | * {@link ReservingState#INVALID} 95 | */ 96 | UNKNOWN_RESERVING_STATE(50004, "未知的预留资源状态"), 97 | 98 | /** 99 | * 当预留资源过期后, 回滚账户余额失败, 可由于CP异常导致 100 | */ 101 | ACCOUNT_ROLLBACK_FAILURE(50006, "账户余额增加失败, 已回滚事务"), 102 | 103 | /** 104 | * cas确认资源失败 105 | */ 106 | FAIL_TO_CONFIRM(50007, "确认资源失败"), 107 | 108 | ; 109 | 110 | private static final Map CACHE; 111 | 112 | static { 113 | final int expectedSize = Capacity.toMapExpectedSize(values().length); 114 | final Map builder = new HashMap<>(expectedSize); 115 | for (StatusCode statusCode : values()) { 116 | builder.put(statusCode.code(), statusCode); 117 | } 118 | CACHE = Collections.unmodifiableMap(builder); 119 | } 120 | 121 | private final int code; 122 | 123 | private final String message; 124 | 125 | StatusCode(int code, String message) { 126 | this.code = code; 127 | this.message = message; 128 | } 129 | 130 | public static StatusCode of(int code) { 131 | final StatusCode status = CACHE.get(code); 132 | if (status == null) { 133 | throw new IllegalArgumentException("No matching constant for [" + code + "]"); 134 | } 135 | return status; 136 | } 137 | 138 | @Override 139 | public int code() { 140 | return code; 141 | } 142 | 143 | @Override 144 | public String message() { 145 | return message; 146 | } 147 | 148 | } 149 | -------------------------------------------------------------------------------- /rest-tcc-projects/rest-tcc-account-client/src/main/java/com/github/prontera/account/model/request/BalanceReservingRequest.java: -------------------------------------------------------------------------------- 1 | package com.github.prontera.account.model.request; 2 | 3 | import io.swagger.annotations.ApiModelProperty; 4 | import lombok.EqualsAndHashCode; 5 | import lombok.Getter; 6 | import lombok.NoArgsConstructor; 7 | import lombok.Setter; 8 | import lombok.ToString; 9 | 10 | import javax.validation.constraints.Max; 11 | import javax.validation.constraints.Min; 12 | import javax.validation.constraints.NotBlank; 13 | import javax.validation.constraints.NotNull; 14 | 15 | /** 16 | * @author Zhao Junjian 17 | * @date 2020/01/22 18 | */ 19 | @Getter 20 | @Setter 21 | @NoArgsConstructor 22 | @ToString 23 | @EqualsAndHashCode 24 | public class BalanceReservingRequest { 25 | 26 | @ApiModelProperty(value = "订单ID", required = true, example = "1") 27 | private @NotNull Long orderId; 28 | 29 | @ApiModelProperty(value = "用户名", required = true, example = "chris") 30 | private @NotBlank String username; 31 | 32 | @ApiModelProperty(value = "扣减金额, 单位元", required = true, example = "47") 33 | private @NotNull @Min(1) Integer amount; 34 | 35 | @ApiModelProperty(value = "期望的预留资源时间", required = true, example = "7") 36 | private @NotNull @Min(1) @Max(900) Integer expectedReservingSeconds; 37 | 38 | } 39 | -------------------------------------------------------------------------------- /rest-tcc-projects/rest-tcc-account-client/src/main/java/com/github/prontera/account/model/request/ConfirmAccountTxnRequest.java: -------------------------------------------------------------------------------- 1 | package com.github.prontera.account.model.request; 2 | 3 | import io.swagger.annotations.ApiModelProperty; 4 | import lombok.EqualsAndHashCode; 5 | import lombok.Getter; 6 | import lombok.NoArgsConstructor; 7 | import lombok.Setter; 8 | import lombok.ToString; 9 | 10 | import javax.validation.constraints.NotNull; 11 | 12 | /** 13 | * @author Zhao Junjian 14 | * @date 2020/01/23 15 | */ 16 | @Getter 17 | @Setter 18 | @NoArgsConstructor 19 | @ToString 20 | @EqualsAndHashCode 21 | public class ConfirmAccountTxnRequest { 22 | 23 | @ApiModelProperty(value = "订单ID", required = true, example = "1") 24 | private @NotNull Long orderId; 25 | 26 | } 27 | -------------------------------------------------------------------------------- /rest-tcc-projects/rest-tcc-account-client/src/main/java/com/github/prontera/account/model/request/QueryAccountRequest.java: -------------------------------------------------------------------------------- 1 | package com.github.prontera.account.model.request; 2 | 3 | import io.swagger.annotations.ApiModelProperty; 4 | import lombok.EqualsAndHashCode; 5 | import lombok.Getter; 6 | import lombok.NoArgsConstructor; 7 | import lombok.Setter; 8 | import lombok.ToString; 9 | 10 | import javax.validation.constraints.NotBlank; 11 | 12 | /** 13 | * @author Zhao Junjian 14 | * @date 2020/01/28 15 | */ 16 | @Getter 17 | @Setter 18 | @NoArgsConstructor 19 | @ToString 20 | @EqualsAndHashCode 21 | public class QueryAccountRequest { 22 | 23 | @ApiModelProperty(value = "用户名", required = true, example = "trump") 24 | private @NotBlank String name; 25 | 26 | } 27 | -------------------------------------------------------------------------------- /rest-tcc-projects/rest-tcc-account-client/src/main/java/com/github/prontera/account/model/request/QueryAccountTxnRequest.java: -------------------------------------------------------------------------------- 1 | package com.github.prontera.account.model.request; 2 | 3 | import io.swagger.annotations.ApiModelProperty; 4 | import lombok.EqualsAndHashCode; 5 | import lombok.Getter; 6 | import lombok.NoArgsConstructor; 7 | import lombok.Setter; 8 | import lombok.ToString; 9 | 10 | import javax.validation.constraints.NotNull; 11 | 12 | /** 13 | * @author Zhao Junjian 14 | * @date 2020/01/23 15 | */ 16 | @Getter 17 | @Setter 18 | @NoArgsConstructor 19 | @ToString 20 | @EqualsAndHashCode 21 | public class QueryAccountTxnRequest { 22 | 23 | @ApiModelProperty(value = "订单ID", required = true, example = "1") 24 | private @NotNull Long orderId; 25 | 26 | } 27 | -------------------------------------------------------------------------------- /rest-tcc-projects/rest-tcc-account-client/src/main/java/com/github/prontera/account/model/request/SignUpRequest.java: -------------------------------------------------------------------------------- 1 | package com.github.prontera.account.model.request; 2 | 3 | import io.swagger.annotations.ApiModelProperty; 4 | import lombok.EqualsAndHashCode; 5 | import lombok.Getter; 6 | import lombok.NoArgsConstructor; 7 | import lombok.Setter; 8 | import lombok.ToString; 9 | 10 | import javax.validation.constraints.NotBlank; 11 | 12 | /** 13 | * @author Zhao Junjian 14 | * @date 2020/01/22 15 | */ 16 | @Getter 17 | @Setter 18 | @NoArgsConstructor 19 | @ToString 20 | @EqualsAndHashCode 21 | public class SignUpRequest { 22 | 23 | @ApiModelProperty(value = "用户名", required = true, example = "trump") 24 | private @NotBlank String name; 25 | 26 | } 27 | -------------------------------------------------------------------------------- /rest-tcc-projects/rest-tcc-account-client/src/main/java/com/github/prontera/account/model/response/BalanceReservingResponse.java: -------------------------------------------------------------------------------- 1 | package com.github.prontera.account.model.response; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 4 | import com.fasterxml.jackson.annotation.JsonInclude; 5 | import com.github.prontera.model.response.ResolvableResponse; 6 | import lombok.EqualsAndHashCode; 7 | import lombok.Getter; 8 | import lombok.NoArgsConstructor; 9 | import lombok.Setter; 10 | import lombok.ToString; 11 | 12 | /** 13 | * @author Zhao Junjian 14 | * @date 2020/01/22 15 | */ 16 | @Getter 17 | @Setter 18 | @NoArgsConstructor 19 | @ToString(callSuper = true) 20 | @EqualsAndHashCode(callSuper = true) 21 | @JsonInclude(JsonInclude.Include.NON_NULL) 22 | @JsonIgnoreProperties(value = {"hibernateLazyInitializer", "handler", "fieldHandler"}, ignoreUnknown = true) 23 | public class BalanceReservingResponse extends ResolvableResponse { 24 | 25 | private static final long serialVersionUID = -5689692298515757935L; 26 | 27 | } 28 | -------------------------------------------------------------------------------- /rest-tcc-projects/rest-tcc-account-client/src/main/java/com/github/prontera/account/model/response/ConfirmAccountTxnResponse.java: -------------------------------------------------------------------------------- 1 | package com.github.prontera.account.model.response; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 4 | import com.fasterxml.jackson.annotation.JsonInclude; 5 | import com.github.prontera.model.response.ResolvableResponse; 6 | import lombok.EqualsAndHashCode; 7 | import lombok.Getter; 8 | import lombok.NoArgsConstructor; 9 | import lombok.Setter; 10 | import lombok.ToString; 11 | 12 | /** 13 | * @author Zhao Junjian 14 | * @date 2020/01/23 15 | */ 16 | @Getter 17 | @Setter 18 | @NoArgsConstructor 19 | @ToString(callSuper = true) 20 | @EqualsAndHashCode(callSuper = true) 21 | @JsonInclude(JsonInclude.Include.NON_NULL) 22 | @JsonIgnoreProperties(value = {"hibernateLazyInitializer", "handler", "fieldHandler"}, ignoreUnknown = true) 23 | public class ConfirmAccountTxnResponse extends ResolvableResponse { 24 | 25 | private static final long serialVersionUID = 5707898402009166406L; 26 | 27 | } 28 | -------------------------------------------------------------------------------- /rest-tcc-projects/rest-tcc-account-client/src/main/java/com/github/prontera/account/model/response/QueryAccountResponse.java: -------------------------------------------------------------------------------- 1 | package com.github.prontera.account.model.response; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 4 | import com.fasterxml.jackson.annotation.JsonInclude; 5 | import com.github.prontera.model.response.ResolvableResponse; 6 | import lombok.EqualsAndHashCode; 7 | import lombok.Getter; 8 | import lombok.NoArgsConstructor; 9 | import lombok.Setter; 10 | import lombok.ToString; 11 | 12 | import javax.validation.constraints.NotBlank; 13 | import javax.validation.constraints.NotNull; 14 | 15 | /** 16 | * @author Zhao Junjian 17 | * @date 2020/01/28 18 | */ 19 | @Getter 20 | @Setter 21 | @NoArgsConstructor 22 | @ToString(callSuper = true) 23 | @EqualsAndHashCode(callSuper = true) 24 | @JsonInclude(JsonInclude.Include.NON_NULL) 25 | @JsonIgnoreProperties(value = {"hibernateLazyInitializer", "handler", "fieldHandler"}, ignoreUnknown = true) 26 | public class QueryAccountResponse extends ResolvableResponse { 27 | 28 | private static final long serialVersionUID = -5689692298515757935L; 29 | 30 | private @NotNull Long id; 31 | 32 | private @NotBlank String name; 33 | 34 | } 35 | -------------------------------------------------------------------------------- /rest-tcc-projects/rest-tcc-account-client/src/main/java/com/github/prontera/account/model/response/QueryAccountTxnResponse.java: -------------------------------------------------------------------------------- 1 | package com.github.prontera.account.model.response; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 4 | import com.fasterxml.jackson.annotation.JsonInclude; 5 | import com.github.prontera.model.response.ResolvableResponse; 6 | import lombok.EqualsAndHashCode; 7 | import lombok.Getter; 8 | import lombok.NoArgsConstructor; 9 | import lombok.Setter; 10 | import lombok.ToString; 11 | 12 | import java.time.LocalDateTime; 13 | 14 | /** 15 | * @author Zhao Junjian 16 | * @date 2020/01/23 17 | */ 18 | @Getter 19 | @Setter 20 | @NoArgsConstructor 21 | @ToString(callSuper = true) 22 | @EqualsAndHashCode(callSuper = true) 23 | @JsonInclude(JsonInclude.Include.NON_NULL) 24 | @JsonIgnoreProperties(value = {"hibernateLazyInitializer", "handler", "fieldHandler"}, ignoreUnknown = true) 25 | public class QueryAccountTxnResponse extends ResolvableResponse { 26 | 27 | private static final long serialVersionUID = 6760667177684097240L; 28 | 29 | private Long userId; 30 | 31 | private Long orderId; 32 | 33 | private Long amount; 34 | 35 | private Integer state; 36 | 37 | private LocalDateTime createAt; 38 | 39 | private LocalDateTime expireAt; 40 | 41 | private LocalDateTime doneAt; 42 | 43 | } 44 | -------------------------------------------------------------------------------- /rest-tcc-projects/rest-tcc-account-client/src/main/java/com/github/prontera/account/model/response/SignUpResponse.java: -------------------------------------------------------------------------------- 1 | package com.github.prontera.account.model.response; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 4 | import com.fasterxml.jackson.annotation.JsonInclude; 5 | import com.github.prontera.model.response.ResolvableResponse; 6 | import lombok.EqualsAndHashCode; 7 | import lombok.Getter; 8 | import lombok.NoArgsConstructor; 9 | import lombok.Setter; 10 | import lombok.ToString; 11 | 12 | /** 13 | * @author Zhao Junjian 14 | * @date 2020/01/22 15 | */ 16 | @Getter 17 | @Setter 18 | @NoArgsConstructor 19 | @ToString(callSuper = true) 20 | @EqualsAndHashCode(callSuper = true) 21 | @JsonInclude(JsonInclude.Include.NON_NULL) 22 | @JsonIgnoreProperties(value = {"hibernateLazyInitializer", "handler", "fieldHandler"}, ignoreUnknown = true) 23 | public class SignUpResponse extends ResolvableResponse { 24 | 25 | private static final long serialVersionUID = -5689692298515757935L; 26 | 27 | } 28 | -------------------------------------------------------------------------------- /rest-tcc-projects/rest-tcc-account/.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 | 12 | ### IntelliJ IDEA ### 13 | .idea 14 | *.iws 15 | *.iml 16 | *.ipr 17 | 18 | ### NetBeans ### 19 | nbproject/private/ 20 | build/ 21 | nbbuild/ 22 | dist/ 23 | nbdist/ 24 | .nb-gradle/ -------------------------------------------------------------------------------- /rest-tcc-projects/rest-tcc-account/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | rest-tcc-account 7 | 0.0.1-SNAPSHOT 8 | jar 9 | 10 | rest-tcc-account 11 | account management, user registry center 12 | 13 | 14 | com.github.prontera 15 | rest-tcc-projects 16 | 0.0.1-SNAPSHOT 17 | 18 | 19 | 20 | src/main/java 21 | 2.10.1 22 | 23 | 24 | 25 | 26 | 27 | com.github.prontera 28 | rest-tcc-commons 29 | 0.0.1-SNAPSHOT 30 | 31 | 32 | com.github.prontera 33 | rest-tcc-account-client 34 | 0.0.1-SNAPSHOT 35 | 36 | 37 | 38 | org.springframework.cloud 39 | spring-cloud-starter-netflix-eureka-client 40 | 41 | 42 | 43 | com.fasterxml.jackson.core 44 | jackson-annotations 45 | ${jackson-annotations.version} 46 | 47 | 48 | 49 | org.springframework.boot 50 | spring-boot-starter-test 51 | test 52 | 53 | 54 | org.junit.vintage 55 | junit-vintage-engine 56 | 57 | 58 | org.springframework.boot 59 | spring-boot-starter-logging 60 | 61 | 62 | 63 | 64 | io.projectreactor 65 | reactor-test 66 | test 67 | 68 | 69 | 70 | 71 | 72 | 73 | org.springframework.boot 74 | spring-boot-maven-plugin 75 | 76 | 77 | 78 | repackage 79 | 80 | 81 | 82 | 83 | 84 | org.mybatis.generator 85 | mybatis-generator-maven-plugin 86 | 87 | 88 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /rest-tcc-projects/rest-tcc-account/src/main/java/com/github/prontera/AccountApplication.java: -------------------------------------------------------------------------------- 1 | package com.github.prontera; 2 | 3 | import com.github.prontera.annotation.MyBatisRepository; 4 | import org.mybatis.spring.annotation.MapperScan; 5 | import org.springframework.boot.SpringApplication; 6 | import org.springframework.boot.autoconfigure.SpringBootApplication; 7 | 8 | /** 9 | * @author Zhao Junjian 10 | * @date 2020/01/17 11 | */ 12 | @MapperScan(basePackages = "com.github.prontera.persistence", annotationClass = MyBatisRepository.class) 13 | @SpringBootApplication 14 | public class AccountApplication { 15 | 16 | public static void main(String[] args) { 17 | SpringApplication.run(AccountApplication.class, args); 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /rest-tcc-projects/rest-tcc-account/src/main/java/com/github/prontera/ContextStartedListener.java: -------------------------------------------------------------------------------- 1 | package com.github.prontera; 2 | 3 | import com.github.prontera.service.AccountService; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.boot.ApplicationArguments; 6 | import org.springframework.boot.ApplicationRunner; 7 | import org.springframework.context.annotation.Lazy; 8 | import org.springframework.stereotype.Component; 9 | 10 | import javax.annotation.Nonnull; 11 | import java.util.Objects; 12 | import java.util.TimeZone; 13 | 14 | /** 15 | * @author Zhao Junjian 16 | * @date 2020/01/23 17 | */ 18 | @Component 19 | public class ContextStartedListener implements ApplicationRunner { 20 | 21 | private final AccountService service; 22 | 23 | @Lazy 24 | @Autowired 25 | public ContextStartedListener(@Nonnull AccountService service) { 26 | this.service = Objects.requireNonNull(service); 27 | } 28 | 29 | @Override 30 | public void run(ApplicationArguments args) { 31 | TimeZone.setDefault(TimeZone.getTimeZone("Asia/Shanghai")); 32 | boostCp(); 33 | } 34 | 35 | protected void boostCp() { 36 | for (int i = 0; i < 7; i++) { 37 | service.findByUsername("chris"); 38 | } 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /rest-tcc-projects/rest-tcc-account/src/main/java/com/github/prontera/annotation/FaultBarrier.java: -------------------------------------------------------------------------------- 1 | package com.github.prontera.annotation; 2 | 3 | import java.lang.annotation.Documented; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | 7 | /** 8 | * @author Zhao Junjian 9 | * @date 2018/05/24 10 | */ 11 | @Retention(RetentionPolicy.RUNTIME) 12 | @Documented 13 | public @interface FaultBarrier { 14 | } 15 | -------------------------------------------------------------------------------- /rest-tcc-projects/rest-tcc-account/src/main/java/com/github/prontera/aspect/FaultBarrierAspect.java: -------------------------------------------------------------------------------- 1 | package com.github.prontera.aspect; 2 | 3 | import com.github.prontera.account.enums.StatusCode; 4 | import com.github.prontera.enums.NumericStatusCode; 5 | import com.github.prontera.exception.InvalidModelException; 6 | import com.github.prontera.exception.ResolvableStatusException; 7 | import com.github.prontera.model.response.ResolvableResponse; 8 | import com.github.prontera.util.Responses; 9 | import com.google.common.base.Strings; 10 | import org.apache.logging.log4j.LogManager; 11 | import org.apache.logging.log4j.Logger; 12 | import org.aspectj.lang.ProceedingJoinPoint; 13 | import org.aspectj.lang.Signature; 14 | import org.aspectj.lang.annotation.Around; 15 | import org.aspectj.lang.annotation.Aspect; 16 | import org.aspectj.lang.reflect.MethodSignature; 17 | import org.springframework.core.Ordered; 18 | 19 | /** 20 | * @author Zhao Junjian 21 | * @date 2020/01/17 22 | */ 23 | @Aspect 24 | public class FaultBarrierAspect implements Ordered { 25 | 26 | private static final Logger LOGGER = LogManager.getLogger(FaultBarrierAspect.class); 27 | 28 | private final int order; 29 | 30 | public FaultBarrierAspect(int order) { 31 | this.order = order; 32 | } 33 | 34 | @SuppressWarnings({"unchecked", "rawtypes"}) 35 | @Around(value = "within(com.github.prontera..*) && (@annotation(com.github.prontera.annotation.FaultBarrier))") 36 | public Object aroundMethod(ProceedingJoinPoint joinPoint) throws Throwable { 37 | Object proceed; 38 | final Signature signature = joinPoint.getSignature(); 39 | final Class returnType = ((MethodSignature) signature).getReturnType(); 40 | try { 41 | proceed = joinPoint.proceed(); 42 | } catch (Exception e) { 43 | if (!ResolvableResponse.class.isAssignableFrom(returnType)) { 44 | throw new IllegalStateException(e); 45 | } 46 | if (e instanceof ResolvableStatusException) { 47 | final NumericStatusCode statusCode = ((ResolvableStatusException) e).getStatusCode(); 48 | final String injectMessage = ((ResolvableStatusException) e).getInjectMessage(); 49 | final String message = Strings.isNullOrEmpty(injectMessage) ? statusCode.message() : injectMessage; 50 | proceed = Responses.generate(returnType, statusCode, message); 51 | } else if (e instanceof InvalidModelException) { 52 | proceed = Responses.generate(returnType, StatusCode.INVALID_MODEL_FIELDS, e.getMessage()); 53 | } else { 54 | LOGGER.error("UnknownServerException, method signature '{}', request params '{}'", signature, joinPoint.getArgs(), e); 55 | final StatusCode unknownException = StatusCode.SERVER_UNKNOWN_ERROR; 56 | proceed = Responses.generate(returnType, unknownException); 57 | } 58 | } 59 | return proceed; 60 | } 61 | 62 | @Override 63 | public int getOrder() { 64 | return order; 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /rest-tcc-projects/rest-tcc-account/src/main/java/com/github/prontera/config/AopConfig.java: -------------------------------------------------------------------------------- 1 | package com.github.prontera.config; 2 | 3 | import com.github.prontera.aspect.FaultBarrierAspect; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | 7 | /** 8 | * @author Zhao Junjian 9 | * @date 2020/01/17 10 | */ 11 | @Configuration 12 | public class AopConfig { 13 | 14 | @Bean 15 | public FaultBarrierAspect faultBarrier() { 16 | return new FaultBarrierAspect(Byte.MAX_VALUE); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /rest-tcc-projects/rest-tcc-account/src/main/java/com/github/prontera/config/BeanConfig.java: -------------------------------------------------------------------------------- 1 | package com.github.prontera.config; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | 5 | /** 6 | * @author Zhao Junjian 7 | * @date 2020/01/17 8 | */ 9 | @Configuration 10 | public class BeanConfig { 11 | 12 | } 13 | -------------------------------------------------------------------------------- /rest-tcc-projects/rest-tcc-account/src/main/java/com/github/prontera/config/SwaggerConfig.java: -------------------------------------------------------------------------------- 1 | package com.github.prontera.config; 2 | 3 | import com.fasterxml.classmate.TypeResolver; 4 | import com.google.common.base.Charsets; 5 | import com.google.common.collect.ImmutableList; 6 | import com.google.common.io.Files; 7 | import lombok.Builder; 8 | import lombok.EqualsAndHashCode; 9 | import lombok.Getter; 10 | import lombok.ToString; 11 | import org.reactivestreams.Publisher; 12 | import org.springframework.context.annotation.Bean; 13 | import org.springframework.context.annotation.Configuration; 14 | import org.springframework.context.annotation.Import; 15 | import org.springframework.core.io.ClassPathResource; 16 | import reactor.core.publisher.Flux; 17 | import reactor.core.publisher.Mono; 18 | import springfox.bean.validators.configuration.BeanValidatorPluginsConfiguration; 19 | import springfox.documentation.builders.RequestHandlerSelectors; 20 | import springfox.documentation.schema.AlternateTypeRules; 21 | import springfox.documentation.schema.WildcardType; 22 | import springfox.documentation.service.ApiInfo; 23 | import springfox.documentation.service.Contact; 24 | import springfox.documentation.spi.DocumentationType; 25 | import springfox.documentation.spring.web.plugins.Docket; 26 | import springfox.documentation.swagger.web.DocExpansion; 27 | import springfox.documentation.swagger.web.ModelRendering; 28 | import springfox.documentation.swagger.web.UiConfiguration; 29 | import springfox.documentation.swagger.web.UiConfigurationBuilder; 30 | import springfox.documentation.swagger2.annotations.EnableSwagger2WebFlux; 31 | 32 | import java.io.File; 33 | import java.io.IOException; 34 | import java.util.Collection; 35 | import java.util.List; 36 | 37 | /** 38 | * @author Zhao Junjian 39 | * @date 2020/01/17 40 | */ 41 | @Configuration 42 | @EnableSwagger2WebFlux 43 | @Import(BeanValidatorPluginsConfiguration.class) 44 | public class SwaggerConfig { 45 | 46 | private static final String DESCRIPTION; 47 | 48 | static { 49 | try { 50 | final ClassPathResource resource = new ClassPathResource("SWAGGER.md"); 51 | if (resource.exists()) { 52 | final List lines = Files.readLines(new File(resource.getPath()), Charsets.UTF_8); 53 | DESCRIPTION = String.join("\n", lines); 54 | } else { 55 | DESCRIPTION = null; 56 | } 57 | } catch (IOException e) { 58 | throw new IllegalArgumentException(e); 59 | } 60 | } 61 | 62 | public SwaggerApiInfo generateApiInfo() { 63 | return SwaggerApiInfo.builder().title("account-plane").version("2.0.0").serviceUrl(null).build(); 64 | } 65 | 66 | @Bean 67 | public Docket configure(TypeResolver typeResolver) { 68 | final SwaggerApiInfo info = generateApiInfo(); 69 | return new Docket(DocumentationType.SWAGGER_2) 70 | .genericModelSubstitutes(Mono.class, Flux.class, Publisher.class) 71 | .select() 72 | .apis(RequestHandlerSelectors.basePackage("com.github.prontera.http")) 73 | .build() 74 | .pathMapping("/") 75 | .useDefaultResponseMessages(false) 76 | .apiInfo(new ApiInfo(info.getTitle(), DESCRIPTION, info.getVersion(), info.getServiceUrl(), new Contact(null, null, null), null, null, ImmutableList.of())) 77 | .alternateTypeRules( 78 | AlternateTypeRules.newRule( 79 | typeResolver.resolve(Collection.class, WildcardType.class), 80 | typeResolver.resolve(List.class, WildcardType.class)) 81 | ) 82 | //.enableUrlTemplating(true) 83 | .forCodeGeneration(false); 84 | } 85 | 86 | @Bean 87 | public UiConfiguration uiConfig() { 88 | return UiConfigurationBuilder.builder() 89 | .defaultModelsExpandDepth(0) 90 | .defaultModelRendering(ModelRendering.MODEL) 91 | .docExpansion(DocExpansion.LIST) 92 | .displayOperationId(false) 93 | .build(); 94 | } 95 | 96 | @Getter 97 | @Builder 98 | @ToString(callSuper = true) 99 | @EqualsAndHashCode 100 | private static class SwaggerApiInfo { 101 | 102 | private final String title; 103 | 104 | private final String version; 105 | 106 | private final String serviceUrl; 107 | 108 | } 109 | 110 | } 111 | -------------------------------------------------------------------------------- /rest-tcc-projects/rest-tcc-account/src/main/java/com/github/prontera/domain/Account.java: -------------------------------------------------------------------------------- 1 | package com.github.prontera.domain; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 4 | import com.fasterxml.jackson.annotation.JsonInclude; 5 | import lombok.EqualsAndHashCode; 6 | import lombok.Getter; 7 | import lombok.NoArgsConstructor; 8 | import lombok.Setter; 9 | import lombok.ToString; 10 | 11 | import java.time.LocalDateTime; 12 | 13 | /** 14 | * @author Zhao Junjian 15 | * @date 2020/01/21 16 | */ 17 | @Getter 18 | @Setter 19 | @NoArgsConstructor 20 | @ToString 21 | @EqualsAndHashCode(callSuper = true) 22 | @JsonInclude(JsonInclude.Include.NON_NULL) 23 | @JsonIgnoreProperties(value = {"hibernateLazyInitializer", "handler", "fieldHandler"}, ignoreUnknown = true) 24 | public class Account extends IdenticalDomain { 25 | private static final long serialVersionUID = 8772201062569990864L; 26 | 27 | private Long id; 28 | 29 | private String name; 30 | 31 | private Long balance; 32 | 33 | private LocalDateTime createAt; 34 | 35 | private LocalDateTime updateAt; 36 | 37 | } 38 | -------------------------------------------------------------------------------- /rest-tcc-projects/rest-tcc-account/src/main/java/com/github/prontera/domain/AccountTransaction.java: -------------------------------------------------------------------------------- 1 | package com.github.prontera.domain; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 4 | import com.fasterxml.jackson.annotation.JsonInclude; 5 | import com.github.prontera.account.enums.ReservingState; 6 | import lombok.EqualsAndHashCode; 7 | import lombok.Getter; 8 | import lombok.NoArgsConstructor; 9 | import lombok.Setter; 10 | import lombok.ToString; 11 | 12 | import java.time.LocalDateTime; 13 | 14 | /** 15 | * @author Zhao Junjian 16 | * @date 2020/01/20 17 | */ 18 | @Getter 19 | @Setter 20 | @NoArgsConstructor 21 | @ToString 22 | @EqualsAndHashCode(callSuper = true) 23 | @JsonInclude(JsonInclude.Include.NON_NULL) 24 | @JsonIgnoreProperties(value = {"hibernateLazyInitializer", "handler", "fieldHandler"}, ignoreUnknown = true) 25 | public class AccountTransaction extends IdenticalDomain { 26 | 27 | private static final long serialVersionUID = 2141147095332403945L; 28 | 29 | private Long id; 30 | 31 | private Long userId; 32 | 33 | private Long orderId; 34 | 35 | private Long amount; 36 | 37 | private ReservingState state; 38 | 39 | private LocalDateTime createAt; 40 | 41 | private LocalDateTime updateAt; 42 | 43 | private LocalDateTime expireAt; 44 | 45 | private LocalDateTime doneAt; 46 | 47 | } 48 | -------------------------------------------------------------------------------- /rest-tcc-projects/rest-tcc-account/src/main/java/com/github/prontera/http/AccountController.java: -------------------------------------------------------------------------------- 1 | package com.github.prontera.http; 2 | 3 | import com.github.prontera.Shifts; 4 | import com.github.prontera.account.enums.StatusCode; 5 | import com.github.prontera.account.model.request.BalanceReservingRequest; 6 | import com.github.prontera.account.model.request.ConfirmAccountTxnRequest; 7 | import com.github.prontera.account.model.request.QueryAccountRequest; 8 | import com.github.prontera.account.model.request.QueryAccountTxnRequest; 9 | import com.github.prontera.account.model.response.BalanceReservingResponse; 10 | import com.github.prontera.account.model.response.ConfirmAccountTxnResponse; 11 | import com.github.prontera.account.model.response.QueryAccountResponse; 12 | import com.github.prontera.account.model.response.QueryAccountTxnResponse; 13 | import com.github.prontera.annotation.FaultBarrier; 14 | import com.github.prontera.domain.AccountTransaction; 15 | import com.github.prontera.service.AccountService; 16 | import com.github.prontera.util.HibernateValidators; 17 | import com.github.prontera.util.Responses; 18 | import io.swagger.annotations.Api; 19 | import io.swagger.annotations.ApiOperation; 20 | import org.springframework.beans.factory.annotation.Autowired; 21 | import org.springframework.context.annotation.Lazy; 22 | import org.springframework.http.MediaType; 23 | import org.springframework.web.bind.annotation.PostMapping; 24 | import org.springframework.web.bind.annotation.RequestBody; 25 | import org.springframework.web.bind.annotation.RequestMapping; 26 | import org.springframework.web.bind.annotation.RestController; 27 | 28 | import javax.annotation.Nonnull; 29 | import java.util.Objects; 30 | import java.util.Optional; 31 | 32 | /** 33 | * 下游不负责状态的自动轮转, 为简化Participant的职责, 只需要提高被动轮转的接口即可, 34 | * 例如本类中的{@link #queryTransaction(QueryAccountTxnRequest)}与{@link #confirmTransaction(ConfirmAccountTxnRequest)} 35 | * 都具备被动轮转的功能, 等待上游的tcc coordinator进行驱动即可 36 | * 37 | * @author Zhao Junjian 38 | * @date 2020/01/22 39 | */ 40 | @Api(tags = "Account-Debugger") 41 | @RestController 42 | @RequestMapping(value = "/", produces = {MediaType.APPLICATION_JSON_VALUE}, consumes = {MediaType.APPLICATION_JSON_VALUE}) 43 | public class AccountController { 44 | 45 | private final AccountService service; 46 | 47 | @Lazy 48 | @Autowired 49 | public AccountController(@Nonnull AccountService service) { 50 | this.service = Objects.requireNonNull(service); 51 | } 52 | 53 | @FaultBarrier 54 | @ApiOperation(value = "根据用户名查询信息", notes = "_") 55 | @PostMapping(value = "/query-by-username") 56 | public QueryAccountResponse queryByUsername(@Nonnull @RequestBody QueryAccountRequest request) { 57 | Objects.requireNonNull(request); 58 | HibernateValidators.throwsIfInvalid(request); 59 | return service.queryByUsername(request); 60 | } 61 | 62 | @FaultBarrier 63 | @ApiOperation(value = "预留资源, 锁定资金", notes = "_") 64 | @PostMapping(value = "/reserve-balance") 65 | public BalanceReservingResponse reserveBalance(@Nonnull @RequestBody BalanceReservingRequest request) { 66 | Objects.requireNonNull(request); 67 | HibernateValidators.throwsIfInvalid(request); 68 | return service.reserving(request); 69 | } 70 | 71 | @FaultBarrier 72 | @ApiOperation(value = "根据订单ID查询预留资源", notes = "如果发现预留资源过了保护期, 将自动归还资金, 具备fsm被动轮状的能力") 73 | @PostMapping(value = "/query-transaction") 74 | public QueryAccountTxnResponse queryTransaction(@Nonnull @RequestBody QueryAccountTxnRequest request) { 75 | Objects.requireNonNull(request); 76 | HibernateValidators.throwsIfInvalid(request); 77 | final Optional nullableTxn = service.cancellableFindTransaction(request.getOrderId()); 78 | if (!nullableTxn.isPresent()) { 79 | Shifts.fatal(StatusCode.ORDER_NOT_EXISTS); 80 | } 81 | final AccountTransaction transaction = nullableTxn.get(); 82 | final QueryAccountTxnResponse response = Responses.generate(QueryAccountTxnResponse.class, StatusCode.OK); 83 | response.setUserId(transaction.getUserId()); 84 | response.setOrderId(transaction.getOrderId()); 85 | response.setAmount(transaction.getAmount()); 86 | response.setCreateAt(transaction.getCreateAt()); 87 | response.setExpireAt(transaction.getExpireAt()); 88 | response.setDoneAt(transaction.getDoneAt()); 89 | response.setState(transaction.getState().val()); 90 | return response; 91 | } 92 | 93 | @FaultBarrier 94 | @ApiOperation(value = "根据订单ID确认预留资源", notes = "具备fsm被动轮转能力") 95 | @PostMapping(value = "/confirm-transaction") 96 | public ConfirmAccountTxnResponse confirmTransaction(@Nonnull @RequestBody ConfirmAccountTxnRequest request) { 97 | Objects.requireNonNull(request); 98 | HibernateValidators.throwsIfInvalid(request); 99 | return service.confirmTransaction(request, 0); 100 | } 101 | 102 | } 103 | -------------------------------------------------------------------------------- /rest-tcc-projects/rest-tcc-account/src/main/java/com/github/prontera/persistence/AccountMapper.java: -------------------------------------------------------------------------------- 1 | package com.github.prontera.persistence; 2 | 3 | import com.github.prontera.annotation.MyBatisRepository; 4 | import com.github.prontera.domain.Account; 5 | import org.apache.ibatis.annotations.Param; 6 | 7 | @MyBatisRepository 8 | public interface AccountMapper extends CrudMapper { 9 | 10 | Account selectByName(@Param("username") String username); 11 | 12 | int deductBalance(@Param("id") Long id, @Param("amount") Long amount); 13 | 14 | int increaseBalance(@Param("id") Long id, @Param("amount") Long amount); 15 | 16 | } 17 | -------------------------------------------------------------------------------- /rest-tcc-projects/rest-tcc-account/src/main/java/com/github/prontera/persistence/AccountTransactionMapper.java: -------------------------------------------------------------------------------- 1 | package com.github.prontera.persistence; 2 | 3 | import com.github.prontera.account.enums.ReservingState; 4 | import com.github.prontera.annotation.MyBatisRepository; 5 | import com.github.prontera.domain.AccountTransaction; 6 | import org.apache.ibatis.annotations.Param; 7 | 8 | @MyBatisRepository 9 | public interface AccountTransactionMapper extends CrudMapper { 10 | 11 | AccountTransaction selectByOrderId(@Param("orderId") long orderId); 12 | 13 | int compareAndSetState(@Param("id") long id, 14 | @Param("expected") ReservingState expected, 15 | @Param("updating") ReservingState updating); 16 | 17 | } 18 | -------------------------------------------------------------------------------- /rest-tcc-projects/rest-tcc-account/src/main/java/com/github/prontera/persistence/handler/ReservingStateHandler.java: -------------------------------------------------------------------------------- 1 | package com.github.prontera.persistence.handler; 2 | 3 | import com.github.prontera.account.enums.ReservingState; 4 | import com.github.prontera.persistence.GenericTypeHandler; 5 | 6 | /** 7 | * @author Zhao Junjian 8 | * @date 2020/01/20 9 | */ 10 | public class ReservingStateHandler extends GenericTypeHandler { 11 | 12 | @Override 13 | public int getEnumIntegerValue(ReservingState parameter) { 14 | return parameter.val(); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /rest-tcc-projects/rest-tcc-account/src/main/java/com/github/prontera/util/Responses.java: -------------------------------------------------------------------------------- 1 | package com.github.prontera.util; 2 | 3 | import com.github.prontera.account.enums.StatusCode; 4 | import com.github.prontera.enums.NumericStatusCode; 5 | import com.github.prontera.exception.ResolvableStatusException; 6 | import com.github.prontera.model.response.ResolvableResponse; 7 | import com.google.common.base.Strings; 8 | import org.objenesis.Objenesis; 9 | import org.objenesis.ObjenesisStd; 10 | import reactor.core.Exceptions; 11 | 12 | import javax.annotation.Nonnull; 13 | import java.util.Objects; 14 | import java.util.function.Supplier; 15 | 16 | /** 17 | * @author Zhao Junjian 18 | * @date 2020/01/20 19 | */ 20 | public final class Responses { 21 | 22 | private static final Objenesis OBJENESIS = new ObjenesisStd(true); 23 | 24 | private Responses() { 25 | } 26 | 27 | public static T generate(@Nonnull Class clazz, @Nonnull NumericStatusCode status) { 28 | return generate(clazz, status, status.message()); 29 | } 30 | 31 | public static T generate(@Nonnull Class clazz, @Nonnull NumericStatusCode status, @Nonnull Object message) { 32 | Objects.requireNonNull(clazz); 33 | Objects.requireNonNull(status); 34 | Objects.requireNonNull(message); 35 | T instance = OBJENESIS.newInstance(clazz); 36 | instance.setSuccessful(NumericStatusCode.isSuccessful(status)); 37 | instance.setCode(status.code()); 38 | instance.setMessage(message.toString()); 39 | return instance; 40 | } 41 | 42 | public static T generate(@Nonnull Class clazz, @Nonnull Throwable throwable) { 43 | Objects.requireNonNull(throwable); 44 | Objects.requireNonNull(clazz); 45 | final Throwable exception = Exceptions.unwrap(throwable); 46 | final Supplier supplier; 47 | if (exception instanceof ResolvableStatusException) { 48 | final NumericStatusCode statusCode = ((ResolvableStatusException) exception).getStatusCode(); 49 | final String injectMessage = ((ResolvableStatusException) exception).getInjectMessage(); 50 | if (Strings.isNullOrEmpty(injectMessage)) { 51 | supplier = () -> generate(clazz, statusCode); 52 | } else { 53 | supplier = () -> generate(clazz, statusCode, injectMessage); 54 | } 55 | } else { 56 | supplier = () -> generate(clazz, StatusCode.SERVER_UNKNOWN_ERROR); 57 | } 58 | return supplier.get(); 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /rest-tcc-projects/rest-tcc-account/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | # SERVER 2 | server.port=8285 3 | spring.application.name=account 4 | # DATASOURCE 5 | spring.datasource.username=chris 6 | spring.datasource.password=123123 7 | spring.datasource.url=jdbc:mysql://localhost:3306/account?useSSL=false&serverTimezone=Asia/Shanghai&connectTimeout=1200&socketTimeout=1200 8 | # MYBATIS 9 | mybatis.type-aliases-package=com.github.prontera.domain 10 | mybatis.type-handlers-package=com.github.prontera.persistence.handler 11 | mybatis.mapper-locations=classpath*:mapper/**/*Mapper.xml 12 | # SPRING CLOUD NETFLIX 13 | eureka.client.service-url.defaultZone=http://localhost:8255/eureka/ 14 | -------------------------------------------------------------------------------- /rest-tcc-projects/rest-tcc-account/src/main/resources/generatorConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 18 | 19 | 21 | 22 | 23 | 24 | 26 | 27 | 28 | 29 | 31 | 32 | 33 | 34 | 37 | 38 | 39 |
40 | 41 | 44 | 46 | 47 | 48 | 49 | 50 |
51 | 52 |
53 |
54 | -------------------------------------------------------------------------------- /rest-tcc-projects/rest-tcc-account/src/main/resources/log4j2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | %d{yyyy-MM-dd HH:mm:ss.SSS} %5p ${hostName} --- [%15.15t] %-40.40c{1.} : %m%n%ex 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /rest-tcc-projects/rest-tcc-account/src/main/resources/mapper/AccountMapper.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | id, name, balance, create_at, update_at 13 | 14 | 20 | 21 | delete 22 | from t_account 23 | where id = #{id,jdbcType=BIGINT} 24 | 25 | 26 | insert into t_account (id, name, balance, 27 | create_at, update_at) 28 | values (#{id,jdbcType=BIGINT}, #{name,jdbcType=VARCHAR}, #{balance,jdbcType=BIGINT}, 29 | #{createAt,jdbcType=TIMESTAMP}, #{updateAt,jdbcType=TIMESTAMP}) 30 | 31 | 32 | insert into t_account 33 | 34 | 35 | id, 36 | 37 | 38 | name, 39 | 40 | 41 | balance, 42 | 43 | 44 | create_at, 45 | 46 | 47 | update_at, 48 | 49 | 50 | 51 | 52 | #{id,jdbcType=BIGINT}, 53 | 54 | 55 | #{name,jdbcType=VARCHAR}, 56 | 57 | 58 | #{balance,jdbcType=BIGINT}, 59 | 60 | 61 | #{createAt,jdbcType=TIMESTAMP}, 62 | 63 | 64 | #{updateAt,jdbcType=TIMESTAMP}, 65 | 66 | 67 | 68 | 69 | update t_account 70 | 71 | 72 | name = #{name,jdbcType=VARCHAR}, 73 | 74 | 75 | balance = #{balance,jdbcType=BIGINT}, 76 | 77 | 78 | create_at = #{createAt,jdbcType=TIMESTAMP}, 79 | 80 | 81 | update_at = #{updateAt,jdbcType=TIMESTAMP}, 82 | 83 | 84 | where id = #{id,jdbcType=BIGINT} 85 | 86 | 87 | update t_account 88 | set name = #{name,jdbcType=VARCHAR}, 89 | balance = #{balance,jdbcType=BIGINT}, 90 | create_at = #{createAt,jdbcType=TIMESTAMP}, 91 | update_at = #{updateAt,jdbcType=TIMESTAMP} 92 | where id = #{id,jdbcType=BIGINT} 93 | 94 | 100 | 101 | update t_account 102 | set balance = balance - #{amount} 103 | where id = #{id} 104 | and balance - #{amount} >= 0 105 | 106 | 107 | update t_account 108 | set balance = balance + #{amount} 109 | where id = #{id} 110 | 111 | 112 | -------------------------------------------------------------------------------- /rest-tcc-projects/rest-tcc-account/src/main/resources/puml/participant-state.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | skinparam monochrome true 3 | scale 1 4 | title Account Transaction State Machine 5 | 6 | ' State 7 | state "TRYING[√]" as TRYING 8 | TRYING: + action: initial "state" 0000 9 | TRYING: + action: reduce corresponding col `balance` in t_account 10 | TRYING: + action: recording final field "amount" 11 | TRYING: + action: recording final field "expired_at" 12 | ''' 13 | state "CONFIRMED[√]" as CONFIRMED 14 | CONFIRMED: + action: update "state" to 0001 15 | CONFIRMED: + action: assign now() to "done_at" 16 | ''' 17 | state "CANCELLED[√]" as CANCELLED 18 | CANCELLED: + action: update "state" to 0010 19 | CANCELLED: + action: rollback `amount` to the col "balance" in t_account 20 | CANCELLED: + action: assign now() to "done_at" 21 | ' Transition 22 | [*] --> TRYING 23 | TRYING --> CONFIRMED: confirm \n [LocalDateTime.now() is before than expired_at] 24 | TRYING --> CANCELLED: cancel \n [LocalDateTime.now() is after than expired_at] 25 | CONFIRMED --> [*] 26 | CANCELLED --> [*] 27 | @enduml 28 | 29 | -------------------------------------------------------------------------------- /rest-tcc-projects/rest-tcc-order/.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 | 12 | ### IntelliJ IDEA ### 13 | .idea 14 | *.iws 15 | *.iml 16 | *.ipr 17 | 18 | ### NetBeans ### 19 | nbproject/private/ 20 | build/ 21 | nbbuild/ 22 | dist/ 23 | nbdist/ 24 | .nb-gradle/ -------------------------------------------------------------------------------- /rest-tcc-projects/rest-tcc-order/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | rest-tcc-order 7 | 0.0.1-SNAPSHOT 8 | jar 9 | 10 | rest-tcc-order 11 | order management 12 | 13 | 14 | com.github.prontera 15 | rest-tcc-projects 16 | 0.0.1-SNAPSHOT 17 | 18 | 19 | 20 | src/main/java 21 | 22 | 23 | 24 | 25 | 26 | com.github.prontera 27 | rest-tcc-commons 28 | 0.0.1-SNAPSHOT 29 | 30 | 31 | com.github.prontera 32 | rest-tcc-account-client 33 | 0.0.1-SNAPSHOT 34 | 35 | 36 | com.github.prontera 37 | rest-tcc-product-client 38 | 0.0.1-SNAPSHOT 39 | 40 | 41 | 42 | org.springframework.cloud 43 | spring-cloud-starter-netflix-eureka-client 44 | 45 | 46 | org.springframework.cloud 47 | spring-cloud-starter-netflix-hystrix 48 | 49 | 50 | 51 | org.springframework.boot 52 | spring-boot-starter-test 53 | test 54 | 55 | 56 | org.junit.vintage 57 | junit-vintage-engine 58 | 59 | 60 | org.springframework.boot 61 | spring-boot-starter-logging 62 | 63 | 64 | 65 | 66 | io.projectreactor 67 | reactor-test 68 | test 69 | 70 | 71 | 72 | 73 | ${project.artifactId}-${project.version} 74 | 75 | 76 | org.springframework.boot 77 | spring-boot-maven-plugin 78 | 79 | 80 | 81 | repackage 82 | 83 | 84 | 85 | 86 | 87 | org.mybatis.generator 88 | mybatis-generator-maven-plugin 89 | 90 | 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /rest-tcc-projects/rest-tcc-order/src/main/java/com/github/prontera/OrderApplication.java: -------------------------------------------------------------------------------- 1 | package com.github.prontera; 2 | 3 | import com.github.prontera.annotation.MyBatisRepository; 4 | import org.mybatis.spring.annotation.MapperScan; 5 | import org.springframework.boot.WebApplicationType; 6 | import org.springframework.boot.autoconfigure.SpringBootApplication; 7 | import org.springframework.boot.builder.SpringApplicationBuilder; 8 | import org.springframework.cloud.client.discovery.EnableDiscoveryClient; 9 | 10 | import java.util.TimeZone; 11 | 12 | /** 13 | * discovery client 14 | * 15 | * @author Zhao Junjian 16 | * @date 2020/01/17 17 | */ 18 | @EnableDiscoveryClient 19 | @SpringBootApplication 20 | @MapperScan(basePackages = "com.github.prontera.persistence", annotationClass = MyBatisRepository.class) 21 | public class OrderApplication { 22 | 23 | static { 24 | TimeZone.setDefault(TimeZone.getTimeZone("Asia/Shanghai")); 25 | } 26 | 27 | public static void main(String[] args) { 28 | new SpringApplicationBuilder(OrderApplication.class) 29 | .web(WebApplicationType.REACTIVE) 30 | .run(args); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /rest-tcc-projects/rest-tcc-order/src/main/java/com/github/prontera/annotation/FaultBarrier.java: -------------------------------------------------------------------------------- 1 | package com.github.prontera.annotation; 2 | 3 | import java.lang.annotation.Documented; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | 7 | /** 8 | * @author Zhao Junjian 9 | * @date 2018/05/24 10 | */ 11 | @Retention(RetentionPolicy.RUNTIME) 12 | @Documented 13 | public @interface FaultBarrier { 14 | } 15 | -------------------------------------------------------------------------------- /rest-tcc-projects/rest-tcc-order/src/main/java/com/github/prontera/aspect/FaultBarrierAspect.java: -------------------------------------------------------------------------------- 1 | package com.github.prontera.aspect; 2 | 3 | import com.github.prontera.enums.NumericStatusCode; 4 | import com.github.prontera.enums.StatusCode; 5 | import com.github.prontera.exception.InvalidModelException; 6 | import com.github.prontera.exception.ResolvableStatusException; 7 | import com.github.prontera.model.response.ResolvableResponse; 8 | import com.github.prontera.util.Responses; 9 | import com.google.common.base.Strings; 10 | import org.apache.logging.log4j.LogManager; 11 | import org.apache.logging.log4j.Logger; 12 | import org.aspectj.lang.ProceedingJoinPoint; 13 | import org.aspectj.lang.Signature; 14 | import org.aspectj.lang.annotation.Around; 15 | import org.aspectj.lang.annotation.Aspect; 16 | import org.aspectj.lang.reflect.MethodSignature; 17 | import org.springframework.core.Ordered; 18 | 19 | /** 20 | * @author Zhao Junjian 21 | * @date 2020/01/17 22 | */ 23 | @Aspect 24 | public class FaultBarrierAspect implements Ordered { 25 | 26 | private static final Logger LOGGER = LogManager.getLogger(FaultBarrierAspect.class); 27 | 28 | private final int order; 29 | 30 | public FaultBarrierAspect(int order) { 31 | this.order = order; 32 | } 33 | 34 | @SuppressWarnings({"unchecked", "rawtypes"}) 35 | @Around(value = "within(com.github.prontera..*) && (@annotation(com.github.prontera.annotation.FaultBarrier))") 36 | public Object aroundMethod(ProceedingJoinPoint joinPoint) throws Throwable { 37 | Object proceed; 38 | final Signature signature = joinPoint.getSignature(); 39 | final Class returnType = ((MethodSignature) signature).getReturnType(); 40 | try { 41 | proceed = joinPoint.proceed(); 42 | } catch (Exception e) { 43 | if (!ResolvableResponse.class.isAssignableFrom(returnType)) { 44 | throw new IllegalStateException(e); 45 | } 46 | if (e instanceof ResolvableStatusException) { 47 | final NumericStatusCode statusCode = ((ResolvableStatusException) e).getStatusCode(); 48 | final String injectMessage = ((ResolvableStatusException) e).getInjectMessage(); 49 | final String message = Strings.isNullOrEmpty(injectMessage) ? statusCode.message() : injectMessage; 50 | proceed = Responses.generate(returnType, statusCode, message); 51 | } else if (e instanceof InvalidModelException) { 52 | proceed = Responses.generate(returnType, StatusCode.INVALID_MODEL_FIELDS, e.getMessage()); 53 | } else { 54 | LOGGER.error("UnknownServerException, method signature '{}', request params '{}'", signature, joinPoint.getArgs(), e); 55 | final StatusCode unknownException = StatusCode.SERVER_UNKNOWN_ERROR; 56 | proceed = Responses.generate(returnType, unknownException); 57 | } 58 | } 59 | return proceed; 60 | } 61 | 62 | @Override 63 | public int getOrder() { 64 | return order; 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /rest-tcc-projects/rest-tcc-order/src/main/java/com/github/prontera/concurrent/Pools.java: -------------------------------------------------------------------------------- 1 | package com.github.prontera.concurrent; 2 | 3 | import com.google.common.collect.Queues; 4 | import com.google.common.util.concurrent.ThreadFactoryBuilder; 5 | 6 | import java.util.concurrent.ExecutorService; 7 | import java.util.concurrent.ScheduledExecutorService; 8 | import java.util.concurrent.ScheduledThreadPoolExecutor; 9 | import java.util.concurrent.ThreadPoolExecutor; 10 | import java.util.concurrent.TimeUnit; 11 | 12 | /** 13 | * @author Zhao Junjian 14 | * @date 2020/01/29 15 | */ 16 | public final class Pools { 17 | 18 | public static final ExecutorService COMPUTATION; 19 | 20 | public static final ExecutorService IO; 21 | 22 | public static final ScheduledExecutorService HEARTBEAT; 23 | 24 | static { 25 | final String name = "computation-pool"; 26 | final int processors = Runtime.getRuntime().availableProcessors(); 27 | COMPUTATION = new ThreadPoolExecutor(processors, processors, 28 | 1, TimeUnit.MINUTES, 29 | Queues.newLinkedBlockingQueue(4096), 30 | new ThreadFactoryBuilder().setNameFormat(name + "-%d").build(), 31 | new LoggableAbortPolicy(name)); 32 | } 33 | 34 | static { 35 | final String name = "io-bound-pool"; 36 | IO = new ThreadPoolExecutor(8, 16, 37 | 1, TimeUnit.MINUTES, 38 | Queues.newLinkedBlockingQueue(4096), 39 | new ThreadFactoryBuilder().setNameFormat(name + "-%d").build(), 40 | new LoggableAbortPolicy(name)); 41 | } 42 | 43 | static { 44 | final String name = "tcc-heartbeat"; 45 | HEARTBEAT = new ScheduledThreadPoolExecutor(1, 46 | new ThreadFactoryBuilder().setNameFormat(name + "-%d").build(), 47 | new LoggableAbortPolicy(name)); 48 | } 49 | 50 | private Pools() { 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /rest-tcc-projects/rest-tcc-order/src/main/java/com/github/prontera/config/AopConfig.java: -------------------------------------------------------------------------------- 1 | package com.github.prontera.config; 2 | 3 | import com.github.prontera.aspect.FaultBarrierAspect; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | 7 | /** 8 | * @author Zhao Junjian 9 | * @date 2020/01/17 10 | */ 11 | @Configuration 12 | public class AopConfig { 13 | @Bean 14 | public FaultBarrierAspect faultBarrier() { 15 | return new FaultBarrierAspect(Byte.MAX_VALUE); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /rest-tcc-projects/rest-tcc-order/src/main/java/com/github/prontera/config/BeanConfig.java: -------------------------------------------------------------------------------- 1 | package com.github.prontera.config; 2 | 3 | import org.springframework.cloud.client.loadbalancer.LoadBalanced; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.web.reactive.function.client.WebClient; 7 | 8 | /** 9 | * @author Zhao Junjian 10 | * @date 2020/01/17 11 | */ 12 | @Configuration 13 | public class BeanConfig { 14 | 15 | @Bean 16 | @LoadBalanced 17 | public WebClient.Builder loadBalancedWebClientBuilder() { 18 | return WebClient.builder(); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /rest-tcc-projects/rest-tcc-order/src/main/java/com/github/prontera/config/SwaggerConfig.java: -------------------------------------------------------------------------------- 1 | package com.github.prontera.config; 2 | 3 | import com.fasterxml.classmate.TypeResolver; 4 | import com.google.common.base.Charsets; 5 | import com.google.common.collect.ImmutableList; 6 | import com.google.common.io.Files; 7 | import lombok.Builder; 8 | import lombok.EqualsAndHashCode; 9 | import lombok.Getter; 10 | import lombok.ToString; 11 | import org.reactivestreams.Publisher; 12 | import org.springframework.context.annotation.Bean; 13 | import org.springframework.context.annotation.Configuration; 14 | import org.springframework.context.annotation.Import; 15 | import org.springframework.core.io.ClassPathResource; 16 | import reactor.core.publisher.Flux; 17 | import reactor.core.publisher.Mono; 18 | import springfox.bean.validators.configuration.BeanValidatorPluginsConfiguration; 19 | import springfox.documentation.builders.RequestHandlerSelectors; 20 | import springfox.documentation.schema.AlternateTypeRules; 21 | import springfox.documentation.schema.WildcardType; 22 | import springfox.documentation.service.ApiInfo; 23 | import springfox.documentation.service.Contact; 24 | import springfox.documentation.spi.DocumentationType; 25 | import springfox.documentation.spring.web.plugins.Docket; 26 | import springfox.documentation.swagger.web.DocExpansion; 27 | import springfox.documentation.swagger.web.ModelRendering; 28 | import springfox.documentation.swagger.web.UiConfiguration; 29 | import springfox.documentation.swagger.web.UiConfigurationBuilder; 30 | import springfox.documentation.swagger2.annotations.EnableSwagger2WebFlux; 31 | 32 | import java.io.File; 33 | import java.io.IOException; 34 | import java.util.Collection; 35 | import java.util.List; 36 | 37 | /** 38 | * @author Zhao Junjian 39 | * @date 2020/01/17 40 | */ 41 | @Configuration 42 | @EnableSwagger2WebFlux 43 | @Import(BeanValidatorPluginsConfiguration.class) 44 | public class SwaggerConfig { 45 | 46 | private static final String DESCRIPTION; 47 | 48 | static { 49 | try { 50 | final ClassPathResource resource = new ClassPathResource("SWAGGER.md"); 51 | if (resource.exists()) { 52 | final List lines = Files.readLines(new File(resource.getPath()), Charsets.UTF_8); 53 | DESCRIPTION = String.join("\n", lines); 54 | } else { 55 | DESCRIPTION = null; 56 | } 57 | } catch (IOException e) { 58 | throw new IllegalArgumentException(e); 59 | } 60 | } 61 | 62 | public SwaggerApiInfo generateApiInfo() { 63 | return SwaggerApiInfo.builder().title("order-plane").version("2.0.0").serviceUrl(null).build(); 64 | } 65 | 66 | @Bean 67 | public Docket configure(TypeResolver typeResolver) { 68 | final SwaggerApiInfo info = generateApiInfo(); 69 | return new Docket(DocumentationType.SWAGGER_2) 70 | .genericModelSubstitutes(Mono.class, Flux.class, Publisher.class) 71 | .select() 72 | .apis(RequestHandlerSelectors.basePackage("com.github.prontera.http")) 73 | .build() 74 | .pathMapping("/") 75 | .useDefaultResponseMessages(false) 76 | .apiInfo(new ApiInfo(info.getTitle(), DESCRIPTION, info.getVersion(), info.getServiceUrl(), new Contact(null, null, null), null, null, ImmutableList.of())) 77 | .alternateTypeRules( 78 | AlternateTypeRules.newRule( 79 | typeResolver.resolve(Collection.class, WildcardType.class), 80 | typeResolver.resolve(List.class, WildcardType.class)) 81 | ) 82 | //.enableUrlTemplating(true) 83 | .forCodeGeneration(false); 84 | } 85 | 86 | @Bean 87 | public UiConfiguration uiConfig() { 88 | return UiConfigurationBuilder.builder() 89 | .defaultModelsExpandDepth(0) 90 | .defaultModelRendering(ModelRendering.MODEL) 91 | .docExpansion(DocExpansion.LIST) 92 | .displayOperationId(false) 93 | .build(); 94 | } 95 | 96 | @Getter 97 | @Builder 98 | @ToString(callSuper = true) 99 | @EqualsAndHashCode 100 | private static class SwaggerApiInfo { 101 | 102 | private final String title; 103 | 104 | private final String version; 105 | 106 | private final String serviceUrl; 107 | 108 | } 109 | 110 | } 111 | -------------------------------------------------------------------------------- /rest-tcc-projects/rest-tcc-order/src/main/java/com/github/prontera/domain/Order.java: -------------------------------------------------------------------------------- 1 | package com.github.prontera.domain; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 4 | import com.fasterxml.jackson.annotation.JsonInclude; 5 | import com.github.prontera.annotation.marker.NonBehavior; 6 | import com.github.prontera.enums.OrderState; 7 | import lombok.EqualsAndHashCode; 8 | import lombok.Getter; 9 | import lombok.NoArgsConstructor; 10 | import lombok.Setter; 11 | import lombok.ToString; 12 | 13 | import java.time.LocalDateTime; 14 | 15 | /** 16 | * @author Zhao Junjian 17 | * @date 2020/01/20 18 | */ 19 | @Getter 20 | @Setter 21 | @NoArgsConstructor 22 | @ToString 23 | @EqualsAndHashCode(callSuper = true) 24 | @JsonInclude(JsonInclude.Include.NON_NULL) 25 | @JsonIgnoreProperties(value = {"hibernateLazyInitializer", "handler", "fieldHandler"}, ignoreUnknown = true) 26 | public class Order extends IdenticalDomain { 27 | 28 | private static final long serialVersionUID = -7328815311359399684L; 29 | 30 | private Long id; 31 | 32 | private Long userId; 33 | 34 | private Long productId; 35 | 36 | private Integer price; 37 | 38 | private Integer quantity; 39 | 40 | private OrderState state; 41 | 42 | private Long guid; 43 | 44 | private LocalDateTime createAt; 45 | 46 | private LocalDateTime updateAt; 47 | 48 | @NonBehavior 49 | private LocalDateTime expireAt; 50 | 51 | @NonBehavior 52 | private LocalDateTime doneAt; 53 | 54 | } 55 | -------------------------------------------------------------------------------- /rest-tcc-projects/rest-tcc-order/src/main/java/com/github/prontera/enums/OrderState.java: -------------------------------------------------------------------------------- 1 | package com.github.prontera.enums; 2 | 3 | import com.github.prontera.util.Capacity; 4 | 5 | import javax.annotation.Nonnull; 6 | import java.util.Collections; 7 | import java.util.HashMap; 8 | import java.util.Map; 9 | import java.util.NoSuchElementException; 10 | import java.util.Objects; 11 | 12 | /** 13 | * @author Zhao Junjian 14 | * @date 2020/01/18 15 | */ 16 | public enum OrderState { 17 | /** 18 | * invalid value 19 | */ 20 | INVALID(Integer.MAX_VALUE), 21 | /** 22 | * 处理中 23 | */ 24 | PENDING(0), 25 | /** 26 | * 交易成功, 依据fsm的设计可以一个虚状态出现, 即可直接删除存储介质上的数据 27 | */ 28 | CONFIRMED(1), 29 | /** 30 | * 交易超时 31 | */ 32 | CANCELLED(2), 33 | /** 34 | * 交易冲突, 事务未按预期完成 35 | */ 36 | CONFLICT(3) 37 | /** LINE SEPARATOR */ 38 | ; 39 | 40 | private static final Map CACHE; 41 | 42 | private static final DefaultParser PARSER; 43 | 44 | private static final DefaultParser GENTLE_PARSER; 45 | 46 | static { 47 | final int expectedSize = Capacity.toMapExpectedSize(values().length); 48 | final Map builder = new HashMap<>(expectedSize); 49 | for (OrderState element : values()) { 50 | builder.put(element.val(), element); 51 | } 52 | CACHE = Collections.unmodifiableMap(builder); 53 | PARSER = new DefaultParser(true); 54 | GENTLE_PARSER = new DefaultParser(false); 55 | } 56 | 57 | private final int val; 58 | 59 | OrderState(int val) { 60 | this.val = val; 61 | } 62 | 63 | public static OrderState parse(int val) { 64 | return PARSER.apply(val); 65 | } 66 | 67 | public static OrderState parseQuietly(int val) { 68 | return GENTLE_PARSER.apply(val); 69 | } 70 | 71 | public static OrderState parse(@Nonnull T val, @Nonnull EnumFunction function) { 72 | Objects.requireNonNull(val); 73 | Objects.requireNonNull(function); 74 | return function.apply(val); 75 | } 76 | 77 | public int val() { 78 | return val; 79 | } 80 | 81 | public boolean isIntermediateState() { 82 | return this == PENDING; 83 | } 84 | 85 | public boolean isFinalState() { 86 | return this == CONFIRMED || this == CANCELLED || this == CONFLICT; 87 | } 88 | 89 | private static final class DefaultParser implements EnumFunction { 90 | private final boolean throwsIfInvalid; 91 | 92 | private DefaultParser(boolean throwsIfInvalid) { 93 | this.throwsIfInvalid = throwsIfInvalid; 94 | } 95 | 96 | @Override 97 | public OrderState apply(@Nonnull Integer source) { 98 | Objects.requireNonNull(source); 99 | OrderState element = CACHE.get(source); 100 | if (element == null) { 101 | if (throwsIfInvalid) { 102 | throw new NoSuchElementException("No matching constant for [" + source + "]"); 103 | } else { 104 | element = INVALID; 105 | } 106 | } 107 | return element; 108 | } 109 | } 110 | 111 | } 112 | 113 | -------------------------------------------------------------------------------- /rest-tcc-projects/rest-tcc-order/src/main/java/com/github/prontera/enums/StatusCode.java: -------------------------------------------------------------------------------- 1 | package com.github.prontera.enums; 2 | 3 | import com.github.prontera.util.Capacity; 4 | 5 | import java.util.Collections; 6 | import java.util.HashMap; 7 | import java.util.Map; 8 | 9 | /** 10 | * @author Zhao Junjian 11 | * @date 2020/01/22 12 | */ 13 | public enum StatusCode implements NumericStatusCode { 14 | 15 | // 20xxx 客户端请求成功 16 | OK(20000, "请求成功"), 17 | 18 | /** 19 | * 接受请求, 将会异步处理 20 | */ 21 | ACCEPTED(20001, "请求已受理"), 22 | 23 | // 40xxx 客户端不合法的请求 24 | /** 25 | * 不被允许的访问 26 | */ 27 | BAD_REQUEST(40000, "访问被拒绝"), 28 | 29 | /** 30 | * 字段校验错误 31 | */ 32 | INVALID_MODEL_FIELDS(40001, "字段校验非法"), 33 | 34 | // 成功接收请求, 但是处理失败 35 | 36 | /** 37 | * 触发限流 38 | */ 39 | RATE_LIMITED(42000, "触发限流规则, 请确认当前操作所允许的QPS"), 40 | 41 | /** 42 | * Duplicate Key 43 | */ 44 | DUPLICATE_KEY(42001, "操作过快, 请稍后再试"), 45 | 46 | /** 47 | * 预留资源已取消 48 | */ 49 | CANCEL(42002, "预留资源已取消"), 50 | 51 | /** 52 | * 资源确认存在冲突 53 | */ 54 | CONFLICT(42003, "资源确认存在冲突"), 55 | 56 | /** 57 | * 用户不存在 58 | */ 59 | USER_NOT_EXISTS(42004, "用户名不存在"), 60 | 61 | /** 62 | * 产品不存在 63 | */ 64 | PRODUCT_NOT_EXISTS(42005, "产品不存在"), 65 | 66 | /** 67 | * 资源预留过期 68 | */ 69 | RESOURCE_CANCELLED(42006, "资源预留过期"), 70 | 71 | /** 72 | * 资源已经处于final state, 无法预留 73 | */ 74 | RESOURCE_CAN_NOT_BE_RESERVED(42007, "资源已经处于final state"), 75 | 76 | /** 77 | * 无该订单信息 78 | */ 79 | ORDER_NOT_EXISTS(42007, "下游无相关订单信息"), 80 | 81 | // 50xxx 服务端异常 82 | /** 83 | * 用于处理未知的服务端错误 84 | */ 85 | SERVER_UNKNOWN_ERROR(50001, "服务端异常, 请稍后再试"), 86 | 87 | /** 88 | * 用于远程调用时的系统出错 89 | */ 90 | SERVER_IS_BUSY_NOW(50002, "系统繁忙, 请稍后再试"), 91 | 92 | /** 93 | * 一般常见于DB连接抖动 94 | */ 95 | DB_LINK_FAILURE(50003, "DB链接失败, 请稍后再试"), 96 | 97 | /** 98 | * 未知的预留资源状态 99 | */ 100 | UNKNOWN_RESERVING_STATE(50004, "未知的预留资源状态, 请联系开发人员"), 101 | 102 | /** 103 | * 未知的RPC响应错误 104 | */ 105 | UNKNOWN_RPC_RESPONSE(50005, "下游rpc响应错误"), 106 | 107 | ; 108 | 109 | private static final Map CACHE; 110 | 111 | static { 112 | final int expectedSize = Capacity.toMapExpectedSize(values().length); 113 | final Map builder = new HashMap<>(expectedSize); 114 | for (StatusCode statusCode : values()) { 115 | builder.put(statusCode.code(), statusCode); 116 | } 117 | CACHE = Collections.unmodifiableMap(builder); 118 | } 119 | 120 | private final int code; 121 | 122 | private final String message; 123 | 124 | StatusCode(int code, String message) { 125 | this.code = code; 126 | this.message = message; 127 | } 128 | 129 | public static StatusCode of(int code) { 130 | final StatusCode status = CACHE.get(code); 131 | if (status == null) { 132 | throw new IllegalArgumentException("No matching constant for [" + code + "]"); 133 | } 134 | return status; 135 | } 136 | 137 | @Override 138 | public int code() { 139 | return code; 140 | } 141 | 142 | @Override 143 | public String message() { 144 | return message; 145 | } 146 | 147 | } 148 | -------------------------------------------------------------------------------- /rest-tcc-projects/rest-tcc-order/src/main/java/com/github/prontera/http/OrderController.java: -------------------------------------------------------------------------------- 1 | package com.github.prontera.http; 2 | 3 | import com.github.prontera.annotation.FaultBarrier; 4 | import com.github.prontera.concurrent.Pools; 5 | import com.github.prontera.model.request.CheckoutRequest; 6 | import com.github.prontera.model.request.DiagnoseRequest; 7 | import com.github.prontera.model.response.CheckoutResponse; 8 | import com.github.prontera.model.response.DiagnoseResponse; 9 | import com.github.prontera.service.OrderService; 10 | import com.github.prontera.util.HibernateValidators; 11 | import io.swagger.annotations.Api; 12 | import io.swagger.annotations.ApiOperation; 13 | import org.springframework.beans.factory.annotation.Autowired; 14 | import org.springframework.context.annotation.Lazy; 15 | import org.springframework.http.MediaType; 16 | import org.springframework.web.bind.annotation.PostMapping; 17 | import org.springframework.web.bind.annotation.RequestBody; 18 | import org.springframework.web.bind.annotation.RequestMapping; 19 | import org.springframework.web.bind.annotation.RestController; 20 | import reactor.core.publisher.Mono; 21 | import reactor.core.scheduler.Schedulers; 22 | 23 | import javax.annotation.Nonnull; 24 | import java.util.Objects; 25 | 26 | /** 27 | * @author Zhao Junjian 28 | * @date 2020/01/20 29 | */ 30 | @Api(tags = "Order-Debugger") 31 | @RestController 32 | @RequestMapping(value = "/orders", produces = {MediaType.APPLICATION_JSON_VALUE}, consumes = {MediaType.APPLICATION_JSON_VALUE}) 33 | public class OrderController { 34 | 35 | private final OrderService orderService; 36 | 37 | @Lazy 38 | @Autowired 39 | public OrderController(@Nonnull OrderService orderService) { 40 | this.orderService = Objects.requireNonNull(orderService); 41 | } 42 | 43 | @FaultBarrier 44 | @ApiOperation(value = "结账", notes = "_") 45 | @PostMapping(value = "/checkout") 46 | public Mono checkout(@Nonnull @RequestBody CheckoutRequest request) { 47 | Objects.requireNonNull(request); 48 | HibernateValidators.throwsIfInvalid(request); 49 | return Mono.fromFuture(() -> orderService.checkout(request)) 50 | .subscribeOn(Schedulers.fromExecutorService(Pools.IO)); 51 | } 52 | 53 | @FaultBarrier 54 | @ApiOperation(value = "诊断订单", notes = "_") 55 | @PostMapping(value = "/diagnose") 56 | public Mono diagnose(@Nonnull @RequestBody DiagnoseRequest request) { 57 | Objects.requireNonNull(request); 58 | HibernateValidators.throwsIfInvalid(request); 59 | return Mono.fromFuture(() -> orderService.diagnose(request)) 60 | .subscribeOn(Schedulers.fromExecutorService(Pools.IO)); 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /rest-tcc-projects/rest-tcc-order/src/main/java/com/github/prontera/model/request/CheckoutRequest.java: -------------------------------------------------------------------------------- 1 | package com.github.prontera.model.request; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 4 | import com.fasterxml.jackson.annotation.JsonInclude; 5 | import io.swagger.annotations.ApiModelProperty; 6 | import lombok.EqualsAndHashCode; 7 | import lombok.Getter; 8 | import lombok.NoArgsConstructor; 9 | import lombok.Setter; 10 | import lombok.ToString; 11 | 12 | import javax.validation.constraints.Min; 13 | import javax.validation.constraints.NotBlank; 14 | import javax.validation.constraints.NotNull; 15 | 16 | /** 17 | * @author Zhao Junjian 18 | * @date 2020/01/20 19 | */ 20 | @Getter 21 | @Setter 22 | @NoArgsConstructor 23 | @ToString 24 | @EqualsAndHashCode 25 | @JsonInclude(JsonInclude.Include.NON_NULL) 26 | @JsonIgnoreProperties(value = {"hibernateLazyInitializer", "handler", "fieldHandler"}, ignoreUnknown = true) 27 | public class CheckoutRequest { 28 | 29 | @ApiModelProperty(value = "GUID, 幂等操作标志", required = true, example = "1") 30 | private @NotNull Long guid; 31 | 32 | @ApiModelProperty(value = "产品名", required = true, example = "ps4") 33 | private @NotBlank String productName; 34 | 35 | @ApiModelProperty(value = "用户名", required = true, example = "chris") 36 | private @NotBlank String username; 37 | 38 | @ApiModelProperty(value = "扣减金额, 单位元", required = true, example = "47") 39 | private @NotNull @Min(1) Integer price; 40 | 41 | @ApiModelProperty(value = "扣减库存数", required = true, example = "1") 42 | private @NotNull @Min(1) Integer quantity; 43 | 44 | } 45 | -------------------------------------------------------------------------------- /rest-tcc-projects/rest-tcc-order/src/main/java/com/github/prontera/model/request/DiagnoseRequest.java: -------------------------------------------------------------------------------- 1 | package com.github.prontera.model.request; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 4 | import com.fasterxml.jackson.annotation.JsonInclude; 5 | import io.swagger.annotations.ApiModelProperty; 6 | import lombok.EqualsAndHashCode; 7 | import lombok.Getter; 8 | import lombok.NoArgsConstructor; 9 | import lombok.Setter; 10 | import lombok.ToString; 11 | 12 | import javax.validation.constraints.NotNull; 13 | 14 | /** 15 | * @author Zhao Junjian 16 | * @date 2020/02/02 17 | */ 18 | @Getter 19 | @Setter 20 | @NoArgsConstructor 21 | @ToString 22 | @EqualsAndHashCode 23 | @JsonInclude(JsonInclude.Include.NON_NULL) 24 | @JsonIgnoreProperties(value = {"hibernateLazyInitializer", "handler", "fieldHandler"}, ignoreUnknown = true) 25 | public class DiagnoseRequest { 26 | 27 | @ApiModelProperty(value = "guid", required = true, example = "1") 28 | private @NotNull Long guid; 29 | 30 | } 31 | -------------------------------------------------------------------------------- /rest-tcc-projects/rest-tcc-order/src/main/java/com/github/prontera/model/response/CheckoutResponse.java: -------------------------------------------------------------------------------- 1 | package com.github.prontera.model.response; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 4 | import com.fasterxml.jackson.annotation.JsonInclude; 5 | import lombok.EqualsAndHashCode; 6 | import lombok.Getter; 7 | import lombok.NoArgsConstructor; 8 | import lombok.Setter; 9 | import lombok.ToString; 10 | 11 | /** 12 | * @author Zhao Junjian 13 | * @date 2020/01/20 14 | */ 15 | @Getter 16 | @Setter 17 | @NoArgsConstructor 18 | @ToString 19 | @EqualsAndHashCode(callSuper = true) 20 | @JsonInclude(JsonInclude.Include.NON_NULL) 21 | @JsonIgnoreProperties(value = {"hibernateLazyInitializer", "handler", "fieldHandler"}, ignoreUnknown = true) 22 | public class CheckoutResponse extends ResolvableResponse { 23 | private static final long serialVersionUID = -835838789617897618L; 24 | } 25 | -------------------------------------------------------------------------------- /rest-tcc-projects/rest-tcc-order/src/main/java/com/github/prontera/model/response/DiagnoseResponse.java: -------------------------------------------------------------------------------- 1 | package com.github.prontera.model.response; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 4 | import com.fasterxml.jackson.annotation.JsonInclude; 5 | import lombok.EqualsAndHashCode; 6 | import lombok.Getter; 7 | import lombok.NoArgsConstructor; 8 | import lombok.Setter; 9 | import lombok.ToString; 10 | 11 | import java.util.Map; 12 | 13 | /** 14 | * @author Zhao Junjian 15 | * @date 2020/02/02 16 | */ 17 | @Getter 18 | @Setter 19 | @NoArgsConstructor 20 | @ToString 21 | @EqualsAndHashCode(callSuper = true) 22 | @JsonInclude(JsonInclude.Include.NON_NULL) 23 | @JsonIgnoreProperties(value = {"hibernateLazyInitializer", "handler", "fieldHandler"}, ignoreUnknown = true) 24 | public class DiagnoseResponse extends ResolvableResponse { 25 | 26 | private static final long serialVersionUID = 2832921602856989179L; 27 | 28 | private Map stateMap; 29 | 30 | } 31 | -------------------------------------------------------------------------------- /rest-tcc-projects/rest-tcc-order/src/main/java/com/github/prontera/persistence/OrderMapper.java: -------------------------------------------------------------------------------- 1 | package com.github.prontera.persistence; 2 | 3 | import com.github.prontera.annotation.MyBatisRepository; 4 | import com.github.prontera.domain.Order; 5 | import com.github.prontera.enums.OrderState; 6 | import org.apache.ibatis.annotations.Param; 7 | 8 | @MyBatisRepository 9 | public interface OrderMapper { 10 | 11 | int deleteByPrimaryKey(Long id); 12 | 13 | int insertSelective(Order record); 14 | 15 | Order selectByPrimaryKey(Long id); 16 | 17 | int updateByPrimaryKeySelective(Order record); 18 | 19 | Order selectByGuid(@Param("guid") Long guid); 20 | 21 | int compareAndSetState(@Param("id") Long id, 22 | @Param("expect") OrderState expect, 23 | @Param("updating") OrderState updating); 24 | 25 | } 26 | -------------------------------------------------------------------------------- /rest-tcc-projects/rest-tcc-order/src/main/java/com/github/prontera/persistence/handler/OrderStateHandler.java: -------------------------------------------------------------------------------- 1 | package com.github.prontera.persistence.handler; 2 | 3 | import com.github.prontera.enums.OrderState; 4 | import com.github.prontera.persistence.GenericTypeHandler; 5 | 6 | /** 7 | * @author Zhao Junjian 8 | * @date 2020/01/20 9 | */ 10 | public class OrderStateHandler extends GenericTypeHandler { 11 | 12 | @Override 13 | public int getEnumIntegerValue(OrderState parameter) { 14 | return parameter.val(); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /rest-tcc-projects/rest-tcc-order/src/main/java/com/github/prontera/util/Responses.java: -------------------------------------------------------------------------------- 1 | package com.github.prontera.util; 2 | 3 | import com.github.prontera.enums.NumericStatusCode; 4 | import com.github.prontera.enums.StatusCode; 5 | import com.github.prontera.exception.ResolvableStatusException; 6 | import com.github.prontera.model.response.ResolvableResponse; 7 | import com.google.common.base.Strings; 8 | import org.objenesis.Objenesis; 9 | import org.objenesis.ObjenesisStd; 10 | import reactor.core.Exceptions; 11 | 12 | import javax.annotation.Nonnull; 13 | import java.util.Objects; 14 | import java.util.function.Supplier; 15 | 16 | /** 17 | * @author Zhao Junjian 18 | * @date 2020/01/20 19 | */ 20 | public final class Responses { 21 | 22 | private static final Objenesis OBJENESIS = new ObjenesisStd(true); 23 | 24 | private Responses() { 25 | } 26 | 27 | public static T generate(@Nonnull Class clazz, @Nonnull NumericStatusCode status) { 28 | return generate(clazz, status, status.message()); 29 | } 30 | 31 | public static T generate(@Nonnull Class clazz, @Nonnull NumericStatusCode status, @Nonnull Object message) { 32 | Objects.requireNonNull(clazz); 33 | Objects.requireNonNull(status); 34 | Objects.requireNonNull(message); 35 | T instance = OBJENESIS.newInstance(clazz); 36 | instance.setSuccessful(NumericStatusCode.isSuccessful(status)); 37 | instance.setCode(status.code()); 38 | instance.setMessage(message.toString()); 39 | return instance; 40 | } 41 | 42 | public static T generate(@Nonnull Class clazz, @Nonnull Throwable throwable) { 43 | Objects.requireNonNull(throwable); 44 | Objects.requireNonNull(clazz); 45 | final Throwable exception = Exceptions.unwrap(throwable); 46 | final Supplier supplier; 47 | if (exception instanceof ResolvableStatusException) { 48 | final NumericStatusCode statusCode = ((ResolvableStatusException) exception).getStatusCode(); 49 | final String injectMessage = ((ResolvableStatusException) exception).getInjectMessage(); 50 | if (Strings.isNullOrEmpty(injectMessage)) { 51 | supplier = () -> generate(clazz, statusCode); 52 | } else { 53 | supplier = () -> generate(clazz, statusCode, injectMessage); 54 | } 55 | } else { 56 | supplier = () -> generate(clazz, StatusCode.SERVER_UNKNOWN_ERROR); 57 | } 58 | return supplier.get(); 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /rest-tcc-projects/rest-tcc-order/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | # SERVER 2 | server.port=8295 3 | spring.application.name=order 4 | # DATASOURCE 5 | spring.datasource.username=chris 6 | spring.datasource.password=123123 7 | spring.datasource.url=jdbc:mysql://localhost:3306/order?useSSL=false&serverTimezone=Asia/Shanghai&connectTimeout=1200&socketTimeout=1200 8 | # MYBATIS 9 | mybatis.type-aliases-package=com.github.prontera.domain 10 | mybatis.type-handlers-package=com.github.prontera.persistence.handler 11 | mybatis.mapper-locations=classpath*:mapper/**/*Mapper.xml 12 | # SPRING CLOUD NETFLIX 13 | eureka.client.service-url.defaultZone=http://localhost:8255/eureka/ 14 | -------------------------------------------------------------------------------- /rest-tcc-projects/rest-tcc-order/src/main/resources/generatorConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 18 | 19 | 21 | 22 | 23 | 24 | 26 | 27 | 28 | 29 | 31 | 32 | 33 | 34 | 37 | 39 | 40 | 41 | 42 | 43 |
44 | 45 |
46 |
47 | -------------------------------------------------------------------------------- /rest-tcc-projects/rest-tcc-order/src/main/resources/log4j2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | %d{yyyy-MM-dd HH:mm:ss.SSS} %5p ${hostName} --- [%15.15t] %-40.40c{1.} : %m%n%ex 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /rest-tcc-projects/rest-tcc-order/src/main/resources/puml/order-state.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | skinparam monochrome true 3 | scale 1 4 | title Order State Machine 5 | 6 | ' State 7 | state "PENDING[√]" as PENDING 8 | PENDING: + action: initial "state" 0000 9 | ''' 10 | state "CONFIRM[√]" as CONFIRM 11 | CONFIRM: + action: update "state" to 0001 12 | CONFIRM: + action: assign now() to "done_at" 13 | ''' 14 | state "CANCEL[√]" as CANCEL 15 | CANCEL: + action: update "state" to 0010 16 | CANCEL: + action: assign now() to "done_at" 17 | ''' 18 | state "CONFLICT[√]" as CONFLICT 19 | CONFLICT: + action: update "state" to 0011 20 | CONFLICT: + action: assign now() to "done_at" 21 | ' Transition 22 | [*] --> PENDING 23 | PENDING --> CONFIRM: confirm \n [all participants confirmed] 24 | PENDING --> CANCEL: cancel \n [LocalDateTime.now() is after than expire_at] 25 | PENDING --> CONFLICT: partial_confirm \n [confirmed a part of it] 26 | CONFIRM --> [*] 27 | CANCEL --> [*] 28 | CONFLICT --> [*] 29 | @enduml 30 | 31 | -------------------------------------------------------------------------------- /rest-tcc-projects/rest-tcc-order/src/test/java/com/github/prontera/DatetimeTest.java: -------------------------------------------------------------------------------- 1 | package com.github.prontera; 2 | 3 | import org.junit.jupiter.api.Assertions; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import java.time.LocalDateTime; 7 | import java.time.ZoneId; 8 | import java.time.temporal.ChronoUnit; 9 | import java.util.TimeZone; 10 | 11 | /** 12 | * @author Zhao Junjian 13 | * @date 2020/01/21 14 | */ 15 | public class DatetimeTest { 16 | 17 | /** 18 | * 可以看出使用static构造器of()函数的时候, 改变zone不会影响输出 19 | */ 20 | @Test 21 | public void testHowTimeZoneSettingInfluencePrintOut_whenUsingOf() { 22 | final LocalDateTime localDateTime = LocalDateTime.of(2020, 1, 20, 19, 0); 23 | System.out.println(localDateTime); 24 | System.out.println(localDateTime.atZone(ZoneId.of("UTC"))); 25 | System.out.println(localDateTime.atZone(ZoneId.of("Asia/Shanghai"))); 26 | TimeZone.setDefault(TimeZone.getTimeZone("UTC")); 27 | System.out.println(localDateTime); 28 | } 29 | 30 | /** 31 | * 有如下结论, {@link LocalDateTime}只会在初始化的时候指定zone, 后续操作无论如何改变zone, 都不会影响输出, zone仅仅是一个标记 32 | */ 33 | @Test 34 | public void testHowTimeZoneSettingInfluencePrintOut_whenUsingNow() { 35 | final LocalDateTime localDateTime = LocalDateTime.now(); 36 | System.out.println(localDateTime); 37 | System.out.println(localDateTime.atZone(ZoneId.of("UTC"))); 38 | System.out.println(localDateTime.atZone(ZoneId.of("Asia/Shanghai"))); 39 | TimeZone.setDefault(TimeZone.getTimeZone("UTC")); 40 | System.out.println(localDateTime); 41 | } 42 | 43 | @Test 44 | public void testTimeBetween() { 45 | Assertions.assertEquals(1, ChronoUnit.SECONDS.between(LocalDateTime.now(), LocalDateTime.now().plusSeconds(1))); 46 | Assertions.assertEquals(-1, ChronoUnit.SECONDS.between(LocalDateTime.now().plusSeconds(1), LocalDateTime.now())); 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /rest-tcc-projects/rest-tcc-product-client/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | com.github.prontera 7 | rest-tcc-projects 8 | 0.0.1-SNAPSHOT 9 | 10 | 4.0.0 11 | 12 | rest-tcc-product-client 13 | 14 | 15 | 0.0.1-SNAPSHOT 16 | 17 | 18 | 19 | 20 | com.github.prontera 21 | rest-tcc-contract 22 | ${rest-tcc-contract.version} 23 | 24 | 25 | com.fasterxml.jackson.core 26 | jackson-annotations 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /rest-tcc-projects/rest-tcc-product-client/src/main/java/com/github/prontera/product/enums/ReservingState.java: -------------------------------------------------------------------------------- 1 | package com.github.prontera.product.enums; 2 | 3 | import com.github.prontera.enums.EnumFunction; 4 | import com.github.prontera.util.Capacity; 5 | 6 | import javax.annotation.Nonnull; 7 | import java.util.Collections; 8 | import java.util.HashMap; 9 | import java.util.Map; 10 | import java.util.NoSuchElementException; 11 | import java.util.Objects; 12 | 13 | /** 14 | * @author Zhao Junjian 15 | * @date 2020/01/20 16 | */ 17 | public enum ReservingState { 18 | /** 19 | * invalid value 20 | */ 21 | INVALID(Integer.MAX_VALUE), 22 | /** 23 | * 处理中 - intermediate state 24 | */ 25 | TRYING(0), 26 | /** 27 | * 交易成功, 依据fsm的设计可以一个虚状态出现, 即可直接删除存储介质上的数据 - final state 28 | */ 29 | CONFIRMED(1), 30 | /** 31 | * 交易超时 - final state 32 | */ 33 | CANCELLED(2), 34 | /** LINE SEPARATOR */ 35 | ; 36 | 37 | private static final Map CACHE; 38 | 39 | private static final DefaultParser PARSER; 40 | 41 | private static final DefaultParser GENTLE_PARSER; 42 | 43 | static { 44 | final int expectedSize = Capacity.toMapExpectedSize(values().length); 45 | final Map builder = new HashMap<>(expectedSize); 46 | for (ReservingState element : values()) { 47 | builder.put(element.val(), element); 48 | } 49 | CACHE = Collections.unmodifiableMap(builder); 50 | PARSER = new DefaultParser(true); 51 | GENTLE_PARSER = new DefaultParser(false); 52 | } 53 | 54 | private final int val; 55 | 56 | ReservingState(int val) { 57 | this.val = val; 58 | } 59 | 60 | public static ReservingState parse(int val) { 61 | return PARSER.apply(val); 62 | } 63 | 64 | public static ReservingState parseQuietly(int val) { 65 | return GENTLE_PARSER.apply(val); 66 | } 67 | 68 | public static ReservingState parse(@Nonnull T val, @Nonnull EnumFunction function) { 69 | Objects.requireNonNull(val); 70 | Objects.requireNonNull(function); 71 | return function.apply(val); 72 | } 73 | 74 | public int val() { 75 | return val; 76 | } 77 | 78 | private static final class DefaultParser implements EnumFunction { 79 | private final boolean throwsIfInvalid; 80 | 81 | private DefaultParser(boolean throwsIfInvalid) { 82 | this.throwsIfInvalid = throwsIfInvalid; 83 | } 84 | 85 | @Override 86 | public ReservingState apply(@Nonnull Integer source) { 87 | Objects.requireNonNull(source); 88 | ReservingState element = CACHE.get(source); 89 | if (element == null) { 90 | if (throwsIfInvalid) { 91 | throw new NoSuchElementException("No matching constant for [" + source + "]"); 92 | } else { 93 | element = INVALID; 94 | } 95 | } 96 | return element; 97 | } 98 | } 99 | 100 | } 101 | 102 | -------------------------------------------------------------------------------- /rest-tcc-projects/rest-tcc-product-client/src/main/java/com/github/prontera/product/enums/StatusCode.java: -------------------------------------------------------------------------------- 1 | package com.github.prontera.product.enums; 2 | 3 | import com.github.prontera.enums.NumericStatusCode; 4 | import com.github.prontera.util.Capacity; 5 | 6 | import java.util.Collections; 7 | import java.util.HashMap; 8 | import java.util.Map; 9 | 10 | /** 11 | * @author Zhao Junjian 12 | */ 13 | public enum StatusCode implements NumericStatusCode { 14 | 15 | // 20xxx 客户端请求成功 16 | OK(20000, "请求成功"), 17 | 18 | /** 19 | * 接受请求, 将会异步处理 20 | */ 21 | ACCEPTED(20001, "请求已受理"), 22 | 23 | /** 24 | * 重复对同一guid进行预留操作, 已保证幂等性 25 | */ 26 | IDEMPOTENT_RESERVING(20002, "幂等预留"), 27 | 28 | // 40xxx 客户端不合法的请求 29 | /** 30 | * 不被允许的访问 31 | */ 32 | BAD_REQUEST(40000, "访问被拒绝"), 33 | 34 | /** 35 | * 字段校验错误 36 | */ 37 | INVALID_MODEL_FIELDS(40001, "字段校验非法"), 38 | 39 | /** 40 | * 产品不存在 41 | */ 42 | PRODUCT_NOT_EXISTS(40003, "产品不存在"), 43 | 44 | /** 45 | * 无该订单信息 46 | */ 47 | ORDER_NOT_EXISTS(40004, "无相关订单信息"), 48 | 49 | // 成功接收请求, 但是处理失败 50 | 51 | /** 52 | * 触发限流 53 | */ 54 | RATE_LIMITED(42000, "触发限流规则, 请确认当前操作所允许的QPS"), 55 | 56 | /** 57 | * Duplicate Key 58 | */ 59 | DUPLICATE_KEY(42001, "操作过快, 请稍后再试"), 60 | 61 | /** 62 | * 库存不足 63 | */ 64 | INSUFFICIENT_INVENTORY(42002, "库存不足"), 65 | 66 | /** 67 | * 超时并取消预留资源 68 | */ 69 | TIMEOUT_AND_CANCELLED(42003, "超时并取消预留资源"), 70 | 71 | /** 72 | * {@link ReservingState}已经处于final state 73 | */ 74 | RESERVING_FINAL_STATE(42004, "资源已处于final state"), 75 | 76 | // 50xxx 服务端异常 77 | /** 78 | * 用于处理未知的服务端错误 79 | */ 80 | SERVER_UNKNOWN_ERROR(50001, "服务端异常, 请稍后再试"), 81 | 82 | /** 83 | * 用于远程调用时的系统出错 84 | */ 85 | SERVER_IS_BUSY_NOW(50002, "系统繁忙, 请稍后再试"), 86 | 87 | /** 88 | * 一般常见于DB连接抖动 89 | */ 90 | DB_LINK_FAILURE(50003, "DB链接失败, 请稍后再试"), 91 | 92 | /** 93 | * {@link ReservingState#INVALID} 94 | */ 95 | UNKNOWN_RESERVING_STATE(50004, "未知的预留资源状态"), 96 | 97 | /** 98 | * 当预留资源过期后, 回滚库存失败, 可由于CP异常导致 99 | */ 100 | ACCOUNT_ROLLBACK_FAILURE(50006, "库存增加失败, 已回滚事务"), 101 | 102 | /** 103 | * cas确认资源失败 104 | */ 105 | FAIL_TO_CONFIRM(50007, "确认资源失败"), 106 | 107 | ; 108 | 109 | private static final Map CACHE; 110 | 111 | static { 112 | final int expectedSize = Capacity.toMapExpectedSize(values().length); 113 | final Map builder = new HashMap<>(expectedSize); 114 | for (StatusCode statusCode : values()) { 115 | builder.put(statusCode.code(), statusCode); 116 | } 117 | CACHE = Collections.unmodifiableMap(builder); 118 | } 119 | 120 | private final int code; 121 | 122 | private final String message; 123 | 124 | StatusCode(int code, String message) { 125 | this.code = code; 126 | this.message = message; 127 | } 128 | 129 | public static StatusCode of(int code) { 130 | final StatusCode status = CACHE.get(code); 131 | if (status == null) { 132 | throw new IllegalArgumentException("No matching constant for [" + code + "]"); 133 | } 134 | return status; 135 | } 136 | 137 | @Override 138 | public int code() { 139 | return code; 140 | } 141 | 142 | @Override 143 | public String message() { 144 | return message; 145 | } 146 | 147 | } 148 | -------------------------------------------------------------------------------- /rest-tcc-projects/rest-tcc-product-client/src/main/java/com/github/prontera/product/model/request/AddProductRequest.java: -------------------------------------------------------------------------------- 1 | package com.github.prontera.product.model.request; 2 | 3 | import lombok.Getter; 4 | import lombok.NoArgsConstructor; 5 | import lombok.Setter; 6 | import lombok.ToString; 7 | 8 | /** 9 | * @author Zhao Junjian 10 | * @date 2020/01/25 11 | */ 12 | @Getter 13 | @Setter 14 | @NoArgsConstructor 15 | @ToString 16 | public class AddProductRequest { 17 | 18 | private String name; 19 | 20 | } 21 | -------------------------------------------------------------------------------- /rest-tcc-projects/rest-tcc-product-client/src/main/java/com/github/prontera/product/model/request/ConfirmProductTxnRequest.java: -------------------------------------------------------------------------------- 1 | package com.github.prontera.product.model.request; 2 | 3 | import io.swagger.annotations.ApiModelProperty; 4 | import lombok.EqualsAndHashCode; 5 | import lombok.Getter; 6 | import lombok.NoArgsConstructor; 7 | import lombok.Setter; 8 | import lombok.ToString; 9 | 10 | import javax.validation.constraints.NotNull; 11 | 12 | /** 13 | * @author Zhao Junjian 14 | * @date 2020/01/25 15 | */ 16 | @Getter 17 | @Setter 18 | @NoArgsConstructor 19 | @ToString 20 | @EqualsAndHashCode 21 | public class ConfirmProductTxnRequest { 22 | 23 | @ApiModelProperty(value = "订单ID", required = true, example = "1") 24 | private @NotNull Long orderId; 25 | 26 | } 27 | -------------------------------------------------------------------------------- /rest-tcc-projects/rest-tcc-product-client/src/main/java/com/github/prontera/product/model/request/InventoryReservingRequest.java: -------------------------------------------------------------------------------- 1 | package com.github.prontera.product.model.request; 2 | 3 | import io.swagger.annotations.ApiModelProperty; 4 | import lombok.EqualsAndHashCode; 5 | import lombok.Getter; 6 | import lombok.NoArgsConstructor; 7 | import lombok.Setter; 8 | import lombok.ToString; 9 | 10 | import javax.validation.constraints.Max; 11 | import javax.validation.constraints.Min; 12 | import javax.validation.constraints.NotBlank; 13 | import javax.validation.constraints.NotNull; 14 | 15 | /** 16 | * @author Zhao Junjian 17 | * @date 2020/01/25 18 | */ 19 | @Getter 20 | @Setter 21 | @NoArgsConstructor 22 | @ToString 23 | @EqualsAndHashCode 24 | public class InventoryReservingRequest { 25 | 26 | @ApiModelProperty(value = "订单ID", required = true, example = "1") 27 | private @NotNull Long orderId; 28 | 29 | @ApiModelProperty(value = "产品名", required = true, example = "ps4") 30 | private @NotBlank String productName; 31 | 32 | @ApiModelProperty(value = "扣减金额, 单位元", required = true, example = "2") 33 | private @NotNull @Min(1) Integer amount; 34 | 35 | @ApiModelProperty(value = "期望的预留资源时间", required = true, example = "7") 36 | private @NotNull @Min(1) @Max(900) Integer expectedReservingSeconds; 37 | 38 | } 39 | -------------------------------------------------------------------------------- /rest-tcc-projects/rest-tcc-product-client/src/main/java/com/github/prontera/product/model/request/QueryProductRequest.java: -------------------------------------------------------------------------------- 1 | package com.github.prontera.product.model.request; 2 | 3 | import io.swagger.annotations.ApiModelProperty; 4 | import lombok.EqualsAndHashCode; 5 | import lombok.Getter; 6 | import lombok.NoArgsConstructor; 7 | import lombok.Setter; 8 | import lombok.ToString; 9 | 10 | import javax.validation.constraints.NotBlank; 11 | 12 | /** 13 | * @author Zhao Junjian 14 | * @date 2020/01/29 15 | */ 16 | @Getter 17 | @Setter 18 | @NoArgsConstructor 19 | @ToString 20 | @EqualsAndHashCode 21 | public class QueryProductRequest { 22 | 23 | @ApiModelProperty(value = "产品名", required = true, example = "ps4") 24 | private @NotBlank String productName; 25 | 26 | } 27 | -------------------------------------------------------------------------------- /rest-tcc-projects/rest-tcc-product-client/src/main/java/com/github/prontera/product/model/request/QueryProductTxnRequest.java: -------------------------------------------------------------------------------- 1 | package com.github.prontera.product.model.request; 2 | 3 | import io.swagger.annotations.ApiModelProperty; 4 | import lombok.EqualsAndHashCode; 5 | import lombok.Getter; 6 | import lombok.NoArgsConstructor; 7 | import lombok.Setter; 8 | import lombok.ToString; 9 | 10 | import javax.validation.constraints.NotNull; 11 | 12 | /** 13 | * @author Zhao Junjian 14 | * @date 2020/01/25 15 | */ 16 | @Getter 17 | @Setter 18 | @NoArgsConstructor 19 | @ToString 20 | @EqualsAndHashCode 21 | public class QueryProductTxnRequest { 22 | 23 | @ApiModelProperty(value = "订单ID", required = true, example = "1") 24 | private @NotNull Long orderId; 25 | 26 | } 27 | -------------------------------------------------------------------------------- /rest-tcc-projects/rest-tcc-product-client/src/main/java/com/github/prontera/product/model/response/AddProductResponse.java: -------------------------------------------------------------------------------- 1 | package com.github.prontera.product.model.response; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 4 | import com.fasterxml.jackson.annotation.JsonInclude; 5 | import com.github.prontera.model.response.ResolvableResponse; 6 | import lombok.EqualsAndHashCode; 7 | import lombok.Getter; 8 | import lombok.NoArgsConstructor; 9 | import lombok.Setter; 10 | import lombok.ToString; 11 | 12 | /** 13 | * @author Zhao Junjian 14 | * @date 2020/01/25 15 | */ 16 | @Getter 17 | @Setter 18 | @NoArgsConstructor 19 | @ToString(callSuper = true) 20 | @EqualsAndHashCode(callSuper = true) 21 | @JsonInclude(JsonInclude.Include.NON_NULL) 22 | @JsonIgnoreProperties(value = {"hibernateLazyInitializer", "handler", "fieldHandler"}, ignoreUnknown = true) 23 | public class AddProductResponse extends ResolvableResponse { 24 | 25 | private static final long serialVersionUID = -6519714637430520843L; 26 | 27 | } 28 | -------------------------------------------------------------------------------- /rest-tcc-projects/rest-tcc-product-client/src/main/java/com/github/prontera/product/model/response/ConfirmProductTxnResponse.java: -------------------------------------------------------------------------------- 1 | package com.github.prontera.product.model.response; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 4 | import com.fasterxml.jackson.annotation.JsonInclude; 5 | import com.github.prontera.model.response.ResolvableResponse; 6 | import lombok.EqualsAndHashCode; 7 | import lombok.Getter; 8 | import lombok.NoArgsConstructor; 9 | import lombok.Setter; 10 | import lombok.ToString; 11 | 12 | /** 13 | * @author Zhao Junjian 14 | * @date 2020/01/25 15 | */ 16 | @Getter 17 | @Setter 18 | @NoArgsConstructor 19 | @ToString(callSuper = true) 20 | @EqualsAndHashCode(callSuper = true) 21 | @JsonInclude(JsonInclude.Include.NON_NULL) 22 | @JsonIgnoreProperties(value = {"hibernateLazyInitializer", "handler", "fieldHandler"}, ignoreUnknown = true) 23 | public class ConfirmProductTxnResponse extends ResolvableResponse { 24 | 25 | private static final long serialVersionUID = 8044407019588824862L; 26 | 27 | } 28 | -------------------------------------------------------------------------------- /rest-tcc-projects/rest-tcc-product-client/src/main/java/com/github/prontera/product/model/response/InventoryReservingResponse.java: -------------------------------------------------------------------------------- 1 | package com.github.prontera.product.model.response; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 4 | import com.fasterxml.jackson.annotation.JsonInclude; 5 | import com.github.prontera.model.response.ResolvableResponse; 6 | import lombok.EqualsAndHashCode; 7 | import lombok.Getter; 8 | import lombok.NoArgsConstructor; 9 | import lombok.Setter; 10 | import lombok.ToString; 11 | 12 | import javax.validation.constraints.Min; 13 | import javax.validation.constraints.NotNull; 14 | 15 | /** 16 | * @author Zhao Junjian 17 | * @date 2020/01/25 18 | */ 19 | @Getter 20 | @Setter 21 | @NoArgsConstructor 22 | @ToString(callSuper = true) 23 | @EqualsAndHashCode(callSuper = true) 24 | @JsonInclude(JsonInclude.Include.NON_NULL) 25 | @JsonIgnoreProperties(value = {"hibernateLazyInitializer", "handler", "fieldHandler"}, ignoreUnknown = true) 26 | public class InventoryReservingResponse extends ResolvableResponse { 27 | 28 | private static final long serialVersionUID = 7824624522752331589L; 29 | 30 | private @NotNull @Min(1) Long reservingSeconds; 31 | 32 | } 33 | -------------------------------------------------------------------------------- /rest-tcc-projects/rest-tcc-product-client/src/main/java/com/github/prontera/product/model/response/QueryProductResponse.java: -------------------------------------------------------------------------------- 1 | package com.github.prontera.product.model.response; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 4 | import com.fasterxml.jackson.annotation.JsonInclude; 5 | import com.github.prontera.model.response.ResolvableResponse; 6 | import lombok.EqualsAndHashCode; 7 | import lombok.Getter; 8 | import lombok.NoArgsConstructor; 9 | import lombok.Setter; 10 | import lombok.ToString; 11 | 12 | import javax.validation.constraints.NotBlank; 13 | import javax.validation.constraints.NotNull; 14 | 15 | /** 16 | * @author Zhao Junjian 17 | * @date 2020/01/29 18 | */ 19 | @Getter 20 | @Setter 21 | @NoArgsConstructor 22 | @ToString(callSuper = true) 23 | @EqualsAndHashCode(callSuper = true) 24 | @JsonInclude(JsonInclude.Include.NON_NULL) 25 | @JsonIgnoreProperties(value = {"hibernateLazyInitializer", "handler", "fieldHandler"}, ignoreUnknown = true) 26 | public class QueryProductResponse extends ResolvableResponse { 27 | 28 | private static final long serialVersionUID = 3072909306392064924L; 29 | 30 | private @NotNull Long id; 31 | 32 | private @NotBlank String name; 33 | 34 | } 35 | -------------------------------------------------------------------------------- /rest-tcc-projects/rest-tcc-product-client/src/main/java/com/github/prontera/product/model/response/QueryProductTxnResponse.java: -------------------------------------------------------------------------------- 1 | package com.github.prontera.product.model.response; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 4 | import com.fasterxml.jackson.annotation.JsonInclude; 5 | import com.github.prontera.model.response.ResolvableResponse; 6 | import lombok.EqualsAndHashCode; 7 | import lombok.Getter; 8 | import lombok.NoArgsConstructor; 9 | import lombok.Setter; 10 | import lombok.ToString; 11 | 12 | import java.time.LocalDateTime; 13 | 14 | /** 15 | * @author Zhao Junjian 16 | * @date 2020/01/25 17 | */ 18 | @Getter 19 | @Setter 20 | @NoArgsConstructor 21 | @ToString(callSuper = true) 22 | @EqualsAndHashCode(callSuper = true) 23 | @JsonInclude(JsonInclude.Include.NON_NULL) 24 | @JsonIgnoreProperties(value = {"hibernateLazyInitializer", "handler", "fieldHandler"}, ignoreUnknown = true) 25 | public class QueryProductTxnResponse extends ResolvableResponse { 26 | 27 | private static final long serialVersionUID = -9159171999056024307L; 28 | 29 | private Long productId; 30 | 31 | private Long orderId; 32 | 33 | private Long amount; 34 | 35 | private Integer state; 36 | 37 | private LocalDateTime createAt; 38 | 39 | private LocalDateTime expireAt; 40 | 41 | private LocalDateTime doneAt; 42 | 43 | } 44 | -------------------------------------------------------------------------------- /rest-tcc-projects/rest-tcc-product/.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 | 12 | ### IntelliJ IDEA ### 13 | .idea 14 | *.iws 15 | *.iml 16 | *.ipr 17 | 18 | ### NetBeans ### 19 | nbproject/private/ 20 | build/ 21 | nbbuild/ 22 | dist/ 23 | nbdist/ 24 | .nb-gradle/ -------------------------------------------------------------------------------- /rest-tcc-projects/rest-tcc-product/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | rest-tcc-product 7 | 0.0.1-SNAPSHOT 8 | jar 9 | 10 | rest-tcc-product 11 | product management 12 | 13 | 14 | com.github.prontera 15 | rest-tcc-projects 16 | 0.0.1-SNAPSHOT 17 | 18 | 19 | 20 | src/main/java 21 | 2.10.1 22 | 23 | 24 | 25 | 26 | com.github.prontera 27 | rest-tcc-commons 28 | 0.0.1-SNAPSHOT 29 | 30 | 31 | com.github.prontera 32 | rest-tcc-product-client 33 | 0.0.1-SNAPSHOT 34 | 35 | 36 | 37 | org.springframework.cloud 38 | spring-cloud-starter-netflix-eureka-client 39 | 40 | 41 | 42 | com.fasterxml.jackson.core 43 | jackson-annotations 44 | ${jackson-annotations.version} 45 | 46 | 47 | 48 | org.springframework.boot 49 | spring-boot-starter-test 50 | test 51 | 52 | 53 | org.junit.vintage 54 | junit-vintage-engine 55 | 56 | 57 | org.springframework.boot 58 | spring-boot-starter-logging 59 | 60 | 61 | 62 | 63 | io.projectreactor 64 | reactor-test 65 | test 66 | 67 | 68 | 69 | 70 | 71 | 72 | org.springframework.boot 73 | spring-boot-maven-plugin 74 | 75 | 76 | 77 | repackage 78 | 79 | 80 | 81 | 82 | 83 | org.mybatis.generator 84 | mybatis-generator-maven-plugin 85 | 86 | 87 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /rest-tcc-projects/rest-tcc-product/src/main/java/com/github/prontera/ContextStartedListener.java: -------------------------------------------------------------------------------- 1 | package com.github.prontera; 2 | 3 | import com.github.prontera.service.ProductService; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.boot.ApplicationArguments; 6 | import org.springframework.boot.ApplicationRunner; 7 | import org.springframework.context.annotation.Lazy; 8 | import org.springframework.stereotype.Component; 9 | 10 | import javax.annotation.Nonnull; 11 | import java.util.Objects; 12 | import java.util.TimeZone; 13 | 14 | /** 15 | * @author Zhao Junjian 16 | * @date 2020/01/23 17 | */ 18 | @Component 19 | public class ContextStartedListener implements ApplicationRunner { 20 | 21 | private final ProductService service; 22 | 23 | @Lazy 24 | @Autowired 25 | public ContextStartedListener(@Nonnull ProductService service) { 26 | this.service = Objects.requireNonNull(service); 27 | } 28 | 29 | @Override 30 | public void run(ApplicationArguments args) { 31 | TimeZone.setDefault(TimeZone.getTimeZone("Asia/Shanghai")); 32 | boostCp(); 33 | } 34 | 35 | protected void boostCp() { 36 | for (int i = 0; i < 47; i++) { 37 | service.findByName("ps4"); 38 | } 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /rest-tcc-projects/rest-tcc-product/src/main/java/com/github/prontera/ProductApplication.java: -------------------------------------------------------------------------------- 1 | package com.github.prontera; 2 | 3 | import com.github.prontera.annotation.MyBatisRepository; 4 | import org.mybatis.spring.annotation.MapperScan; 5 | import org.springframework.boot.SpringApplication; 6 | import org.springframework.boot.autoconfigure.SpringBootApplication; 7 | 8 | /** 9 | * @author Zhao Junjian 10 | * @date 2020/01/17 11 | */ 12 | @MapperScan(basePackages = "com.github.prontera.persistence", annotationClass = MyBatisRepository.class) 13 | @SpringBootApplication 14 | public class ProductApplication { 15 | 16 | public static void main(String[] args) { 17 | SpringApplication.run(ProductApplication.class, args); 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /rest-tcc-projects/rest-tcc-product/src/main/java/com/github/prontera/annotation/FaultBarrier.java: -------------------------------------------------------------------------------- 1 | package com.github.prontera.annotation; 2 | 3 | import java.lang.annotation.Documented; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | 7 | /** 8 | * @author Zhao Junjian 9 | * @date 2018/05/24 10 | */ 11 | @Retention(RetentionPolicy.RUNTIME) 12 | @Documented 13 | public @interface FaultBarrier { 14 | } 15 | -------------------------------------------------------------------------------- /rest-tcc-projects/rest-tcc-product/src/main/java/com/github/prontera/aspect/FaultBarrierAspect.java: -------------------------------------------------------------------------------- 1 | package com.github.prontera.aspect; 2 | 3 | import com.github.prontera.enums.NumericStatusCode; 4 | import com.github.prontera.exception.InvalidModelException; 5 | import com.github.prontera.exception.ResolvableStatusException; 6 | import com.github.prontera.model.response.ResolvableResponse; 7 | import com.github.prontera.product.enums.StatusCode; 8 | import com.github.prontera.util.Responses; 9 | import com.google.common.base.Strings; 10 | import org.apache.logging.log4j.LogManager; 11 | import org.apache.logging.log4j.Logger; 12 | import org.aspectj.lang.ProceedingJoinPoint; 13 | import org.aspectj.lang.Signature; 14 | import org.aspectj.lang.annotation.Around; 15 | import org.aspectj.lang.annotation.Aspect; 16 | import org.aspectj.lang.reflect.MethodSignature; 17 | import org.springframework.core.Ordered; 18 | 19 | /** 20 | * @author Zhao Junjian 21 | * @date 2020/01/17 22 | */ 23 | @Aspect 24 | public class FaultBarrierAspect implements Ordered { 25 | 26 | private static final Logger LOGGER = LogManager.getLogger(FaultBarrierAspect.class); 27 | 28 | private final int order; 29 | 30 | public FaultBarrierAspect(int order) { 31 | this.order = order; 32 | } 33 | 34 | @SuppressWarnings({"unchecked", "rawtypes"}) 35 | @Around(value = "within(com.github.prontera..*) && (@annotation(com.github.prontera.annotation.FaultBarrier))") 36 | public Object aroundMethod(ProceedingJoinPoint joinPoint) throws Throwable { 37 | Object proceed; 38 | final Signature signature = joinPoint.getSignature(); 39 | final Class returnType = ((MethodSignature) signature).getReturnType(); 40 | try { 41 | proceed = joinPoint.proceed(); 42 | } catch (Exception e) { 43 | if (!ResolvableResponse.class.isAssignableFrom(returnType)) { 44 | throw new IllegalStateException(e); 45 | } 46 | if (e instanceof ResolvableStatusException) { 47 | final NumericStatusCode statusCode = ((ResolvableStatusException) e).getStatusCode(); 48 | final String injectMessage = ((ResolvableStatusException) e).getInjectMessage(); 49 | final String message = Strings.isNullOrEmpty(injectMessage) ? statusCode.message() : injectMessage; 50 | proceed = Responses.generate(returnType, statusCode, message); 51 | } else if (e instanceof InvalidModelException) { 52 | proceed = Responses.generate(returnType, StatusCode.INVALID_MODEL_FIELDS, e.getMessage()); 53 | } else { 54 | LOGGER.error("UnknownServerException, method signature '{}', request params '{}'", signature, joinPoint.getArgs(), e); 55 | final StatusCode unknownException = StatusCode.SERVER_UNKNOWN_ERROR; 56 | proceed = Responses.generate(returnType, unknownException); 57 | } 58 | } 59 | return proceed; 60 | } 61 | 62 | @Override 63 | public int getOrder() { 64 | return order; 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /rest-tcc-projects/rest-tcc-product/src/main/java/com/github/prontera/config/AopConfig.java: -------------------------------------------------------------------------------- 1 | package com.github.prontera.config; 2 | 3 | import com.github.prontera.aspect.FaultBarrierAspect; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | 7 | /** 8 | * @author Zhao Junjian 9 | * @date 2020/01/17 10 | */ 11 | @Configuration 12 | public class AopConfig { 13 | 14 | @Bean 15 | public FaultBarrierAspect faultBarrier() { 16 | return new FaultBarrierAspect(Byte.MAX_VALUE); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /rest-tcc-projects/rest-tcc-product/src/main/java/com/github/prontera/config/BeanConfig.java: -------------------------------------------------------------------------------- 1 | package com.github.prontera.config; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | 5 | /** 6 | * @author Zhao Junjian 7 | * @date 2020/01/17 8 | */ 9 | @Configuration 10 | public class BeanConfig { 11 | 12 | } 13 | -------------------------------------------------------------------------------- /rest-tcc-projects/rest-tcc-product/src/main/java/com/github/prontera/config/SwaggerConfig.java: -------------------------------------------------------------------------------- 1 | package com.github.prontera.config; 2 | 3 | import com.fasterxml.classmate.TypeResolver; 4 | import com.google.common.base.Charsets; 5 | import com.google.common.collect.ImmutableList; 6 | import com.google.common.io.Files; 7 | import lombok.Builder; 8 | import lombok.EqualsAndHashCode; 9 | import lombok.Getter; 10 | import lombok.ToString; 11 | import org.reactivestreams.Publisher; 12 | import org.springframework.context.annotation.Bean; 13 | import org.springframework.context.annotation.Configuration; 14 | import org.springframework.context.annotation.Import; 15 | import org.springframework.core.io.ClassPathResource; 16 | import reactor.core.publisher.Flux; 17 | import reactor.core.publisher.Mono; 18 | import springfox.bean.validators.configuration.BeanValidatorPluginsConfiguration; 19 | import springfox.documentation.builders.RequestHandlerSelectors; 20 | import springfox.documentation.schema.AlternateTypeRules; 21 | import springfox.documentation.schema.WildcardType; 22 | import springfox.documentation.service.ApiInfo; 23 | import springfox.documentation.service.Contact; 24 | import springfox.documentation.spi.DocumentationType; 25 | import springfox.documentation.spring.web.plugins.Docket; 26 | import springfox.documentation.swagger.web.DocExpansion; 27 | import springfox.documentation.swagger.web.ModelRendering; 28 | import springfox.documentation.swagger.web.UiConfiguration; 29 | import springfox.documentation.swagger.web.UiConfigurationBuilder; 30 | import springfox.documentation.swagger2.annotations.EnableSwagger2WebFlux; 31 | 32 | import java.io.File; 33 | import java.io.IOException; 34 | import java.util.Collection; 35 | import java.util.List; 36 | 37 | /** 38 | * @author Zhao Junjian 39 | * @date 2020/01/17 40 | */ 41 | @Configuration 42 | @EnableSwagger2WebFlux 43 | @Import(BeanValidatorPluginsConfiguration.class) 44 | public class SwaggerConfig { 45 | 46 | private static final String DESCRIPTION; 47 | 48 | static { 49 | try { 50 | final ClassPathResource resource = new ClassPathResource("SWAGGER.md"); 51 | if (resource.exists()) { 52 | final List lines = Files.readLines(new File(resource.getPath()), Charsets.UTF_8); 53 | DESCRIPTION = String.join("\n", lines); 54 | } else { 55 | DESCRIPTION = null; 56 | } 57 | } catch (IOException e) { 58 | throw new IllegalArgumentException(e); 59 | } 60 | } 61 | 62 | public SwaggerApiInfo generateApiInfo() { 63 | return SwaggerApiInfo.builder().title("product-plane").version("2.0.0").serviceUrl(null).build(); 64 | } 65 | 66 | @Bean 67 | public Docket configure(TypeResolver typeResolver) { 68 | final SwaggerApiInfo info = generateApiInfo(); 69 | return new Docket(DocumentationType.SWAGGER_2) 70 | .genericModelSubstitutes(Mono.class, Flux.class, Publisher.class) 71 | .select() 72 | .apis(RequestHandlerSelectors.basePackage("com.github.prontera")) 73 | .build() 74 | .pathMapping("/") 75 | .useDefaultResponseMessages(false) 76 | .apiInfo(new ApiInfo(info.getTitle(), DESCRIPTION, info.getVersion(), info.getServiceUrl(), new Contact(null, null, null), null, null, ImmutableList.of())) 77 | .alternateTypeRules( 78 | AlternateTypeRules.newRule( 79 | typeResolver.resolve(Collection.class, WildcardType.class), 80 | typeResolver.resolve(List.class, WildcardType.class)) 81 | ) 82 | //.enableUrlTemplating(true) 83 | .forCodeGeneration(false); 84 | } 85 | 86 | @Bean 87 | public UiConfiguration uiConfig() { 88 | return UiConfigurationBuilder.builder() 89 | .defaultModelsExpandDepth(0) 90 | .defaultModelRendering(ModelRendering.MODEL) 91 | .docExpansion(DocExpansion.LIST) 92 | .displayOperationId(false) 93 | .build(); 94 | } 95 | 96 | @Getter 97 | @Builder 98 | @ToString(callSuper = true) 99 | @EqualsAndHashCode 100 | private static class SwaggerApiInfo { 101 | 102 | private final String title; 103 | 104 | private final String version; 105 | 106 | private final String serviceUrl; 107 | 108 | } 109 | 110 | } 111 | -------------------------------------------------------------------------------- /rest-tcc-projects/rest-tcc-product/src/main/java/com/github/prontera/domain/Product.java: -------------------------------------------------------------------------------- 1 | package com.github.prontera.domain; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 4 | import com.fasterxml.jackson.annotation.JsonInclude; 5 | import lombok.EqualsAndHashCode; 6 | import lombok.Getter; 7 | import lombok.NoArgsConstructor; 8 | import lombok.Setter; 9 | import lombok.ToString; 10 | 11 | import java.time.LocalDateTime; 12 | 13 | /** 14 | * @author Zhao Junjian 15 | * @date 2020/01/24 16 | */ 17 | @Getter 18 | @Setter 19 | @NoArgsConstructor 20 | @ToString 21 | @EqualsAndHashCode(callSuper = true) 22 | @JsonInclude(JsonInclude.Include.NON_NULL) 23 | @JsonIgnoreProperties(value = {"hibernateLazyInitializer", "handler", "fieldHandler"}, ignoreUnknown = true) 24 | public class Product extends IdenticalDomain { 25 | 26 | private static final long serialVersionUID = -735618077843393503L; 27 | 28 | private Long id; 29 | 30 | private String name; 31 | 32 | private Long inventory; 33 | 34 | private LocalDateTime createAt; 35 | 36 | private LocalDateTime updateAt; 37 | 38 | } 39 | -------------------------------------------------------------------------------- /rest-tcc-projects/rest-tcc-product/src/main/java/com/github/prontera/domain/ProductTransaction.java: -------------------------------------------------------------------------------- 1 | package com.github.prontera.domain; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 4 | import com.fasterxml.jackson.annotation.JsonInclude; 5 | import com.github.prontera.product.enums.ReservingState; 6 | import lombok.EqualsAndHashCode; 7 | import lombok.Getter; 8 | import lombok.NoArgsConstructor; 9 | import lombok.Setter; 10 | import lombok.ToString; 11 | 12 | import java.time.LocalDateTime; 13 | 14 | /** 15 | * @author Zhao Junjian 16 | * @date 2020/01/24 17 | */ 18 | @Getter 19 | @Setter 20 | @NoArgsConstructor 21 | @ToString 22 | @EqualsAndHashCode(callSuper = true) 23 | @JsonInclude(JsonInclude.Include.NON_NULL) 24 | @JsonIgnoreProperties(value = {"hibernateLazyInitializer", "handler", "fieldHandler"}, ignoreUnknown = true) 25 | public class ProductTransaction extends IdenticalDomain { 26 | private static final long serialVersionUID = -1997296571733378076L; 27 | 28 | private Long id; 29 | 30 | private Long productId; 31 | 32 | private Long orderId; 33 | 34 | private Long amount; 35 | 36 | private ReservingState state; 37 | 38 | private LocalDateTime createAt; 39 | 40 | private LocalDateTime updateAt; 41 | 42 | private LocalDateTime expireAt; 43 | 44 | private LocalDateTime doneAt; 45 | 46 | } 47 | -------------------------------------------------------------------------------- /rest-tcc-projects/rest-tcc-product/src/main/java/com/github/prontera/http/ProductController.java: -------------------------------------------------------------------------------- 1 | package com.github.prontera.http; 2 | 3 | import com.github.prontera.Shifts; 4 | import com.github.prontera.annotation.FaultBarrier; 5 | import com.github.prontera.domain.ProductTransaction; 6 | import com.github.prontera.product.enums.StatusCode; 7 | import com.github.prontera.product.model.request.ConfirmProductTxnRequest; 8 | import com.github.prontera.product.model.request.InventoryReservingRequest; 9 | import com.github.prontera.product.model.request.QueryProductRequest; 10 | import com.github.prontera.product.model.request.QueryProductTxnRequest; 11 | import com.github.prontera.product.model.response.ConfirmProductTxnResponse; 12 | import com.github.prontera.product.model.response.InventoryReservingResponse; 13 | import com.github.prontera.product.model.response.QueryProductResponse; 14 | import com.github.prontera.product.model.response.QueryProductTxnResponse; 15 | import com.github.prontera.service.ProductService; 16 | import com.github.prontera.util.HibernateValidators; 17 | import com.github.prontera.util.Responses; 18 | import io.swagger.annotations.Api; 19 | import io.swagger.annotations.ApiOperation; 20 | import org.springframework.beans.factory.annotation.Autowired; 21 | import org.springframework.context.annotation.Lazy; 22 | import org.springframework.http.MediaType; 23 | import org.springframework.web.bind.annotation.PostMapping; 24 | import org.springframework.web.bind.annotation.RequestBody; 25 | import org.springframework.web.bind.annotation.RequestMapping; 26 | import org.springframework.web.bind.annotation.RestController; 27 | 28 | import javax.annotation.Nonnull; 29 | import java.util.Objects; 30 | import java.util.Optional; 31 | 32 | /** 33 | * @author Zhao Junjian 34 | * @date 2020/01/25 35 | */ 36 | @Api(tags = "Product-Debugger") 37 | @RestController 38 | @RequestMapping(value = "/", produces = {MediaType.APPLICATION_JSON_VALUE}, consumes = {MediaType.APPLICATION_JSON_VALUE}) 39 | public class ProductController { 40 | 41 | private final ProductService service; 42 | 43 | @Lazy 44 | @Autowired 45 | public ProductController(@Nonnull ProductService service) { 46 | this.service = Objects.requireNonNull(service); 47 | } 48 | 49 | @FaultBarrier 50 | @ApiOperation(value = "根据产品名查询信息", notes = "_") 51 | @PostMapping(value = "/query-by-product-name") 52 | public QueryProductResponse queryByProductName(@Nonnull @RequestBody QueryProductRequest request) { 53 | Objects.requireNonNull(request); 54 | HibernateValidators.throwsIfInvalid(request); 55 | return service.queryByProductName(request); 56 | } 57 | 58 | @FaultBarrier 59 | @ApiOperation(value = "预留资源, 锁定库存", notes = "_") 60 | @PostMapping(value = "/reserve-inventory") 61 | public InventoryReservingResponse reserveInventory(@Nonnull @RequestBody InventoryReservingRequest request) { 62 | Objects.requireNonNull(request); 63 | HibernateValidators.throwsIfInvalid(request); 64 | return service.reserving(request); 65 | } 66 | 67 | @FaultBarrier 68 | @ApiOperation(value = "根据订单ID查询预留资源", notes = "如果发现预留资源过了保护期, 将自动归还资金, 具备fsm被动轮状的能力") 69 | @PostMapping(value = "/query-transaction") 70 | public QueryProductTxnResponse queryTransaction(@Nonnull @RequestBody QueryProductTxnRequest request) { 71 | Objects.requireNonNull(request); 72 | HibernateValidators.throwsIfInvalid(request); 73 | final Optional nullableTxn = service.cancellableFindTransaction(request.getOrderId()); 74 | if (!nullableTxn.isPresent()) { 75 | Shifts.fatal(StatusCode.ORDER_NOT_EXISTS); 76 | } 77 | final ProductTransaction transaction = nullableTxn.get(); 78 | final QueryProductTxnResponse response = Responses.generate(QueryProductTxnResponse.class, StatusCode.OK); 79 | response.setProductId(transaction.getProductId()); 80 | response.setOrderId(transaction.getOrderId()); 81 | response.setAmount(transaction.getAmount()); 82 | response.setCreateAt(transaction.getCreateAt()); 83 | response.setExpireAt(transaction.getExpireAt()); 84 | response.setDoneAt(transaction.getDoneAt()); 85 | response.setState(transaction.getState().val()); 86 | return response; 87 | } 88 | 89 | @FaultBarrier 90 | @ApiOperation(value = "根据订单ID确认预留资源", notes = "具备fsm被动轮转能力") 91 | @PostMapping(value = "/confirm-transaction") 92 | public ConfirmProductTxnResponse confirmTransaction(@Nonnull @RequestBody ConfirmProductTxnRequest request) { 93 | Objects.requireNonNull(request); 94 | HibernateValidators.throwsIfInvalid(request); 95 | return service.confirmTransaction(request, 0); 96 | } 97 | 98 | } 99 | -------------------------------------------------------------------------------- /rest-tcc-projects/rest-tcc-product/src/main/java/com/github/prontera/persistence/ProductMapper.java: -------------------------------------------------------------------------------- 1 | package com.github.prontera.persistence; 2 | 3 | import com.github.prontera.annotation.MyBatisRepository; 4 | import com.github.prontera.domain.Product; 5 | import org.apache.ibatis.annotations.Param; 6 | 7 | @MyBatisRepository 8 | public interface ProductMapper extends CrudMapper { 9 | Product selectByName(@Param("name") String name); 10 | 11 | int deductInventory(@Param("id") Long id, @Param("amount") Long amount); 12 | 13 | int increaseInventory(@Param("id") Long id, @Param("amount") Long amount); 14 | } 15 | -------------------------------------------------------------------------------- /rest-tcc-projects/rest-tcc-product/src/main/java/com/github/prontera/persistence/ProductTransactionMapper.java: -------------------------------------------------------------------------------- 1 | package com.github.prontera.persistence; 2 | 3 | import com.github.prontera.annotation.MyBatisRepository; 4 | import com.github.prontera.domain.ProductTransaction; 5 | import com.github.prontera.product.enums.ReservingState; 6 | import org.apache.ibatis.annotations.Param; 7 | 8 | @MyBatisRepository 9 | public interface ProductTransactionMapper extends CrudMapper { 10 | 11 | ProductTransaction selectByOrderId(@Param("orderId") long orderId); 12 | 13 | int compareAndSetState(@Param("id") long id, 14 | @Param("expected") ReservingState expected, 15 | @Param("updating") ReservingState updating); 16 | 17 | } 18 | -------------------------------------------------------------------------------- /rest-tcc-projects/rest-tcc-product/src/main/java/com/github/prontera/persistence/handler/ReservingStateHandler.java: -------------------------------------------------------------------------------- 1 | package com.github.prontera.persistence.handler; 2 | 3 | import com.github.prontera.persistence.GenericTypeHandler; 4 | import com.github.prontera.product.enums.ReservingState; 5 | 6 | /** 7 | * @author Zhao Junjian 8 | * @date 2020/01/20 9 | */ 10 | public class ReservingStateHandler extends GenericTypeHandler { 11 | 12 | @Override 13 | public int getEnumIntegerValue(ReservingState parameter) { 14 | return parameter.val(); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /rest-tcc-projects/rest-tcc-product/src/main/java/com/github/prontera/util/Responses.java: -------------------------------------------------------------------------------- 1 | package com.github.prontera.util; 2 | 3 | import com.github.prontera.enums.NumericStatusCode; 4 | import com.github.prontera.exception.ResolvableStatusException; 5 | import com.github.prontera.model.response.ResolvableResponse; 6 | import com.github.prontera.product.enums.StatusCode; 7 | import com.google.common.base.Strings; 8 | import org.objenesis.Objenesis; 9 | import org.objenesis.ObjenesisStd; 10 | import reactor.core.Exceptions; 11 | 12 | import javax.annotation.Nonnull; 13 | import java.util.Objects; 14 | import java.util.function.Supplier; 15 | 16 | /** 17 | * @author Zhao Junjian 18 | * @date 2020/01/20 19 | */ 20 | public final class Responses { 21 | 22 | private static final Objenesis OBJENESIS = new ObjenesisStd(true); 23 | 24 | private Responses() { 25 | } 26 | 27 | public static T generate(@Nonnull Class clazz, @Nonnull NumericStatusCode status) { 28 | return generate(clazz, status, status.message()); 29 | } 30 | 31 | public static T generate(@Nonnull Class clazz, @Nonnull NumericStatusCode status, @Nonnull Object message) { 32 | Objects.requireNonNull(clazz); 33 | Objects.requireNonNull(status); 34 | Objects.requireNonNull(message); 35 | T instance = OBJENESIS.newInstance(clazz); 36 | instance.setSuccessful(NumericStatusCode.isSuccessful(status)); 37 | instance.setCode(status.code()); 38 | instance.setMessage(message.toString()); 39 | return instance; 40 | } 41 | 42 | public static T generate(@Nonnull Class clazz, @Nonnull Throwable throwable) { 43 | Objects.requireNonNull(throwable); 44 | Objects.requireNonNull(clazz); 45 | final Throwable exception = Exceptions.unwrap(throwable); 46 | final Supplier supplier; 47 | if (exception instanceof ResolvableStatusException) { 48 | final NumericStatusCode statusCode = ((ResolvableStatusException) exception).getStatusCode(); 49 | final String injectMessage = ((ResolvableStatusException) exception).getInjectMessage(); 50 | if (Strings.isNullOrEmpty(injectMessage)) { 51 | supplier = () -> generate(clazz, statusCode); 52 | } else { 53 | supplier = () -> generate(clazz, statusCode, injectMessage); 54 | } 55 | } else { 56 | supplier = () -> generate(clazz, StatusCode.SERVER_UNKNOWN_ERROR); 57 | } 58 | return supplier.get(); 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /rest-tcc-projects/rest-tcc-product/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | # SERVER 2 | server.port=8265 3 | spring.application.name=product 4 | # DATASOURCE 5 | spring.datasource.username=chris 6 | spring.datasource.password=123123 7 | spring.datasource.url=jdbc:mysql://localhost:3306/product?useSSL=false&serverTimezone=Asia/Shanghai&connectTimeout=1200&socketTimeout=1200 8 | # MYBATIS 9 | mybatis.type-aliases-package=com.github.prontera.domain 10 | mybatis.type-handlers-package=com.github.prontera.persistence.handler 11 | mybatis.mapper-locations=classpath*:mapper/**/*Mapper.xml 12 | # SPRING CLOUD NETFLIX 13 | eureka.client.service-url.defaultZone=http://localhost:8255/eureka/ 14 | -------------------------------------------------------------------------------- /rest-tcc-projects/rest-tcc-product/src/main/resources/generatorConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 18 | 19 | 21 | 22 | 23 | 24 | 26 | 27 | 28 | 29 | 31 | 32 | 33 | 34 | 37 | 38 | 39 |
40 | 41 | 44 | 46 | 47 | 48 | 49 | 50 |
51 | 52 |
53 |
54 | -------------------------------------------------------------------------------- /rest-tcc-projects/rest-tcc-product/src/main/resources/log4j2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | %d{yyyy-MM-dd HH:mm:ss.SSS} %5p ${hostName} --- [%15.15t] %-40.40c{1.} : %m%n%ex 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /rest-tcc-projects/rest-tcc-product/src/main/resources/mapper/ProductMapper.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | id, name, inventory, create_at, update_at 13 | 14 | 20 | 21 | delete 22 | from t_product 23 | where id = #{id,jdbcType=BIGINT} 24 | 25 | 26 | insert into t_product (id, name, inventory, 27 | create_at, update_at) 28 | values (#{id,jdbcType=BIGINT}, #{name,jdbcType=VARCHAR}, #{inventory,jdbcType=BIGINT}, 29 | #{createAt,jdbcType=TIMESTAMP}, #{updateAt,jdbcType=TIMESTAMP}) 30 | 31 | 32 | insert into t_product 33 | 34 | 35 | id, 36 | 37 | 38 | name, 39 | 40 | 41 | inventory, 42 | 43 | 44 | create_at, 45 | 46 | 47 | update_at, 48 | 49 | 50 | 51 | 52 | #{id,jdbcType=BIGINT}, 53 | 54 | 55 | #{name,jdbcType=VARCHAR}, 56 | 57 | 58 | #{inventory,jdbcType=BIGINT}, 59 | 60 | 61 | #{createAt,jdbcType=TIMESTAMP}, 62 | 63 | 64 | #{updateAt,jdbcType=TIMESTAMP}, 65 | 66 | 67 | 68 | 69 | update t_product 70 | 71 | 72 | name = #{name,jdbcType=VARCHAR}, 73 | 74 | 75 | inventory = #{inventory,jdbcType=BIGINT}, 76 | 77 | 78 | create_at = #{createAt,jdbcType=TIMESTAMP}, 79 | 80 | 81 | update_at = #{updateAt,jdbcType=TIMESTAMP}, 82 | 83 | 84 | where id = #{id,jdbcType=BIGINT} 85 | 86 | 87 | update t_product 88 | set name = #{name,jdbcType=VARCHAR}, 89 | inventory = #{inventory,jdbcType=BIGINT}, 90 | create_at = #{createAt,jdbcType=TIMESTAMP}, 91 | update_at = #{updateAt,jdbcType=TIMESTAMP} 92 | where id = #{id,jdbcType=BIGINT} 93 | 94 | 100 | 101 | update t_product 102 | set inventory = inventory - #{amount} 103 | where id = #{id} 104 | and inventory - #{amount} >= 0 105 | 106 | 107 | update t_product 108 | set inventory = inventory + #{amount} 109 | where id = #{id} 110 | 111 | 112 | -------------------------------------------------------------------------------- /rest-tcc-projects/rest-tcc-product/src/main/resources/puml/participant-state.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | skinparam monochrome true 3 | scale 1 4 | title Product Transaction State Machine 5 | 6 | ' State 7 | state "TRYING[√]" as TRYING 8 | TRYING: + action: initial "state" 0000 9 | TRYING: + action: reduce corresponding col `inventory` in t_product 10 | TRYING: + action: recording final field "amount" 11 | TRYING: + action: recording final field "expired_at" 12 | ''' 13 | state "CONFIRMED[√]" as CONFIRMED 14 | CONFIRMED: + action: update "state" to 0001 15 | CONFIRMED: + action: assign now() to "done_at" 16 | ''' 17 | state "CANCELLED[√]" as CANCELLED 18 | CANCELLED: + action: update "state" to 0010 19 | CANCELLED: + action: rollback `amount` to the col "inventory" in t_product 20 | CANCELLED: + action: assign now() to "done_at" 21 | ' Transition 22 | [*] --> TRYING 23 | TRYING --> CONFIRMED: confirm \n [LocalDateTime.now() is before than expired_at] 24 | TRYING --> CANCELLED: cancel \n [LocalDateTime.now() is after than expired_at] 25 | CONFIRMED --> [*] 26 | CANCELLED --> [*] 27 | @enduml 28 | 29 | -------------------------------------------------------------------------------- /rest-tcc-projects/rest-tcc-service-discovery/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | rest-tcc-projects 7 | com.github.prontera 8 | 0.0.1-SNAPSHOT 9 | 10 | 4.0.0 11 | 12 | rest-tcc-service-discovery 13 | 14 | 15 | 16 | 17 | org.springframework.cloud 18 | spring-cloud-starter-netflix-eureka-server 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /rest-tcc-projects/rest-tcc-service-discovery/src/main/java/com/github/prontera/ServiceDiscoveryApplication.java: -------------------------------------------------------------------------------- 1 | package com.github.prontera; 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 | /** 8 | * @author Zhao Junjian 9 | * @date 2020/01/20 10 | */ 11 | @EnableEurekaServer 12 | @SpringBootApplication 13 | public class ServiceDiscoveryApplication { 14 | 15 | public static void main(String[] args) { 16 | SpringApplication.run(ServiceDiscoveryApplication.class, args); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /rest-tcc-projects/rest-tcc-service-discovery/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | # SERVER 2 | server.port=8255 3 | spring.application.name=eureka-server 4 | # SPRING CLOUD NETFLIX 5 | eureka.instance.hostname=${HOST_NAME:${HOST:localhost}} 6 | eureka.client.register-with-eureka=false 7 | eureka.client.fetch-registry=false 8 | eureka.client.service-url.defaultZone=http://${eureka.instance.hostname}:${server.port}/eureka/ 9 | --------------------------------------------------------------------------------