├── .gitignore ├── .serverless ├── cloudformation-template-create-stack.json ├── cloudformation-template-update-stack.json └── serverless-state.json ├── README.md ├── build.gradle ├── pom.xml ├── sam.yaml ├── serverless.yml └── src ├── assembly └── bin.xml └── main ├── java └── com │ └── github │ └── fenixsoft │ ├── StreamLambdaHandler.java │ └── bookstore │ ├── BookstoreApplication.java │ ├── applicaiton │ ├── AccountApplicationService.java │ ├── ProductApplicationService.java │ └── payment │ │ ├── PaymentApplicationService.java │ │ └── dto │ │ └── Settlement.java │ ├── domain │ ├── BaseEntity.java │ ├── account │ │ ├── Account.java │ │ ├── AccountRepository.java │ │ └── validation │ │ │ ├── AccountValidation.java │ │ │ ├── AuthenticatedAccount.java │ │ │ ├── ExistsAccount.java │ │ │ ├── NotConflictAccount.java │ │ │ └── UniqueAccount.java │ ├── auth │ │ ├── AuthenticAccount.java │ │ ├── AuthenticAccountRepository.java │ │ ├── Role.java │ │ ├── provider │ │ │ ├── PreAuthenticatedAuthenticationProvider.java │ │ │ └── UsernamePasswordAuthenticationProvider.java │ │ └── service │ │ │ ├── AuthenticAccountDetailsService.java │ │ │ ├── JWTAccessToken.java │ │ │ ├── JWTAccessTokenService.java │ │ │ └── OAuthClientDetailsService.java │ ├── payment │ │ ├── Payment.java │ │ ├── PaymentRepository.java │ │ ├── PaymentService.java │ │ ├── Stockpile.java │ │ ├── StockpileRepository.java │ │ ├── StockpileService.java │ │ ├── Wallet.java │ │ ├── WalletRepository.java │ │ ├── WalletService.java │ │ └── validation │ │ │ ├── SettlementValidator.java │ │ │ └── SufficientStock.java │ └── warehouse │ │ ├── Advertisement.java │ │ ├── AdvertisementRepository.java │ │ ├── Product.java │ │ ├── ProductRepository.java │ │ ├── ProductService.java │ │ └── Specification.java │ ├── infrastructure │ ├── cache │ │ └── CacheConfiguration.java │ ├── configuration │ │ ├── AuthenticationServerConfiguration.java │ │ ├── AuthorizationServerConfiguration.java │ │ ├── JerseyConfiguration.java │ │ ├── ResourceServerConfiguration.java │ │ └── WebSecurityConfiguration.java │ ├── jaxrs │ │ ├── AccessDeniedExceptionMapper.java │ │ ├── BaseExceptionMapper.java │ │ ├── CodedMessage.java │ │ ├── CommonResponse.java │ │ └── ViolationExceptionMapper.java │ └── utility │ │ └── Encryption.java │ └── resource │ ├── AccountResource.java │ ├── AdvertisementResource.java │ ├── PaymentResource.java │ ├── ProductResource.java │ └── SettlementResource.java └── resources ├── application-mysql.yml ├── application-test.yml ├── application.yml ├── banner.txt ├── db ├── hsqldb │ ├── data.sql │ └── schema.sql └── mysql │ ├── data.sql │ ├── schema.sql │ └── user.sql └── static ├── favicon.ico ├── index.html └── static ├── board ├── gitalk.css ├── gitalk.html └── gitalk.min.js ├── carousel ├── ai.png ├── fenix.png ├── fenix2.png └── jvm3.png ├── cover ├── ai.jpg ├── fenix.png ├── jvm1.jpg ├── jvm2.jpg ├── jvm3.jpg ├── jvms.jpg ├── jvms8.jpg └── osgi.jpg ├── css └── app.acff6d0da14b65448084a2639e44b2ef.css ├── desc ├── OSGi.jpg ├── ai.jpg ├── fenix.jpg ├── jvm2.jpg ├── jvm3.jpg └── jvms.jpg ├── fonts ├── element-icons.535877f.woff └── element-icons.732389d.ttf ├── img ├── bg2.ef8085e.png ├── cc-logo.3653e37.png ├── logo-color.5500ec5.png └── logo-gray-light.84aa074.png └── js ├── 0.c178f427b3d08777c70f.js ├── 1.adddfb80b9441d8bc19d.js ├── 2.626ed94f3752555e21f0.js ├── 3.bc7f0b2154007257c317.js ├── 4.b4e48a42cf742af20851.js ├── 5.d375cbd6c7e1463cdbed.js ├── 6.68562501db5734ef1531.js ├── 7.184a5e39cc0c624f6a6d.js ├── 8.176f9455c3442c06ebf6.js ├── 9.527be297aba1594ffe0d.js ├── app.ea66dc0be78c3ed2ae63.js ├── manifest.1aeebcd3a724536ddb12.js └── vendor.c2f13a2146485051ae24.js /.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | !**/src/main/** 5 | !**/src/test/** 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | .sts4-cache 15 | 16 | ### IntelliJ IDEA ### 17 | .idea 18 | *.iws 19 | *.iml 20 | *.ipr 21 | 22 | ### NetBeans ### 23 | /nbproject/private/ 24 | /nbbuild/ 25 | /dist/ 26 | /nbdist/ 27 | /.nb-gradle/ 28 | build/ 29 | 30 | ### VS Code ### 31 | .vscode/ 32 | -------------------------------------------------------------------------------- /.serverless/cloudformation-template-create-stack.json: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion": "2010-09-09", 3 | "Description": "The AWS CloudFormation template for this Serverless application", 4 | "Resources": { 5 | "ServerlessDeploymentBucket": { 6 | "Type": "AWS::S3::Bucket", 7 | "Properties": { 8 | "BucketEncryption": { 9 | "ServerSideEncryptionConfiguration": [ 10 | { 11 | "ServerSideEncryptionByDefault": { 12 | "SSEAlgorithm": "AES256" 13 | } 14 | } 15 | ] 16 | } 17 | } 18 | }, 19 | "ServerlessDeploymentBucketPolicy": { 20 | "Type": "AWS::S3::BucketPolicy", 21 | "Properties": { 22 | "Bucket": { 23 | "Ref": "ServerlessDeploymentBucket" 24 | }, 25 | "PolicyDocument": { 26 | "Statement": [ 27 | { 28 | "Action": "s3:*", 29 | "Effect": "Deny", 30 | "Principal": "*", 31 | "Resource": [ 32 | { 33 | "Fn::Join": [ 34 | "", 35 | [ 36 | "arn:", 37 | { 38 | "Ref": "AWS::Partition" 39 | }, 40 | ":s3:::", 41 | { 42 | "Ref": "ServerlessDeploymentBucket" 43 | }, 44 | "/*" 45 | ] 46 | ] 47 | } 48 | ], 49 | "Condition": { 50 | "Bool": { 51 | "aws:SecureTransport": false 52 | } 53 | } 54 | } 55 | ] 56 | } 57 | } 58 | } 59 | }, 60 | "Outputs": { 61 | "ServerlessDeploymentBucketName": { 62 | "Value": { 63 | "Ref": "ServerlessDeploymentBucket" 64 | } 65 | } 66 | } 67 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fenix's BookStore后端:以AWS Lambda无服务架构实现 2 | 3 | 4 | 5 |

6 | 7 | logo 8 | 9 |

10 |

11 | 12 | License 13 | Document License 14 | About Author 15 |

16 | 17 | 18 | 19 |
20 | 21 | 如果你此时并不曾了解过什么是“The Fenix Project”,建议先阅读这部分内容。 22 | 23 | 无服务架构(Serverless)与微服务架构本身没有继承替代关系,它们并不是同一种层次的架构,无服务的云函数可以作为微服务的一种实现方式,甚至可能是未来很主流的实现方式。在这部文档中我们的话题主要还是聚焦在如何解决分布式架构下的种种问题,相对而言无服务架构并非重点,不过为保证架构演进的完整性,笔者仍然建立了无服务架构的简单演示工程。 24 | 25 | 不过,由于无服务架构原理上就决定了它对程序的启动性能十分敏感,这天生就不利于Java程序,尤其不利于Spring这类启动时组装的CDI框架。因此基于Java的程序,除非使用GraalVM做提前编译、将Spring的大部分Bean提前初始化,或者迁移至[Quarkus](https://quarkus.io/)这以原生程序为目标的框架上,否则是很难实际用于生产的。 26 | 27 | ## 运行程序 28 | 29 | Serverless架构的Fenix's Bookstore基于[亚马逊AWS Lambda](https://amazonaws-china.com/cn/lambda/)平台运行,这是最早商用,也是目前全球规模最大的Serverless运行平台。从2018年开始,中国的主流云服务厂商,如阿里云、腾讯云都推出了各自的Serverless云计算环境,如需在这些平台上运行Fenix's Bookstore,应根据平台提供的Java SDK对StreamLambdaHandler的代码进行少许调整。 30 | 31 | 假设你已经完成[AWS注册](https://aws.amazon.com/premiumsupport/knowledge-center/create-and-activate-aws-account/)、配置[AWS CLI环境](https://amazonaws-china.com/cn/cli/)以及IAM账号的前提下,可通过以下几种途径,可以运行程序,浏览最终的效果: 32 | 33 | - 通过AWS SAM(Serverless Application Model) Local在本地运行:
AWS CLI中附有SAM CLI,但是版本较旧,可通过[如下地址](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html)安装最新版本的SAM CLI。另外,SAM需要Docker运行环境支持,可参考[此处](/appendix/deployment-env-setup/setup-docker.html)部署。
首先编译应用出二进制包,执行以下标准Maven打包命令即可:
34 | 35 | ``` 36 | $ mvn clean package 37 | ``` 38 | 39 | 根据pom.xml中assembly-zip的设置,打包将不会生成SpringBoot Fat JAR,而是产生适用于AWS Lambda的ZIP包。打包后,确认已在target目录生成ZIP文件,且文件名称与代码中提供了sam.yaml中配置的一致,在工程根目录下运行如下命令启动本地SAM测试: 40 | 41 | ```bash 42 | $ sam local start-api --template sam.yaml 43 | ``` 44 | 45 | 在浏览器访问:[http://localhost:3000](http://localhost:3000),系统预置了一个用户(user:icyfenix,pw:123456),也可以注册新用户来测试。 46 | 47 | - 通过AWS Serverless CLI将本地ZIP包上传至云端运行:
确认已配置AWS凭证后,工程中已经提供了serverless.yml配置文件,确认文件中ZIP的路径与实际Maven生成的一致,然后在命令行执行: 48 | 49 | ```bash 50 | $ sls deploy 51 | ``` 52 | 53 | 此时Serverless CLI会自动将ZIP文件上传至AWS S3,然后生成对应的Layers和API Gateway,运行结果如下所示: 54 | 55 | ``` 56 | $ sls deploy 57 | Serverless: Packaging service... 58 | Serverless: Uploading CloudFormation file to S3... 59 | Serverless: Uploading artifacts... 60 | Serverless: Uploading service bookstore-serverless-awslambda-1.0-SNAPSHOT-lambda-package.zip file to S3 (53.58 MB)... 61 | Serverless: Validating template... 62 | Serverless: Updating Stack... 63 | Serverless: Checking Stack update progress... 64 | .............. 65 | Serverless: Stack update finished... 66 | Service Information 67 | service: spring-boot-serverless 68 | stage: dev 69 | region: us-east-1 70 | stack: spring-boot-serverless-dev 71 | resources: 10 72 | api keys: 73 | None 74 | endpoints: 75 | GET - https://cc1oj8hirl.execute-api.us-east-1.amazonaws.com/dev/ 76 | functions: 77 | springBootServerless: spring-boot-serverless-dev-springBootServerless 78 | layers: 79 | None 80 | Serverless: Removing old service artifacts from S3... 81 | ``` 82 | 83 | 访问输出结果中的地址(譬如上面显示的https://cc1oj8hirl.execute-api.us-east-1.amazonaws.com/dev/)即可浏览结果。
需要注意,由于Serverless对响应速度的要求本来就较高,所以不建议再采用HSQLDB数据库作来运行程序了,每次冷启动都重置一次数据库本身也并不合理。代码中有提供MySQL的Schema,建议采用AWS RDB MySQL/MariaDB作为数据库来运行。 84 | 85 | ## 协议 86 | 87 | - 本作品代码部分采用[Apache 2.0协议](https://www.apache.org/licenses/LICENSE-2.0)进行许可。遵循许可的前提下,你可以自由地对代码进行修改,再发布,可以将代码用作商业用途。但要求你: 88 | - **署名**:在原有代码和衍生代码中,保留原作者署名及代码来源信息。 89 | - **保留许可证**:在原有代码和衍生代码中,保留Apache 2.0协议文件。 90 | 91 | - 本作品文档部分采用[知识共享署名 4.0 国际许可协议](http://creativecommons.org/licenses/by/4.0/)进行许可。 遵循许可的前提下,你可以自由地共享,包括在任何媒介上以任何形式复制、发行本作品,亦可以自由地演绎、修改、转换或以本作品为基础进行二次创作。但要求你: 92 | - **署名**:应在使用本文档的全部或部分内容时候,注明原作者及来源信息。 93 | - **非商业性使用**:不得用于商业出版或其他任何带有商业性质的行为。如需商业使用,请联系作者。 94 | - **相同方式共享的条件**:在本文档基础上演绎、修改的作品,应当继续以知识共享署名 4.0国际许可协议进行许可。 95 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'org.springframework.boot' version '2.1.1.RELEASE' 3 | } 4 | apply plugin: 'java' 5 | 6 | repositories { 7 | jcenter() 8 | mavenLocal() 9 | mavenCentral() 10 | } 11 | 12 | dependencies { 13 | compile ( 14 | 'org.springframework.boot:spring-boot-starter-web:2.1.1.RELEASE', 15 | 'com.amazonaws.serverless:aws-serverless-java-container-spring:[1.0,)', 16 | 'io.symphonia:lambda-logging:1.0.1' 17 | ) 18 | 19 | testCompile("junit:junit:4.12") 20 | } 21 | 22 | task buildZip(type: Zip) { 23 | from compileJava 24 | from processResources 25 | into('lib') { 26 | from(configurations.compileClasspath) { 27 | exclude 'tomcat-embed-*' 28 | } 29 | } 30 | } 31 | 32 | build.dependsOn buildZip 33 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.github.fenixsoft 8 | bookstore-serverless-awslambda 9 | 1.0-SNAPSHOT 10 | jar 11 | 12 | https://github.com/awslabs/aws-serverless-java-container 13 | bookstore-serverless-awslambda 14 | Serverless Architecture Demonstrate with AWS Lambda 15 | 16 | 17 | org.springframework.boot 18 | spring-boot-starter-parent 19 | 2.1.6.RELEASE 20 | 21 | 22 | 23 | 1.8 24 | 1.8 25 | 1.8 26 | -Dfile.encoding=UTF-8 27 | 28 | 29 | 30 | 31 | com.amazonaws.serverless 32 | aws-serverless-java-container-spring 33 | 1.5.1 34 | 35 | 36 | 37 | org.springframework.boot 38 | spring-boot-starter-data-jpa 39 | 40 | 41 | org.springframework.boot 42 | spring-boot-starter-web 43 | 44 | 45 | org.springframework.boot 46 | spring-boot-starter-jersey 47 | 48 | 49 | org.springframework.boot 50 | spring-boot-starter-security 51 | 52 | 53 | org.springframework.security.oauth 54 | spring-security-oauth2 55 | 2.3.5.RELEASE 56 | 57 | 58 | org.springframework.security 59 | spring-security-jwt 60 | 1.0.10.RELEASE 61 | 62 | 63 | org.springframework.boot 64 | spring-boot-starter-cache 65 | 66 | 67 | com.github.ben-manes.caffeine 68 | caffeine 69 | 2.6.2 70 | 71 | 72 | org.springframework.boot 73 | spring-boot-starter-test 74 | test 75 | 76 | 77 | org.junit.vintage 78 | junit-vintage-engine 79 | 80 | 81 | 82 | 83 | org.springframework.security 84 | spring-security-test 85 | test 86 | 87 | 88 | 89 | org.hsqldb 90 | hsqldb 91 | runtime 92 | 93 | 94 | mysql 95 | mysql-connector-java 96 | runtime 97 | 98 | 99 | 100 | 101 | 102 | 103 | shaded-jar 104 | 105 | 106 | 107 | org.apache.maven.plugins 108 | maven-shade-plugin 109 | 2.3 110 | 111 | false 112 | 113 | 114 | 115 | package 116 | 117 | shade 118 | 119 | 120 | 121 | 122 | org.apache.tomcat.embed:* 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | assembly-zip 134 | 135 | true 136 | 137 | 138 | 139 | 140 | 141 | org.apache.maven.plugins 142 | maven-jar-plugin 143 | 3.1.1 144 | 145 | 146 | default-jar 147 | none 148 | 149 | 150 | 151 | 152 | 153 | org.apache.maven.plugins 154 | maven-dependency-plugin 155 | 3.1.1 156 | 157 | 158 | copy-dependencies 159 | package 160 | 161 | copy-dependencies 162 | 163 | 164 | ${project.build.directory}${file.separator}lib 165 | runtime 166 | 167 | 168 | 169 | 170 | 171 | org.apache.maven.plugins 172 | maven-assembly-plugin 173 | 3.1.0 174 | 175 | 176 | zip-assembly 177 | package 178 | 179 | single 180 | 181 | 182 | ${project.artifactId}-${project.version} 183 | 184 | src${file.separator}assembly${file.separator}bin.xml 185 | 186 | false 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | -------------------------------------------------------------------------------- /sam.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Transform: AWS::Serverless-2016-10-31 3 | Description: AWS Serverless Spring Boot 2 API - com.github.fenixsoft::bookstore-serverless-awslambda 4 | Globals: 5 | Api: 6 | EndpointConfiguration: REGIONAL 7 | 8 | Resources: 9 | BookstoreServerlessAwslambdaFunction: 10 | Type: AWS::Serverless::Function 11 | Properties: 12 | Handler: com.github.fenixsoft.StreamLambdaHandler::handleRequest 13 | Runtime: java8 14 | CodeUri: target/bookstore-serverless-awslambda-1.0-SNAPSHOT-lambda-package.zip 15 | MemorySize: 512 16 | Policies: AWSLambdaBasicExecutionRole 17 | Timeout: 30 18 | Events: 19 | GetResource: 20 | Type: Api 21 | Properties: 22 | Path: /{proxy+} 23 | Method: any 24 | 25 | Outputs: 26 | BookstoreServerlessAwslambdaApi: 27 | Description: URL for application 28 | Value: !Sub 'https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/' 29 | Export: 30 | Name: BookstoreServerlessAwslambdaApi 31 | -------------------------------------------------------------------------------- /serverless.yml: -------------------------------------------------------------------------------- 1 | service: spring-boot-serverless 2 | 3 | provider: 4 | name: aws 5 | runtime: java8 6 | 7 | package: 8 | artifact: target/bookstore-serverless-awslambda-1.0-SNAPSHOT-lambda-package.zip 9 | 10 | functions: 11 | springBootServerless: 12 | handler: com.github.fenixsoft.StreamLambdaHandler::handleRequest 13 | events: 14 | - http: 15 | path: / 16 | method: get 17 | timeout: 30 18 | -------------------------------------------------------------------------------- /src/assembly/bin.xml: -------------------------------------------------------------------------------- 1 | 4 | lambda-package 5 | 6 | zip 7 | 8 | false 9 | 10 | 11 | 12 | ${project.build.directory}${file.separator}lib 13 | lib 14 | 15 | tomcat-embed* 16 | 17 | 18 | 19 | 20 | ${project.build.directory}${file.separator}classes 21 | 22 | ** 23 | 24 | ${file.separator} 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/main/java/com/github/fenixsoft/StreamLambdaHandler.java: -------------------------------------------------------------------------------- 1 | package com.github.fenixsoft; 2 | 3 | 4 | import com.amazonaws.serverless.exceptions.ContainerInitializationException; 5 | import com.amazonaws.serverless.proxy.model.AwsProxyRequest; 6 | import com.amazonaws.serverless.proxy.model.AwsProxyResponse; 7 | import com.amazonaws.serverless.proxy.spring.SpringLambdaContainerHandler; 8 | import com.amazonaws.services.lambda.runtime.Context; 9 | import com.amazonaws.services.lambda.runtime.RequestStreamHandler; 10 | import com.github.fenixsoft.bookstore.BookstoreApplication; 11 | 12 | import java.io.IOException; 13 | import java.io.InputStream; 14 | import java.io.OutputStream; 15 | 16 | 17 | public class StreamLambdaHandler implements RequestStreamHandler { 18 | private static SpringLambdaContainerHandler handler; 19 | 20 | static { 21 | try { 22 | handler = SpringLambdaContainerHandler.getAwsProxyHandler(BookstoreApplication.class); 23 | } catch (ContainerInitializationException e) { 24 | // if we fail here. We re-throw the exception to force another cold start 25 | e.printStackTrace(); 26 | throw new RuntimeException("Could not initialize Spring framework", e); 27 | } 28 | } 29 | 30 | @Override 31 | public void handleRequest(InputStream inputStream, OutputStream outputStream, Context context) 32 | throws IOException { 33 | handler.proxyStream(inputStream, outputStream, context); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/com/github/fenixsoft/bookstore/BookstoreApplication.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2020. the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. More information from: 15 | * 16 | * https://github.com/fenixsoft 17 | */ 18 | 19 | package com.github.fenixsoft.bookstore; 20 | 21 | import org.springframework.boot.SpringApplication; 22 | import org.springframework.boot.autoconfigure.SpringBootApplication; 23 | import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; 24 | import org.springframework.cache.annotation.EnableCaching; 25 | import org.springframework.context.annotation.Bean; 26 | import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; 27 | import org.springframework.web.servlet.HandlerAdapter; 28 | import org.springframework.web.servlet.HandlerMapping; 29 | import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter; 30 | import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; 31 | 32 | @SpringBootApplication 33 | @EnableCaching 34 | @EnableGlobalMethodSecurity(prePostEnabled = true, jsr250Enabled = true) 35 | public class BookstoreApplication extends SpringBootServletInitializer { 36 | 37 | /* 38 | * Create required HandlerMapping, to avoid several default HandlerMapping instances being created 39 | */ 40 | @Bean 41 | public HandlerMapping handlerMapping() { 42 | return new RequestMappingHandlerMapping(); 43 | } 44 | 45 | /* 46 | * Create required HandlerAdapter, to avoid several default HandlerAdapter instances being created 47 | */ 48 | @Bean 49 | public HandlerAdapter handlerAdapter() { 50 | return new RequestMappingHandlerAdapter(); 51 | } 52 | 53 | public static void main(String[] args) { 54 | SpringApplication.run(BookstoreApplication.class, args); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/com/github/fenixsoft/bookstore/applicaiton/AccountApplicationService.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2020. the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. More information from: 15 | * 16 | * https://github.com/fenixsoft 17 | */ 18 | 19 | package com.github.fenixsoft.bookstore.applicaiton; 20 | 21 | import com.github.fenixsoft.bookstore.domain.account.Account; 22 | import com.github.fenixsoft.bookstore.domain.account.AccountRepository; 23 | import com.github.fenixsoft.bookstore.infrastructure.utility.Encryption; 24 | 25 | import javax.inject.Inject; 26 | import javax.inject.Named; 27 | import javax.transaction.Transactional; 28 | 29 | /** 30 | * 用户资源的应用服务接口 31 | * 32 | * @author icyfenix@gmail.com 33 | * @date 2020/3/10 17:46 34 | **/ 35 | @Named 36 | @Transactional 37 | public class AccountApplicationService { 38 | 39 | @Inject 40 | private AccountRepository repository; 41 | 42 | @Inject 43 | private Encryption encoder; 44 | 45 | public void createAccount(Account account) { 46 | account.setPassword(encoder.encode(account.getPassword())); 47 | repository.save(account); 48 | } 49 | 50 | public Account findAccountByUsername(String username) { 51 | return repository.findByUsername(username); 52 | } 53 | 54 | public void updateAccount(Account account) { 55 | repository.save(account); 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/com/github/fenixsoft/bookstore/applicaiton/ProductApplicationService.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2020. the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. More information from: 15 | * 16 | * https://github.com/fenixsoft 17 | */ 18 | 19 | package com.github.fenixsoft.bookstore.applicaiton; 20 | 21 | import com.github.fenixsoft.bookstore.domain.payment.Stockpile; 22 | import com.github.fenixsoft.bookstore.domain.payment.StockpileService; 23 | import com.github.fenixsoft.bookstore.domain.warehouse.Product; 24 | import com.github.fenixsoft.bookstore.domain.warehouse.ProductService; 25 | 26 | import javax.inject.Inject; 27 | import javax.inject.Named; 28 | import javax.transaction.Transactional; 29 | 30 | /** 31 | * 产品的应用服务接口 32 | * 33 | * @author icyfenix@gmail.com 34 | * @date 2020/3/15 20:05 35 | **/ 36 | @Named 37 | @Transactional 38 | public class ProductApplicationService { 39 | 40 | @Inject 41 | private ProductService service; 42 | 43 | @Inject 44 | private StockpileService stockpileService; 45 | 46 | /** 47 | * 获取仓库中所有的货物信息 48 | */ 49 | public Iterable getAllProducts() { 50 | return service.getAllProducts(); 51 | } 52 | 53 | /** 54 | * 获取仓库中指定的货物信息 55 | */ 56 | public Product getProduct(Integer id) { 57 | return service.getProduct(id); 58 | } 59 | 60 | /** 61 | * 创建或更新产品信息 62 | */ 63 | public Product saveProduct(Product product) { 64 | return service.saveProduct(product); 65 | } 66 | 67 | /** 68 | * 删除指定产品 69 | */ 70 | public void removeProduct(Integer id) { 71 | service.removeProduct(id); 72 | } 73 | 74 | 75 | /** 76 | * 根据产品查询库存 77 | */ 78 | public Stockpile getStockpile(Integer productId) { 79 | return stockpileService.getByProductId(productId); 80 | } 81 | 82 | /** 83 | * 将指定的产品库存调整为指定数额 84 | */ 85 | public void setStockpileAmountByProductId(Integer productId, Integer amount) { 86 | stockpileService.set(productId, amount); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/main/java/com/github/fenixsoft/bookstore/applicaiton/payment/PaymentApplicationService.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2020. the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. More information from: 15 | * 16 | * https://github.com/fenixsoft 17 | */ 18 | 19 | package com.github.fenixsoft.bookstore.applicaiton.payment; 20 | 21 | import com.github.fenixsoft.bookstore.applicaiton.payment.dto.Settlement; 22 | import com.github.fenixsoft.bookstore.domain.payment.*; 23 | import com.github.fenixsoft.bookstore.domain.warehouse.ProductService; 24 | import org.springframework.cache.Cache; 25 | 26 | import javax.annotation.Resource; 27 | import javax.inject.Inject; 28 | import javax.inject.Named; 29 | import javax.transaction.Transactional; 30 | 31 | /** 32 | * 支付应用务 33 | * 34 | * @author icyfenix@gmail.com 35 | * @date 2020/3/12 16:29 36 | **/ 37 | @Named 38 | @Transactional 39 | public class PaymentApplicationService { 40 | 41 | @Inject 42 | private PaymentService paymentService; 43 | 44 | @Inject 45 | private ProductService productService; 46 | 47 | @Inject 48 | private WalletService walletService; 49 | 50 | @Resource(name = "settlement") 51 | private Cache settlementCache; 52 | 53 | /** 54 | * 根据结算清单的内容执行,生成对应的支付单 55 | */ 56 | public Payment executeBySettlement(Settlement bill) { 57 | // 从服务中获取商品的价格,计算要支付的总价(安全原因,这个不能由客户端传上来) 58 | productService.replenishProductInformation(bill); 59 | // 冻结部分库存(保证有货提供),生成付款单 60 | Payment payment = paymentService.producePayment(bill); 61 | // 设立解冻定时器(超时未支付则释放冻结的库存和资金) 62 | paymentService.setupAutoThawedTrigger(payment); 63 | return payment; 64 | } 65 | 66 | /** 67 | * 完成支付 68 | * 立即取消解冻定时器,执行扣减库存和资金 69 | */ 70 | public void accomplishPayment(Integer accountId, String payId) { 71 | // 订单从冻结状态变为派送状态,扣减库存 72 | double price = paymentService.accomplish(payId); 73 | // 扣减货款 74 | walletService.decrease(accountId, price); 75 | // 支付成功的清除缓存 76 | settlementCache.evict(payId); 77 | } 78 | 79 | /** 80 | * 取消支付 81 | * 立即触发解冻定时器,释放库存和资金 82 | */ 83 | public void cancelPayment(String payId) { 84 | // 释放冻结的库存 85 | paymentService.cancel(payId); 86 | // 支付成功的清除缓存 87 | settlementCache.evict(payId); 88 | } 89 | 90 | } 91 | -------------------------------------------------------------------------------- /src/main/java/com/github/fenixsoft/bookstore/applicaiton/payment/dto/Settlement.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2020. the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. More information from: 15 | * 16 | * https://github.com/fenixsoft 17 | */ 18 | 19 | package com.github.fenixsoft.bookstore.applicaiton.payment.dto; 20 | 21 | import com.fasterxml.jackson.annotation.JsonProperty; 22 | import com.github.fenixsoft.bookstore.domain.warehouse.Product; 23 | 24 | import javax.validation.constraints.Min; 25 | import javax.validation.constraints.NotEmpty; 26 | import javax.validation.constraints.NotNull; 27 | import javax.validation.constraints.Size; 28 | import java.util.Collection; 29 | import java.util.Map; 30 | 31 | /** 32 | * 支付结算单模型 33 | * 34 | * @author icyfenix@gmail.com 35 | * @date 2020/3/12 11:35 36 | **/ 37 | public class Settlement { 38 | 39 | @Size(min = 1, message = "结算单中缺少商品清单") 40 | private Collection items; 41 | 42 | @NotNull(message = "结算单中缺少配送信息") 43 | private Purchase purchase; 44 | 45 | /** 46 | * 购物清单中的商品信息 47 | * 基于安全原因(避免篡改价格),改信息不会取客户端的,需在服务端根据商品ID再查询出来 48 | */ 49 | public transient Map productMap; 50 | 51 | public Collection getItems() { 52 | return items; 53 | } 54 | 55 | public void setItems(Collection items) { 56 | this.items = items; 57 | } 58 | 59 | public Purchase getPurchase() { 60 | return purchase; 61 | } 62 | 63 | public void setPurchase(Purchase purchase) { 64 | this.purchase = purchase; 65 | } 66 | 67 | /** 68 | * 结算单中要购买的商品 69 | */ 70 | public static class Item { 71 | @NotNull(message = "结算单中必须有明确的商品数量") 72 | @Min(value = 1, message = "结算单中商品数量至少为一件") 73 | private Integer amount; 74 | 75 | @JsonProperty("id") 76 | @NotNull(message = "结算单中必须有明确的商品信息") 77 | private Integer productId; 78 | 79 | public Integer getAmount() { 80 | return amount; 81 | } 82 | 83 | public void setAmount(Integer amount) { 84 | this.amount = amount; 85 | } 86 | 87 | public Integer getProductId() { 88 | return productId; 89 | } 90 | 91 | public void setProductId(Integer productId) { 92 | this.productId = productId; 93 | } 94 | } 95 | 96 | /** 97 | * 结算单中的配送信息 98 | */ 99 | public static class Purchase { 100 | 101 | private Boolean delivery = true; 102 | 103 | @NotEmpty(message = "配送信息中缺少支付方式") 104 | private String pay; 105 | 106 | @NotEmpty(message = "配送信息中缺少收件人姓名") 107 | private String name; 108 | 109 | @NotEmpty(message = "配送信息中缺少收件人电话") 110 | private String telephone; 111 | 112 | @NotEmpty(message = "配送信息中缺少收件地址") 113 | private String location; 114 | 115 | public Boolean getDelivery() { 116 | return delivery; 117 | } 118 | 119 | public void setDelivery(Boolean delivery) { 120 | this.delivery = delivery; 121 | } 122 | 123 | public String getPay() { 124 | return pay; 125 | } 126 | 127 | public void setPay(String pay) { 128 | this.pay = pay; 129 | } 130 | 131 | public String getName() { 132 | return name; 133 | } 134 | 135 | public void setName(String name) { 136 | this.name = name; 137 | } 138 | 139 | public String getTelephone() { 140 | return telephone; 141 | } 142 | 143 | public void setTelephone(String telephone) { 144 | this.telephone = telephone; 145 | } 146 | 147 | public String getLocation() { 148 | return location; 149 | } 150 | 151 | public void setLocation(String location) { 152 | this.location = location; 153 | } 154 | } 155 | 156 | } 157 | -------------------------------------------------------------------------------- /src/main/java/com/github/fenixsoft/bookstore/domain/BaseEntity.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2020. the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. More information from: 15 | * 16 | * https://github.com/fenixsoft 17 | */ 18 | 19 | package com.github.fenixsoft.bookstore.domain; 20 | 21 | import javax.persistence.GeneratedValue; 22 | import javax.persistence.GenerationType; 23 | import javax.persistence.Id; 24 | import javax.persistence.MappedSuperclass; 25 | import java.io.Serializable; 26 | 27 | /** 28 | * JavaBean领域对象的共同基类,定义了ID属性和其访问字段 29 | * 30 | * @author icyfenix@gmail.com 31 | * @date 2020/3/6 10:52 32 | **/ 33 | @MappedSuperclass 34 | public class BaseEntity implements Serializable { 35 | 36 | @Id 37 | @GeneratedValue(strategy = GenerationType.IDENTITY) 38 | private Integer id; 39 | 40 | public Integer getId() { 41 | return id; 42 | } 43 | 44 | public void setId(Integer id) { 45 | this.id = id; 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/com/github/fenixsoft/bookstore/domain/account/Account.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2020. the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. More information from: 15 | * 16 | * https://github.com/fenixsoft 17 | */ 18 | 19 | package com.github.fenixsoft.bookstore.domain.account; 20 | 21 | import com.fasterxml.jackson.annotation.JsonProperty; 22 | import com.github.fenixsoft.bookstore.domain.BaseEntity; 23 | 24 | import javax.persistence.Column; 25 | import javax.persistence.Entity; 26 | import javax.validation.constraints.Email; 27 | import javax.validation.constraints.NotEmpty; 28 | import javax.validation.constraints.Pattern; 29 | 30 | /** 31 | * 用户实体 32 | * 33 | * @author icyfenix@gmail.com 34 | * @date 2020/3/6 23:08 35 | */ 36 | @Entity 37 | public class Account extends BaseEntity { 38 | 39 | @NotEmpty(message = "用户不允许为空") 40 | private String username; 41 | 42 | // 密码字段不参与序列化(但反序列化是参与的)、不参与更新(但插入是参与的) 43 | // 这意味着密码字段不会在获取对象(很多操作都会关联用户对象)的时候泄漏出去; 44 | // 也意味着此时“修改密码”一类的功能无法以用户对象资源的接口来处理(因为更新对象时密码不会被更新),需要单独提供接口去完成 45 | @JsonProperty(access = JsonProperty.Access.WRITE_ONLY) 46 | @Column(updatable = false) 47 | private String password; 48 | 49 | @NotEmpty(message = "用户姓名不允许为空") 50 | private String name; 51 | 52 | private String avatar; 53 | 54 | @Pattern(regexp = "1\\d{10}", message = "手机号格式不正确") 55 | private String telephone; 56 | 57 | @Email(message = "邮箱格式不正确") 58 | private String email; 59 | 60 | private String location; 61 | 62 | public String getUsername() { 63 | return username; 64 | } 65 | 66 | public void setUsername(String username) { 67 | this.username = username; 68 | } 69 | 70 | public String getPassword() { 71 | return password; 72 | } 73 | 74 | public void setPassword(String password) { 75 | this.password = password; 76 | } 77 | 78 | public String getName() { 79 | return name; 80 | } 81 | 82 | public void setName(String name) { 83 | this.name = name; 84 | } 85 | 86 | public String getAvatar() { 87 | return avatar; 88 | } 89 | 90 | public void setAvatar(String avatar) { 91 | this.avatar = avatar; 92 | } 93 | 94 | public String getTelephone() { 95 | return telephone; 96 | } 97 | 98 | public void setTelephone(String telephone) { 99 | this.telephone = telephone; 100 | } 101 | 102 | public String getEmail() { 103 | return email; 104 | } 105 | 106 | public void setEmail(String email) { 107 | this.email = email; 108 | } 109 | 110 | public String getLocation() { 111 | return location; 112 | } 113 | 114 | public void setLocation(String location) { 115 | this.location = location; 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/main/java/com/github/fenixsoft/bookstore/domain/account/AccountRepository.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2020. the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. More information from: 15 | * 16 | * https://github.com/fenixsoft 17 | */ 18 | 19 | package com.github.fenixsoft.bookstore.domain.account; 20 | 21 | import org.springframework.cache.annotation.CacheConfig; 22 | import org.springframework.cache.annotation.CacheEvict; 23 | import org.springframework.cache.annotation.Cacheable; 24 | import org.springframework.cache.annotation.Caching; 25 | import org.springframework.data.repository.CrudRepository; 26 | 27 | import java.util.Collection; 28 | import java.util.Optional; 29 | 30 | /** 31 | * 用户对象数据仓库 32 | * 33 | * @author icyfenix@gmail.com 34 | * @date 2020/3/6 23:10 35 | **/ 36 | @CacheConfig(cacheNames = "repository.account") 37 | public interface AccountRepository extends CrudRepository { 38 | 39 | @Override 40 | Iterable findAll(); 41 | 42 | 43 | @Cacheable(key = "#username") 44 | Account findByUsername(String username); 45 | 46 | /** 47 | * 判断唯一性,用户名、邮箱、电话不允许任何一个重复 48 | */ 49 | boolean existsByUsernameOrEmailOrTelephone(String username, String email, String telephone); 50 | 51 | /** 52 | * 判断唯一性,用户名、邮箱、电话不允许任何一个重复 53 | */ 54 | Collection findByUsernameOrEmailOrTelephone(String username, String email, String telephone); 55 | 56 | /** 57 | * 判断存在性,用户名存在即为存在 58 | */ 59 | @Cacheable(key = "#username") 60 | boolean existsByUsername(String username); 61 | 62 | 63 | // 覆盖以下父类中需要处理缓存失效的方法 64 | // 父类取不到CacheConfig的配置信息,所以不能抽象成一个通用的父类接口中完成 65 | @Caching(evict = { 66 | @CacheEvict(key = "#entity.id"), 67 | @CacheEvict(key = "#entity.username") 68 | }) 69 | S save(S entity); 70 | 71 | @CacheEvict 72 | Iterable saveAll(Iterable entities); 73 | 74 | @Cacheable(key = "#id") 75 | Optional findById(Integer id); 76 | 77 | @Cacheable(key = "#id") 78 | boolean existsById(Integer id); 79 | 80 | @CacheEvict(key = "#id") 81 | void deleteById(Integer id); 82 | 83 | @CacheEvict(key = "#entity.id") 84 | void delete(Account entity); 85 | 86 | @CacheEvict(allEntries = true) 87 | void deleteAll(Iterable entities); 88 | 89 | @CacheEvict(allEntries = true) 90 | void deleteAll(); 91 | } 92 | -------------------------------------------------------------------------------- /src/main/java/com/github/fenixsoft/bookstore/domain/account/validation/AccountValidation.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2020. the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. More information from: 15 | * 16 | * https://github.com/fenixsoft 17 | */ 18 | 19 | package com.github.fenixsoft.bookstore.domain.account.validation; 20 | 21 | import com.github.fenixsoft.bookstore.domain.account.Account; 22 | import com.github.fenixsoft.bookstore.domain.account.AccountRepository; 23 | import com.github.fenixsoft.bookstore.domain.auth.AuthenticAccount; 24 | import org.springframework.security.core.context.SecurityContextHolder; 25 | 26 | import javax.inject.Inject; 27 | import javax.validation.ConstraintValidator; 28 | import javax.validation.ConstraintValidatorContext; 29 | import java.lang.annotation.Annotation; 30 | import java.util.Collection; 31 | import java.util.function.Predicate; 32 | 33 | /** 34 | * 用户对象校验器 35 | *

36 | * 如,新增用户时,判断该用户对象是否允许唯一,在修改用户时,判断该用户是否存在 37 | * 38 | * @author icyfenix@gmail.com 39 | * @date 2020/3/11 14:22 40 | **/ 41 | public class AccountValidation implements ConstraintValidator { 42 | 43 | @Inject 44 | protected AccountRepository repository; 45 | 46 | protected Predicate predicate = c -> true; 47 | 48 | @Override 49 | public boolean isValid(Account value, ConstraintValidatorContext context) { 50 | // 在JPA持久化时,默认采用Hibernate实现,插入、更新时都会调用BeanValidationEventListener进行验证 51 | // 而验证行为应该尽可能在外层进行,Resource中已经通过@Vaild注解触发过一次验证,这里会导致重复执行 52 | // 正常途径是使用分组验证避免,但@Vaild不支持分组,@Validated支持,却又是Spring的私有标签 53 | // 另一个途径是设置Hibernate配置文件中的javax.persistence.validation.mode参数为“none”,这个参数在Spring的yml中未提供桥接 54 | // 为了避免涉及到数据库操作的验证重复进行,在这里做增加此空值判断,利用Hibernate验证时验证器不是被Spring创建的特点绕开 55 | return repository == null || predicate.test(value); 56 | } 57 | 58 | public static class ExistsAccountValidator extends AccountValidation { 59 | public void initialize(ExistsAccount constraintAnnotation) { 60 | predicate = c -> repository.existsById(c.getId()); 61 | } 62 | } 63 | 64 | public static class AuthenticatedAccountValidator extends AccountValidation { 65 | public void initialize(AuthenticatedAccount constraintAnnotation) { 66 | predicate = c -> { 67 | Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); 68 | if ("anonymousUser".equals(principal)) { 69 | return false; 70 | } else { 71 | AuthenticAccount loginUser = (AuthenticAccount) principal; 72 | return c.getId().equals(loginUser.getId()); 73 | } 74 | }; 75 | } 76 | } 77 | 78 | public static class UniqueAccountValidator extends AccountValidation { 79 | public void initialize(UniqueAccount constraintAnnotation) { 80 | predicate = c -> !repository.existsByUsernameOrEmailOrTelephone(c.getUsername(), c.getEmail(), c.getTelephone()); 81 | } 82 | } 83 | 84 | public static class NotConflictAccountValidator extends AccountValidation { 85 | public void initialize(NotConflictAccount constraintAnnotation) { 86 | predicate = c -> { 87 | Collection collection = repository.findByUsernameOrEmailOrTelephone(c.getUsername(), c.getEmail(), c.getTelephone()); 88 | // 将用户名、邮件、电话改成与现有完全不重复的,或者只与自己重复的,就不算冲突 89 | return collection.isEmpty() || (collection.size() == 1 && collection.iterator().next().getId().equals(c.getId())); 90 | }; 91 | } 92 | } 93 | 94 | } 95 | -------------------------------------------------------------------------------- /src/main/java/com/github/fenixsoft/bookstore/domain/account/validation/AuthenticatedAccount.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2020. the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. More information from: 15 | * 16 | * https://github.com/fenixsoft 17 | */ 18 | 19 | package com.github.fenixsoft.bookstore.domain.account.validation; 20 | 21 | import javax.validation.Constraint; 22 | import javax.validation.Payload; 23 | import java.lang.annotation.Documented; 24 | import java.lang.annotation.Retention; 25 | import java.lang.annotation.Target; 26 | 27 | import static java.lang.annotation.ElementType.*; 28 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 29 | 30 | /** 31 | * 代表用户必须与当前登陆的用户一致 32 | * 相当于使用Spring Security的@PreAuthorize("#{user.name == authentication.name}")的验证 33 | * 34 | * @author icyfenix@gmail.com 35 | * @date 2020/3/16 11:06 36 | **/ 37 | @Documented 38 | @Retention(RUNTIME) 39 | @Target({FIELD, METHOD, PARAMETER, TYPE}) 40 | @Constraint(validatedBy = AccountValidation.AuthenticatedAccountValidator.class) 41 | public @interface AuthenticatedAccount { 42 | String message() default "不是当前登陆用户"; 43 | 44 | Class[] groups() default {}; 45 | 46 | Class[] payload() default {}; 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/com/github/fenixsoft/bookstore/domain/account/validation/ExistsAccount.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2020. the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. More information from: 15 | * 16 | * https://github.com/fenixsoft 17 | */ 18 | 19 | package com.github.fenixsoft.bookstore.domain.account.validation; 20 | 21 | import javax.validation.Constraint; 22 | import javax.validation.Payload; 23 | import java.lang.annotation.Documented; 24 | import java.lang.annotation.Retention; 25 | import java.lang.annotation.Target; 26 | 27 | import static java.lang.annotation.ElementType.*; 28 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 29 | 30 | /** 31 | * 代表一个用户在数据仓库中是存在的 32 | * 33 | * @author icyfenix@gmail.com 34 | * @date 2020/3/11 19:51 35 | **/ 36 | @Documented 37 | @Retention(RUNTIME) 38 | @Target({FIELD, METHOD, PARAMETER, TYPE}) 39 | @Constraint(validatedBy = AccountValidation.ExistsAccountValidator.class) 40 | public @interface ExistsAccount { 41 | String message() default "用户不存在"; 42 | 43 | Class[] groups() default {}; 44 | 45 | Class[] payload() default {}; 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/com/github/fenixsoft/bookstore/domain/account/validation/NotConflictAccount.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2020. the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. More information from: 15 | * 16 | * https://github.com/fenixsoft 17 | */ 18 | 19 | package com.github.fenixsoft.bookstore.domain.account.validation; 20 | 21 | import javax.validation.Constraint; 22 | import javax.validation.Payload; 23 | import java.lang.annotation.Documented; 24 | import java.lang.annotation.Retention; 25 | import java.lang.annotation.Target; 26 | 27 | import static java.lang.annotation.ElementType.*; 28 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 29 | 30 | /** 31 | * 表示一个用户的信息是无冲突的 32 | *

33 | * “无冲突”是指该用户的敏感信息与其他用户不重合,譬如将一个注册用户的邮箱,修改成与另外一个已存在的注册用户一致的值,这便是冲突 34 | * 35 | * @author icyfenix@gmail.com 36 | * @date 2020/3/11 20:19 37 | **/ 38 | @Documented 39 | @Retention(RUNTIME) 40 | @Target({FIELD, METHOD, PARAMETER, TYPE}) 41 | @Constraint(validatedBy = AccountValidation.NotConflictAccountValidator.class) 42 | public @interface NotConflictAccount { 43 | String message() default "用户名称、邮箱、手机号码与现存用户产生重复"; 44 | 45 | Class[] groups() default {}; 46 | 47 | Class[] payload() default {}; 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/com/github/fenixsoft/bookstore/domain/account/validation/UniqueAccount.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2020. the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. More information from: 15 | * 16 | * https://github.com/fenixsoft 17 | */ 18 | 19 | package com.github.fenixsoft.bookstore.domain.account.validation; 20 | 21 | import javax.validation.Constraint; 22 | import javax.validation.Payload; 23 | import java.lang.annotation.Documented; 24 | import java.lang.annotation.Retention; 25 | import java.lang.annotation.Target; 26 | 27 | import static java.lang.annotation.ElementType.*; 28 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 29 | 30 | /** 31 | * 表示一个用户是唯一的 32 | *

33 | * 唯一不仅仅是用户名,还要求手机、邮箱均不允许重复 34 | * 35 | * @author icyfenix@gmail.com 36 | * @date 2020/3/11 19:49 37 | **/ 38 | @Documented 39 | @Retention(RUNTIME) 40 | @Target({FIELD, METHOD, PARAMETER, TYPE}) 41 | @Constraint(validatedBy = AccountValidation.UniqueAccountValidator.class) 42 | public @interface UniqueAccount { 43 | String message() default "用户名称、邮箱、手机号码均不允许与现存用户重复"; 44 | 45 | Class[] groups() default {}; 46 | 47 | Class[] payload() default {}; 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/com/github/fenixsoft/bookstore/domain/auth/AuthenticAccount.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2020. the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. More information from: 15 | * 16 | * https://github.com/fenixsoft 17 | */ 18 | 19 | package com.github.fenixsoft.bookstore.domain.auth; 20 | 21 | import com.github.fenixsoft.bookstore.domain.account.Account; 22 | import org.springframework.beans.BeanUtils; 23 | import org.springframework.security.core.GrantedAuthority; 24 | import org.springframework.security.core.authority.SimpleGrantedAuthority; 25 | import org.springframework.security.core.userdetails.UserDetails; 26 | 27 | import java.util.Collection; 28 | import java.util.HashSet; 29 | 30 | /** 31 | * 认证用户模型 32 | *

33 | * 用户注册之后,包含其业务属性,如姓名、电话、地址,用于业务发生,存储于Account对象中 34 | * 也包含其用于认证的属性,譬如密码、角色、是否停用,存储于AuthenticAccount对象中 35 | * 36 | * @author icyfenix@gmail.com 37 | * @date 2020/3/7 20:46 38 | **/ 39 | public class AuthenticAccount extends Account implements UserDetails { 40 | 41 | public AuthenticAccount() { 42 | super(); 43 | authorities.add(new SimpleGrantedAuthority(Role.USER)); 44 | } 45 | 46 | public AuthenticAccount(Account origin) { 47 | this(); 48 | BeanUtils.copyProperties(origin, this); 49 | if (getId() == 1) { 50 | // 由于没有做用户管理功能,默认给系统中第一个用户赋予管理员角色 51 | authorities.add(new SimpleGrantedAuthority(Role.ADMIN)); 52 | } 53 | } 54 | 55 | /** 56 | * 该用户拥有的授权,譬如读取权限、修改权限、增加权限等等 57 | */ 58 | private Collection authorities = new HashSet<>(); 59 | 60 | @Override 61 | public Collection getAuthorities() { 62 | return authorities; 63 | } 64 | 65 | public void setAuthorities(Collection authorities) { 66 | this.authorities = authorities; 67 | } 68 | 69 | /** 70 | * 账号是否过期 71 | */ 72 | @Override 73 | public boolean isAccountNonExpired() { 74 | return true; 75 | } 76 | 77 | /** 78 | * 是否锁定 79 | */ 80 | @Override 81 | public boolean isAccountNonLocked() { 82 | return true; 83 | } 84 | 85 | /** 86 | * 密码是否过期 87 | */ 88 | @Override 89 | public boolean isCredentialsNonExpired() { 90 | return true; 91 | } 92 | 93 | /** 94 | * 是否被锁定 95 | */ 96 | @Override 97 | public boolean isEnabled() { 98 | return true; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/main/java/com/github/fenixsoft/bookstore/domain/auth/AuthenticAccountRepository.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2020. the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. More information from: 15 | * 16 | * https://github.com/fenixsoft 17 | */ 18 | 19 | package com.github.fenixsoft.bookstore.domain.auth; 20 | 21 | import com.github.fenixsoft.bookstore.domain.account.AccountRepository; 22 | import org.springframework.beans.factory.annotation.Autowired; 23 | import org.springframework.security.core.userdetails.UsernameNotFoundException; 24 | import org.springframework.security.oauth2.common.exceptions.UnauthorizedUserException; 25 | import org.springframework.stereotype.Component; 26 | 27 | import java.util.Optional; 28 | 29 | /** 30 | * 认证用户的数据仓库 31 | * 32 | * @author icyfenix@gmail.com 33 | * @date 2020/3/8 15:21 34 | **/ 35 | @Component 36 | public class AuthenticAccountRepository { 37 | 38 | @Autowired 39 | private AccountRepository databaseUserRepo; 40 | 41 | public AuthenticAccount findByUsername(String username) { 42 | return new AuthenticAccount(Optional.ofNullable(databaseUserRepo.findByUsername(username)).orElseThrow(() -> new UsernameNotFoundException("用户" + username + "不存在"))); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/com/github/fenixsoft/bookstore/domain/auth/Role.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2020. the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. More information from: 15 | * 16 | * https://github.com/fenixsoft 17 | */ 18 | 19 | package com.github.fenixsoft.bookstore.domain.auth; 20 | 21 | /** 22 | * 角色常量类,目前系统中只有2种角色:用户,管理员 23 | * 24 | * @author icyfenix@gmail.com 25 | * @date 2020/3/16 11:32 26 | **/ 27 | public interface Role { 28 | String USER = "ROLE_USER"; 29 | String ADMIN = "ROLE_ADMIN"; 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/github/fenixsoft/bookstore/domain/auth/provider/PreAuthenticatedAuthenticationProvider.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2020. the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. More information from: 15 | * 16 | * https://github.com/fenixsoft 17 | */ 18 | 19 | package com.github.fenixsoft.bookstore.domain.auth.provider; 20 | 21 | import com.github.fenixsoft.bookstore.domain.auth.AuthenticAccount; 22 | import org.springframework.security.authentication.AuthenticationProvider; 23 | import org.springframework.security.authentication.DisabledException; 24 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; 25 | import org.springframework.security.core.Authentication; 26 | import org.springframework.security.core.AuthenticationException; 27 | import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken; 28 | import org.springframework.security.web.authentication.preauth.PreAuthenticatedCredentialsNotFoundException; 29 | 30 | import javax.inject.Named; 31 | 32 | /** 33 | * 预验证身份认证器 34 | *

35 | * 预验证是指身份已经在其他地方(第三方)确认过 36 | * 预验证器的目的是将第三方身份管理系统集成到具有Spring安全性的Spring应用程序中,在本项目中,用于JWT令牌过期后重刷新时的验证 37 | * 此时只要检查用户是否有停用、锁定、密码过期、账号过期等问题,如果没有,可根据JWT令牌的刷新过期期限,重新给客户端发放访问令牌 38 | * 39 | * @author icyfenix@gmail.com 40 | * @date 2020/3/10 11:25 41 | * @see Pre-Authentication Scenarios 42 | **/ 43 | @Named 44 | public class PreAuthenticatedAuthenticationProvider implements AuthenticationProvider { 45 | 46 | @Override 47 | public Authentication authenticate(Authentication authentication) throws AuthenticationException { 48 | if (authentication.getPrincipal() instanceof UsernamePasswordAuthenticationToken) { 49 | AuthenticAccount user = (AuthenticAccount) ((UsernamePasswordAuthenticationToken) authentication.getPrincipal()).getPrincipal(); 50 | // 检查用户没有停用、锁定、密码过期、账号过期等问题 51 | // 在本项目中这些功能都未启用,实际上此检查肯定是会通过的,但为了严谨和日后扩展,还是依次进行了检查 52 | if (user.isEnabled() && user.isCredentialsNonExpired() && user.isAccountNonExpired() && user.isCredentialsNonExpired()) { 53 | return new PreAuthenticatedAuthenticationToken(user, "", user.getAuthorities()); 54 | } else { 55 | throw new DisabledException("用户状态不正确"); 56 | } 57 | } else { 58 | throw new PreAuthenticatedCredentialsNotFoundException("预验证失败,传上来的令牌是怎么来的?"); 59 | } 60 | } 61 | 62 | /** 63 | * 判断该验证器能处理哪些认证 64 | */ 65 | @Override 66 | public boolean supports(Class clazz) { 67 | return clazz.equals(PreAuthenticatedAuthenticationToken.class); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/com/github/fenixsoft/bookstore/domain/auth/provider/UsernamePasswordAuthenticationProvider.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2020. the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. More information from: 15 | * 16 | * https://github.com/fenixsoft 17 | */ 18 | 19 | package com.github.fenixsoft.bookstore.domain.auth.provider; 20 | 21 | import com.github.fenixsoft.bookstore.domain.auth.service.AuthenticAccountDetailsService; 22 | import org.springframework.security.authentication.AuthenticationManager; 23 | import org.springframework.security.authentication.AuthenticationProvider; 24 | import org.springframework.security.authentication.BadCredentialsException; 25 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; 26 | import org.springframework.security.core.Authentication; 27 | import org.springframework.security.core.AuthenticationException; 28 | import org.springframework.security.core.userdetails.UserDetails; 29 | import org.springframework.security.crypto.password.PasswordEncoder; 30 | 31 | import javax.inject.Inject; 32 | import javax.inject.Named; 33 | 34 | /** 35 | * 基于用户名、密码的身份认证器 36 | * 该身份认证器会被{@link AuthenticationManager}验证管理器调用 37 | * 验证管理器支持多种验证方式,这里基于用户名、密码的的身份认证是方式之一 38 | * 39 | * @author icyfenix@gmail.com 40 | * @date 2020/3/7 21:45 41 | */ 42 | @Named 43 | public class UsernamePasswordAuthenticationProvider implements AuthenticationProvider { 44 | 45 | @Inject 46 | private AuthenticAccountDetailsService authenticAccountDetailsService; 47 | 48 | @Inject 49 | private PasswordEncoder passwordEncoder; 50 | 51 | /** 52 | * 认证处理 53 | *

54 | * 根据用户名查询用户资料,对比资料中加密后的密码 55 | * 结果将返回一个Authentication的实现类(此处为UsernamePasswordAuthenticationToken)则代表认证成功 56 | * 返回null或者抛出AuthenticationException的子类异常则代表认证失败 57 | */ 58 | @Override 59 | public Authentication authenticate(Authentication authentication) throws AuthenticationException { 60 | String username = authentication.getName().toLowerCase(); 61 | String password = (String) authentication.getCredentials(); 62 | // AuthenticationException的子类定义了多种认证失败的类型,这里仅处“理用户不存在”、“密码不正确”两种 63 | // 用户不存在的话会直接由loadUserByUsername()抛出异常 64 | UserDetails user = authenticAccountDetailsService.loadUserByUsername(username); 65 | if (!passwordEncoder.matches(password, user.getPassword())) throw new BadCredentialsException("密码不正确"); 66 | // 认证通过,返回令牌 67 | return new UsernamePasswordAuthenticationToken(user, password, user.getAuthorities()); 68 | 69 | } 70 | 71 | /** 72 | * 判断该验证器能处理哪些认证 73 | */ 74 | @Override 75 | public boolean supports(Class clazz) { 76 | return clazz.equals(UsernamePasswordAuthenticationToken.class); 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /src/main/java/com/github/fenixsoft/bookstore/domain/auth/service/AuthenticAccountDetailsService.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2020. the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. More information from: 15 | * 16 | * https://github.com/fenixsoft 17 | */ 18 | 19 | package com.github.fenixsoft.bookstore.domain.auth.service; 20 | 21 | import com.github.fenixsoft.bookstore.domain.auth.AuthenticAccountRepository; 22 | import org.springframework.security.core.userdetails.UserDetails; 23 | import org.springframework.security.core.userdetails.UserDetailsService; 24 | import org.springframework.security.core.userdetails.UsernameNotFoundException; 25 | 26 | import javax.inject.Inject; 27 | import javax.inject.Named; 28 | import java.util.Optional; 29 | 30 | /** 31 | * 认证用户信息查询服务 32 | *

33 | * {@link UserDetailsService}接口定义了从外部(数据库、LDAP,任何地方)根据用户名查询到 34 | */ 35 | @Named 36 | public class AuthenticAccountDetailsService implements UserDetailsService { 37 | 38 | @Inject 39 | private AuthenticAccountRepository accountRepository; 40 | 41 | /** 42 | * 根据用户名查询用户角色、权限等信息 43 | * 如果用户名无法查询到对应的用户,或者权限不满足,请直接抛出{@link UsernameNotFoundException},勿返回null 44 | */ 45 | @Override 46 | public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { 47 | return accountRepository.findByUsername(username); 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/com/github/fenixsoft/bookstore/domain/auth/service/JWTAccessToken.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2020. the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. More information from: 15 | * 16 | * https://github.com/fenixsoft 17 | */ 18 | 19 | package com.github.fenixsoft.bookstore.domain.auth.service; 20 | 21 | import org.springframework.security.core.Authentication; 22 | import org.springframework.security.core.GrantedAuthority; 23 | import org.springframework.security.core.userdetails.UserDetailsService; 24 | import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken; 25 | import org.springframework.security.oauth2.common.OAuth2AccessToken; 26 | import org.springframework.security.oauth2.provider.OAuth2Authentication; 27 | import org.springframework.security.oauth2.provider.token.DefaultAccessTokenConverter; 28 | import org.springframework.security.oauth2.provider.token.DefaultUserAuthenticationConverter; 29 | import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter; 30 | 31 | import javax.inject.Inject; 32 | import javax.inject.Named; 33 | import java.util.HashMap; 34 | import java.util.Map; 35 | 36 | /** 37 | * JWT访问令牌 38 | *

39 | * JWT令牌的结构为三部分组成:[令牌头(Header)].[负载信息(Payload)].[签名(Signature)] 40 | * 令牌头:定义了令牌的元数据,如令牌采用的签名算法,默认为HMAC SHA256算法 41 | * 负载信息:由签发者自定义的数据,一般会包括过期时间(Expire)、授权范围(Authority)、令牌ID编号(JTI)等 42 | * 签名:签名是使用私钥和头部指定的算法,前两部分进行的数字签名,防止数据被篡改。 43 | * 以上,令牌头和负载均为JSON结构,进行Base64URLEncode之后进行签名,然后用“.”连接,构成令牌报文 44 | *

45 | * Spring Security OAuth2的{@link JwtAccessTokenConverter}提供了令牌的基础结构(令牌头、部分负载,如过期时间、JTI)的转换实现 46 | * 继承此类,在加入自己定义的负载信息即可使用。一般来说负载中至少要告知服务端当前用户是谁,但又不应存放过多信息导致HTTP Header过大,尤其不应存放敏感信息。 47 | * 48 | * @author icyfenix@gmail.com 49 | * @date 2020/3/9 9:46 50 | */ 51 | @Named 52 | public class JWTAccessToken extends JwtAccessTokenConverter { 53 | 54 | // 签名私钥 55 | // 此处内容是我随便写的UUID,按照JWT约定默认是256Bit的,其实任何格式都可以,只是要注意保密,不要公开出去 56 | private static final String JWT_TOKEN_SIGNING_PRIVATE_KEY = "601304E0-8AD4-40B0-BD51-0B432DC47461"; 57 | 58 | @Inject 59 | JWTAccessToken(UserDetailsService userDetailsService) { 60 | // 设置签名私钥 61 | setSigningKey(JWT_TOKEN_SIGNING_PRIVATE_KEY); 62 | // 设置从资源请求中带上来的JWT令牌转换回安全上下文中的用户信息的查询服务 63 | // 如果不设置该服务,则从JWT令牌获得的Principal就只有一个用户名(令牌中确实就只存了用户名) 64 | // 将用户用户信息查询服务提供给默认的令牌转换器,使得转换令牌时自动根据用户名还原出完整的用户对象 65 | // 这方便了后面编码(可以在直接获得登陆用户信息),但也稳定地为每次请求增加了一次(从数据库/缓存)查询,自行取舍 66 | DefaultUserAuthenticationConverter converter = new DefaultUserAuthenticationConverter(); 67 | converter.setUserDetailsService(userDetailsService); 68 | ((DefaultAccessTokenConverter) getAccessTokenConverter()).setUserTokenConverter(converter); 69 | } 70 | 71 | /** 72 | * 增强令牌 73 | * 增强主要就是在令牌的负载中加入额外的信息 74 | */ 75 | @Override 76 | public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) { 77 | Authentication user = authentication.getUserAuthentication(); 78 | String[] authorities = user.getAuthorities().stream().map(GrantedAuthority::getAuthority).toArray(String[]::new); 79 | Map payLoad = new HashMap<>(); 80 | // Spring Security OAuth的JWT令牌默认实现中就加入了一个“user_name”的项存储了当前用户名 81 | // 这里主要是出于演示Payload的用途,以及方便客户端获取(否则客户端要从令牌中解码Base64来获取),设置了一个“username”,两者的内容是一致的 82 | payLoad.put("username", user.getName()); 83 | payLoad.put("authorities", authorities); 84 | ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(payLoad); 85 | return super.enhance(accessToken, authentication); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/main/java/com/github/fenixsoft/bookstore/domain/auth/service/JWTAccessTokenService.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2020. the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. More information from: 15 | * 16 | * https://github.com/fenixsoft 17 | */ 18 | 19 | package com.github.fenixsoft.bookstore.domain.auth.service; 20 | 21 | import org.springframework.security.authentication.AuthenticationManager; 22 | import org.springframework.security.oauth2.provider.token.DefaultTokenServices; 23 | import org.springframework.security.oauth2.provider.token.store.JwtTokenStore; 24 | 25 | import javax.inject.Inject; 26 | import javax.inject.Named; 27 | 28 | /** 29 | * JWT访问令牌服务 30 | *

31 | * 在此服务中提供了令牌如何存储、携带哪些信息、如何签名、持续多长时间等相关内容的定义 32 | * 令牌服务应当会被授权服务器{@link com.github.fenixsoft.bookstore.infrastructure.configuration.AuthorizationServerConfiguration}注册验证Endpoint时候调用到 33 | * 34 | * @author icyfenix@gmail.com 35 | * @date 2020/3/8 11:07 36 | **/ 37 | @Named 38 | public class JWTAccessTokenService extends DefaultTokenServices { 39 | 40 | /** 41 | * 构建JWT令牌,并进行默认的配置 42 | */ 43 | @Inject 44 | public JWTAccessTokenService(JWTAccessToken token, OAuthClientDetailsService clientService, AuthenticationManager authenticationManager) { 45 | // 设置令牌的持久化容器 46 | // 令牌持久化有多种方式,单节点服务可以存放在Session中,集群可以存放在Redis中 47 | // 而JWT是后端无状态、前端存储的解决方案,Token的存储由前端完成 48 | setTokenStore(new JwtTokenStore(token)); 49 | // 令牌支持的客户端详情 50 | setClientDetailsService(clientService); 51 | // 设置验证管理器,在鉴权的时候需要用到 52 | setAuthenticationManager(authenticationManager); 53 | // 定义令牌的额外负载 54 | setTokenEnhancer(token); 55 | // access_token有效期,单位:秒,默认12小时 56 | setAccessTokenValiditySeconds(60 * 60 * 3); 57 | // refresh_token的有效期,单位:秒, 默认30天 58 | // 这决定了客户端选择“记住当前登录用户”的最长时效,即失效前都不用再请求用户赋权了 59 | setRefreshTokenValiditySeconds(60 * 60 * 24 * 15); 60 | // 是否支持refresh_token,默认false 61 | setSupportRefreshToken(true); 62 | // 是否复用refresh_token,默认为true 63 | // 如果为false,则每次请求刷新都会删除旧的refresh_token,创建新的refresh_token 64 | setReuseRefreshToken(true); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/com/github/fenixsoft/bookstore/domain/auth/service/OAuthClientDetailsService.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2020. the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. More information from: 15 | * 16 | * https://github.com/fenixsoft 17 | */ 18 | 19 | package com.github.fenixsoft.bookstore.domain.auth.service; 20 | 21 | import org.springframework.security.crypto.password.PasswordEncoder; 22 | import org.springframework.security.oauth2.config.annotation.builders.InMemoryClientDetailsServiceBuilder; 23 | import org.springframework.security.oauth2.provider.ClientDetails; 24 | import org.springframework.security.oauth2.provider.ClientDetailsService; 25 | import org.springframework.security.oauth2.provider.ClientRegistrationException; 26 | 27 | import javax.annotation.PostConstruct; 28 | import javax.inject.Inject; 29 | import javax.inject.Named; 30 | 31 | /** 32 | * OAuth2客户端类型定义 33 | *

34 | * OAuth2支持四种授权模式,这里仅定义了密码模式(Resource Owner Password Credentials Grant)一种 35 | * OAuth2作为开放的(面向不同服务提供商)授权协议,要求用户提供明文用户名、密码的这种“密码模式”并不常用 36 | * 而这里可以采用是因为前端(BookStore FrontEnd)与后端服务是属于同一个服务提供者的,实质上不存在密码会不会被第三方保存的敏感问题 37 | * 如果永远只考虑单体架构、单一服务提供者,则并无引入OAuth的必要,Spring Security的表单认证就能很良好、便捷地解决认证和授权的问题 38 | * 这里使用密码模式来解决,是为了下一阶段演示微服务化后,服务之间鉴权作准备,以便后续扩展以及对比。 39 | * 40 | * @author icyfenix@gmail.com 41 | * @date 2020/3/7 19:45 42 | **/ 43 | @Named 44 | public class OAuthClientDetailsService implements ClientDetailsService { 45 | 46 | /** 47 | * 客户端ID 48 | * 这里的客户端就是指本项目的前端代码 49 | */ 50 | private static final String CLIENT_ID = "bookstore_frontend"; 51 | /** 52 | * 客户端密钥 53 | * 在OAuth2协议中,ID是可以公开的,密钥应当保密,密钥用以证明当前申请授权的客户端是未被冒充的 54 | */ 55 | private static final String CLIENT_SECRET = "bookstore_secret"; 56 | 57 | @Inject 58 | private PasswordEncoder passwordEncoder; 59 | 60 | private ClientDetailsService clientDetailsService; 61 | 62 | /** 63 | * 构造密码授权模式 64 | *

65 | * 由于实质上只有一个客户端,所以就不考虑存储和客户端的增删改查了,直接在内存中配置出客户端的信息 66 | *

67 | * 授权Endpoint示例: 68 | * /oauth/token?grant_type=password & username=#USER# & password=#PWD# & client_id=bookstore_frontend & client_secret=bookstore_secret 69 | * 刷新令牌Endpoint示例: 70 | * /oauth/token?grant_type=refresh_token & refresh_token=#REFRESH_TOKEN# & client_id=bookstore_frontend & client_secret=bookstore_secret 71 | */ 72 | @PostConstruct 73 | public void init() throws Exception { 74 | InMemoryClientDetailsServiceBuilder builder = new InMemoryClientDetailsServiceBuilder(); 75 | // 提供客户端ID和密钥,并指定该客户端支持密码授权、刷新令牌两种访问类型 76 | builder.withClient(CLIENT_ID) 77 | .secret(passwordEncoder.encode(CLIENT_SECRET)) 78 | .scopes("BROWSER") 79 | .authorizedGrantTypes("password", "refresh_token"); 80 | clientDetailsService = builder.build(); 81 | } 82 | 83 | /** 84 | * 外部根据客户端ID查询验证方式 85 | */ 86 | @Override 87 | public ClientDetails loadClientByClientId(String clientId) throws ClientRegistrationException { 88 | return clientDetailsService.loadClientByClientId(clientId); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/main/java/com/github/fenixsoft/bookstore/domain/payment/Payment.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2020. the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. More information from: 15 | * 16 | * https://github.com/fenixsoft 17 | */ 18 | 19 | package com.github.fenixsoft.bookstore.domain.payment; 20 | 21 | import com.github.fenixsoft.bookstore.domain.BaseEntity; 22 | import com.github.fenixsoft.bookstore.domain.account.Account; 23 | import org.springframework.security.core.context.SecurityContextHolder; 24 | 25 | import javax.persistence.Entity; 26 | import java.util.Date; 27 | import java.util.UUID; 28 | 29 | /** 30 | * 支付单模型 31 | *

32 | * 就是传到客户端让用户给扫码或者其他别的方式付钱的对象 33 | * 34 | * @author icyfenix@gmail.com 35 | * @date 2020/3/12 17:07 36 | **/ 37 | @Entity 38 | public class Payment extends BaseEntity { 39 | 40 | /** 41 | * 支付状态 42 | */ 43 | public enum State { 44 | /** 45 | * 等待支付中 46 | */ 47 | WAITING, 48 | /** 49 | * 已取消 50 | */ 51 | CANCEL, 52 | /** 53 | * 已支付 54 | */ 55 | PAYED, 56 | /** 57 | * 已超时回滚(未支付,并且商品已恢复) 58 | */ 59 | TIMEOUT 60 | } 61 | 62 | public Payment() { 63 | } 64 | 65 | public Payment(Double totalPrice, Long expires) { 66 | setTotalPrice(totalPrice); 67 | setExpires(expires); 68 | setCreateTime(new Date()); 69 | setPayState(State.WAITING); 70 | // 下面这两个是随便写的,实际应该根据情况调用支付服务,返回待支付的ID 71 | setPayId(UUID.randomUUID().toString()); 72 | // 产生支付单的时候一定是有用户的 73 | Account account = (Account) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); 74 | setPaymentLink("/pay/modify/" + getPayId() + "?state=PAYED&accountId=" + account.getId()); 75 | } 76 | 77 | private Date createTime; 78 | 79 | private String payId; 80 | 81 | private Double totalPrice; 82 | 83 | private Long expires; 84 | 85 | private String paymentLink; 86 | 87 | private State payState; 88 | 89 | public String getPayId() { 90 | return payId; 91 | } 92 | 93 | public void setPayId(String payId) { 94 | this.payId = payId; 95 | } 96 | 97 | public Date getCreateTime() { 98 | return createTime; 99 | } 100 | 101 | public void setCreateTime(Date createTime) { 102 | this.createTime = createTime; 103 | } 104 | 105 | public Long getExpires() { 106 | return expires; 107 | } 108 | 109 | public void setExpires(Long expires) { 110 | this.expires = expires; 111 | } 112 | 113 | public String getPaymentLink() { 114 | return paymentLink; 115 | } 116 | 117 | public void setPaymentLink(String paymentLink) { 118 | this.paymentLink = paymentLink; 119 | } 120 | 121 | public Double getTotalPrice() { 122 | return totalPrice; 123 | } 124 | 125 | public void setTotalPrice(Double totalPrice) { 126 | this.totalPrice = totalPrice; 127 | } 128 | 129 | public State getPayState() { 130 | return payState; 131 | } 132 | 133 | public void setPayState(State payState) { 134 | this.payState = payState; 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/main/java/com/github/fenixsoft/bookstore/domain/payment/PaymentRepository.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2020. the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. More information from: 15 | * 16 | * https://github.com/fenixsoft 17 | */ 18 | 19 | package com.github.fenixsoft.bookstore.domain.payment; 20 | 21 | import org.springframework.data.repository.CrudRepository; 22 | 23 | import java.util.Optional; 24 | 25 | /** 26 | * 支付单数据仓库 27 | * 28 | * @author icyfenix@gmail.com 29 | * @date 2020/3/12 23:25 30 | **/ 31 | public interface PaymentRepository extends CrudRepository { 32 | 33 | Payment getByPayId(String payId); 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/com/github/fenixsoft/bookstore/domain/payment/PaymentService.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2020. the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. More information from: 15 | * 16 | * https://github.com/fenixsoft 17 | */ 18 | 19 | package com.github.fenixsoft.bookstore.domain.payment; 20 | 21 | import com.github.fenixsoft.bookstore.applicaiton.payment.dto.Settlement; 22 | import com.github.fenixsoft.bookstore.infrastructure.cache.CacheConfiguration; 23 | import org.slf4j.Logger; 24 | import org.slf4j.LoggerFactory; 25 | import org.springframework.cache.Cache; 26 | 27 | import javax.annotation.Resource; 28 | import javax.inject.Inject; 29 | import javax.inject.Named; 30 | import javax.persistence.EntityNotFoundException; 31 | import java.util.Objects; 32 | import java.util.Timer; 33 | import java.util.TimerTask; 34 | 35 | /** 36 | * 支付单相关的领域服务 37 | * 38 | * @author icyfenix@gmail.com 39 | * @date 2020/3/12 23:24 40 | **/ 41 | @Named 42 | public class PaymentService { 43 | /** 44 | * 默认支付单超时时间:2分钟(缓存TTL时间的一半) 45 | */ 46 | private static final long DEFAULT_PRODUCT_FROZEN_EXPIRES = CacheConfiguration.SYSTEM_DEFAULT_EXPIRES / 2; 47 | 48 | private static final Logger log = LoggerFactory.getLogger(PaymentService.class); 49 | 50 | private final Timer timer = new Timer(); 51 | 52 | @Inject 53 | private StockpileService stockpileService; 54 | 55 | @Inject 56 | private PaymentRepository paymentRepository; 57 | 58 | @Resource(name = "settlement") 59 | private Cache settlementCache; 60 | 61 | 62 | /** 63 | * 生成支付单 64 | *

65 | * 根据结算单冻结指定的货物,计算总价,生成支付单 66 | */ 67 | public Payment producePayment(Settlement bill) { 68 | Double total = bill.getItems().stream().mapToDouble(i -> { 69 | stockpileService.frozen(i.getProductId(), i.getAmount()); 70 | return bill.productMap.get(i.getProductId()).getPrice() * i.getAmount(); 71 | }).sum() + 12; // 12元固定运费,客户端写死的,这里陪着演一下,避免总价对不上 72 | Payment payment = new Payment(total, DEFAULT_PRODUCT_FROZEN_EXPIRES); 73 | paymentRepository.save(payment); 74 | // 将支付单存入缓存 75 | settlementCache.put(payment.getPayId(), bill); 76 | log.info("创建支付订单,总额:{}", payment.getTotalPrice()); 77 | return payment; 78 | } 79 | 80 | /** 81 | * 完成支付单 82 | *

83 | * 意味着客户已经完成付款,这个方法在正式业务中应当作为三方支付平台的回调,而演示项目就直接由客户端发起调用了 84 | */ 85 | public double accomplish(String payId) { 86 | synchronized (payId.intern()) { 87 | Payment payment = paymentRepository.getByPayId(payId); 88 | if (payment.getPayState() == Payment.State.WAITING) { 89 | payment.setPayState(Payment.State.PAYED); 90 | paymentRepository.save(payment); 91 | accomplishSettlement(Payment.State.PAYED, payment.getPayId()); 92 | log.info("编号为{}的支付单已处理完成,等待支付", payId); 93 | return payment.getTotalPrice(); 94 | } else { 95 | throw new UnsupportedOperationException("当前订单不允许支付,当前状态为:" + payment.getPayState()); 96 | } 97 | } 98 | } 99 | 100 | /** 101 | * 取消支付单 102 | *

103 | * 客户取消支付单,此时应当立即释放处于冻结状态的库存 104 | * 由于支付单的存储中应该保存而未持久化的购物明细(在Settlement中),所以这步就不做处理了,等2分钟后在触发器中释放 105 | */ 106 | public void cancel(String payId) { 107 | synchronized (payId.intern()) { 108 | Payment payment = paymentRepository.getByPayId(payId); 109 | if (payment.getPayState() == Payment.State.WAITING) { 110 | payment.setPayState(Payment.State.CANCEL); 111 | paymentRepository.save(payment); 112 | accomplishSettlement(Payment.State.CANCEL, payment.getPayId()); 113 | log.info("编号为{}的支付单已被取消", payId); 114 | } else { 115 | throw new UnsupportedOperationException("当前订单不允许取消,当前状态为:" + payment.getPayState()); 116 | } 117 | } 118 | } 119 | 120 | /** 121 | * 设置支付单自动冲销解冻的触发器 122 | *

123 | * 如果在触发器超时之后,如果支付单未仍未被支付(状态是WAITING) 124 | * 则自动执行冲销,将冻结的库存商品解冻,以便其他人可以购买,并将Payment的状态修改为ROLLBACK。 125 | *

126 | * 注意: 127 | * 使用TimerTask意味着节点带有状态,这在分布式应用中是必须明确【反对】的,如以下缺陷: 128 | * 1. 如果要考虑支付订单的取消场景,无论支付状态如何,这个TimerTask到时间之后都应当被执行。不应尝试使用TimerTask::cancel来取消任务。 129 | * 因为只有带有上下文状态的节点才能完成取消操作,如果要在集群中这样做,就必须使用支持集群的定时任务(如Quartz)以保证多节点下能够正常取消任务。 130 | * 2. 如果节点被重启、同样会面临到状态的丢失,导致一部分处于冻结的触发器永远无法被执行,所以需要系统启动时根据数据库状态有一个恢复TimeTask的的操作 131 | * 3. 即时只考虑正常支付的情况,真正生产环境中这种代码需要一个支持集群的同步锁(如用Redis实现互斥量),避免解冻支付和该支付单被完成两个事件同时在不同的节点中发生 132 | */ 133 | public void setupAutoThawedTrigger(Payment payment) { 134 | timer.schedule(new TimerTask() { 135 | public void run() { 136 | synchronized (payment.getPayId().intern()) { 137 | // 使用2分钟之前的Payment到数据库中查出当前的Payment 138 | Payment currentPayment = paymentRepository.findById(payment.getId()).orElseThrow(() -> new EntityNotFoundException(payment.getId().toString())); 139 | if (currentPayment.getPayState() == Payment.State.WAITING) { 140 | log.info("支付单{}当前状态为:WAITING,转变为:TIMEOUT", payment.getId()); 141 | accomplishSettlement(Payment.State.TIMEOUT, payment.getPayId()); 142 | } 143 | } 144 | } 145 | }, payment.getExpires()); 146 | } 147 | 148 | /** 149 | * 根据支付状态,实际调整库存(扣减库存或者解冻) 150 | */ 151 | private void accomplishSettlement(Payment.State endState, String payId) { 152 | Settlement settlement = (Settlement) Objects.requireNonNull(Objects.requireNonNull(settlementCache.get(payId)).get()); 153 | settlement.getItems().forEach(i -> { 154 | if (endState == Payment.State.PAYED) { 155 | stockpileService.decrease(i.getProductId(), i.getAmount()); 156 | } else { 157 | // 其他状态,无论是TIMEOUT还是CANCEL,都进行解冻 158 | stockpileService.thawed(i.getProductId(), i.getAmount()); 159 | } 160 | }); 161 | } 162 | 163 | } 164 | -------------------------------------------------------------------------------- /src/main/java/com/github/fenixsoft/bookstore/domain/payment/Stockpile.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2020. the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. More information from: 15 | * 16 | * https://github.com/fenixsoft 17 | */ 18 | 19 | package com.github.fenixsoft.bookstore.domain.payment; 20 | 21 | import com.github.fenixsoft.bookstore.domain.BaseEntity; 22 | import com.github.fenixsoft.bookstore.domain.warehouse.Product; 23 | 24 | import javax.persistence.Entity; 25 | import javax.persistence.FetchType; 26 | import javax.persistence.JoinColumn; 27 | import javax.persistence.OneToOne; 28 | 29 | /** 30 | * 商品库存 31 | * 32 | * @author icyfenix@gmail.com 33 | * @date 2020/3/12 16:34 34 | **/ 35 | @Entity 36 | public class Stockpile extends BaseEntity { 37 | 38 | private Integer amount; 39 | 40 | private Integer frozen; 41 | 42 | @OneToOne(fetch = FetchType.LAZY) 43 | @JoinColumn(name = "product_id") 44 | private transient Product product; 45 | 46 | public Integer getAmount() { 47 | return amount; 48 | } 49 | 50 | public void setAmount(Integer amount) { 51 | this.amount = amount; 52 | } 53 | 54 | public void frozen(Integer number) { 55 | this.amount -= number; 56 | this.frozen += number; 57 | } 58 | 59 | public void thawed(Integer number) { 60 | frozen(-1 * number); 61 | } 62 | 63 | public void decrease(Integer number) { 64 | this.frozen -= number; 65 | } 66 | 67 | public void increase(Integer number) { 68 | this.amount += number; 69 | } 70 | 71 | public Product getProduct() { 72 | return product; 73 | } 74 | 75 | public void setProduct(Product product) { 76 | this.product = product; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/main/java/com/github/fenixsoft/bookstore/domain/payment/StockpileRepository.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2020. the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. More information from: 15 | * 16 | * https://github.com/fenixsoft 17 | */ 18 | 19 | package com.github.fenixsoft.bookstore.domain.payment; 20 | 21 | import org.springframework.data.repository.CrudRepository; 22 | 23 | /** 24 | * 库存数据仓库 25 | * 26 | * @author icyfenix@gmail.com 27 | * @date 2020/3/12 16:36 28 | **/ 29 | public interface StockpileRepository extends CrudRepository { 30 | 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/github/fenixsoft/bookstore/domain/payment/StockpileService.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2020. the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. More information from: 15 | * 16 | * https://github.com/fenixsoft 17 | */ 18 | 19 | package com.github.fenixsoft.bookstore.domain.payment; 20 | 21 | import org.slf4j.Logger; 22 | import org.slf4j.LoggerFactory; 23 | 24 | import javax.inject.Inject; 25 | import javax.inject.Named; 26 | import javax.persistence.EntityNotFoundException; 27 | 28 | /** 29 | * 商品库存的领域服务 30 | * 31 | * @author icyfenix@gmail.com 32 | * @date 2020/3/12 20:23 33 | **/ 34 | @Named 35 | public class StockpileService { 36 | 37 | private static final Logger log = LoggerFactory.getLogger(StockpileService.class); 38 | 39 | @Inject 40 | private StockpileRepository repository; 41 | 42 | /** 43 | * 根据产品查询库存 44 | */ 45 | public Stockpile getByProductId(Integer productId) { 46 | return repository.findById(productId).orElseThrow(() -> new EntityNotFoundException(productId.toString())); 47 | } 48 | 49 | /** 50 | * 货物售出 51 | * 从冻结状态的货物中扣减 52 | */ 53 | public void decrease(Integer productId, Integer amount) { 54 | Stockpile stock = repository.findById(productId).orElseThrow(() -> new EntityNotFoundException(productId.toString())); 55 | stock.decrease(amount); 56 | repository.save(stock); 57 | log.info("库存出库,商品:{},数量:{}", productId, amount); 58 | } 59 | 60 | /** 61 | * 货物增加 62 | * 增加指定数量货物至正常货物状态 63 | */ 64 | public void increase(Integer productId, Integer amount) { 65 | Stockpile stock = repository.findById(productId).orElseThrow(() -> new EntityNotFoundException(productId.toString())); 66 | stock.increase(amount); 67 | repository.save(stock); 68 | log.info("库存入库,商品:{},数量:{}", productId, amount); 69 | } 70 | 71 | 72 | /** 73 | * 货物冻结 74 | * 从正常货物中移动指定数量至冻结状态 75 | */ 76 | public void frozen(Integer productId, Integer amount) { 77 | Stockpile stock = repository.findById(productId).orElseThrow(() -> new EntityNotFoundException(productId.toString())); 78 | stock.frozen(amount); 79 | repository.save(stock); 80 | log.info("冻结库存,商品:{},数量:{}", productId, amount); 81 | } 82 | 83 | /** 84 | * 货物解冻 85 | * 从冻结货物中移动指定数量至正常状态 86 | */ 87 | public void thawed(Integer productId, Integer amount) { 88 | Stockpile stock = repository.findById(productId).orElseThrow(() -> new EntityNotFoundException(productId.toString())); 89 | stock.thawed(amount); 90 | repository.save(stock); 91 | log.info("解冻库存,商品:{},数量:{}", productId, amount); 92 | } 93 | 94 | /** 95 | * 设置货物数量 96 | */ 97 | public void set(Integer productId, Integer amount) { 98 | Stockpile stock = repository.findById(productId).orElseThrow(() -> new EntityNotFoundException(productId.toString())); 99 | stock.setAmount(amount); 100 | repository.save(stock); 101 | } 102 | 103 | } 104 | -------------------------------------------------------------------------------- /src/main/java/com/github/fenixsoft/bookstore/domain/payment/Wallet.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2020. the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. More information from: 15 | * 16 | * https://github.com/fenixsoft 17 | */ 18 | 19 | package com.github.fenixsoft.bookstore.domain.payment; 20 | 21 | import com.github.fenixsoft.bookstore.domain.BaseEntity; 22 | import com.github.fenixsoft.bookstore.domain.account.Account; 23 | 24 | import javax.persistence.Entity; 25 | import javax.persistence.FetchType; 26 | import javax.persistence.JoinColumn; 27 | import javax.persistence.OneToOne; 28 | 29 | /** 30 | * 用户钱包 31 | * 32 | * @author icyfenix@gmail.com 33 | * @date 2020/3/12 16:30 34 | **/ 35 | 36 | @Entity 37 | public class Wallet extends BaseEntity { 38 | 39 | // 这里是偷懒,正式项目中请使用BigDecimal来表示金额 40 | private Double money; 41 | 42 | @OneToOne(fetch = FetchType.LAZY) 43 | @JoinColumn(name = "account_id") 44 | private Account account; 45 | 46 | public Double getMoney() { 47 | return money; 48 | } 49 | 50 | public void setMoney(Double money) { 51 | this.money = money; 52 | } 53 | 54 | public Account getAccount() { 55 | return account; 56 | } 57 | 58 | public void setAccount(Account account) { 59 | this.account = account; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/com/github/fenixsoft/bookstore/domain/payment/WalletRepository.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2020. the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. More information from: 15 | * 16 | * https://github.com/fenixsoft 17 | */ 18 | 19 | package com.github.fenixsoft.bookstore.domain.payment; 20 | 21 | import org.springframework.data.repository.CrudRepository; 22 | 23 | import java.util.Optional; 24 | 25 | /** 26 | * 钱包数据仓库 27 | * 28 | * @author icyfenix@gmail.com 29 | * @date 2020/3/12 16:35 30 | **/ 31 | public interface WalletRepository extends CrudRepository { 32 | 33 | Optional findByAccountId(Integer accountId); 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/com/github/fenixsoft/bookstore/domain/payment/WalletService.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2020. the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. More information from: 15 | * 16 | * https://github.com/fenixsoft 17 | */ 18 | 19 | package com.github.fenixsoft.bookstore.domain.payment; 20 | 21 | import com.github.fenixsoft.bookstore.domain.account.Account; 22 | import org.slf4j.Logger; 23 | import org.slf4j.LoggerFactory; 24 | 25 | import javax.inject.Inject; 26 | import javax.inject.Named; 27 | 28 | /** 29 | * 用户钱包的领域服务 30 | *

31 | * 由于本工程中冻结、解冻款项的方法是为了在微服务中演示TCC事务所准备的,单体服务中由于与本地事务一同提交,无需用到 32 | * 33 | * @author icyfenix@gmail.com 34 | * @date 2020/3/12 20:23 35 | **/ 36 | @Named 37 | public class WalletService { 38 | 39 | private static final Logger log = LoggerFactory.getLogger(WalletService.class); 40 | 41 | @Inject 42 | private WalletRepository repository; 43 | 44 | /** 45 | * 账户资金减少 46 | */ 47 | public void decrease(Integer accountId, Double amount) { 48 | Wallet wallet = repository.findByAccountId(accountId).orElseGet(() -> { 49 | Wallet newWallet = new Wallet(); 50 | Account account = new Account(); 51 | account.setId(accountId); 52 | newWallet.setMoney(0D); 53 | newWallet.setAccount(account); 54 | repository.save(newWallet); 55 | return newWallet; 56 | }); 57 | if (wallet.getMoney() > amount) { 58 | wallet.setMoney(wallet.getMoney() - amount); 59 | repository.save(wallet); 60 | log.info("支付成功。用户余额:{},本次消费:{}", wallet.getMoney(), amount); 61 | } else { 62 | throw new RuntimeException("用户余额不足以支付,请先充值"); 63 | } 64 | } 65 | 66 | /** 67 | * 账户资金增加(演示程序,没有做充值入口,实际这个方法无用) 68 | */ 69 | public void increase(Integer accountId, Double amount) { 70 | } 71 | 72 | // 以下两个方法是为TCC事务准备的,在单体架构中不需要实现 73 | 74 | /** 75 | * 账户资金冻结 76 | * 从正常资金中移动指定数量至冻结状态 77 | */ 78 | public void frozen(Integer accountId, Double amount) { 79 | } 80 | 81 | /** 82 | * 账户资金解冻 83 | * 从冻结资金中移动指定数量至正常状态 84 | */ 85 | public void thawed(Integer accountId, Double amount) { 86 | } 87 | 88 | 89 | } 90 | -------------------------------------------------------------------------------- /src/main/java/com/github/fenixsoft/bookstore/domain/payment/validation/SettlementValidator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2020. the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. More information from: 15 | * 16 | * https://github.com/fenixsoft 17 | */ 18 | 19 | package com.github.fenixsoft.bookstore.domain.payment.validation; 20 | 21 | import com.github.fenixsoft.bookstore.applicaiton.payment.dto.Settlement; 22 | import com.github.fenixsoft.bookstore.domain.payment.StockpileService; 23 | 24 | import javax.inject.Inject; 25 | import javax.validation.ConstraintValidator; 26 | import javax.validation.ConstraintValidatorContext; 27 | 28 | /** 29 | * 结算单验证器 30 | *

31 | * 结算单能够成功执行的约束是清单中每一项商品的库存量都足够。 32 | *

33 | * 这个验证器的目的不在于保证商品高并发情况(如秒杀活动)下不超卖,而在于避免库存不足时仍可下单。高并发下的超卖是一种“不可重复读”现象 34 | * (即读取过的数据在事务期间被另一个事务改变),如要严谨地避免,需要把数据库的隔离级别从默认的“Read Committed”提升至“Repeatable Read” 35 | * 除了MySQL(InnoDB)外,主流的数据库,如Oracle、SQLServer默认都是Read committed,提升隔离级别会显著影响数据库的并发能力。 36 | * 37 | * @author icyfenix@gmail.com 38 | * @date 2020/3/16 9:02 39 | **/ 40 | public class SettlementValidator implements ConstraintValidator { 41 | 42 | @Inject 43 | private StockpileService service; 44 | 45 | @Override 46 | public boolean isValid(Settlement value, ConstraintValidatorContext context) { 47 | return value.getItems().stream().noneMatch(i -> service.getByProductId(i.getProductId()).getAmount() < i.getAmount()); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/com/github/fenixsoft/bookstore/domain/payment/validation/SufficientStock.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2020. the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. More information from: 15 | * 16 | * https://github.com/fenixsoft 17 | */ 18 | 19 | package com.github.fenixsoft.bookstore.domain.payment.validation; 20 | 21 | import javax.validation.Constraint; 22 | import javax.validation.Payload; 23 | import java.lang.annotation.Documented; 24 | import java.lang.annotation.Retention; 25 | import java.lang.annotation.Target; 26 | 27 | import static java.lang.annotation.ElementType.*; 28 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 29 | 30 | /** 31 | * 判断结算单中货物存量是充足的 32 | * 33 | * @author icyfenix@gmail.com 34 | * @date 2020/3/16 8:59 35 | **/ 36 | @Documented 37 | @Retention(RUNTIME) 38 | @Target({FIELD, METHOD, PARAMETER, TYPE}) 39 | @Constraint(validatedBy = SettlementValidator.class) 40 | public @interface SufficientStock { 41 | String message() default "商品库存不足"; 42 | 43 | Class[] groups() default {}; 44 | 45 | Class[] payload() default {}; 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/com/github/fenixsoft/bookstore/domain/warehouse/Advertisement.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2020. the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. More information from: 15 | * 16 | * https://github.com/fenixsoft 17 | */ 18 | 19 | package com.github.fenixsoft.bookstore.domain.warehouse; 20 | 21 | import com.github.fenixsoft.bookstore.domain.BaseEntity; 22 | 23 | import javax.persistence.Column; 24 | import javax.persistence.Entity; 25 | import javax.validation.constraints.NotEmpty; 26 | import javax.validation.constraints.NotNull; 27 | 28 | /** 29 | * 广告对象模型 30 | * 31 | * @author icyfenix@gmail.com 32 | * @date 2020/3/7 10:49 33 | **/ 34 | @Entity 35 | public class Advertisement extends BaseEntity { 36 | 37 | @NotEmpty(message = "广告图片不允许为空") 38 | private String image; 39 | 40 | @NotNull(message = "广告应当有关联的商品") 41 | @Column(name = "product_id") 42 | private Integer productId; 43 | 44 | public String getImage() { 45 | return image; 46 | } 47 | 48 | public void setImage(String image) { 49 | this.image = image; 50 | } 51 | 52 | public Integer getProductId() { 53 | return productId; 54 | } 55 | 56 | public void setProductId(Integer productId) { 57 | this.productId = productId; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/com/github/fenixsoft/bookstore/domain/warehouse/AdvertisementRepository.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2020. the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. More information from: 15 | * 16 | * https://github.com/fenixsoft 17 | */ 18 | 19 | package com.github.fenixsoft.bookstore.domain.warehouse; 20 | 21 | import org.springframework.dao.DataAccessException; 22 | import org.springframework.data.repository.CrudRepository; 23 | 24 | /** 25 | * 广告对象数据仓库 26 | * 27 | * @author icyfenix@gmail.com 28 | * @date 2020/3/7 10:51 29 | **/ 30 | public interface AdvertisementRepository extends CrudRepository { 31 | Iterable findAll() throws DataAccessException; 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/github/fenixsoft/bookstore/domain/warehouse/Product.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2020. the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. More information from: 15 | * 16 | * https://github.com/fenixsoft 17 | */ 18 | 19 | package com.github.fenixsoft.bookstore.domain.warehouse; 20 | 21 | import com.github.fenixsoft.bookstore.domain.BaseEntity; 22 | 23 | import javax.persistence.*; 24 | import javax.validation.constraints.Max; 25 | import javax.validation.constraints.Min; 26 | import javax.validation.constraints.NotEmpty; 27 | import javax.validation.constraints.NotNull; 28 | import java.util.Set; 29 | 30 | /** 31 | * 商品对象模型 32 | * 33 | * @author icyfenix@gmail.com 34 | * @date 2020/3/6 10:43 35 | */ 36 | @Entity 37 | public class Product extends BaseEntity { 38 | 39 | @NotEmpty(message = "商品名称不允许为空") 40 | private String title; 41 | 42 | @NotNull(message = "商品应当有明确的价格") 43 | @Min(value = 0, message = "商品价格最低为零") 44 | // 这里是偷懒,正式场合使用BigDecimal来表示金额 45 | private Double price; 46 | 47 | @Min(value = 0, message = "评分最低为0") 48 | @Max(value = 10, message = "评分最高为10") 49 | private Float rate; 50 | 51 | private String description; 52 | 53 | private String cover; 54 | 55 | private String detail; 56 | 57 | @OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL) 58 | @JoinColumn(name = "product_id") 59 | private Set specifications; 60 | 61 | public String getTitle() { 62 | return title; 63 | } 64 | 65 | public void setTitle(String title) { 66 | this.title = title; 67 | } 68 | 69 | public Double getPrice() { 70 | return price; 71 | } 72 | 73 | public void setPrice(Double price) { 74 | this.price = price; 75 | } 76 | 77 | public Float getRate() { 78 | return rate; 79 | } 80 | 81 | public void setRate(Float rate) { 82 | this.rate = rate; 83 | } 84 | 85 | public String getDescription() { 86 | return description; 87 | } 88 | 89 | public void setDescription(String description) { 90 | this.description = description; 91 | } 92 | 93 | public String getCover() { 94 | return cover; 95 | } 96 | 97 | public void setCover(String cover) { 98 | this.cover = cover; 99 | } 100 | 101 | public String getDetail() { 102 | return detail; 103 | } 104 | 105 | public void setDetail(String detail) { 106 | this.detail = detail; 107 | } 108 | 109 | public Set getSpecifications() { 110 | return specifications; 111 | } 112 | 113 | public void setSpecifications(Set specifications) { 114 | this.specifications = specifications; 115 | } 116 | 117 | 118 | } 119 | -------------------------------------------------------------------------------- /src/main/java/com/github/fenixsoft/bookstore/domain/warehouse/ProductRepository.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2020. the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. More information from: 15 | * 16 | * https://github.com/fenixsoft 17 | */ 18 | 19 | package com.github.fenixsoft.bookstore.domain.warehouse; 20 | 21 | import org.springframework.data.repository.CrudRepository; 22 | 23 | import java.util.Collection; 24 | 25 | /** 26 | * 商品对象数据仓库 27 | * 28 | * @author icyfenix@gmail.com 29 | * @date 2020/3/6 20:56 30 | **/ 31 | public interface ProductRepository extends CrudRepository { 32 | 33 | Collection findByIdIn(Collection ids); 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/com/github/fenixsoft/bookstore/domain/warehouse/ProductService.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2020. the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. More information from: 15 | * 16 | * https://github.com/fenixsoft 17 | */ 18 | 19 | package com.github.fenixsoft.bookstore.domain.warehouse; 20 | 21 | import com.github.fenixsoft.bookstore.applicaiton.payment.dto.Settlement; 22 | 23 | import javax.inject.Inject; 24 | import javax.inject.Named; 25 | import java.util.List; 26 | import java.util.function.Function; 27 | import java.util.stream.Collectors; 28 | 29 | /** 30 | * 产品领域服务 31 | * 32 | * @author icyfenix@gmail.com 33 | * @date 2020/3/12 20:58 34 | **/ 35 | @Named 36 | public class ProductService { 37 | 38 | @Inject 39 | private ProductRepository repository; 40 | 41 | /** 42 | * 根据结算单中货物的ID,填充货物的完整信息到结算单对象上 43 | */ 44 | public void replenishProductInformation(Settlement bill) { 45 | List ids = bill.getItems().stream().map(Settlement.Item::getProductId).collect(Collectors.toList()); 46 | bill.productMap = repository.findByIdIn(ids).stream().collect(Collectors.toMap(Product::getId, Function.identity())); 47 | } 48 | 49 | /** 50 | * 获取仓库中所有的货物信息 51 | */ 52 | public Iterable getAllProducts() { 53 | return repository.findAll(); 54 | } 55 | 56 | /** 57 | * 获取仓库中指定的货物信息 58 | */ 59 | public Product getProduct(Integer id) { 60 | return repository.findById(id).orElse(null); 61 | } 62 | 63 | /** 64 | * 创建或者更新产品信息 65 | */ 66 | public Product saveProduct(Product product) { 67 | return repository.save(product); 68 | } 69 | 70 | /** 71 | * 删除指定产品 72 | */ 73 | public void removeProduct(Integer id) { 74 | repository.deleteById(id); 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /src/main/java/com/github/fenixsoft/bookstore/domain/warehouse/Specification.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2020. the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. More information from: 15 | * 16 | * https://github.com/fenixsoft 17 | */ 18 | 19 | package com.github.fenixsoft.bookstore.domain.warehouse; 20 | 21 | import com.github.fenixsoft.bookstore.domain.BaseEntity; 22 | 23 | import javax.persistence.Column; 24 | import javax.persistence.Entity; 25 | import javax.validation.constraints.NotEmpty; 26 | import javax.validation.constraints.NotNull; 27 | 28 | /** 29 | * 商品规格 30 | * 31 | * @author icyfenix@gmail.com 32 | * @date 2020/3/6 19:33 33 | **/ 34 | @Entity 35 | public class Specification extends BaseEntity { 36 | 37 | @NotEmpty(message = "商品规格名称不允许为空") 38 | private String item; 39 | 40 | @NotEmpty(message = "商品规格内容不允许为空") 41 | private String value; 42 | 43 | @NotNull(message = "商品规格必须归属于指定商品") 44 | @Column(name = "product_id") 45 | private Integer productId; 46 | 47 | public String getItem() { 48 | return item; 49 | } 50 | 51 | public void setItem(String item) { 52 | this.item = item; 53 | } 54 | 55 | public String getValue() { 56 | return value; 57 | } 58 | 59 | public void setValue(String value) { 60 | this.value = value; 61 | } 62 | 63 | public Integer getProductId() { 64 | return productId; 65 | } 66 | 67 | public void setProductId(Integer productId) { 68 | this.productId = productId; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/main/java/com/github/fenixsoft/bookstore/infrastructure/cache/CacheConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.github.fenixsoft.bookstore.infrastructure.cache; 2 | 3 | import com.github.benmanes.caffeine.cache.Caffeine; 4 | import org.springframework.cache.Cache; 5 | import org.springframework.cache.CacheManager; 6 | import org.springframework.cache.caffeine.CaffeineCache; 7 | import org.springframework.cache.caffeine.CaffeineCacheManager; 8 | import org.springframework.context.annotation.Bean; 9 | import org.springframework.context.annotation.Configuration; 10 | 11 | import java.util.concurrent.TimeUnit; 12 | 13 | /** 14 | * 为系统提供一些代码上使用的缓存 15 | * 16 | * @author icyfenix@gmail.com 17 | * @date 2020/4/7 17:38 18 | **/ 19 | @Configuration 20 | public class CacheConfiguration { 21 | 22 | /** 23 | * 系统默认缓存TTL时间:4分钟 24 | * 一些需要用到缓存的数据,譬如支付单,需要按此数据来规划过期时间 25 | */ 26 | public static final long SYSTEM_DEFAULT_EXPIRES = 4 * 60 * 1000; 27 | 28 | @Bean 29 | public CacheManager configCacheManager() { 30 | CaffeineCacheManager manager = new CaffeineCacheManager(); 31 | manager.setCaffeine(Caffeine.newBuilder().expireAfterWrite(SYSTEM_DEFAULT_EXPIRES, TimeUnit.MILLISECONDS)); 32 | return manager; 33 | } 34 | 35 | @Bean(name = "settlement") 36 | public Cache getSettlementTTLCache() { 37 | return new CaffeineCache("settlement", Caffeine.newBuilder().expireAfterAccess(SYSTEM_DEFAULT_EXPIRES, TimeUnit.MILLISECONDS).build()); 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/com/github/fenixsoft/bookstore/infrastructure/configuration/AuthenticationServerConfiguration.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2020. the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. More information from: 15 | * 16 | * https://github.com/fenixsoft 17 | */ 18 | 19 | package com.github.fenixsoft.bookstore.infrastructure.configuration; 20 | 21 | import com.github.fenixsoft.bookstore.domain.auth.provider.PreAuthenticatedAuthenticationProvider; 22 | import com.github.fenixsoft.bookstore.domain.auth.provider.UsernamePasswordAuthenticationProvider; 23 | import com.github.fenixsoft.bookstore.domain.auth.service.AuthenticAccountDetailsService; 24 | import org.springframework.beans.factory.annotation.Autowired; 25 | import org.springframework.context.annotation.Bean; 26 | import org.springframework.context.annotation.Configuration; 27 | import org.springframework.security.authentication.AuthenticationManager; 28 | import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; 29 | import org.springframework.security.config.annotation.web.builders.WebSecurity; 30 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 31 | import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 32 | import org.springframework.security.crypto.password.PasswordEncoder; 33 | 34 | /** 35 | * Spring Security的用户认证服务器配置 36 | *

37 | * 借用Spring Security作为认证服务器,告知服务器通过怎样的途径去查询用户、加密密码和验证用户真伪 38 | * 我们实际上并不使用Spring Security提供的认证表单,而是选择了前端通过OAuth2的密码模式,在授权过程中同时完成认证 39 | * 由于服务端整套安全机制(方法授权判断、OAuth2密码模式的用户认证、密码的加密算法)仍然是构建在Spring Security基础之上 40 | * 所以我们的认证服务、用户信息服务仍然继承着Spring Security提供的基类,并在这里注册到Spring Security当中 41 | * 42 | * @author icyfenix@gmail.com 43 | * @date 2020/3/7 19:41 44 | **/ 45 | @Configuration 46 | @EnableWebSecurity 47 | public class AuthenticationServerConfiguration extends WebSecurityConfiguration { 48 | 49 | @Autowired 50 | private AuthenticAccountDetailsService authenticAccountDetailsService; 51 | 52 | @Autowired 53 | private UsernamePasswordAuthenticationProvider userProvider; 54 | 55 | @Autowired 56 | private PreAuthenticatedAuthenticationProvider preProvider; 57 | 58 | @Autowired 59 | private PasswordEncoder encoder; 60 | 61 | 62 | /** 63 | * 需要把AuthenticationManager主动暴漏出来 64 | * 以便在授权服务器{@link AuthorizationServerConfiguration}中可以使用它来完成用户名、密码的认证 65 | */ 66 | @Bean 67 | @Override 68 | public AuthenticationManager authenticationManagerBean() throws Exception { 69 | return super.authenticationManagerBean(); 70 | } 71 | 72 | /** 73 | * 配置Spring Security的安全认证服务 74 | * Spring Security的Web安全设置,将在资源服务器配置{@link ResourceServerConfiguration}中完成 75 | */ 76 | @Override 77 | protected void configure(AuthenticationManagerBuilder auth) throws Exception { 78 | auth.userDetailsService(authenticAccountDetailsService).passwordEncoder(encoder); 79 | auth.authenticationProvider(userProvider); 80 | auth.authenticationProvider(preProvider); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/main/java/com/github/fenixsoft/bookstore/infrastructure/configuration/AuthorizationServerConfiguration.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2020. the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. More information from: 15 | * 16 | * https://github.com/fenixsoft 17 | */ 18 | 19 | package com.github.fenixsoft.bookstore.infrastructure.configuration; 20 | 21 | import com.github.fenixsoft.bookstore.domain.auth.service.AuthenticAccountDetailsService; 22 | import com.github.fenixsoft.bookstore.domain.auth.service.JWTAccessTokenService; 23 | import com.github.fenixsoft.bookstore.domain.auth.service.OAuthClientDetailsService; 24 | import org.springframework.beans.factory.annotation.Autowired; 25 | import org.springframework.context.annotation.Configuration; 26 | import org.springframework.http.HttpMethod; 27 | import org.springframework.security.authentication.AuthenticationManager; 28 | import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; 29 | import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; 30 | import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; 31 | import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer; 32 | import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer; 33 | 34 | /** 35 | * Spring Security OAuth2 授权服务器配置 36 | *

37 | * 在该配置中,设置了授权服务Endpoint的相关信息(端点的位置、请求方法、使用怎样的令牌、支持怎样的客户端) 38 | * 以及针对OAuth2的密码模式所需要的用户身份认证服务和用户详情查询服务 39 | * 40 | * @author icyfenix@gmail.com 41 | * @date 2020/3/7 17:38 42 | **/ 43 | @Configuration 44 | @EnableAuthorizationServer 45 | public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter { 46 | 47 | /** 48 | * 令牌服务 49 | */ 50 | @Autowired 51 | private JWTAccessTokenService tokenService; 52 | 53 | /** 54 | * OAuth2客户端信息服务 55 | */ 56 | @Autowired 57 | private OAuthClientDetailsService clientService; 58 | 59 | /** 60 | * 认证服务管理器 61 | *

62 | * 一个认证服务管理器里面包含着多个可以从事不同认证类型的认证提供者(Provider) 63 | * 认证服务由认证服务器{@link AuthenticationServerConfiguration}定义并提供注入源 64 | */ 65 | @Autowired 66 | private AuthenticationManager authenticationManager; 67 | 68 | /** 69 | * 用户信息服务 70 | */ 71 | @Autowired 72 | private AuthenticAccountDetailsService accountService; 73 | 74 | 75 | /** 76 | * 配置客户端详情服务 77 | */ 78 | @Override 79 | public void configure(ClientDetailsServiceConfigurer clients) throws Exception { 80 | clients.withClientDetails(clientService); 81 | } 82 | 83 | /** 84 | * 配置授权的服务Endpoint 85 | *

86 | * Spring Security OAuth2会根据配置的认证服务、用户详情服务、令牌服务自动生成以下端点: 87 | * /oauth/authorize:授权端点 88 | * /oauth/token:令牌端点 89 | * /oauth/confirm_access:用户确认授权提交端点 90 | * /oauth/error:授权服务错误信息端点 91 | * /oauth/check_token:用于资源服务访问的令牌解析端点 92 | * /oauth/token_key:提供公有密匙的端点,如果JWT采用的是非对称加密加密算法,则资源服务其在鉴权时就需要这个公钥来解码 93 | * 如有必要,这些端点可以使用pathMapping()方法来修改它们的位置,使用prefix()方法来设置路径前缀 94 | */ 95 | @Override 96 | public void configure(AuthorizationServerEndpointsConfigurer endpoint) { 97 | endpoint.authenticationManager(authenticationManager) 98 | .userDetailsService(accountService) 99 | .tokenServices(tokenService) 100 | //控制TokenEndpoint端点请求访问的类型,默认HttpMethod.POST 101 | .allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST); 102 | } 103 | 104 | /** 105 | * 配置OAuth2发布出来的Endpoint本身的安全约束 106 | *

107 | * 这些端点的默认访问规则原本是: 108 | * 1. 端点开启了HTTP Basic Authentication,通过allowFormAuthenticationForClients()关闭,即允许通过表单来验证 109 | * 2. 端点的访问均为denyAll(),可以在这里通过SpringEL表达式来改变为permitAll() 110 | */ 111 | @Override 112 | public void configure(AuthorizationServerSecurityConfigurer security) { 113 | security.allowFormAuthenticationForClients().tokenKeyAccess("permitAll()").checkTokenAccess("permitAll()"); 114 | } 115 | 116 | } 117 | -------------------------------------------------------------------------------- /src/main/java/com/github/fenixsoft/bookstore/infrastructure/configuration/JerseyConfiguration.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2020. the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. More information from: 15 | * 16 | * https://github.com/fenixsoft 17 | */ 18 | 19 | package com.github.fenixsoft.bookstore.infrastructure.configuration; 20 | 21 | import org.glassfish.jersey.server.ResourceConfig; 22 | import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider; 23 | import org.springframework.context.annotation.Configuration; 24 | import org.springframework.core.type.filter.AnnotationTypeFilter; 25 | import org.springframework.util.ClassUtils; 26 | 27 | import javax.ws.rs.ApplicationPath; 28 | import javax.ws.rs.Path; 29 | import javax.ws.rs.ext.Provider; 30 | import java.util.Objects; 31 | import java.util.stream.Collectors; 32 | 33 | /** 34 | * Jersey服务器配置 35 | *

36 | * 使用Jersey来提供对JAX-RS(JSR 370:Java API for Restful Web Services)的支持 37 | * 这里设置了所有服务的前缀路径“restful”和restful服务资源的包路径 38 | * 39 | * @author icyfenix@gmail.com 40 | * @date 2020/3/6 21:10 41 | **/ 42 | @Configuration 43 | @ApplicationPath("/restful") 44 | public class JerseyConfiguration extends ResourceConfig { 45 | public JerseyConfiguration() { 46 | scanPackages("com.github.fenixsoft.bookstore.resource"); 47 | scanPackages("com.github.fenixsoft.bookstore.infrastructure.jaxrs"); 48 | } 49 | 50 | /** 51 | * Jersey的packages()方法在Jar形式运行下有问题,这里修理一下 52 | */ 53 | private void scanPackages(String scanPackage) { 54 | ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(false); 55 | scanner.addIncludeFilter(new AnnotationTypeFilter(Path.class)); 56 | scanner.addIncludeFilter(new AnnotationTypeFilter(Provider.class)); 57 | this.registerClasses(scanner.findCandidateComponents(scanPackage).stream() 58 | .map(beanDefinition -> ClassUtils.resolveClassName(Objects.requireNonNull(beanDefinition.getBeanClassName()), this.getClassLoader())) 59 | .collect(Collectors.toSet())); 60 | } 61 | 62 | 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/com/github/fenixsoft/bookstore/infrastructure/configuration/ResourceServerConfiguration.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2020. the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. More information from: 15 | * 16 | * https://github.com/fenixsoft 17 | */ 18 | 19 | package com.github.fenixsoft.bookstore.infrastructure.configuration; 20 | 21 | import com.github.fenixsoft.bookstore.domain.auth.service.JWTAccessTokenService; 22 | import org.springframework.beans.factory.annotation.Autowired; 23 | import org.springframework.context.annotation.Configuration; 24 | import org.springframework.security.access.annotation.Secured; 25 | import org.springframework.security.access.prepost.PostAuthorize; 26 | import org.springframework.security.access.prepost.PreAuthorize; 27 | import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; 28 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 29 | import org.springframework.security.config.http.SessionCreationPolicy; 30 | import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; 31 | import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter; 32 | import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer; 33 | 34 | import javax.annotation.security.RolesAllowed; 35 | 36 | /** 37 | * 资源服务器配置 38 | *

39 | * 配置资源服务访问权限,主流有两种方式: 40 | * 一是在这里通过{@link HttpSecurity}的antMatchers方法集中配置 41 | * 二是启用全局方法级安全支持{@link EnableGlobalMethodSecurity} 在各个资源的访问方法前,通过注解来逐个配置,使用的注解包括有: 42 | * JSR 250标准注解{@link RolesAllowed},可完整替代Spring的{@link Secured}功能 43 | * 以及可以使用EL表达式的Spring注解{@link PreAuthorize}、{@link PostAuthorize} 44 | * 45 | * @author icyfenix@gmail.com 46 | * @date 2020/3/7 19:43 47 | **/ 48 | @Configuration 49 | @EnableResourceServer 50 | public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter { 51 | 52 | @Autowired 53 | private JWTAccessTokenService tokenService; 54 | 55 | /** 56 | * 配置HTTP访问相关的安全选项 57 | */ 58 | public void configure(HttpSecurity http) throws Exception { 59 | // 基于JWT来绑定用户状态,所以服务端可以是无状态的 60 | http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); 61 | // 关闭CSRF(Cross Site Request Forgery)跨站请求伪造的防御 62 | // 因为需要状态存储CSRF Token才能开启该功能 63 | http.csrf().disable(); 64 | // 关闭HTTP Header中的X-Frame-Options选项,允许页面在frame标签中打开 65 | http.headers().frameOptions().disable(); 66 | // 设置服务的安全规则 67 | http.authorizeRequests().antMatchers("/oauth/**").permitAll(); 68 | } 69 | 70 | @Override 71 | public void configure(ResourceServerSecurityConfigurer resources) { 72 | resources.tokenServices(tokenService); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/main/java/com/github/fenixsoft/bookstore/infrastructure/configuration/WebSecurityConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.github.fenixsoft.bookstore.infrastructure.configuration; 2 | 3 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 4 | import org.springframework.security.config.annotation.web.builders.WebSecurity; 5 | import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 6 | 7 | /** 8 | * Spring Security安全配置 9 | *

10 | * 移除静态资源目录的安全控制,避免Spring Security默认禁止HTTP缓存的行为 11 | * 12 | * @author icyfenix@gmail.com 13 | * @date 2020/4/8 0:09 14 | **/ 15 | public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter { 16 | 17 | @Override 18 | protected void configure(HttpSecurity http) throws Exception { 19 | http.headers().cacheControl().disable(); 20 | } 21 | 22 | @Override 23 | public void configure(WebSecurity web) { 24 | web.ignoring().antMatchers("/static/**"); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/github/fenixsoft/bookstore/infrastructure/jaxrs/AccessDeniedExceptionMapper.java: -------------------------------------------------------------------------------- 1 | package com.github.fenixsoft.bookstore.infrastructure.jaxrs; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.security.access.AccessDeniedException; 6 | 7 | import javax.servlet.http.HttpServletRequest; 8 | import javax.ws.rs.core.Context; 9 | import javax.ws.rs.core.Response; 10 | import javax.ws.rs.ext.ExceptionMapper; 11 | import javax.ws.rs.ext.Provider; 12 | 13 | /** 14 | * 用于统一处理在Resource中由于Spring Security授权访问产生的异常信息 15 | * 16 | * @author icyfenix@gmail.com 17 | * @date 2020/4/7 0:09 18 | **/ 19 | @Provider 20 | public class AccessDeniedExceptionMapper implements ExceptionMapper { 21 | 22 | private static final Logger log = LoggerFactory.getLogger(AccessDeniedExceptionMapper.class); 23 | 24 | @Context 25 | private HttpServletRequest request; 26 | 27 | @Override 28 | public Response toResponse(AccessDeniedException exception) { 29 | log.warn("越权访问被禁止 {}: {}", request.getMethod(), request.getPathInfo()); 30 | return CommonResponse.send(Response.Status.FORBIDDEN, exception.getMessage()); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/github/fenixsoft/bookstore/infrastructure/jaxrs/BaseExceptionMapper.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2020. the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. More information from: 15 | * 16 | * https://github.com/fenixsoft 17 | */ 18 | 19 | package com.github.fenixsoft.bookstore.infrastructure.jaxrs; 20 | 21 | import org.slf4j.Logger; 22 | import org.slf4j.LoggerFactory; 23 | 24 | import javax.ws.rs.core.Response; 25 | import javax.ws.rs.ext.ExceptionMapper; 26 | import javax.ws.rs.ext.Provider; 27 | 28 | /** 29 | * 用于兜底的全局处理器,如果其他所有的Mapper都不合适,将由此处理把错误带到前端 30 | * 31 | * @author icyfenix@gmail.c 32 | * @date 2020/3/12 16:43 33 | **/ 34 | @Provider 35 | public class BaseExceptionMapper implements ExceptionMapper { 36 | 37 | private static final Logger log = LoggerFactory.getLogger(BaseExceptionMapper.class); 38 | 39 | @Override 40 | public Response toResponse(Throwable exception) { 41 | log.error(exception.getMessage(), exception); 42 | return CommonResponse.failure(exception.getMessage()); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/com/github/fenixsoft/bookstore/infrastructure/jaxrs/CodedMessage.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2020. the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. More information from: 15 | * 16 | * https://github.com/fenixsoft 17 | */ 18 | 19 | package com.github.fenixsoft.bookstore.infrastructure.jaxrs; 20 | 21 | import com.fasterxml.jackson.annotation.JsonInclude; 22 | 23 | /** 24 | * 带编码的实体容器 25 | *

26 | * 一般来说REST服务应采用HTTP Status Code带回错误信息编码 27 | * 但很多前端开发都习惯以JSON-RPC的风格处理异常,所以仍然保留这个编码容器 28 | * 用于返回给客户端以形式为“{code,message,data}”的对象格式 29 | * 30 | * @author icyfenix@gmail.com 31 | * @date 2020/3/6 15:34 32 | */ 33 | @JsonInclude(JsonInclude.Include.NON_NULL) 34 | public class CodedMessage { 35 | /** 36 | * 约定的成功标志 37 | */ 38 | public static final Integer CODE_SUCCESS = 0; 39 | /** 40 | * 默认的失败标志,其他失败含义可以自定义 41 | */ 42 | public static final Integer CODE_DEFAULT_FAILURE = 1; 43 | 44 | private Integer code; 45 | private String message; 46 | private Object data; 47 | 48 | public CodedMessage(Integer code, String message) { 49 | setCode(code); 50 | setMessage(message); 51 | } 52 | 53 | public Integer getCode() { 54 | return code; 55 | } 56 | 57 | public void setCode(Integer code) { 58 | this.code = code; 59 | } 60 | 61 | public String getMessage() { 62 | return message; 63 | } 64 | 65 | public void setMessage(String message) { 66 | this.message = message; 67 | } 68 | 69 | public Object getData() { 70 | return data; 71 | } 72 | 73 | public void setData(Object data) { 74 | this.data = data; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/main/java/com/github/fenixsoft/bookstore/infrastructure/jaxrs/CommonResponse.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2020. the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. More information from: 15 | * 16 | * https://github.com/fenixsoft 17 | */ 18 | 19 | package com.github.fenixsoft.bookstore.infrastructure.jaxrs; 20 | 21 | import org.slf4j.Logger; 22 | import org.slf4j.LoggerFactory; 23 | 24 | import javax.ws.rs.core.MediaType; 25 | import javax.ws.rs.core.Response; 26 | import java.util.function.Consumer; 27 | 28 | /** 29 | * 为了简化编码而设计的HTTP Response对象包装类和工具集 30 | *

31 | * 带有服务状态编码的(带有Code字段的)JavaBean领域对象包装类 32 | * Code字段的通常用于服务消费者判定该请求的业务处理是否成功。 33 | *

34 | * 统一约定: 35 | * - 当服务调用正常完成,返回Code一律以0表示 36 | * - 当服务调用产生异常,可自定义不为0的Code值,此时Message字段作为返回客户端的详细信息 37 | * 38 | * @author icyfenix@gmail.com 39 | * @date 2020/3/6 15:46 40 | **/ 41 | public abstract class CommonResponse { 42 | 43 | private static final Logger log = LoggerFactory.getLogger(CommonResponse.class); 44 | 45 | /** 46 | * 向客户端发送自定义操作信息 47 | */ 48 | public static Response send(Response.Status status, String message) { 49 | Integer code = status.getFamily() == Response.Status.Family.SUCCESSFUL ? CodedMessage.CODE_SUCCESS : CodedMessage.CODE_DEFAULT_FAILURE; 50 | return Response.status(status).type(MediaType.APPLICATION_JSON).entity(new CodedMessage(code, message)).build(); 51 | } 52 | 53 | /** 54 | * 向客户端发送操作失败的信息 55 | */ 56 | public static Response failure(String message) { 57 | return send(Response.Status.INTERNAL_SERVER_ERROR, message); 58 | } 59 | 60 | /** 61 | * 向客户端发送操作成功的信息 62 | */ 63 | public static Response success(String message) { 64 | return send(Response.Status.OK, message); 65 | } 66 | 67 | /** 68 | * 向客户端发送操作成功的信息 69 | */ 70 | public static Response success() { 71 | return send(Response.Status.OK, "操作已成功"); 72 | } 73 | 74 | /** 75 | * 执行操作,并根据操作是否成功返回给客户端相应信息 76 | * 封装了在服务端接口中很常见的执行操作,成功返回成功标志、失败返回失败标志的通用操作,用于简化编码 77 | */ 78 | public static Response op(Runnable executor) { 79 | return op(executor, e -> log.error(e.getMessage(), e)); 80 | } 81 | 82 | /** 83 | * 执行操作(带自定义的失败处理),并根据操作是否成功返回给客户端相应信息 84 | * 封装了在服务端接口中很常见的执行操作,成功返回成功标志、失败返回失败标志的通用操作,用于简化编码 85 | */ 86 | public static Response op(Runnable executor, Consumer exceptionConsumer) { 87 | try { 88 | executor.run(); 89 | return CommonResponse.success(); 90 | } catch (Exception e) { 91 | exceptionConsumer.accept(e); 92 | return CommonResponse.failure(e.getMessage()); 93 | } 94 | } 95 | 96 | } 97 | -------------------------------------------------------------------------------- /src/main/java/com/github/fenixsoft/bookstore/infrastructure/jaxrs/ViolationExceptionMapper.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2020. the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. More information from: 15 | * 16 | * https://github.com/fenixsoft 17 | */ 18 | 19 | package com.github.fenixsoft.bookstore.infrastructure.jaxrs; 20 | 21 | import org.slf4j.Logger; 22 | import org.slf4j.LoggerFactory; 23 | 24 | import javax.validation.ConstraintViolation; 25 | import javax.validation.ConstraintViolationException; 26 | import javax.ws.rs.core.Response; 27 | import javax.ws.rs.ext.ExceptionMapper; 28 | import javax.ws.rs.ext.Provider; 29 | import java.util.stream.Collectors; 30 | 31 | /** 32 | * 用于统一处理在Resource中由于验证器验证失败而带回客户端的错误信息 33 | * 34 | * @author icyfenix@gmail.com 35 | * @date 2020/3/10 23:37 36 | **/ 37 | @Provider 38 | public class ViolationExceptionMapper implements ExceptionMapper { 39 | 40 | private static final Logger log = LoggerFactory.getLogger(ViolationExceptionMapper.class); 41 | 42 | @Override 43 | public Response toResponse(ConstraintViolationException exception) { 44 | log.warn("客户端传入了校验结果为非法的数据", exception); 45 | String msg = exception.getConstraintViolations().stream().map(ConstraintViolation::getMessage).collect(Collectors.joining(";")); 46 | return CommonResponse.send(Response.Status.BAD_REQUEST, msg); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/com/github/fenixsoft/bookstore/infrastructure/utility/Encryption.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2020. the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. More information from: 15 | * 16 | * https://github.com/fenixsoft 17 | */ 18 | 19 | package com.github.fenixsoft.bookstore.infrastructure.utility; 20 | 21 | import org.springframework.context.annotation.Bean; 22 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 23 | import org.springframework.security.crypto.password.PasswordEncoder; 24 | 25 | import javax.inject.Named; 26 | import java.util.Optional; 27 | 28 | /** 29 | * 默认的加密工具 30 | * 31 | * @author icyfenix@gmail.com 32 | * @date 2020/3/10 18:02 33 | **/ 34 | @Named 35 | public class Encryption { 36 | 37 | /** 38 | * 配置认证使用的密码加密算法:BCrypt 39 | * 由于在Spring Security很多验证器中都要用到{@link PasswordEncoder}的加密,所以这里要添加@Bean注解发布出去 40 | */ 41 | @Bean 42 | public PasswordEncoder passwordEncoder() { 43 | return new BCryptPasswordEncoder(); 44 | } 45 | 46 | 47 | /** 48 | * 使用默认加密算法进行编码 49 | */ 50 | public String encode(CharSequence rawPassword) { 51 | return passwordEncoder().encode(Optional.ofNullable(rawPassword).orElse("")); 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/com/github/fenixsoft/bookstore/resource/AccountResource.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2020. the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. More information from: 15 | * 16 | * https://github.com/fenixsoft 17 | */ 18 | 19 | package com.github.fenixsoft.bookstore.resource; 20 | 21 | import com.github.fenixsoft.bookstore.applicaiton.AccountApplicationService; 22 | import com.github.fenixsoft.bookstore.domain.account.Account; 23 | import com.github.fenixsoft.bookstore.domain.account.validation.AuthenticatedAccount; 24 | import com.github.fenixsoft.bookstore.domain.account.validation.NotConflictAccount; 25 | import com.github.fenixsoft.bookstore.domain.account.validation.UniqueAccount; 26 | import com.github.fenixsoft.bookstore.infrastructure.jaxrs.CommonResponse; 27 | import org.springframework.cache.annotation.CacheConfig; 28 | import org.springframework.cache.annotation.CacheEvict; 29 | import org.springframework.cache.annotation.Cacheable; 30 | import org.springframework.stereotype.Component; 31 | 32 | import javax.inject.Inject; 33 | import javax.validation.Valid; 34 | import javax.ws.rs.*; 35 | import javax.ws.rs.core.MediaType; 36 | import javax.ws.rs.core.Response; 37 | 38 | /** 39 | * 用户资源 40 | *

41 | * 对客户端以Restful形式暴露资源,提供对用户资源{@link Account}的管理入口 42 | * 43 | * @author icyfenix@gmail.com 44 | * @date 2020/3/6 20:52 45 | **/ 46 | @Path("/accounts") 47 | @Component 48 | @CacheConfig(cacheNames = "resource.account") 49 | @Produces(MediaType.APPLICATION_JSON) 50 | @Consumes(MediaType.APPLICATION_JSON) 51 | public class AccountResource { 52 | 53 | @Inject 54 | private AccountApplicationService service; 55 | 56 | /** 57 | * 根据用户名称获取用户详情 58 | */ 59 | @GET 60 | @Path("/{username}") 61 | @Cacheable(key = "#username") 62 | public Account getUser(@PathParam("username") String username) { 63 | return service.findAccountByUsername(username); 64 | } 65 | 66 | /** 67 | * 创建新的用户 68 | */ 69 | @POST 70 | @CacheEvict(key = "#user.username") 71 | public Response createUser(@Valid @UniqueAccount Account user) { 72 | return CommonResponse.op(() -> service.createAccount(user)); 73 | } 74 | 75 | /** 76 | * 更新用户信息 77 | */ 78 | @PUT 79 | @CacheEvict(key = "#user.username") 80 | public Response updateUser(@Valid @AuthenticatedAccount @NotConflictAccount Account user) { 81 | return CommonResponse.op(() -> service.updateAccount(user)); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/main/java/com/github/fenixsoft/bookstore/resource/AdvertisementResource.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2020. the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. More information from: 15 | * 16 | * https://github.com/fenixsoft 17 | */ 18 | 19 | package com.github.fenixsoft.bookstore.resource; 20 | 21 | import com.github.fenixsoft.bookstore.domain.warehouse.Advertisement; 22 | import com.github.fenixsoft.bookstore.domain.warehouse.AdvertisementRepository; 23 | import org.springframework.cache.annotation.Cacheable; 24 | import org.springframework.stereotype.Component; 25 | 26 | import javax.inject.Inject; 27 | import javax.ws.rs.GET; 28 | import javax.ws.rs.Path; 29 | import javax.ws.rs.Produces; 30 | import javax.ws.rs.core.MediaType; 31 | 32 | /** 33 | * 广告相关的资源 34 | * 35 | * @author icyfenix@gmail.com 36 | * @date 2020/3/7 10:48 37 | **/ 38 | @Path("/advertisements") 39 | @Component 40 | @Produces(MediaType.APPLICATION_JSON) 41 | public class AdvertisementResource { 42 | 43 | @Inject 44 | AdvertisementRepository repository; 45 | 46 | @GET 47 | @Cacheable("resource.advertisements") 48 | public Iterable getAllAdvertisements() { 49 | return repository.findAll(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/com/github/fenixsoft/bookstore/resource/PaymentResource.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2020. the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. More information from: 15 | * 16 | * https://github.com/fenixsoft 17 | */ 18 | 19 | package com.github.fenixsoft.bookstore.resource; 20 | 21 | import com.github.fenixsoft.bookstore.applicaiton.payment.PaymentApplicationService; 22 | import com.github.fenixsoft.bookstore.domain.account.Account; 23 | import com.github.fenixsoft.bookstore.domain.auth.AuthenticAccount; 24 | import com.github.fenixsoft.bookstore.domain.auth.Role; 25 | import com.github.fenixsoft.bookstore.domain.payment.Payment; 26 | import com.github.fenixsoft.bookstore.domain.payment.Stockpile; 27 | import com.github.fenixsoft.bookstore.infrastructure.jaxrs.CommonResponse; 28 | import org.springframework.security.core.context.SecurityContextHolder; 29 | import org.springframework.stereotype.Component; 30 | 31 | import javax.annotation.security.RolesAllowed; 32 | import javax.inject.Inject; 33 | import javax.ws.rs.*; 34 | import javax.ws.rs.core.MediaType; 35 | import javax.ws.rs.core.Response; 36 | 37 | /** 38 | * 支付单相关的资源 39 | * 40 | * @author icyfenix@gmail.com 41 | * @date 2020/3/13 12:52 42 | **/ 43 | @Path("/pay") 44 | @Component 45 | @Produces(MediaType.APPLICATION_JSON) 46 | public class PaymentResource { 47 | 48 | @Inject 49 | private PaymentApplicationService service; 50 | 51 | /** 52 | * 修改支付单据的状态 53 | */ 54 | @PATCH 55 | @Path("/{payId}") 56 | @RolesAllowed(Role.USER) 57 | public Response updatePaymentState(@PathParam("payId") String payId, @QueryParam("state") Payment.State state) { 58 | Account account = (Account) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); 59 | return updatePaymentStateAlias(payId, account.getId(), state); 60 | } 61 | 62 | /** 63 | * 修改支付单状态的GET方法别名 64 | * 考虑到该动作要由二维码扫描来触发,只能进行GET请求,所以增加一个别名以便通过二维码调用 65 | * 这个方法原本应该作为银行支付接口的回调,不控制调用权限(谁付款都行),但都认为是购买用户付的款 66 | */ 67 | @GET 68 | @Path("/modify/{payId}") 69 | public Response updatePaymentStateAlias(@PathParam("payId") String payId, @QueryParam("accountId") Integer accountId, @QueryParam("state") Payment.State state) { 70 | if (state == Payment.State.PAYED) { 71 | return CommonResponse.op(() -> service.accomplishPayment(accountId, payId)); 72 | } else { 73 | return CommonResponse.op(() -> service.cancelPayment(payId)); 74 | } 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /src/main/java/com/github/fenixsoft/bookstore/resource/ProductResource.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2020. the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. More information from: 15 | * 16 | * https://github.com/fenixsoft 17 | */ 18 | 19 | package com.github.fenixsoft.bookstore.resource; 20 | 21 | import com.github.fenixsoft.bookstore.applicaiton.ProductApplicationService; 22 | import com.github.fenixsoft.bookstore.domain.auth.Role; 23 | import com.github.fenixsoft.bookstore.domain.payment.Stockpile; 24 | import com.github.fenixsoft.bookstore.domain.warehouse.Product; 25 | import com.github.fenixsoft.bookstore.infrastructure.jaxrs.CommonResponse; 26 | import org.springframework.cache.annotation.CacheConfig; 27 | import org.springframework.cache.annotation.CacheEvict; 28 | import org.springframework.cache.annotation.Cacheable; 29 | import org.springframework.cache.annotation.Caching; 30 | import org.springframework.stereotype.Component; 31 | 32 | import javax.annotation.security.RolesAllowed; 33 | import javax.inject.Inject; 34 | import javax.validation.Valid; 35 | import javax.ws.rs.*; 36 | import javax.ws.rs.core.MediaType; 37 | import javax.ws.rs.core.Response; 38 | 39 | /** 40 | * 产品相关的资源 41 | * 42 | * @author icyfenix@gmail.com 43 | * @date 2020/3/6 20:52 44 | **/ 45 | 46 | @Path("/products") 47 | @Component 48 | @CacheConfig(cacheNames = "resource.product") 49 | @Produces(MediaType.APPLICATION_JSON) 50 | public class ProductResource { 51 | 52 | @Inject 53 | ProductApplicationService service; 54 | 55 | /** 56 | * 获取仓库中所有的货物信息 57 | */ 58 | @GET 59 | @Cacheable(key = "'ALL_PRODUCT'") 60 | public Iterable getAllProducts() { 61 | return service.getAllProducts(); 62 | } 63 | 64 | /** 65 | * 获取仓库中指定的货物信息 66 | */ 67 | @GET 68 | @Path("/{id}") 69 | @Cacheable(key = "#id") 70 | public Product getProduct(@PathParam("id") Integer id) { 71 | return service.getProduct(id); 72 | } 73 | 74 | /** 75 | * 更新产品信息 76 | */ 77 | @PUT 78 | @Caching(evict = { 79 | @CacheEvict(key = "#product.id"), 80 | @CacheEvict(key = "'ALL_PRODUCT'") 81 | }) 82 | @RolesAllowed(Role.ADMIN) 83 | public Response updateProduct(@Valid Product product) { 84 | return CommonResponse.op(() -> service.saveProduct(product)); 85 | } 86 | 87 | /** 88 | * 创建新的产品 89 | */ 90 | @POST 91 | @Caching(evict = { 92 | @CacheEvict(key = "#product.id"), 93 | @CacheEvict(key = "'ALL_PRODUCT'") 94 | }) 95 | @RolesAllowed(Role.ADMIN) 96 | public Product createProduct(@Valid Product product) { 97 | return service.saveProduct(product); 98 | } 99 | 100 | /** 101 | * 创建新的产品 102 | */ 103 | @DELETE 104 | @Path("/{id}") 105 | @Caching(evict = { 106 | @CacheEvict(key = "#id"), 107 | @CacheEvict(key = "'ALL_PRODUCT'") 108 | }) 109 | @RolesAllowed(Role.ADMIN) 110 | public Response removeProduct(@PathParam("id") Integer id) { 111 | return CommonResponse.op(() -> service.removeProduct(id)); 112 | } 113 | 114 | /** 115 | * 将指定的产品库存调整为指定数额 116 | */ 117 | @PATCH 118 | @Path("/stockpile/{productId}") 119 | @RolesAllowed(Role.ADMIN) 120 | public Response updateStockpile(@PathParam("productId") Integer productId, @QueryParam("amount") Integer amount) { 121 | return CommonResponse.op(() -> service.setStockpileAmountByProductId(productId, amount)); 122 | } 123 | 124 | /** 125 | * 根据产品查询库存 126 | */ 127 | @GET 128 | @Path("/stockpile/{productId}") 129 | @RolesAllowed(Role.ADMIN) 130 | public Stockpile queryStockpile(@PathParam("productId") Integer productId) { 131 | return service.getStockpile(productId); 132 | } 133 | 134 | } 135 | -------------------------------------------------------------------------------- /src/main/java/com/github/fenixsoft/bookstore/resource/SettlementResource.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2020. the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. More information from: 15 | * 16 | * https://github.com/fenixsoft 17 | */ 18 | 19 | package com.github.fenixsoft.bookstore.resource; 20 | 21 | import com.github.fenixsoft.bookstore.applicaiton.payment.PaymentApplicationService; 22 | import com.github.fenixsoft.bookstore.applicaiton.payment.dto.Settlement; 23 | import com.github.fenixsoft.bookstore.domain.auth.Role; 24 | import com.github.fenixsoft.bookstore.domain.payment.Payment; 25 | import com.github.fenixsoft.bookstore.domain.payment.validation.SufficientStock; 26 | import org.springframework.stereotype.Component; 27 | 28 | import javax.annotation.security.RolesAllowed; 29 | import javax.inject.Inject; 30 | import javax.validation.Valid; 31 | import javax.ws.rs.Consumes; 32 | import javax.ws.rs.POST; 33 | import javax.ws.rs.Path; 34 | import javax.ws.rs.Produces; 35 | import javax.ws.rs.core.MediaType; 36 | 37 | /** 38 | * 结算清单相关的资源 39 | * 40 | * @author icyfenix@gmail.com 41 | * @date 2020/3/12 11:23 42 | **/ 43 | @Path("/settlements") 44 | @Component 45 | @Produces(MediaType.APPLICATION_JSON) 46 | @Consumes(MediaType.APPLICATION_JSON) 47 | public class SettlementResource { 48 | 49 | @Inject 50 | private PaymentApplicationService service; 51 | 52 | /** 53 | * 提交一张交易结算单,根据结算单中的物品,生成支付单 54 | */ 55 | @POST 56 | @RolesAllowed(Role.USER) 57 | public Payment executeSettlement(@Valid @SufficientStock Settlement settlement) { 58 | return service.executeBySettlement(settlement); 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /src/main/resources/application-mysql.yml: -------------------------------------------------------------------------------- 1 | #请在启动参数中加入--spring.profiles.active=mysql以激活本配置文件 2 | database: mysql 3 | 4 | spring: 5 | datasource: 6 | url: "jdbc:mysql://mysql_lan:3306/bookstore?useUnicode=true&characterEncoding=utf-8" 7 | username: "root" 8 | password: "[4321qwer]" 9 | initialization-mode: always 10 | -------------------------------------------------------------------------------- /src/main/resources/application-test.yml: -------------------------------------------------------------------------------- 1 | database: hsqldb 2 | 3 | spring: 4 | datasource: 5 | schema: "classpath:db/${database}/schema.sql" 6 | data: "classpath:db/${database}/data.sql" 7 | sql-script-encoding: UTF-8 8 | jpa: 9 | show-sql: true 10 | hibernate: 11 | ddl-auto: none 12 | 13 | logging: 14 | pattern: 15 | console: "%clr(%d{HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:%5p}) %clr(-){faint} %clr([%t]){faint} %clr(%-40logger{39}){cyan}[%line]%clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:%wEx}" 16 | level: 17 | root: INFO 18 | com.github.fenixsoft: DEBUG 19 | -------------------------------------------------------------------------------- /src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | database: hsqldb 2 | 3 | spring: 4 | datasource: 5 | schema: "classpath:db/${database}/schema.sql" 6 | data: "classpath:db/${database}/data.sql" 7 | sql-script-encoding: UTF-8 8 | jpa: 9 | show-sql: false 10 | hibernate: 11 | ddl-auto: none 12 | open-in-view: false 13 | resources: 14 | chain: 15 | compressed: true 16 | cache: true 17 | cache: 18 | period: 86400 19 | 20 | logging: 21 | pattern: 22 | console: "%clr(%d{HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:%5p}) %clr(-){faint} %clr([%t]){faint} %clr(%-40logger{39}){cyan}[%line]%clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:%wEx}" 23 | level: 24 | root: INFO 25 | -------------------------------------------------------------------------------- /src/main/resources/banner.txt: -------------------------------------------------------------------------------- 1 | ______ _ ____ __ _____ __ 2 | / ____/__ ____ (_) __ / __ )____ ____ / /_/ ___// /_____ ________ 3 | / /_ / _ \/ __ \/ / |/_/ / __ / __ \/ __ \/ __/\__ \/ __/ __ \/ ___/ _ \ 4 | / __/ / __/ / / / /> < / /_/ / /_/ / /_/ / /_ ___/ / /_/ /_/ / / / __/ 5 | /_/ \___/_/ /_/_/_/|_| /_____/\____/\____/\__//____/\__/\____/_/ \___/ 6 | https://icyfenix.cn 7 | -------------------------------------------------------------------------------- /src/main/resources/db/hsqldb/data.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO product VALUES (1, '深入理解Java虚拟机(第3版)', 129, 9.6, '

这是一部从工作原理和工程实践两个维度深入剖析JVM的著作,是计算机领域公认的经典,繁体版在台湾也颇受欢迎。

自2011年上市以来,前两个版本累计印刷36次,销量超过30万册,两家主要网络书店的评论近90000条,内容上近乎零差评,是原创计算机图书领域不可逾越的丰碑,第3版在第2版的基础上做了重大修订,内容更丰富、实战性更强:根据新版JDK对内容进行了全方位的修订和升级,围绕新技术和生产实践新增逾10万字,包含近50%的全新内容,并对第2版中含糊、瑕疵和错误内容进行了修正。

全书一共13章,分为五大部分:

第一部分(第1章)走近Java

系统介绍了Java的技术体系、发展历程、虚拟机家族,以及动手编译JDK,了解这部分内容能对学习JVM提供良好的指引。

第二部分(第2~5章)自动内存管理

详细讲解了Java的内存区域与内存溢出、垃圾收集器与内存分配策略、虚拟机性能监控与故障排除等与自动内存管理相关的内容,以及10余个经典的性能优化案例和优化方法;

第三部分(第6~9章)虚拟机执行子系统

深入分析了虚拟机执行子系统,包括类文件结构、虚拟机类加载机制、虚拟机字节码执行引擎,以及多个类加载及其执行子系统的实战案例;

第四部分(第10~11章)程序编译与代码优化

详细讲解了程序的前、后端编译与优化,包括前端的易用性优化措施,如泛型、主动装箱拆箱、条件编译等的内容的深入分析;以及后端的性能优化措施,如虚拟机的热点探测方法、HotSpot 的即时编译器、提前编译器,以及各种常见的编译期优化技术;

第五部分(第12~13章)高效并发

主要讲解了Java实现高并发的原理,包括Java的内存模型、线程与协程,以及线程安全和锁优化。

全书以实战为导向,通过大量与实际生产环境相结合的案例分析和展示了解决各种Java技术难题的方案和技巧。

','/static/cover/jvm3.jpg','/static/desc/jvm3.jpg'); 2 | INSERT INTO product VALUES (2, '智慧的疆界', 69, 9.1, '

这是一部对人工智能充满敬畏之心的匠心之作,由《深入理解Java虚拟机》作者耗时一年完成,它将带你从奠基人物、历史事件、学术理论、研究成果、技术应用等5个维度全面读懂人工智能。

\n

本书以时间为主线,用专业的知识、通俗的语言、巧妙的内容组织方式,详细讲解了人工智能这个学科的全貌、能解决什么问题、面临怎样的困难、尝试过哪些努力、取得过多少成绩、未来将向何方发展,尽可能消除人工智能的神秘感,把阳春白雪的人工智能从科学的殿堂推向公众面前。

','/static/cover/ai.jpg','/static/desc/ai.jpg'); 3 | INSERT INTO product VALUES (3, 'Java虚拟机规范(Java SE 8)', 79, 7.7, '

本书完整而准确地阐释了Java虚拟机各方面的细节,围绕Java虚拟机整体架构、编译器、class文件格式、加载、链接与初始化、指令集等核心主题对Java虚拟机进行全面而深入的分析,深刻揭示Java虚拟机的工作原理。同时,书中不仅完整地讲述了由Java SE 8所引入的新特性,例如对包含默认实现代码的接口方法所做的调用,还讲述了为支持类型注解及方法参数注解而对class文件格式所做的扩展,并阐明了class文件中各属性的含义,以及字节码验证的规则。

','/static/cover/jvms8.jpg',''); 4 | INSERT INTO product VALUES (4, '深入理解Java虚拟机(第2版)', 79, 9.0, '

《深入理解Java虚拟机:JVM高级特性与最佳实践(第2版)》内容简介:第1版两年内印刷近10次,4家网上书店的评论近4?000条,98%以上的评论全部为5星级的好评,是整个Java图书领域公认的经典著作和超级畅销书,繁体版在台湾也十分受欢迎。第2版在第1版的基础上做了很大的改进:根据最新的JDK 1.7对全书内容进行了全面的升级和补充;增加了大量处理各种常见JVM问题的技巧和最佳实践;增加了若干与生产环境相结合的实战案例;对第1版中的错误和不足之处的修正;等等。第2版不仅技术更新、内容更丰富,而且实战性更强。

《深入理解Java虚拟机:JVM高级特性与最佳实践(第2版)》共分为五大部分,围绕内存管理、执行子系统、程序编译与优化、高效并发等核心主题对JVM进行了全面而深入的分析,深刻揭示了JVM的工作原理。

第一部分从宏观的角度介绍了整个Java技术体系、Java和JVM的发展历程、模块化,以及JDK的编译,这对理解书中后面内容有重要帮助。

第二部分讲解了JVM的自动内存管理,包括虚拟机内存区域的划分原理以及各种内存溢出异常产生的原因;常见的垃圾收集算法以及垃圾收集器的特点和工作原理;常见虚拟机监控与故障处理工具的原理和使用方法。

第三部分分析了虚拟机的执行子系统,包括类文件结构、虚拟机类加载机制、虚拟机字节码执行引擎。

第四部分讲解了程序的编译与代码的优化,阐述了泛型、自动装箱拆箱、条件编译等语法糖的原理;讲解了虚拟机的热点探测方法、HotSpot的即时编译器、编译触发条件,以及如何从虚拟机外部观察和分析JIT编译的数据和结果;

第五部分探讨了Java实现高效并发的原理,包括JVM内存模型的结构和操作;原子性、可见性和有序性在Java内存模型中的体现;先行发生原则的规则和使用;线程在Java语言中的实现原理;虚拟机实现高效并发所做的一系列锁优化措施。

','/static/cover/jvm2.jpg','/static/desc/jvm2.jpg'); 5 | INSERT INTO product VALUES (5, 'Java虚拟机规范(Java SE 7)', 69, 8.9, '

本书整合了自1999年《Java虚拟机规范(第2版)》发布以来Java世界所出现的技术变化。另外,还修正了第2版中的许多错误,以及对目前主流Java虚拟机实现来说已经过时的内容。最后还处理了一些Java虚拟机和Java语言概念的模糊之处。

2004年发布的Java SE 5.0版为Java语言带来了翻天覆地的变化,但是对Java虚拟机设计的影响则相对较小。在Java SE 7这个版本中,我们扩充了class文件格式以便支持新的Java语言特性,譬如泛型和变长参数方法等。

','/static/cover/jvms.jpg','/static/desc/jvms.jpg'); 6 | INSERT INTO product VALUES (6, '深入理解OSGi', 79, 7.7, '

本书是原创Java技术图书领域继《深入理解Java虚拟机》后的又一实力之作,也是全球首本基于最新OSGi R5.0规范的著作。理论方面,既全面解读了OSGi规范,深刻揭示了OSGi原理,详细讲解了OSGi服务,又系统地介绍了Equinox框架的使用方法,并通过源码分析了该框架的工作机制;实践方面,不仅包含一些典型的案例,还总结了大量的最佳实践,极具实践指导意义。

全书共14章,分4个部分。第一部分(第1章):走近OSGi,主要介绍了什么是OSGi以及为什么要使用OSGi。第二部分(第2~4章):OSGi规范与原理,对最新的OSGi R5.0中的核心规范进行了全面的解读,首先讲解了OSGi模块的建立、描述、依赖关系的处理,然后讲解了Bundle的启动原理和调度管理,最后讲解了与本地及远程服务相关的内容。第三部分:OSGi服务与Equinox应用实践(第5~11章),不仅详细讲解了OSGi服务纲要规范和企业级规范中最常用的几个子规范和服务的技术细节,还通过一个基于Equinox的BBS案例演示了Equinox的使用方法,最重要的是还通过源码分析了Equinox关键功能的实现机制和原理。第四部分:最佳实践(第12~14章),总结了大量关于OSGi的最佳实践,包括从Bundle如何命名、模块划分、依赖关系处理到保持OSGi动态性、管理程序启动顺序、使用API基线管理模块版本等各方面的实践技巧,此外还介绍了Spring DM的原理以及如何在OSGi环节中进行程序测试。

','/static/cover/osgi.jpg','/static/desc/OSGi.jpg'); 7 | INSERT INTO product VALUES (7, '深入理解Java虚拟机', 69, 8.6, '

作为一位Java程序员,你是否也曾经想深入理解Java虚拟机,但是却被它的复杂和深奥拒之门外?没关系,本书极尽化繁为简之妙,能带领你在轻松中领略Java虚拟机的奥秘。本书是近年来国内出版的唯一一本与Java虚拟机相关的专著,也是唯一一本同时从核心理论和实际运用这两个角度去探讨Java虚拟机的著作,不仅理论分析得透彻,而且书中包含的典型案例和最佳实践也极具现实指导意义。

全书共分为五大部分。第一部分从宏观的角度介绍了整个Java技术体系的过去、现在和未来,以及如何独立地编译一个OpenJDK7,这对理解后面的内容很有帮助。第二部分讲解了JVM的自动内存管理,包括虚拟机内存区域的划分原理以及各种内存溢出异常产生的原因;常见的垃圾收集算法以及垃圾收集器的特点和工作原理;常见的虚拟机的监控与调试工具的原理和使用方法。第三部分分析了虚拟机的执行子系统,包括Class的文件结构以及如何存储和访问Class中的数据;虚拟机的类创建机制以及类加载器的工作原理和它对虚拟机的意义;虚拟机字节码的执行引擎以及它在实行代码时涉及的内存结构。第四部分讲解了程序的编译与代码的优化,阐述了泛型、自动装箱拆箱、条件编译等语法糖的原理;讲解了虚拟机的热点探测方法、HotSpot的即时编译器、编译触发条件,以及如何从虚拟机外部观察和分析JIT编译的数据和结果。第五部分探讨了Java实现高效并发的原理,包括JVM内存模型的结构和操作;原子性、可见性和有序性在Java内存模型中的体现;先行发生原则的规则和使用;线程在Java语言中的实现原理;虚拟机实现高效并发所做的一系列锁优化措施。

','/static/cover/jvm1.jpg',''); 8 | INSERT INTO product VALUES (8, '现代软件架构探索', 0, 0, '

这是什么?

简单地说,这是针对软件开发中不同架构、技术方案(如单体架构、微服务、服务网格、无服务架构、云原生等等)的演示程序。包含可以作为实际项目开发参考的样例代码(PetStore-Like-Project),以及作者对这些架构的使用方法、优劣理解。

','/static/cover/fenix.png','/static/desc/fenix.jpg'); 9 | 10 | INSERT INTO specification VALUES (1, '作者','周志明',1); 11 | INSERT INTO specification VALUES (2, '副标题','JVM高级特性与最佳实践',1); 12 | INSERT INTO specification VALUES (3, 'ISBN','9787111641247',1); 13 | INSERT INTO specification VALUES (4, '书名','深入理解Java虚拟机(第3版)',1); 14 | INSERT INTO specification VALUES (5, '页数', '540',1); 15 | INSERT INTO specification VALUES (6, '丛书','华章原创精品',1); 16 | INSERT INTO specification VALUES (7, '出版社','机械工业出版社',1); 17 | INSERT INTO specification VALUES (8, '出版年','2019-12',1); 18 | INSERT INTO specification VALUES (9, '装帧','平装',1); 19 | INSERT INTO specification VALUES (10, '作者','周志明',2); 20 | INSERT INTO specification VALUES (11, 'ISBN','9787111610496',2); 21 | INSERT INTO specification VALUES (12, '书名','智慧的疆界',2); 22 | INSERT INTO specification VALUES (13, '副标题','从图灵机到人工智能',2); 23 | INSERT INTO specification VALUES (14, '页数','413',2); 24 | INSERT INTO specification VALUES (15, '出版社','机械工业出版社',2); 25 | INSERT INTO specification VALUES (16, '出版年','2018-1-1',2); 26 | INSERT INTO specification VALUES (17, '装帧','平装',2); 27 | INSERT INTO specification VALUES (18, '作者','Tim Lindholm / Frank Yellin 等',3); 28 | INSERT INTO specification VALUES (19, '译者','爱飞翔 / 周志明 / 等 ',3); 29 | INSERT INTO specification VALUES (20, '原作名','The Java Virtual Machine Specification, Java SE 8 Edition',3); 30 | INSERT INTO specification VALUES (21, '丛书','Java核心技术系列',3); 31 | INSERT INTO specification VALUES (22, 'ISBN','9787111501596',3); 32 | INSERT INTO specification VALUES (23, '页数','330',3); 33 | INSERT INTO specification VALUES (24, '出版社','机械工业出版社',3); 34 | INSERT INTO specification VALUES (25, '出版年','2015-6',3); 35 | INSERT INTO specification VALUES (26, '装帧','平装',3) 36 | INSERT INTO specification VALUES (27, '作者','周志明',4); 37 | INSERT INTO specification VALUES (28, '副标题','JVM高级特性与最佳实践',4); 38 | INSERT INTO specification VALUES (29, 'ISBN','9787111421900',4); 39 | INSERT INTO specification VALUES (30, '书名','深入理解Java虚拟机(第2版)',4); 40 | INSERT INTO specification VALUES (31, '页数', '433',4); 41 | INSERT INTO specification VALUES (32, '丛书','华章原创精品',4); 42 | INSERT INTO specification VALUES (33, '出版社','机械工业出版社',4); 43 | INSERT INTO specification VALUES (34, '出版年','2013-9-1',4); 44 | INSERT INTO specification VALUES (35, '装帧','平装',4); 45 | INSERT INTO specification VALUES (36, '作者','Tim Lindholm / Frank Yellin 等',5); 46 | INSERT INTO specification VALUES (37, '译者','周志明 / 薛笛 / 吴璞渊 / 冶秀刚',5); 47 | INSERT INTO specification VALUES (38, '原作名','The Java Virtual Machine Specification, Java SE 7 Edition',5); 48 | INSERT INTO specification VALUES (39, '副标题','从图灵机到人工智能',5); 49 | INSERT INTO specification VALUES (40, 'ISBN','9787111445159',5); 50 | INSERT INTO specification VALUES (41, '页数','316',5); 51 | INSERT INTO specification VALUES (42, '出版社','机械工业出版社',5); 52 | INSERT INTO specification VALUES (43, '丛书','Java核心技术系列',5); 53 | INSERT INTO specification VALUES (44, '出版年','2014-1',5); 54 | INSERT INTO specification VALUES (45, '装帧','平装',5); 55 | INSERT INTO specification VALUES (46, '作者','周志明 / 谢小明 ',6); 56 | INSERT INTO specification VALUES (47, '副标题','Equinox原理、应用与最佳实践',6); 57 | INSERT INTO specification VALUES (48, 'ISBN','9787111408871',6); 58 | INSERT INTO specification VALUES (49, '书名','智慧的疆界',6); 59 | INSERT INTO specification VALUES (50, '丛书','华章原创精品',6); 60 | INSERT INTO specification VALUES (51, '页数','432',6); 61 | INSERT INTO specification VALUES (52, '出版社','机械工业出版社',6); 62 | INSERT INTO specification VALUES (53, '出版年','2013-2-25',6); 63 | INSERT INTO specification VALUES (54, '装帧','平装',6); 64 | INSERT INTO specification VALUES (55, '作者','周志明',7); 65 | INSERT INTO specification VALUES (56, '副标题','JVM高级特性与最佳实践',7); 66 | INSERT INTO specification VALUES (57, 'ISBN','9787111349662',7); 67 | INSERT INTO specification VALUES (58, '书名','深入理解Java虚拟机',7); 68 | INSERT INTO specification VALUES (59, '页数','387',7); 69 | INSERT INTO specification VALUES (60, '出版社','机械工业出版社',7); 70 | INSERT INTO specification VALUES (61, '出版年','2011-6',7); 71 | INSERT INTO specification VALUES (62, '装帧','平装',7); 72 | INSERT INTO specification VALUES (63, '作者','周志明',8); 73 | INSERT INTO specification VALUES (64, 'ISBN','OpenDocument',8); 74 | INSERT INTO specification VALUES (65, '书名','现代软件架构探索',8); 75 | INSERT INTO specification VALUES (66, '页数','0',8); 76 | INSERT INTO specification VALUES (67, '出版社','OpenDocument',8); 77 | INSERT INTO specification VALUES (68, '出版年','2020-2',8); 78 | INSERT INTO specification VALUES (69, '装帧','在线',8); 79 | 80 | INSERT INTO advertisement VALUES (1, '/static/carousel/fenix2.png',8); 81 | INSERT INTO advertisement VALUES (2, '/static/carousel/ai.png',2); 82 | INSERT INTO advertisement VALUES (3, '/static/carousel/jvm3.png',1); 83 | 84 | INSERT INTO stockpile VALUES (1, 30, 0, 1); 85 | INSERT INTO stockpile VALUES (2, 30, 0, 2); 86 | INSERT INTO stockpile VALUES (3, 30, 0, 3); 87 | INSERT INTO stockpile VALUES (4, 30, 0, 4); 88 | INSERT INTO stockpile VALUES (5, 30, 0, 5); 89 | INSERT INTO stockpile VALUES (6, 30, 0, 6); 90 | INSERT INTO stockpile VALUES (7, 30, 0, 7); 91 | INSERT INTO stockpile VALUES (8, 30, 0, 8); 92 | 93 | INSERT INTO account VALUES (1, 'icyfenix', '$2a$10$iIim4LtpT2yjxU2YVNDuO.yb1Z2lq86vYBZleAeuIh2aFXjyoMCM.' , '周志明', '', '18888888888', 'icyfenix@gmail.com', '唐家湾港湾大道科技一路3号远光软件股份有限公司'); 94 | INSERT INTO wallet VALUES (1, 300, 1); 95 | -------------------------------------------------------------------------------- /src/main/resources/db/hsqldb/schema.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE wallet IF EXISTS; 2 | DROP TABLE account IF EXISTS; 3 | DROP TABLE specification IF EXISTS; 4 | DROP TABLE advertisement IF EXISTS; 5 | DROP TABLE stockpile IF EXISTS; 6 | DROP TABLE product IF EXISTS; 7 | DROP TABLE payment IF EXISTS; 8 | 9 | CREATE TABLE account 10 | ( 11 | id INTEGER IDENTITY PRIMARY KEY, 12 | username VARCHAR(50), 13 | password VARCHAR(100), 14 | name VARCHAR(50), 15 | avatar VARCHAR(100), 16 | telephone VARCHAR(20), 17 | email VARCHAR(100), 18 | location VARCHAR(100) 19 | ); 20 | CREATE UNIQUE INDEX account_user ON account (username); 21 | CREATE UNIQUE INDEX account_telephone ON account (telephone); 22 | CREATE UNIQUE INDEX account_email ON account (email); 23 | 24 | CREATE TABLE wallet 25 | ( 26 | id INTEGER IDENTITY PRIMARY KEY, 27 | money DECIMAL, 28 | account_id INTEGER 29 | ); 30 | ALTER TABLE wallet 31 | ADD CONSTRAINT fk_wallet_account FOREIGN KEY (account_id) REFERENCES account (id) ON DELETE CASCADE; 32 | 33 | CREATE TABLE product 34 | ( 35 | id INTEGER IDENTITY PRIMARY KEY, 36 | title VARCHAR(50), 37 | price DECIMAL, 38 | rate FLOAT, 39 | description VARCHAR(8000), 40 | cover VARCHAR(100), 41 | detail VARCHAR(100) 42 | ); 43 | CREATE INDEX product_title ON product (title); 44 | 45 | CREATE TABLE stockpile 46 | ( 47 | id INTEGER IDENTITY PRIMARY KEY, 48 | amount INTEGER, 49 | frozen INTEGER, 50 | product_id INTEGER 51 | ); 52 | ALTER TABLE stockpile 53 | ADD CONSTRAINT fk_stockpile_product FOREIGN KEY (product_id) REFERENCES product (id) ON DELETE CASCADE; 54 | 55 | CREATE TABLE specification 56 | ( 57 | id INTEGER IDENTITY PRIMARY KEY, 58 | item VARCHAR(50), 59 | value VARCHAR(100), 60 | product_id INTEGER 61 | ); 62 | ALTER TABLE specification 63 | ADD CONSTRAINT fk_specification_product FOREIGN KEY (product_id) REFERENCES product (id) ON DELETE CASCADE; 64 | 65 | CREATE TABLE advertisement 66 | ( 67 | id INTEGER IDENTITY PRIMARY KEY, 68 | image VARCHAR(100), 69 | product_id INTEGER 70 | ); 71 | ALTER TABLE advertisement 72 | ADD CONSTRAINT fk_advertisement_product FOREIGN KEY (product_id) REFERENCES product (id) ON DELETE CASCADE; 73 | 74 | CREATE TABLE payment 75 | ( 76 | id INTEGER IDENTITY PRIMARY KEY, 77 | pay_id VARCHAR(100), 78 | create_time DATETIME, 79 | total_price DECIMAL, 80 | expires INTEGER NOT NULL, 81 | payment_link VARCHAR(300), 82 | pay_state VARCHAR(20) 83 | ); 84 | -------------------------------------------------------------------------------- /src/main/resources/db/mysql/schema.sql: -------------------------------------------------------------------------------- 1 | DROP TABLE IF EXISTS specification; 2 | DROP TABLE IF EXISTS advertisement; 3 | DROP TABLE IF EXISTS stockpile; 4 | DROP TABLE IF EXISTS payment; 5 | DROP TABLE IF EXISTS wallet; 6 | DROP TABLE IF EXISTS account; 7 | DROP TABLE IF EXISTS product; 8 | 9 | CREATE TABLE IF NOT EXISTS account 10 | ( 11 | id INTEGER UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, 12 | username VARCHAR(50), 13 | password VARCHAR(100), 14 | name VARCHAR(50), 15 | avatar VARCHAR(100), 16 | telephone VARCHAR(20), 17 | email VARCHAR(100), 18 | location VARCHAR(100), 19 | INDEX (username) 20 | ) engine = InnoDB; 21 | 22 | CREATE TABLE IF NOT EXISTS wallet 23 | ( 24 | id INTEGER UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, 25 | money DECIMAL, 26 | account_id INTEGER UNSIGNED, 27 | FOREIGN KEY (account_id) REFERENCES account (id) ON DELETE CASCADE 28 | ) engine = InnoDB; 29 | 30 | CREATE TABLE IF NOT EXISTS product 31 | ( 32 | id INTEGER UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, 33 | title VARCHAR(50), 34 | price DECIMAL, 35 | rate FLOAT, 36 | description VARCHAR(8000), 37 | cover VARCHAR(100), 38 | detail VARCHAR(100), 39 | INDEX (title) 40 | ) engine = InnoDB; 41 | 42 | CREATE TABLE IF NOT EXISTS stockpile 43 | ( 44 | id INTEGER UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, 45 | amount INTEGER, 46 | frozen INTEGER, 47 | product_id INTEGER UNSIGNED, 48 | FOREIGN KEY (product_id) REFERENCES product (id) ON DELETE CASCADE 49 | ) engine = InnoDB; 50 | 51 | CREATE TABLE IF NOT EXISTS specification 52 | ( 53 | id INTEGER UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, 54 | item VARCHAR(50), 55 | value VARCHAR(100), 56 | product_id INTEGER UNSIGNED, 57 | FOREIGN KEY (product_id) REFERENCES product (id) ON DELETE CASCADE 58 | ) engine = InnoDB; 59 | 60 | CREATE TABLE IF NOT EXISTS advertisement 61 | ( 62 | id INTEGER UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, 63 | image VARCHAR(100), 64 | product_id INTEGER UNSIGNED, 65 | FOREIGN KEY (product_id) REFERENCES product (id) ON DELETE CASCADE 66 | ) engine = InnoDB; 67 | 68 | CREATE TABLE IF NOT EXISTS payment 69 | ( 70 | id INTEGER UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, 71 | pay_id VARCHAR(100), 72 | create_time DATETIME, 73 | total_price DECIMAL, 74 | expires INTEGER NOT NULL, 75 | payment_link VARCHAR(300), 76 | pay_state VARCHAR(20) 77 | ) engine = InnoDB; 78 | -------------------------------------------------------------------------------- /src/main/resources/db/mysql/user.sql: -------------------------------------------------------------------------------- 1 | CREATE DATABASE IF NOT EXISTS bookstore; 2 | 3 | ALTER DATABASE bookstore 4 | DEFAULT CHARACTER SET utf8 5 | DEFAULT COLLATE utf8_general_ci; 6 | 7 | GRANT ALL PRIVILEGES ON bookstore.* TO 'bookstore@%' IDENTIFIED BY 'bookstore'; 8 | -------------------------------------------------------------------------------- /src/main/resources/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fenixsoft/serverless_arch_awslambda/3e80ecf2a4a6582b13459c410db887f3d92b6634/src/main/resources/static/favicon.ico -------------------------------------------------------------------------------- /src/main/resources/static/index.html: -------------------------------------------------------------------------------- 1 | Fenix's BookStore
-------------------------------------------------------------------------------- /src/main/resources/static/static/board/gitalk.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
6 | 7 | 20 | 21 | -------------------------------------------------------------------------------- /src/main/resources/static/static/carousel/ai.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fenixsoft/serverless_arch_awslambda/3e80ecf2a4a6582b13459c410db887f3d92b6634/src/main/resources/static/static/carousel/ai.png -------------------------------------------------------------------------------- /src/main/resources/static/static/carousel/fenix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fenixsoft/serverless_arch_awslambda/3e80ecf2a4a6582b13459c410db887f3d92b6634/src/main/resources/static/static/carousel/fenix.png -------------------------------------------------------------------------------- /src/main/resources/static/static/carousel/fenix2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fenixsoft/serverless_arch_awslambda/3e80ecf2a4a6582b13459c410db887f3d92b6634/src/main/resources/static/static/carousel/fenix2.png -------------------------------------------------------------------------------- /src/main/resources/static/static/carousel/jvm3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fenixsoft/serverless_arch_awslambda/3e80ecf2a4a6582b13459c410db887f3d92b6634/src/main/resources/static/static/carousel/jvm3.png -------------------------------------------------------------------------------- /src/main/resources/static/static/cover/ai.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fenixsoft/serverless_arch_awslambda/3e80ecf2a4a6582b13459c410db887f3d92b6634/src/main/resources/static/static/cover/ai.jpg -------------------------------------------------------------------------------- /src/main/resources/static/static/cover/fenix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fenixsoft/serverless_arch_awslambda/3e80ecf2a4a6582b13459c410db887f3d92b6634/src/main/resources/static/static/cover/fenix.png -------------------------------------------------------------------------------- /src/main/resources/static/static/cover/jvm1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fenixsoft/serverless_arch_awslambda/3e80ecf2a4a6582b13459c410db887f3d92b6634/src/main/resources/static/static/cover/jvm1.jpg -------------------------------------------------------------------------------- /src/main/resources/static/static/cover/jvm2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fenixsoft/serverless_arch_awslambda/3e80ecf2a4a6582b13459c410db887f3d92b6634/src/main/resources/static/static/cover/jvm2.jpg -------------------------------------------------------------------------------- /src/main/resources/static/static/cover/jvm3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fenixsoft/serverless_arch_awslambda/3e80ecf2a4a6582b13459c410db887f3d92b6634/src/main/resources/static/static/cover/jvm3.jpg -------------------------------------------------------------------------------- /src/main/resources/static/static/cover/jvms.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fenixsoft/serverless_arch_awslambda/3e80ecf2a4a6582b13459c410db887f3d92b6634/src/main/resources/static/static/cover/jvms.jpg -------------------------------------------------------------------------------- /src/main/resources/static/static/cover/jvms8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fenixsoft/serverless_arch_awslambda/3e80ecf2a4a6582b13459c410db887f3d92b6634/src/main/resources/static/static/cover/jvms8.jpg -------------------------------------------------------------------------------- /src/main/resources/static/static/cover/osgi.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fenixsoft/serverless_arch_awslambda/3e80ecf2a4a6582b13459c410db887f3d92b6634/src/main/resources/static/static/cover/osgi.jpg -------------------------------------------------------------------------------- /src/main/resources/static/static/desc/OSGi.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fenixsoft/serverless_arch_awslambda/3e80ecf2a4a6582b13459c410db887f3d92b6634/src/main/resources/static/static/desc/OSGi.jpg -------------------------------------------------------------------------------- /src/main/resources/static/static/desc/ai.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fenixsoft/serverless_arch_awslambda/3e80ecf2a4a6582b13459c410db887f3d92b6634/src/main/resources/static/static/desc/ai.jpg -------------------------------------------------------------------------------- /src/main/resources/static/static/desc/fenix.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fenixsoft/serverless_arch_awslambda/3e80ecf2a4a6582b13459c410db887f3d92b6634/src/main/resources/static/static/desc/fenix.jpg -------------------------------------------------------------------------------- /src/main/resources/static/static/desc/jvm2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fenixsoft/serverless_arch_awslambda/3e80ecf2a4a6582b13459c410db887f3d92b6634/src/main/resources/static/static/desc/jvm2.jpg -------------------------------------------------------------------------------- /src/main/resources/static/static/desc/jvm3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fenixsoft/serverless_arch_awslambda/3e80ecf2a4a6582b13459c410db887f3d92b6634/src/main/resources/static/static/desc/jvm3.jpg -------------------------------------------------------------------------------- /src/main/resources/static/static/desc/jvms.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fenixsoft/serverless_arch_awslambda/3e80ecf2a4a6582b13459c410db887f3d92b6634/src/main/resources/static/static/desc/jvms.jpg -------------------------------------------------------------------------------- /src/main/resources/static/static/fonts/element-icons.535877f.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fenixsoft/serverless_arch_awslambda/3e80ecf2a4a6582b13459c410db887f3d92b6634/src/main/resources/static/static/fonts/element-icons.535877f.woff -------------------------------------------------------------------------------- /src/main/resources/static/static/fonts/element-icons.732389d.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fenixsoft/serverless_arch_awslambda/3e80ecf2a4a6582b13459c410db887f3d92b6634/src/main/resources/static/static/fonts/element-icons.732389d.ttf -------------------------------------------------------------------------------- /src/main/resources/static/static/img/bg2.ef8085e.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fenixsoft/serverless_arch_awslambda/3e80ecf2a4a6582b13459c410db887f3d92b6634/src/main/resources/static/static/img/bg2.ef8085e.png -------------------------------------------------------------------------------- /src/main/resources/static/static/img/cc-logo.3653e37.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fenixsoft/serverless_arch_awslambda/3e80ecf2a4a6582b13459c410db887f3d92b6634/src/main/resources/static/static/img/cc-logo.3653e37.png -------------------------------------------------------------------------------- /src/main/resources/static/static/img/logo-color.5500ec5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fenixsoft/serverless_arch_awslambda/3e80ecf2a4a6582b13459c410db887f3d92b6634/src/main/resources/static/static/img/logo-color.5500ec5.png -------------------------------------------------------------------------------- /src/main/resources/static/static/img/logo-gray-light.84aa074.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fenixsoft/serverless_arch_awslambda/3e80ecf2a4a6582b13459c410db887f3d92b6634/src/main/resources/static/static/img/logo-gray-light.84aa074.png -------------------------------------------------------------------------------- /src/main/resources/static/static/js/0.c178f427b3d08777c70f.js: -------------------------------------------------------------------------------- 1 | webpackJsonp([0],{"7WGw":function(t,e,s){"use strict";var a={name:"PayStepIndicator",props:{step:Number}},i={render:function(){var t=this.$createElement,e=this._self._c||t;return e("el-card",{staticClass:"box-card",staticStyle:{"margin-top":"20px"}},[e("div",{staticClass:"header",attrs:{slot:"header"},slot:"header"},[e("span",[this._v("购买流程")])]),this._v(" "),e("div",{staticClass:"content",staticStyle:{padding:"0 100px"}},[e("el-steps",{attrs:{active:this.step,"align-center":""}},[e("el-step",{attrs:{title:"我的购物车",description:"在购物车中确认每件商品的价格、数量"}}),this._v(" "),e("el-step",{attrs:{title:"我的结算单",description:"在结算单中确认配送地址、支付信息"}}),this._v(" "),e("el-step",{attrs:{title:"支付",description:"通过微信、支付宝完成付款,等待收货"}})],1)],1)])},staticRenderFns:[]};var n=s("VU/8")(a,i,!1,function(t){s("ne/a")},"data-v-9c9995a8",null);e.a=n.exports},"ne/a":function(t,e){}}); -------------------------------------------------------------------------------- /src/main/resources/static/static/js/3.bc7f0b2154007257c317.js: -------------------------------------------------------------------------------- 1 | webpackJsonp([3],{"2Lpz":function(e,t){},Cp5v:function(e,t){},P7ry:function(e,t,a){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r=a("Xxa5"),n=a.n(r),o=a("exGp"),s=a.n(o),i=a("Dd8w"),l=a.n(i),c=a("NYxO"),u=a("gyMJ"),p={render:function(){var e=this,t=e.$createElement,r=e._self._c||t;return r("div",[r("img",{staticClass:"logo",attrs:{src:a("lgL1")}}),e._v(" "),r("span",{staticClass:"title"},[e._v("Fenix's Bookstore")]),e._v(" "),r("el-form",{ref:"login-form",staticClass:"login-form",attrs:{model:e.authorization,rules:e.rules}},[r("el-form-item",{attrs:{prop:"name"}},[r("el-input",{attrs:{placeholder:"请输入用户"},model:{value:e.authorization.name,callback:function(t){e.$set(e.authorization,"name",t)},expression:"authorization.name"}},[r("template",{slot:"prepend"},[r("i",{staticClass:"el-icon-user"})])],2)],1),e._v(" "),r("el-form-item",{attrs:{prop:"password"}},[r("el-input",{attrs:{placeholder:"请输入密码","show-password":""},model:{value:e.authorization.password,callback:function(t){e.$set(e.authorization,"password",t)},expression:"authorization.password"}},[r("template",{slot:"prepend"},[r("i",{staticClass:"el-icon-unlock"})])],2)],1),e._v(" "),r("el-select",{staticStyle:{width:"370px"},attrs:{placeholder:"请选择语言"},model:{value:e.authorization.language,callback:function(t){e.$set(e.authorization,"language",t)},expression:"authorization.language"}},[r("el-option",{attrs:{label:" 中文",value:"zhCN"}}),e._v(" "),r("el-option",{attrs:{label:" 英文(无效,国际化预留)",value:"enUS"}}),e._v(" "),r("template",{slot:"prefix"},[r("div",{staticClass:"select-prefix"},[r("i",{staticClass:"el-icon-map-location"})])])],2),e._v(" "),r("template",{slot:"prepend"},[r("i",{staticClass:"el-icon-map-location"})]),e._v(" "),r("div",{staticClass:"actions"},[r("el-checkbox",{staticClass:"check",model:{value:e.authorization.rememberMe,callback:function(t){e.$set(e.authorization,"rememberMe",t)},expression:"authorization.rememberMe"}},[e._v("\n 自动登录\n ")]),e._v(" "),r("el-button",{staticStyle:{float:"right",display:"inline-block",padding:"0 10px 0 0"},attrs:{type:"text"},on:{click:function(t){return e.$emit("changeMode")}}},[e._v("注册新用户\n ")]),e._v(" "),r("el-button",{staticStyle:{width:"100%",display:"block",margin:"50px 0 0 0"},attrs:{type:"primary"},on:{click:e.login}},[e._v("登录")])],1),e._v(" "),r("hr"),e._v(" "),r("div",{staticStyle:{"text-align":"center"}},[e._v("\n 登录代表你已同意\n "),r("el-tooltip",{attrs:{effect:"dark",content:"演示用途,并没有写",placement:"bottom"}},[r("el-button",{attrs:{type:"text"}},[e._v("用户协议")])],1),e._v("\n 和\n "),r("el-tooltip",{attrs:{effect:"dark",content:"也是没有写",placement:"bottom"}},[r("el-button",{staticStyle:{"margin-left":"0"},attrs:{type:"text"}},[e._v("隐私政策")])],1)],1)],2)],1)},staticRenderFns:[]};var m=a("VU/8")({name:"LoginForm",data:function(){return{authorization:{name:"",password:"",language:"zhCN",rememberMe:!1},rules:{name:[{required:!0,message:"请输入用户名称",trigger:"blur"}],password:[{required:!0,message:"请输入密码",trigger:"blur"}]}}},methods:{login:function(){var e=this;this.$refs["login-form"].validate(function(t){if(!t)return!1;e.$emit("login",e.authorization)})}}},p,!1,function(e){a("Cp5v")},"data-v-73f7116f",null).exports,d={name:"RegistrationForm",data:function(){return{account:{username:"",email:"",password:"",telephone:""},rules:{username:[{required:!0,message:"请填写用户名",trigger:"blur"}],name:[{required:!0,message:"请填写真实姓名",trigger:"blur"}],password:[{required:!0,message:"请填写密码",trigger:"blur"}],email:[{required:!0,message:"请填写邮箱",trigger:"blur"},{type:"email",message:"不符合邮箱格式",trigger:"blur"}],telephone:[{required:!0,message:"请填写手机",trigger:"blur"}]}}},methods:l()({},Object(c.d)("user",["setupSession"]),{registerAccount:function(){var e=this;this.$refs.account_form.validate(function(t){if(!t)return!1;e.submitRegistration()})},submitRegistration:function(){var e=this;return s()(n.a.mark(function t(){var a,r;return n.a.wrap(function(t){for(;;)switch(t.prev=t.next){case 0:return t.prev=0,t.next=3,u.a.account.registerAccount(e.account);case 3:if(a=t.sent,(r=a.data).code!==u.a.constants.REMOTE_OPERATION_SUCCESS){t.next=14;break}return t.next=8,u.a.auth.login(e.account.username,e.account.password);case 8:return(r=t.sent.data).rememberMe=!1,r.language="zhCN",e.setupSession(r),e.$router.push("/"),t.abrupt("return");case 14:t.next=20;break;case 16:t.prev=16,t.t0=t.catch(0),console.error(t.t0),e.$alert(t.t0.message,"出现异常");case 20:case"end":return t.stop()}},t,e,[[0,16]])}))()}})},g={render:function(){var e=this,t=e.$createElement,r=e._self._c||t;return r("div",[r("img",{staticClass:"logo",attrs:{src:a("lgL1")}}),e._v(" "),r("span",{staticClass:"title"},[e._v("新用户注册")]),e._v(" "),r("el-form",{ref:"account_form",staticClass:"account_form",attrs:{model:e.account,rules:e.rules,"label-position":"left"}},[r("el-form-item",{attrs:{prop:"user"}},[r("el-input",{attrs:{placeholder:"请输入用户名"},model:{value:e.account.username,callback:function(t){e.$set(e.account,"username",t)},expression:"account.username"}},[r("template",{slot:"prepend"},[r("i",{staticClass:"el-icon-user"})])],2)],1),e._v(" "),r("el-form-item",{attrs:{prop:"password"}},[r("el-input",{attrs:{placeholder:"请输入密码","show-password":""},model:{value:e.account.password,callback:function(t){e.$set(e.account,"password",t)},expression:"account.password"}},[r("template",{slot:"prepend"},[r("i",{staticClass:"el-icon-unlock"})])],2)],1),e._v(" "),r("el-form-item",{attrs:{prop:"name"}},[r("el-input",{attrs:{placeholder:"请输入真实姓名"},model:{value:e.account.name,callback:function(t){e.$set(e.account,"name",t)},expression:"account.name"}},[r("template",{slot:"prepend"},[r("i",{staticClass:"el-icon-user"})])],2)],1),e._v(" "),r("el-form-item",{attrs:{prop:"email"}},[r("el-input",{attrs:{placeholder:"请输入邮箱"},model:{value:e.account.email,callback:function(t){e.$set(e.account,"email",t)},expression:"account.email"}},[r("template",{slot:"prepend"},[r("i",{staticClass:"el-icon-receiving"})])],2)],1),e._v(" "),r("el-form-item",{attrs:{prop:"telephone"}},[r("el-input",{attrs:{placeholder:"请输入手机"},model:{value:e.account.telephone,callback:function(t){e.$set(e.account,"telephone",t)},expression:"account.telephone"}},[r("template",{slot:"prepend"},[r("i",{staticClass:"el-icon-phone-outline"})])],2)],1),e._v(" "),r("div",{staticClass:"actions"},[r("el-button",{staticClass:"action_button",attrs:{type:"primary"},on:{click:e.registerAccount}},[e._v("注册")]),e._v(" "),r("el-button",{staticClass:"action_button",on:{click:function(t){return e.$emit("changeMode")}}},[e._v("返回\n ")])],1)],1)],1)},staticRenderFns:[]};var f={name:"Login",components:{LoginForm:m,RegistrationForm:a("VU/8")(d,g,!1,function(e){a("2Lpz")},"data-v-48aa8087",null).exports},data:function(){return{registrationMode:!1}},computed:{nextPath:function(){return this.$route.query.redirect?this.$route.query.redirect:"/"}},methods:l()({},Object(c.d)("user",["setupSession"]),{login:function(e){var t=this;return s()(n.a.mark(function a(){var r,o;return n.a.wrap(function(a){for(;;)switch(a.prev=a.next){case 0:return a.prev=0,a.next=3,u.a.auth.login(e.name,e.password);case 3:r=a.sent,(o=r.data).rememberMe=e.rememberMe,o.language=e.language,t.setupSession(o),t.$router.push(t.nextPath),a.next=14;break;case 11:a.prev=11,a.t0=a.catch(0),t.$alert(a.t0.message,"出现异常");case 14:case"end":return a.stop()}},a,t,[[0,11]])}))()}})},v={render:function(){var e=this,t=e.$createElement,a=e._self._c||t;return a("div",{staticClass:"bg"},[a("div",{staticClass:"dialog dialog-shadow"},[e.registrationMode?e._e():a("LoginForm",{on:{changeMode:function(t){e.registrationMode=!e.registrationMode},login:e.login}}),e._v(" "),e.registrationMode?a("RegistrationForm",{on:{changeMode:function(t){e.registrationMode=!e.registrationMode}}}):e._e()],1)])},staticRenderFns:[]};var h=a("VU/8")(f,v,!1,function(e){a("bl2P")},"data-v-350afc81",null);t.default=h.exports},bl2P:function(e,t){},lgL1:function(e,t,a){e.exports=a.p+"static/img/logo-color.5500ec5.png"}}); -------------------------------------------------------------------------------- /src/main/resources/static/static/js/4.b4e48a42cf742af20851.js: -------------------------------------------------------------------------------- 1 | webpackJsonp([4],{"/V72":function(t,e){},"2+ps":function(t,e){},GBxx:function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r=n("Xxa5"),i=n.n(r),a=n("exGp"),s=n.n(a),o=n("gyMJ"),c={name:"Carousel",data:function(){return{advertisements:[]}},created:function(){var t=this;return s()(i.a.mark(function e(){return i.a.wrap(function(e){for(;;)switch(e.prev=e.next){case 0:return e.next=2,o.a.warehouse.getAdvertisements();case 2:t.advertisements=e.sent.data;case 3:case"end":return e.stop()}},e,t)}))()},methods:{loadDetail:function(t){this.$router.push("/detail/"+t)}}},u={render:function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("el-carousel",{attrs:{interval:5e3,type:"card",height:"400px"}},t._l(t.advertisements,function(e){return n("el-carousel-item",{key:e.id},[n("el-image",{staticClass:"image",attrs:{src:e.image},on:{click:function(n){return t.loadDetail(e.productId)}}})],1)}),1)},staticRenderFns:[]};var l=n("VU/8")(c,u,!1,function(t){n("/V72")},"data-v-5faa9004",null).exports,d=n("Dd8w"),f=n.n(d),v=n("NYxO"),p={name:"Cabinet",data:function(){return{books:[]}},computed:f()({},Object(v.e)("user",["favorite","account"]),Object(v.e)("cart",["items"])),created:function(){var t=this;return s()(i.a.mark(function e(){return i.a.wrap(function(e){for(;;)switch(e.prev=e.next){case 0:return e.next=2,o.a.warehouse.getAllProducts();case 2:t.books=e.sent.data;case 3:case"end":return e.stop()}},e,t)}))()},methods:f()({},Object(v.d)("user",["addFavorite","removeFavorite"]),Object(v.d)("cart",["addCartItem","removeCartItem"]),Object(v.b)("cart",["setupSettlementBillWithDefaultValue"]),{isFavorite:function(t){return this.favorite.includes(t)},isInCart:function(t){return this.items.find(function(e){return e.id===t})},updateFavorite:function(t){this.isFavorite(t)?this.removeFavorite(t):this.addFavorite(t),this.$notify({title:"成功",message:"恭喜你,已成功更新收藏夹",iconClass:"el-icon-star-on",type:"success"})},updateCart:function(t){this.isInCart(t)?this.removeCartItem(t):this.addCartItem(f()({},this.books.find(function(e){return e.id===t}))),this.$notify({title:"成功",message:"恭喜你,已成功更新购物车",iconClass:"el-icon-s-goods",type:"success"})},loadDetail:function(t){this.$router.push("/detail/"+t)},pureText:function(t){return o.a.stringUtil.pureText(t)},goDirectSettlement:function(t){var e=f()({},t,{amount:1});this.setupSettlementBillWithDefaultValue({items:[e]}),this.$router.push("/settle")}})},m={render:function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("el-card",{staticClass:"box-card"},[n("div",{staticClass:"header",attrs:{slot:"header"},slot:"header"},[n("span",[t._v("热销书籍")])]),t._v(" "),n("el-row",{attrs:{gutter:0}},t._l(t.books,function(e){return n("el-col",{key:e.id,staticClass:"book-container",attrs:{span:6}},[n("el-image",{staticClass:"image",attrs:{src:e.cover},on:{click:function(n){return t.loadDetail(e.id)}}}),t._v(" "),n("div",{staticStyle:{padding:"14px"}},[n("span",{attrs:{id:"price"}},[t._v("¥ "+t._s(e.price.toFixed(2)))]),t._v(" "),n("span",{attrs:{id:"title"}},[t._v(t._s(e.title))]),t._v(" "),n("span",{attrs:{id:"description"}},[t._v(t._s(t.pureText(e.description)))]),t._v(" "),n("div",{attrs:{id:"actions"}},[n("el-button",{attrs:{icon:"el-icon-money",circle:""},on:{click:function(n){return t.goDirectSettlement(e)}}}),t._v(" "),n("el-button",{attrs:{icon:t.isInCart(e.id)?"el-icon-s-goods":"el-icon-goods",circle:""},on:{click:function(n){return t.updateCart(e.id)}}}),t._v(" "),n("el-button",{attrs:{icon:t.isFavorite(e.id)?"el-icon-star-on":"el-icon-star-off",circle:""},on:{click:function(n){return t.updateFavorite(e.id)}}})],1)])],1)}),1)],1)},staticRenderFns:[]};var h={name:"MainPage",components:{Carousel:l,Cabinet:n("VU/8")(p,m,!1,function(t){n("eX9T")},"data-v-43226b54",null).exports}},_={render:function(){var t=this.$createElement,e=this._self._c||t;return e("div",[e("Carousel"),this._v(" "),e("Cabinet")],1)},staticRenderFns:[]};var b=n("VU/8")(h,_,!1,function(t){n("2+ps")},"data-v-24b3789e",null);e.default=b.exports},eX9T:function(t,e){}}); -------------------------------------------------------------------------------- /src/main/resources/static/static/js/5.d375cbd6c7e1463cdbed.js: -------------------------------------------------------------------------------- 1 | webpackJsonp([5],{SmJH:function(t,e){},eRfs:function(t,e){},"f/ib":function(t,e){},hplB:function(t,e,r){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var o=r("woOf"),a=r.n(o),i=r("Xxa5"),n=r.n(i),s=r("exGp"),c=r.n(s),l=r("Dd8w"),u=r.n(l),d=r("gyMJ"),p=r("NYxO"),f={name:"ProductManage",props:{createMode:Boolean,product:Object},data:function(){return{inputVisible:!1,inputValue:""}},methods:{refresh:function(){},submitProduct:function(){var t=this;return c()(n.a.mark(function e(){var r,o;return n.a.wrap(function(e){for(;;)switch(e.prev=e.next){case 0:return r=d.a.stringUtil.pureText(t.product.description),r=d.a.stringUtil.transToHTML(r),o=t.createMode?d.a.warehouse.createProduct:d.a.warehouse.updateProduct,e.prev=3,e.next=6,o(u()({},t.product,{description:r}));case 6:t.$notify({title:"操作成功",message:t.createMode?"商品已成功创建":"商品信息已修改",type:"success"}),t.$emit("updated"),e.next=13;break;case 10:e.prev=10,e.t0=e.catch(3),t.$notify({title:"操作失败",message:e.t0.message,type:"error"});case 13:case"end":return e.stop()}},e,t,[[3,10]])}))()},removeSpecification:function(t){this.product.specifications.splice(this.product.specifications.indexOf(t),1)},showInput:function(){var t=this;this.inputVisible=!0,this.$nextTick(function(e){t.$refs.saveTagInput.$refs.input.focus()})},handleInputConfirm:function(){var t=this.inputValue;if(t)if((t=t.replace(":",":")).indexOf(":")>0){var e=t.split(":");this.product.specifications.push({item:e[0],value:e[1],productId:this.product.id})}else this.$alert("产品规格应该以“项目:值”的形式录入");this.inputVisible=!1,this.inputValue=""}}},m={render:function(){var t=this,e=t.$createElement,r=t._self._c||e;return r("el-form",{attrs:{model:t.product,"label-position":"left"}},[r("el-form-item",{attrs:{label:"商品名称","label-width":"100px"}},[r("el-input",{attrs:{autocomplete:"off"},model:{value:t.product.title,callback:function(e){t.$set(t.product,"title",e)},expression:"product.title"}})],1),t._v(" "),r("el-form-item",{attrs:{label:"商品售价","label-width":"100px"}},[r("el-input",{attrs:{autocomplete:"off"},model:{value:t.product.price,callback:function(e){t.$set(t.product,"price",e)},expression:"product.price"}})],1),t._v(" "),r("el-form-item",{attrs:{label:"商品评分","label-width":"100px"}},[r("el-input",{attrs:{autocomplete:"off"},model:{value:t.product.rate,callback:function(e){t.$set(t.product,"rate",e)},expression:"product.rate"}})],1),t._v(" "),r("el-form-item",{attrs:{label:"封面图片","label-width":"100px"}},[r("el-input",{attrs:{autocomplete:"off"},model:{value:t.product.cover,callback:function(e){t.$set(t.product,"cover",e)},expression:"product.cover"}})],1),t._v(" "),r("el-form-item",{attrs:{label:"详情图片","label-width":"100px"}},[r("el-input",{attrs:{autocomplete:"off"},model:{value:t.product.detail,callback:function(e){t.$set(t.product,"detail",e)},expression:"product.detail"}})],1),t._v(" "),r("el-form-item",{attrs:{label:"商品规格"}},[r("div",{staticStyle:{"padding-top":"40px"}},[t._l(t.product.specifications,function(e){return r("el-tag",{key:e.item,attrs:{closable:"",size:"medium","disable-transitions":!1},on:{close:function(r){return t.removeSpecification(e)}}},[t._v("\n "+t._s(e.item+":"+e.value)+"\n ")])}),t._v(" "),t.inputVisible?r("el-input",{ref:"saveTagInput",staticClass:"input-new-tag",attrs:{size:"small"},on:{blur:t.handleInputConfirm},nativeOn:{keyup:function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"enter",13,e.key,"Enter")?null:t.handleInputConfirm(e)}},model:{value:t.inputValue,callback:function(e){t.inputValue=e},expression:"inputValue"}}):r("el-button",{staticClass:"button-new-tag",attrs:{size:"small"},on:{click:t.showInput}},[t._v("+ New Spec")])],2)]),t._v(" "),r("el-form-item",{attrs:{label:"商品描述"}},[r("el-input",{attrs:{type:"textarea",autosize:{minRows:7,maxRows:7}},model:{value:t.product.description,callback:function(e){t.$set(t.product,"description",e)},expression:"product.description"}})],1),t._v(" "),r("div",{staticStyle:{"text-align":"right","padding-right":"40px"}},[r("el-button",{on:{click:function(e){return t.$emit("dismiss")}}},[t._v("取 消")]),t._v(" "),r("el-button",{attrs:{type:"primary"},on:{click:t.submitProduct}},[t._v("确 定")])],1)],1)},staticRenderFns:[]};var v=r("VU/8")(f,m,!1,function(t){r("eRfs")},"data-v-16e227fa",null).exports,b={name:"StockManage",props:{product:Object,stock:Object},data:function(){return{}},methods:{submitStock:function(){var t=this;return c()(n.a.mark(function e(){return n.a.wrap(function(e){for(;;)switch(e.prev=e.next){case 0:return e.prev=0,e.next=3,d.a.warehouse.updateStock(t.product.id,t.stock.amount);case 3:t.$notify({title:"操作成功",message:t.createMode?"商品已成功创建":"商品信息已修改",type:"success"}),t.$emit("dismiss"),e.next=10;break;case 7:e.prev=7,e.t0=e.catch(0),t.$notify({title:"操作失败",message:e.t0.message,type:"error"});case 10:case"end":return e.stop()}},e,t,[[0,7]])}))()}}},h={render:function(){var t=this,e=t.$createElement,r=t._self._c||e;return r("div",[r("span",{staticClass:"title"},[t._v(t._s(this.product.title))]),t._v(" "),r("el-form",{attrs:{model:t.product,"label-position":"right",inline:!0}},[r("el-form-item",{attrs:{label:"可用库存","label-width":"135px"}},[r("el-input-number",{model:{value:t.stock.amount,callback:function(e){t.$set(t.stock,"amount",e)},expression:"stock.amount"}})],1),t._v(" "),r("el-form-item",{attrs:{label:"冻结库存","label-width":"135px"}},[r("el-input-number",{attrs:{disabled:""},model:{value:t.stock.frozen,callback:function(e){t.$set(t.stock,"frozen",e)},expression:"stock.frozen"}})],1)],1),t._v(" "),r("div",{staticStyle:{"text-align":"right","padding-right":"40px"}},[r("el-button",{on:{click:function(e){return t.$emit("dismiss")}}},[t._v("取 消")]),t._v(" "),r("el-button",{attrs:{type:"primary"},on:{click:t.submitStock}},[t._v("确 定")])],1)],1)},staticRenderFns:[]};var k={name:"WarehousePage",components:{ProductManage:v,StockManage:r("VU/8")(b,h,!1,function(t){r("SmJH")},"data-v-1730db62",null).exports},data:function(){return{products:[],product:{title:"",price:0,rate:0,cover:"",desc:"",description:"",specifications:[]},stock:{amount:0,frozen:0},createMode:!1,stockMode:!1,dialogFormVisible:!1}},created:function(){this.loadProducts()},methods:u()({},Object(p.d)("cart",["removeCartItem"]),{pureText:function(t){return d.a.stringUtil.pureText(t)},loadProducts:function(){var t=this;return c()(n.a.mark(function e(){return n.a.wrap(function(e){for(;;)switch(e.prev=e.next){case 0:return t.dialogFormVisible=!1,e.next=3,d.a.warehouse.getAllProducts();case 3:t.products=e.sent.data;case 4:case"end":return e.stop()}},e,t)}))()},manageProduct:function(t){this.product=a()(this.product,t),this.product.description=d.a.stringUtil.transToReturn(t.description),this.createMode=!1,this.stockMode=!1,this.dialogFormVisible=!0},manageStock:function(t){var e=this;return c()(n.a.mark(function r(){var o,a;return n.a.wrap(function(r){for(;;)switch(r.prev=r.next){case 0:return r.next=2,d.a.warehouse.queryStock(t.id);case 2:o=r.sent,a=o.data,e.stock=a,e.product=t,e.stockMode=!0,e.dialogFormVisible=!0;case 8:case"end":return r.stop()}},r,e)}))()},removeProduct:function(t){var e=this;return c()(n.a.mark(function r(){return n.a.wrap(function(r){for(;;)switch(r.prev=r.next){case 0:return r.prev=0,r.next=3,d.a.warehouse.removeProduct(t);case 3:e.removeCartItem(t),e.loadProducts(),e.$notify({title:"操作成功",message:"商品已删除",type:"success"}),r.next=11;break;case 8:r.prev=8,r.t0=r.catch(0),e.$notify({title:"操作失败",message:r.t0.message,type:"error"});case 11:case"end":return r.stop()}},r,e,[[0,8]])}))()}})},x={render:function(){var t=this,e=t.$createElement,r=t._self._c||e;return r("el-card",{staticClass:"box-card"},[r("div",{staticClass:"header",attrs:{slot:"header"},slot:"header"},[r("span",[t._v("商品及库存管理")]),t._v(" "),r("span",{staticStyle:{float:"right"}},[r("el-button",{attrs:{type:"primary",size:"mini"},on:{click:function(e){t.stockMode=!1,t.createMode=t.dialogFormVisible=!0}}},[t._v("新增商品")])],1)]),t._v(" "),r("div",{staticClass:"content"},[r("el-table",{ref:"productTable",staticStyle:{width:"100%"},attrs:{data:t.products}},[r("el-table-column",{attrs:{label:"图片",width:"150"},scopedSlots:t._u([{key:"default",fn:function(t){return[r("img",{staticStyle:{width:"120px"},attrs:{src:t.row.cover}})]}}])}),t._v(" "),r("el-table-column",{attrs:{prop:"title",label:"商品名称",sortable:"",width:"250"}}),t._v(" "),r("el-table-column",{attrs:{prop:"rate",label:"评分",width:"80",sortable:""}}),t._v(" "),r("el-table-column",{attrs:{label:"商品简介",sortable:""},scopedSlots:t._u([{key:"default",fn:function(e){return[r("span",{staticClass:"description"},[t._v(t._s(t.pureText(e.row.description)))])]}}])}),t._v(" "),r("el-table-column",{attrs:{prop:"price",label:"单价",width:"80",sortable:""}}),t._v(" "),r("el-table-column",{attrs:{label:"操作",width:"140"},scopedSlots:t._u([{key:"default",fn:function(e){return[r("el-link",{attrs:{type:"primary"},on:{click:function(r){return t.manageProduct(e.row)}}},[t._v("修改")]),t._v(" "),r("el-link",{attrs:{type:"primary"},on:{click:function(r){return t.manageStock(e.row)}}},[t._v("库存")]),t._v(" "),r("el-popconfirm",{attrs:{confirmButtonText:"确定",cancelButtonText:"我手抖了",icon:"el-icon-info",iconColor:"red",title:"确定删除这个商品吗?"},on:{onConfirm:function(r){return t.removeProduct(e.row.id)}}},[r("el-link",{attrs:{slot:"reference",type:"danger"},slot:"reference"},[t._v("删除")])],1)]}}])})],1),t._v(" "),r("el-dialog",{attrs:{title:"商品信息",visible:t.dialogFormVisible},on:{"update:visible":function(e){t.dialogFormVisible=e}}},[r("ProductManage",{directives:[{name:"show",rawName:"v-show",value:!t.stockMode,expression:"!stockMode"}],attrs:{product:t.product,"create-mode":t.createMode},on:{dismiss:function(e){t.dialogFormVisible=!1},updated:t.loadProducts}}),t._v(" "),r("StockManage",{directives:[{name:"show",rawName:"v-show",value:t.stockMode,expression:"stockMode"}],attrs:{product:t.product,stock:t.stock},on:{dismiss:function(e){t.dialogFormVisible=!1}}})],1)],1)])},staticRenderFns:[]};var g=r("VU/8")(k,x,!1,function(t){r("f/ib")},"data-v-81f8549e",null);e.default=g.exports}}); -------------------------------------------------------------------------------- /src/main/resources/static/static/js/8.176f9455c3442c06ebf6.js: -------------------------------------------------------------------------------- 1 | webpackJsonp([8],{ZLgQ:function(t,e){},k7C0:function(t,e,a){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var s={render:function(){var t=this.$createElement,e=this._self._c||t;return e("el-card",{staticClass:"box-card",attrs:{"body-style":{padding:"0 10px 0 10px"}}},[e("div",{staticClass:"header",attrs:{slot:"header"},slot:"header"},[e("span",[this._v("讨论")]),this._v(" "),e("span",{staticClass:"comment"},[this._v("本功能通过Gitalk使用GitHub的Issues提供服务,请使用GitHub账号登录")])]),this._v(" "),e("div",[e("iframe",{staticStyle:{width:"1300px",height:"1000px"},attrs:{src:"/static/board/gitalk.html",frameborder:"0"}})])])},staticRenderFns:[]};var i=a("VU/8")({name:"CommentPage"},s,!1,function(t){a("ZLgQ")},"data-v-a8ab5f44",null);e.default=i.exports}}); -------------------------------------------------------------------------------- /src/main/resources/static/static/js/9.527be297aba1594ffe0d.js: -------------------------------------------------------------------------------- 1 | webpackJsonp([9],{TgyC:function(t,e,a){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var n=a("Dd8w"),l=a.n(n),i=a("NYxO"),s={name:"CartPage",components:{PayStepIndicator:a("7WGw").a},data:function(){return{multipleSelection:[]}},computed:l()({},Object(i.e)("cart",["items"]),{total:function(){return this.multipleSelection.reduce(function(t,e){return t+e.price*e.amount},0)}}),mounted:function(){this.toggleSelection(this.items)},methods:l()({},Object(i.d)("cart",["adjustCartItems","removeCartItem"]),Object(i.b)("cart",["setupSettlementBillWithDefaultValue"]),{adjustAmount:function(t,e,a){var n=l()({},t);n.amount=e-a,this.adjustCartItems(n)},removeItem:function(t){var e=this;this.removeCartItem(t),this.$nextTick(function(){return e.toggleSelection(e.items)})},toggleSelection:function(t){var e=this;t?t.forEach(function(t){e.$refs.cartTable.toggleRowSelection(t)}):this.$refs.cartTable.clearSelection()},goSettlement:function(){this.setupSettlementBillWithDefaultValue({items:this.multipleSelection}),this.$router.push("/settle")},handleSelectionChange:function(t){this.multipleSelection=t}})},o={render:function(){var t=this,e=t.$createElement,a=t._self._c||e;return a("div",[a("el-card",{staticClass:"box-card"},[a("div",{staticClass:"header",attrs:{slot:"header"},slot:"header"},[a("span",[t._v("我的购物车")]),t._v(" "),a("span",{staticClass:"comment"},[t._v("温馨提示:产品是否购买成功,以最终支付为准哦,请尽快完成结算")])]),t._v(" "),a("div",{staticClass:"content"},[a("el-table",{ref:"cartTable",staticStyle:{width:"100%"},attrs:{data:t.items},on:{"selection-change":t.handleSelectionChange}},[a("el-table-column",{attrs:{type:"selection",width:"55",fixed:"","show-overflow-tooltip":""}}),t._v(" "),a("el-table-column",{attrs:{label:"图片",width:"150"},scopedSlots:t._u([{key:"default",fn:function(t){return[a("img",{staticStyle:{width:"120px"},attrs:{src:t.row.cover}})]}}])}),t._v(" "),a("el-table-column",{attrs:{prop:"title",label:"商品名称",sortable:""}}),t._v(" "),a("el-table-column",{attrs:{prop:"price",label:"单价",width:"100",sortable:""}}),t._v(" "),a("el-table-column",{attrs:{label:"数量",width:"170",sortable:""},scopedSlots:t._u([{key:"default",fn:function(e){return[a("el-input-number",{attrs:{size:"mini",min:0,max:10,value:e.row.amount},on:{change:function(a,n){t.adjustAmount(e.row,a,n)}}})]}}])}),t._v(" "),a("el-table-column",{attrs:{label:"小计",width:"120",sortable:""},scopedSlots:t._u([{key:"default",fn:function(e){return[a("span",{staticClass:"subtotal"},[t._v(t._s(e.row.price*e.row.amount)+" 元")])]}}])}),t._v(" "),a("el-table-column",{attrs:{label:"操作",width:"120"},scopedSlots:t._u([{key:"default",fn:function(e){return[a("el-button",{attrs:{plain:"",size:"mini",type:"danger"},on:{click:function(a){return t.removeItem(e.row.id)}}},[t._v("删除")])]}}])})],1),t._v(" "),a("div",{staticClass:"actions"},[t._v("\n "+t._s("购物车中共计 "+t.items.length+" 件商品,已选择其中 "+t.multipleSelection.length+" 件")+"\n "),a("div",{staticClass:"total"},[t._v("\n 总计: "),a("span",{staticClass:"pay_price"},[t._v(t._s(this.total))]),t._v(" 元\n "),a("div",{staticClass:"pay_action"},[a("el-button",{staticStyle:{position:"relative",top:"-6px"},attrs:{size:"large",type:"primary",disabled:this.total<=0},on:{click:t.goSettlement}},[t._v("¥ 选好了,去结算\n ")])],1)])])],1)]),t._v(" "),a("PayStepIndicator",{attrs:{step:1}})],1)},staticRenderFns:[]};var r=a("VU/8")(s,o,!1,function(t){a("fn1c")},"data-v-1316e37e",null);e.default=r.exports},fn1c:function(t,e){}}); -------------------------------------------------------------------------------- /src/main/resources/static/static/js/manifest.1aeebcd3a724536ddb12.js: -------------------------------------------------------------------------------- 1 | !function(e){var n=window.webpackJsonp;window.webpackJsonp=function(r,c,a){for(var f,i,u,d=0,s=[];d