├── .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 |
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 |
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 |
--------------------------------------------------------------------------------