├── frontend ├── src │ ├── assets │ │ ├── .gitkeep │ │ └── brand.png │ ├── app │ │ ├── app.component.css │ │ ├── pages │ │ │ ├── card │ │ │ │ ├── card.component.css │ │ │ │ ├── card.component.html │ │ │ │ └── card.component.ts │ │ │ ├── cart │ │ │ │ ├── cart.component.css │ │ │ │ ├── cart.component.html │ │ │ │ └── cart.component.ts │ │ │ ├── login │ │ │ │ ├── login.component.css │ │ │ │ ├── login.component.ts │ │ │ │ └── login.component.html │ │ │ ├── order │ │ │ │ ├── order.component.css │ │ │ │ ├── order.component.html │ │ │ │ └── order.component.ts │ │ │ ├── signup │ │ │ │ ├── signup.component.css │ │ │ │ ├── signup.component.ts │ │ │ │ └── signup.component.html │ │ │ ├── product-detail │ │ │ │ ├── detail.component.css │ │ │ │ ├── detail.component.html │ │ │ │ └── detail.component.ts │ │ │ ├── user-edit │ │ │ │ ├── user-detail.component.css │ │ │ │ ├── user-detail.component.ts │ │ │ │ └── user-detail.component.html │ │ │ ├── order-detail │ │ │ │ ├── order-detail.component.css │ │ │ │ ├── order-detail.component.ts │ │ │ │ └── order-detail.component.html │ │ │ ├── product-edit │ │ │ │ ├── product-edit.component.css │ │ │ │ ├── product-edit.component.ts │ │ │ │ └── product-edit.component.html │ │ │ └── product-list │ │ │ │ ├── product.list.component.css │ │ │ │ ├── product.list.component.html │ │ │ │ └── product.list.component.ts │ │ ├── parts │ │ │ ├── navigation │ │ │ │ ├── navigation.component.css │ │ │ │ ├── navigation.component.ts │ │ │ │ └── navigation.component.html │ │ │ └── pagination │ │ │ │ ├── pagination.component.css │ │ │ │ ├── pagination.component.ts │ │ │ │ └── pagination.component.html │ │ ├── enum │ │ │ ├── ProductStatus.ts │ │ │ ├── CategoryType.ts │ │ │ ├── OrderStatus.ts │ │ │ └── Role.ts │ │ ├── models │ │ │ ├── Item.ts │ │ │ ├── Cart.ts │ │ │ ├── Order.ts │ │ │ ├── User.ts │ │ │ ├── ProductInOrder.ts │ │ │ └── productInfo.ts │ │ ├── response │ │ │ └── JwtResponse.ts │ │ ├── app.component.ts │ │ ├── app.component.html │ │ ├── _interceptors │ │ │ ├── error-interceptor.service.ts │ │ │ └── jwt-interceptor.service.ts │ │ ├── services │ │ │ ├── order.service.ts │ │ │ ├── product.service.ts │ │ │ ├── user.service.ts │ │ │ └── cart.service.ts │ │ ├── _guards │ │ │ └── auth.guard.ts │ │ ├── app.module.ts │ │ ├── app-routing.module.ts │ │ └── mockData.ts │ ├── favicon.ico │ ├── environments │ │ ├── environment.prod.ts │ │ └── environment.ts │ ├── tsconfig.app.json │ ├── tsconfig.spec.json │ ├── styles.css │ ├── tslint.json │ ├── main.ts │ ├── browserslist │ ├── index.html │ ├── test.ts │ ├── karma.conf.js │ └── polyfills.ts ├── Dockerfile ├── e2e │ ├── src │ │ ├── app.po.ts │ │ └── app.e2e-spec.ts │ ├── tsconfig.e2e.json │ └── protractor.conf.js ├── .editorconfig ├── tsconfig.json ├── nginx │ └── default.conf ├── .gitignore ├── README.md ├── package.json ├── tslint.json └── angular.json ├── backend ├── Dockerfile ├── src │ ├── main │ │ ├── java │ │ │ └── me │ │ │ │ └── zhulin │ │ │ │ └── shopapi │ │ │ │ ├── enums │ │ │ │ ├── CodeEnum.java │ │ │ │ ├── OrderStatusEnum.java │ │ │ │ ├── ProductStatusEnum.java │ │ │ │ └── ResultEnum.java │ │ │ │ ├── api │ │ │ │ ├── rest-client.env.json │ │ │ │ ├── CategoryController.java │ │ │ │ ├── ProductController.java │ │ │ │ ├── test.http │ │ │ │ ├── OrderController.java │ │ │ │ ├── CartController.java │ │ │ │ └── UserController.java │ │ │ │ ├── repository │ │ │ │ ├── ProductInOrderRepository.java │ │ │ │ ├── CartRepository.java │ │ │ │ ├── UserRepository.java │ │ │ │ ├── ProductCategoryRepository.java │ │ │ │ ├── ProductInfoRepository.java │ │ │ │ └── OrderRepository.java │ │ │ │ ├── form │ │ │ │ └── ItemForm.java │ │ │ │ ├── service │ │ │ │ ├── ProductInOrderService.java │ │ │ │ ├── UserService.java │ │ │ │ ├── CategoryService.java │ │ │ │ ├── CartService.java │ │ │ │ ├── OrderService.java │ │ │ │ ├── ProductService.java │ │ │ │ └── impl │ │ │ │ │ ├── ProductInOrderServiceImpl.java │ │ │ │ │ ├── CategoryServiceImpl.java │ │ │ │ │ ├── UserServiceImpl.java │ │ │ │ │ ├── CartServiceImpl.java │ │ │ │ │ ├── OrderServiceImpl.java │ │ │ │ │ └── ProductServiceImpl.java │ │ │ │ ├── vo │ │ │ │ ├── request │ │ │ │ │ └── LoginForm.java │ │ │ │ └── response │ │ │ │ │ ├── JwtResponse.java │ │ │ │ │ └── CategoryPage.java │ │ │ │ ├── exception │ │ │ │ └── MyException.java │ │ │ │ ├── entity │ │ │ │ ├── ProductCategory.java │ │ │ │ ├── Cart.java │ │ │ │ ├── ProductInfo.java │ │ │ │ ├── User.java │ │ │ │ ├── OrderMain.java │ │ │ │ └── ProductInOrder.java │ │ │ │ ├── security │ │ │ │ ├── JWT │ │ │ │ │ ├── JwtEntryPoint.java │ │ │ │ │ ├── JwtProvider.java │ │ │ │ │ └── JwtFilter.java │ │ │ │ └── SpringSecurityConfig.java │ │ │ │ └── ShopApiApplication.java │ │ └── resources │ │ │ ├── application.yml │ │ │ └── application-docker.yml │ └── test │ │ └── java │ │ └── me │ │ └── zhulin │ │ └── shopapi │ │ ├── api │ │ └── CartControllerTest.java │ │ ├── ShopApiApplicationTests.java │ │ └── service │ │ └── impl │ │ ├── CategoryServiceImplTest.java │ │ ├── ProductInOrderServiceImplTest.java │ │ ├── UserServiceImplTest.java │ │ ├── CartServiceImplTest.java │ │ └── ProductServiceImplTest.java ├── .gitignore └── pom.xml ├── docker-compose.yml ├── LICENSE ├── .gitignore └── README.md /frontend/src/assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/app/app.component.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/app/pages/card/card.component.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/app/pages/cart/cart.component.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/app/pages/login/login.component.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/app/pages/order/order.component.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/app/pages/signup/signup.component.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/app/pages/product-detail/detail.component.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/app/pages/user-edit/user-detail.component.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/app/parts/navigation/navigation.component.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/app/parts/pagination/pagination.component.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/app/pages/order-detail/order-detail.component.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/app/pages/product-edit/product-edit.component.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/app/pages/product-list/product.list.component.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/app/enum/ProductStatus.ts: -------------------------------------------------------------------------------- 1 | export enum ProductStatus { 2 | "Available", "Unavailable" 3 | } 4 | -------------------------------------------------------------------------------- /frontend/src/app/enum/CategoryType.ts: -------------------------------------------------------------------------------- 1 | export enum CategoryType { 2 | "Books", "Food", "Clothes", "Drink" 3 | } 4 | -------------------------------------------------------------------------------- /frontend/src/app/enum/OrderStatus.ts: -------------------------------------------------------------------------------- 1 | export enum OrderStatus { 2 | "New", 3 | "Finished", 4 | "Cenceled" 5 | } 6 | -------------------------------------------------------------------------------- /frontend/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhulinn/SpringBoot-Angular7-Online-Shopping-Store/HEAD/frontend/src/favicon.ico -------------------------------------------------------------------------------- /frontend/src/assets/brand.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhulinn/SpringBoot-Angular7-Online-Shopping-Store/HEAD/frontend/src/assets/brand.png -------------------------------------------------------------------------------- /frontend/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | export const apiUrl = '/api'; 5 | -------------------------------------------------------------------------------- /frontend/src/app/enum/Role.ts: -------------------------------------------------------------------------------- 1 | export enum Role { 2 | Customer = 'ROLE_CUSTOMER', 3 | Employee = 'ROLE_EMPLOYEE', 4 | Manager = 'ROLE_MANAGER' 5 | } 6 | -------------------------------------------------------------------------------- /backend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM openjdk:11-oracle 2 | VOLUME /tmp 3 | COPY target/*.jar app.jar 4 | ENTRYPOINT ["java", "-jar","/app.jar", "--spring.profiles.active=docker"] 5 | -------------------------------------------------------------------------------- /frontend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:1.13.3-alpine 2 | COPY /nginx/default.conf /etc/nginx/conf.d/ 3 | RUN rm -rf /usr/share/nginx/html/* 4 | COPY dist/shop /usr/share/nginx/html -------------------------------------------------------------------------------- /frontend/src/app/models/Item.ts: -------------------------------------------------------------------------------- 1 | import {ProductInfo} from "./productInfo"; 2 | 3 | export class Item { 4 | quantity: number; 5 | productInfo: ProductInfo 6 | 7 | } 8 | -------------------------------------------------------------------------------- /frontend/src/app/models/Cart.ts: -------------------------------------------------------------------------------- 1 | import {ProductInOrder} from "./ProductInOrder"; 2 | 3 | 4 | export class Cart { 5 | cartId: number; 6 | products: ProductInOrder[]; 7 | } 8 | -------------------------------------------------------------------------------- /frontend/src/app/response/JwtResponse.ts: -------------------------------------------------------------------------------- 1 | export class JwtResponse { 2 | token: string; 3 | type: string; 4 | account: string; 5 | name: string; 6 | role: string; 7 | 8 | } 9 | 10 | -------------------------------------------------------------------------------- /backend/src/main/java/me/zhulin/shopapi/enums/CodeEnum.java: -------------------------------------------------------------------------------- 1 | package me.zhulin.shopapi.enums; 2 | 3 | /** 4 | * Created By Zhu Lin on 3/9/2018. 5 | */ 6 | public interface CodeEnum { 7 | Integer getCode(); 8 | 9 | } 10 | -------------------------------------------------------------------------------- /frontend/src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "types": [] 6 | }, 7 | "exclude": [ 8 | "test.ts", 9 | "**/*.spec.ts" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /frontend/e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo() { 5 | return browser.get('/'); 6 | } 7 | 8 | getTitleText() { 9 | return element(by.css('app-root h1')).getText(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /frontend/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-root', 5 | templateUrl: './app.component.html', 6 | styleUrls: ['./app.component.css'] 7 | }) 8 | export class AppComponent { 9 | title = 'shop'; 10 | } 11 | -------------------------------------------------------------------------------- /frontend/e2e/tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "types": [ 8 | "jasmine", 9 | "jasminewd2", 10 | "node" 11 | ] 12 | } 13 | } -------------------------------------------------------------------------------- /frontend/src/app/models/Order.ts: -------------------------------------------------------------------------------- 1 | export class Order { 2 | orderId: number; 3 | buyerEmail: string; 4 | buyerName: string; 5 | buyerPhone: string; 6 | buyerAddress: string; 7 | orderAmount: string; 8 | orderStatus: string; 9 | createTime: string; 10 | updateTime: string; 11 | 12 | } 13 | -------------------------------------------------------------------------------- /frontend/.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /backend/src/test/java/me/zhulin/shopapi/api/CartControllerTest.java: -------------------------------------------------------------------------------- 1 | package me.zhulin.shopapi.api; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | /** 8 | * Created By Zhu Lin on 1/2/2019. 9 | */ 10 | public class CartControllerTest { 11 | 12 | @Test 13 | public void getCart() { 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /backend/src/main/java/me/zhulin/shopapi/api/rest-client.env.json: -------------------------------------------------------------------------------- 1 | { 2 | "dev": { 3 | "host-url": "http://localhost:8080/api", 4 | "token": "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJtYW5hZ2VyMUBlbWFpbC5jb20iLCJpYXQiOjE1NDY2Njk5MjksImV4cCI6MTU0Njc1NjMyOX0.OJdSH3wrQrCkU-pc3efPL2CX1t8H1SVWkLbzuQ_3e_-NUvFuwU8snaKZcmhG5kh5ffBUwDId4s8Hb5HjythGaw" 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /frontend/src/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/spec", 5 | "types": [ 6 | "jasmine", 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | "test.ts", 12 | "polyfills.ts" 13 | ], 14 | "include": [ 15 | "**/*.spec.ts", 16 | "**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /frontend/src/styles.css: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | @import "~bootstrap/dist/css/bootstrap.min.css"; 3 | 4 | .ng-valid[required], .ng-valid.required { 5 | border-left: 5px solid #42A948; /* green */ 6 | } 7 | 8 | .ng-touched.ng-invalid:not(form) { 9 | border-left: 5px solid #a94442; /* red */ 10 | } 11 | -------------------------------------------------------------------------------- /frontend/e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | 3 | describe('workspace-project App', () => { 4 | let page: AppPage; 5 | 6 | beforeEach(() => { 7 | page = new AppPage(); 8 | }); 9 | 10 | it('should display welcome message', () => { 11 | page.navigateTo(); 12 | expect(page.getTitleText()).toEqual('Welcome to shop!'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | !.mvn/wrapper/maven-wrapper.jar 3 | 4 | ### STS ### 5 | .apt_generated 6 | .classpath 7 | .factorypath 8 | .project 9 | .settings 10 | .springBeans 11 | .sts4-cache 12 | 13 | ### IntelliJ IDEA ### 14 | .idea 15 | *.iws 16 | *.iml 17 | *.ipr 18 | 19 | ### NetBeans ### 20 | /nbproject/private/ 21 | /build/ 22 | /nbbuild/ 23 | /dist/ 24 | /nbdist/ 25 | /.nb-gradle/ -------------------------------------------------------------------------------- /backend/src/main/java/me/zhulin/shopapi/repository/ProductInOrderRepository.java: -------------------------------------------------------------------------------- 1 | package me.zhulin.shopapi.repository; 2 | 3 | import me.zhulin.shopapi.entity.ProductInOrder; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | 6 | /** 7 | * Created By Zhu Lin on 3/14/2018. 8 | */ 9 | public interface ProductInOrderRepository extends JpaRepository { 10 | 11 | } 12 | -------------------------------------------------------------------------------- /frontend/src/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tslint.json", 3 | "rules": { 4 | "directive-selector": [ 5 | true, 6 | "attribute", 7 | "app", 8 | "camelCase" 9 | ], 10 | "component-selector": [ 11 | true, 12 | "element", 13 | "app", 14 | "kebab-case" 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | db: 4 | image: postgres:9.4.5 5 | environment: 6 | POSTGRES_USER: postgres 7 | POSTGRES_PASSWORD: root 8 | backend: 9 | build: backend/. 10 | ports: 11 | - "8080:8080" 12 | depends_on: 13 | - db 14 | frontend: 15 | build: frontend/. 16 | ports: 17 | - "80:80" 18 | depends_on: 19 | - backend 20 | -------------------------------------------------------------------------------- /backend/src/main/java/me/zhulin/shopapi/repository/CartRepository.java: -------------------------------------------------------------------------------- 1 | package me.zhulin.shopapi.repository; 2 | 3 | import me.zhulin.shopapi.entity.Cart; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | import org.springframework.stereotype.Repository; 6 | 7 | /** 8 | * Created By Zhu Lin on 1/2/2019. 9 | */ 10 | 11 | public interface CartRepository extends JpaRepository { 12 | } 13 | -------------------------------------------------------------------------------- /frontend/src/app/models/User.ts: -------------------------------------------------------------------------------- 1 | import {Role} from "../enum/Role"; 2 | 3 | export class User { 4 | 5 | email: string; 6 | 7 | password: string; 8 | 9 | name: string; 10 | 11 | phone: string; 12 | 13 | address: string; 14 | 15 | active: boolean; 16 | 17 | role: string; 18 | 19 | constructor(){ 20 | this.active = true; 21 | this.role = Role.Customer; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /frontend/src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import {environment} from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule) 12 | .catch(err => console.error(err)); 13 | -------------------------------------------------------------------------------- /backend/src/main/java/me/zhulin/shopapi/form/ItemForm.java: -------------------------------------------------------------------------------- 1 | package me.zhulin.shopapi.form; 2 | 3 | import lombok.Data; 4 | 5 | import javax.validation.constraints.Min; 6 | import javax.validation.constraints.NotEmpty; 7 | 8 | /** 9 | * Created By Zhu Lin on 3/11/2018. 10 | */ 11 | @Data 12 | public class ItemForm { 13 | @Min(value = 1) 14 | private Integer quantity; 15 | @NotEmpty 16 | private String productId; 17 | } 18 | -------------------------------------------------------------------------------- /backend/src/main/java/me/zhulin/shopapi/service/ProductInOrderService.java: -------------------------------------------------------------------------------- 1 | package me.zhulin.shopapi.service; 2 | 3 | import me.zhulin.shopapi.entity.ProductInOrder; 4 | import me.zhulin.shopapi.entity.User; 5 | 6 | /** 7 | * Created By Zhu Lin on 1/3/2019. 8 | */ 9 | public interface ProductInOrderService { 10 | void update(String itemId, Integer quantity, User user); 11 | ProductInOrder findOne(String itemId, User user); 12 | } 13 | -------------------------------------------------------------------------------- /backend/src/main/java/me/zhulin/shopapi/vo/request/LoginForm.java: -------------------------------------------------------------------------------- 1 | package me.zhulin.shopapi.vo.request; 2 | 3 | import lombok.Data; 4 | 5 | import javax.validation.constraints.NotBlank; 6 | import javax.validation.constraints.Size; 7 | 8 | /** 9 | * Created By Zhu Lin on 1/1/2019. 10 | */ 11 | @Data 12 | public class LoginForm { 13 | @NotBlank 14 | private String username; 15 | @NotBlank 16 | private String password; 17 | } 18 | -------------------------------------------------------------------------------- /frontend/src/browserslist: -------------------------------------------------------------------------------- 1 | # This file is currently used by autoprefixer to adjust CSS to support the below specified browsers 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | # 5 | # For IE 9-11 support, please remove 'not' from the last line of the file and adjust as needed 6 | 7 | > 0.5% 8 | last 2 versions 9 | Firefox ESR 10 | not dead 11 | not IE 9-11 -------------------------------------------------------------------------------- /backend/src/main/java/me/zhulin/shopapi/service/UserService.java: -------------------------------------------------------------------------------- 1 | package me.zhulin.shopapi.service; 2 | 3 | 4 | import me.zhulin.shopapi.entity.User; 5 | 6 | import java.util.Collection; 7 | 8 | /** 9 | * Created By Zhu Lin on 3/13/2018. 10 | */ 11 | public interface UserService { 12 | User findOne(String email); 13 | 14 | Collection findByRole(String role); 15 | 16 | User save(User user); 17 | 18 | User update(User user); 19 | } 20 | -------------------------------------------------------------------------------- /backend/src/main/java/me/zhulin/shopapi/repository/UserRepository.java: -------------------------------------------------------------------------------- 1 | package me.zhulin.shopapi.repository; 2 | 3 | 4 | import me.zhulin.shopapi.entity.User; 5 | import org.springframework.data.jpa.repository.JpaRepository; 6 | 7 | import java.util.Collection; 8 | 9 | /** 10 | * Created By Zhu Lin on 3/13/2018. 11 | */ 12 | 13 | public interface UserRepository extends JpaRepository { 14 | User findByEmail(String email); 15 | Collection findAllByRole(String role); 16 | 17 | } 18 | -------------------------------------------------------------------------------- /frontend/src/app/parts/pagination/pagination.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, Input, OnInit} from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-pagination', 5 | templateUrl: './pagination.component.html', 6 | styleUrls: ['./pagination.component.css'] 7 | }) 8 | export class PaginationComponent implements OnInit { 9 | 10 | @Input() currentPage: any; 11 | 12 | constructor() { 13 | } 14 | 15 | ngOnInit() { 16 | } 17 | 18 | counter(i = 1) { 19 | return new Array(i); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "outDir": "./dist/out-tsc", 6 | "sourceMap": true, 7 | "declaration": false, 8 | "module": "es2015", 9 | "moduleResolution": "node", 10 | "emitDecoratorMetadata": true, 11 | "experimentalDecorators": true, 12 | "importHelpers": true, 13 | "target": "es5", 14 | "typeRoots": [ 15 | "node_modules/@types" 16 | ], 17 | "lib": [ 18 | "es2018", 19 | "dom" 20 | ] 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /frontend/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Shop 6 | 7 | 8 | 9 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /backend/src/main/java/me/zhulin/shopapi/service/CategoryService.java: -------------------------------------------------------------------------------- 1 | package me.zhulin.shopapi.service; 2 | 3 | import me.zhulin.shopapi.entity.ProductCategory; 4 | 5 | import java.util.List; 6 | 7 | /** 8 | * Created By Zhu Lin on 3/10/2018. 9 | */ 10 | public interface CategoryService { 11 | 12 | List findAll(); 13 | 14 | ProductCategory findByCategoryType(Integer categoryType); 15 | 16 | List findByCategoryTypeIn(List categoryTypeList); 17 | 18 | ProductCategory save(ProductCategory productCategory); 19 | 20 | 21 | } 22 | -------------------------------------------------------------------------------- /backend/src/main/java/me/zhulin/shopapi/service/CartService.java: -------------------------------------------------------------------------------- 1 | package me.zhulin.shopapi.service; 2 | 3 | import me.zhulin.shopapi.entity.Cart; 4 | import me.zhulin.shopapi.entity.ProductInOrder; 5 | import me.zhulin.shopapi.entity.User; 6 | 7 | import java.util.Collection; 8 | 9 | /** 10 | * Created By Zhu Lin on 3/10/2018. 11 | */ 12 | public interface CartService { 13 | Cart getCart(User user); 14 | 15 | void mergeLocalCart(Collection productInOrders, User user); 16 | 17 | void delete(String itemId, User user); 18 | 19 | void checkout(User user); 20 | } 21 | -------------------------------------------------------------------------------- /backend/src/test/java/me/zhulin/shopapi/ShopApiApplicationTests.java: -------------------------------------------------------------------------------- 1 | package me.zhulin.shopapi; 2 | 3 | import me.zhulin.shopapi.service.impl.*; 4 | import org.junit.runner.RunWith; 5 | import org.junit.runners.Suite; 6 | 7 | @RunWith(Suite.class) 8 | @Suite.SuiteClasses({ 9 | CartServiceImplTest.class, 10 | CategoryServiceImplTest.class, 11 | OrderServiceImplTest.class, 12 | ProductInOrderServiceImplTest.class, 13 | ProductServiceImplTest.class, 14 | UserServiceImplTest.class 15 | }) 16 | public class ShopApiApplicationTests { 17 | 18 | } 19 | 20 | -------------------------------------------------------------------------------- /backend/src/main/java/me/zhulin/shopapi/enums/OrderStatusEnum.java: -------------------------------------------------------------------------------- 1 | package me.zhulin.shopapi.enums; 2 | 3 | /** 4 | * Created By Zhu Lin on 3/14/2018. 5 | */ 6 | public enum OrderStatusEnum implements CodeEnum { 7 | NEW(0, "New OrderMain"), 8 | FINISHED(1, "Finished"), 9 | CANCELED(2, "Canceled") 10 | ; 11 | 12 | private int code; 13 | private String msg; 14 | 15 | OrderStatusEnum(Integer code, String msg) { 16 | this.code = code; 17 | this.msg = msg; 18 | } 19 | 20 | @Override 21 | public Integer getCode() { 22 | return code; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /backend/src/main/java/me/zhulin/shopapi/exception/MyException.java: -------------------------------------------------------------------------------- 1 | package me.zhulin.shopapi.exception; 2 | 3 | 4 | import me.zhulin.shopapi.enums.ResultEnum; 5 | 6 | /** 7 | * Created By Zhu Lin on 3/10/2018. 8 | */ 9 | public class MyException extends RuntimeException { 10 | 11 | private Integer code; 12 | 13 | public MyException(ResultEnum resultEnum) { 14 | super(resultEnum.getMessage()); 15 | 16 | this.code = resultEnum.getCode(); 17 | } 18 | 19 | public MyException(Integer code, String message) { 20 | super(message); 21 | this.code = code; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /backend/src/main/java/me/zhulin/shopapi/vo/response/JwtResponse.java: -------------------------------------------------------------------------------- 1 | package me.zhulin.shopapi.vo.response; 2 | 3 | import lombok.Data; 4 | 5 | /** 6 | * Created By Zhu Lin on 1/1/2019. 7 | */ 8 | @Data 9 | public class JwtResponse { 10 | private String token; 11 | private String type = "Bearer"; 12 | private String account; 13 | private String name; 14 | private String role; 15 | 16 | public JwtResponse(String token, String account, String name, String role) { 17 | this.account = account; 18 | this.name = name; 19 | this.token = token; 20 | this.role = role; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /frontend/src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 5 |
6 |
7 |
8 |
9 |
10 | 11 |
12 | 13 | 14 | 15 | 19 | 20 |
21 |
22 | -------------------------------------------------------------------------------- /frontend/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/zone-testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: any; 11 | 12 | // First, initialize the Angular testing environment. 13 | getTestBed().initTestEnvironment( 14 | BrowserDynamicTestingModule, 15 | platformBrowserDynamicTesting() 16 | ); 17 | // Then we find all the tests. 18 | const context = require.context('./', true, /\.spec\.ts$/); 19 | // And load the modules. 20 | context.keys().map(context); 21 | -------------------------------------------------------------------------------- /backend/src/main/java/me/zhulin/shopapi/repository/ProductCategoryRepository.java: -------------------------------------------------------------------------------- 1 | package me.zhulin.shopapi.repository; 2 | 3 | import me.zhulin.shopapi.entity.ProductCategory; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | 6 | import java.util.List; 7 | 8 | /** 9 | * Created By Zhu Lin on 3/9/2018. 10 | */ 11 | public interface ProductCategoryRepository extends JpaRepository { 12 | // Some category 13 | List findByCategoryTypeInOrderByCategoryTypeAsc(List categoryTypes); 14 | // All category 15 | List findAllByOrderByCategoryType(); 16 | // One category 17 | ProductCategory findByCategoryType(Integer categoryType); 18 | } 19 | -------------------------------------------------------------------------------- /frontend/nginx/default.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | 4 | sendfile on; 5 | 6 | default_type application/octet-stream; 7 | 8 | location /api/ { 9 | proxy_pass http://backend:8080/api/; 10 | proxy_set_header Host $host; 11 | } 12 | 13 | gzip on; 14 | gzip_http_version 1.1; 15 | gzip_disable "MSIE [1-6]\."; 16 | gzip_min_length 256; 17 | gzip_vary on; 18 | gzip_proxied expired no-cache no-store private auth; 19 | gzip_types text/plain text/css application/json application/javascript application/x-javascript text/xml application/xml application/xml+rss text/javascript; 20 | gzip_comp_level 9; 21 | 22 | root /usr/share/nginx/html; 23 | 24 | location / { 25 | try_files $uri $uri/ /index.html =404; 26 | } 27 | } -------------------------------------------------------------------------------- /backend/src/main/java/me/zhulin/shopapi/service/OrderService.java: -------------------------------------------------------------------------------- 1 | package me.zhulin.shopapi.service; 2 | 3 | 4 | import me.zhulin.shopapi.entity.OrderMain; 5 | import org.springframework.data.domain.Page; 6 | import org.springframework.data.domain.Pageable; 7 | 8 | /** 9 | * Created By Zhu Lin on 3/14/2018. 10 | */ 11 | 12 | public interface OrderService { 13 | Page findAll(Pageable pageable); 14 | 15 | Page findByStatus(Integer status, Pageable pageable); 16 | 17 | Page findByBuyerEmail(String email, Pageable pageable); 18 | 19 | Page findByBuyerPhone(String phone, Pageable pageable); 20 | 21 | OrderMain findOne(Long orderId); 22 | 23 | 24 | OrderMain finish(Long orderId); 25 | 26 | OrderMain cancel(Long orderId); 27 | 28 | } 29 | -------------------------------------------------------------------------------- /frontend/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false 7 | }; 8 | export const apiUrl = '//localhost:8080/api'; 9 | 10 | /* 11 | * For easier debugging in development mode, you can import the following file 12 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 13 | * 14 | * This import should be commented out in production mode because it will have a negative impact 15 | * on performance if an error is thrown. 16 | */ 17 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 18 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | 8 | # dependencies 9 | /node* 10 | /node_modules 11 | 12 | # IDEs and editors 13 | /web.iml 14 | /node_modules 15 | 16 | # profiling files 17 | chrome-profiler-events.json 18 | speed-measure-plugin.json 19 | 20 | # IDEs and editors 21 | /.idea 22 | .project 23 | .classpath 24 | .c9/ 25 | *.launch 26 | .settings/ 27 | *.sublime-workspace 28 | 29 | # IDE - VSCode 30 | .vscode/* 31 | !.vscode/settings.json 32 | !.vscode/tasks.json 33 | !.vscode/launch.json 34 | !.vscode/extensions.json 35 | 36 | # misc 37 | /.sass-cache 38 | /connect.lock 39 | /coverage 40 | /libpeerconnection.log 41 | npm-debug.log 42 | yarn-error.log 43 | testem.log 44 | /typings 45 | 46 | # System Files 47 | .DS_Store 48 | Thumbs.db 49 | -------------------------------------------------------------------------------- /backend/src/main/java/me/zhulin/shopapi/enums/ProductStatusEnum.java: -------------------------------------------------------------------------------- 1 | package me.zhulin.shopapi.enums; 2 | 3 | import lombok.Getter; 4 | 5 | /** 6 | * Created By Zhu Lin on 3/9/2018. 7 | */ 8 | @Getter 9 | public enum ProductStatusEnum implements CodeEnum{ 10 | UP(0, "Available"), 11 | DOWN(1, "Unavailable") 12 | ; 13 | private Integer code; 14 | private String message; 15 | 16 | ProductStatusEnum(Integer code, String message) { 17 | this.code = code; 18 | this.message = message; 19 | } 20 | 21 | public String getStatus(Integer code) { 22 | 23 | for(ProductStatusEnum statusEnum : ProductStatusEnum.values()) { 24 | if(statusEnum.getCode() == code) return statusEnum.getMessage(); 25 | } 26 | return ""; 27 | } 28 | 29 | public Integer getCode() { 30 | return code; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /frontend/src/app/models/ProductInOrder.ts: -------------------------------------------------------------------------------- 1 | import {ProductInfo} from "./productInfo"; 2 | 3 | export class ProductInOrder { 4 | productId: string; 5 | productName: string; 6 | productPrice: number; 7 | productStock: number; 8 | productDescription: string; 9 | productIcon: string; 10 | categoryType: number; 11 | count: number; 12 | 13 | constructor(productInfo:ProductInfo, quantity = 1){ 14 | this.productId = productInfo.productId; 15 | this.productName = productInfo.productName; 16 | this.productPrice = productInfo.productPrice; 17 | this.productStock = productInfo.productStock; 18 | this.productDescription = productInfo.productDescription;; 19 | this.productIcon = productInfo.productIcon; 20 | this.categoryType = productInfo.categoryType; 21 | this.count = quantity; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /frontend/e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // Protractor configuration file, see link for more information 2 | // https://github.com/angular/protractor/blob/master/lib/config.ts 3 | 4 | const { SpecReporter } = require('jasmine-spec-reporter'); 5 | 6 | exports.config = { 7 | allScriptsTimeout: 11000, 8 | specs: [ 9 | './src/**/*.e2e-spec.ts' 10 | ], 11 | capabilities: { 12 | 'browserName': 'chrome' 13 | }, 14 | directConnect: true, 15 | baseUrl: 'http://localhost:4200/', 16 | framework: 'jasmine', 17 | jasmineNodeOpts: { 18 | showColors: true, 19 | defaultTimeoutInterval: 30000, 20 | print: function() {} 21 | }, 22 | onPrepare() { 23 | require('ts-node').register({ 24 | project: require('path').join(__dirname, './tsconfig.e2e.json') 25 | }); 26 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 27 | } 28 | }; -------------------------------------------------------------------------------- /backend/src/main/java/me/zhulin/shopapi/repository/ProductInfoRepository.java: -------------------------------------------------------------------------------- 1 | package me.zhulin.shopapi.repository; 2 | 3 | import me.zhulin.shopapi.entity.ProductInfo; 4 | import org.springframework.data.domain.Page; 5 | import org.springframework.data.domain.Pageable; 6 | import org.springframework.data.jpa.repository.JpaRepository; 7 | 8 | /** 9 | * Created By Zhu Lin on 3/10/2018. 10 | */ 11 | public interface ProductInfoRepository extends JpaRepository { 12 | ProductInfo findByProductId(String id); 13 | // onsale product 14 | Page findAllByProductStatusOrderByProductIdAsc(Integer productStatus, Pageable pageable); 15 | 16 | // product in one category 17 | Page findAllByCategoryTypeOrderByProductIdAsc(Integer categoryType, Pageable pageable); 18 | 19 | Page findAllByOrderByProductId(Pageable pageable); 20 | 21 | } 22 | -------------------------------------------------------------------------------- /backend/src/main/java/me/zhulin/shopapi/vo/response/CategoryPage.java: -------------------------------------------------------------------------------- 1 | package me.zhulin.shopapi.vo.response; 2 | 3 | import me.zhulin.shopapi.entity.ProductInfo; 4 | import org.springframework.data.domain.Page; 5 | 6 | /** 7 | * Created By Zhu Lin on 12/28/2018. 8 | */ 9 | public class CategoryPage { 10 | private String category; 11 | private Page page; 12 | 13 | public CategoryPage(String category, Page page) { 14 | this.category = category; 15 | this.page = page; 16 | } 17 | 18 | public String getCategory() { 19 | return category; 20 | } 21 | 22 | public void setCategory(String category) { 23 | this.category = category; 24 | } 25 | 26 | public Page getPage() { 27 | return page; 28 | } 29 | 30 | public void setPage(Page page) { 31 | this.page = page; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /frontend/src/app/pages/signup/signup.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit} from '@angular/core'; 2 | import {Location} from '@angular/common'; 3 | import {User} from "../../models/User"; 4 | import {UserService} from "../../services/user.service"; 5 | import {Router} from "@angular/router"; 6 | 7 | @Component({ 8 | selector: 'app-signup', 9 | templateUrl: './signup.component.html', 10 | styleUrls: ['./signup.component.css'] 11 | }) 12 | export class SignupComponent implements OnInit { 13 | 14 | user: User; 15 | 16 | constructor( private location: Location, 17 | private userService: UserService, 18 | private router: Router) { 19 | this.user = new User(); 20 | 21 | } 22 | 23 | 24 | 25 | ngOnInit() { 26 | 27 | 28 | } 29 | onSubmit() { 30 | this.userService.signUp(this.user).subscribe(u => { 31 | this.router.navigate(['/login']); 32 | }, 33 | e => {}); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /backend/src/main/java/me/zhulin/shopapi/repository/OrderRepository.java: -------------------------------------------------------------------------------- 1 | package me.zhulin.shopapi.repository; 2 | 3 | 4 | import me.zhulin.shopapi.entity.OrderMain; 5 | import org.springframework.data.domain.Page; 6 | import org.springframework.data.domain.Pageable; 7 | import org.springframework.data.jpa.repository.JpaRepository; 8 | 9 | /** 10 | * Created By Zhu Lin on 3/14/2018. 11 | */ 12 | public interface OrderRepository extends JpaRepository { 13 | OrderMain findByOrderId(Long orderId); 14 | 15 | 16 | Page findAllByOrderStatusOrderByCreateTimeDesc(Integer orderStatus, Pageable pageable); 17 | 18 | 19 | Page findAllByBuyerEmailOrderByOrderStatusAscCreateTimeDesc(String buyerEmail, Pageable pageable); 20 | 21 | Page findAllByOrderByOrderStatusAscCreateTimeDesc(Pageable pageable); 22 | 23 | Page findAllByBuyerPhoneOrderByOrderStatusAscCreateTimeDesc(String buyerPhone, Pageable pageable); 24 | } 25 | -------------------------------------------------------------------------------- /frontend/src/app/pages/order-detail/order-detail.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit} from '@angular/core'; 2 | import {Observable} from "rxjs"; 3 | import {OrderService} from "../../services/order.service"; 4 | import {Order} from "../../models/Order"; 5 | import {ActivatedRoute} from "@angular/router"; 6 | 7 | @Component({ 8 | selector: 'app-order-detail', 9 | templateUrl: './order-detail.component.html', 10 | styleUrls: ['./order-detail.component.css'] 11 | }) 12 | export class OrderDetailComponent implements OnInit { 13 | 14 | constructor(private orderService: OrderService, 15 | private route: ActivatedRoute) { 16 | } 17 | 18 | order$: Observable; 19 | 20 | ngOnInit() { 21 | // this.items$ = this.route.paramMap.pipe( 22 | // map(paramMap =>paramMap.get('id')), 23 | // switchMap((id:string) => this.orderService.show(id)) 24 | // ) 25 | this.order$ = this.orderService.show(this.route.snapshot.paramMap.get('id')); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /backend/src/main/java/me/zhulin/shopapi/entity/ProductCategory.java: -------------------------------------------------------------------------------- 1 | package me.zhulin.shopapi.entity; 2 | 3 | import lombok.Data; 4 | import org.hibernate.annotations.DynamicUpdate; 5 | import org.hibernate.annotations.NaturalId; 6 | 7 | import javax.persistence.Entity; 8 | import javax.persistence.GeneratedValue; 9 | import javax.persistence.Id; 10 | import java.io.Serializable; 11 | import java.util.Date; 12 | 13 | /** 14 | * Created By Zhu Lin on 3/9/2018. 15 | */ 16 | @Entity 17 | @Data 18 | @DynamicUpdate 19 | public class ProductCategory implements Serializable { 20 | @Id 21 | @GeneratedValue 22 | private Integer categoryId; 23 | 24 | private String categoryName; 25 | 26 | @NaturalId 27 | private Integer categoryType; 28 | 29 | private Date createTime; 30 | 31 | private Date updateTime; 32 | 33 | 34 | public ProductCategory() { 35 | } 36 | 37 | public ProductCategory(String categoryName, Integer categoryType) { 38 | this.categoryName = categoryName; 39 | this.categoryType = categoryType; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /frontend/src/app/_interceptors/error-interceptor.service.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | import {HttpEvent, HttpHandler, HttpInterceptor, HttpRequest} from "@angular/common/http"; 3 | import {UserService} from "../services/user.service"; 4 | import {Observable, throwError} from "rxjs"; 5 | import {catchError} from "rxjs/operators"; 6 | import {Router} from "@angular/router"; 7 | 8 | @Injectable({ 9 | providedIn: 'root' 10 | }) 11 | export class ErrorInterceptor implements HttpInterceptor { 12 | constructor(private userService: UserService, 13 | private router: Router) { 14 | } 15 | 16 | intercept(request: HttpRequest, next: HttpHandler): Observable> { 17 | return next.handle(request).pipe(catchError(err => { 18 | if (err.status === 401) { 19 | // auto logout if 401 response returned from api 20 | this.userService.logout(); 21 | this.router.navigate(['/login']); 22 | } 23 | 24 | const error = err.error || err.statusText; 25 | return throwError(error); 26 | })) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /frontend/src/app/_interceptors/jwt-interceptor.service.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | import {UserService} from "../services/user.service"; 3 | import {HttpEvent, HttpHandler, HttpInterceptor, HttpRequest} from "@angular/common/http"; 4 | import {Observable} from "rxjs"; 5 | 6 | @Injectable({ 7 | providedIn: 'root' 8 | }) 9 | export class JwtInterceptor implements HttpInterceptor { 10 | 11 | 12 | constructor(private userService: UserService, 13 | ) { 14 | 15 | } 16 | 17 | intercept(request: HttpRequest, next: HttpHandler): Observable> { 18 | // add authorization header with jwt token if available 19 | const currentUser = this.userService.currentUserValue; 20 | if (currentUser && currentUser.token) { 21 | request = request.clone({ 22 | setHeaders: { 23 | Authorization: `${currentUser.type} ${currentUser.token}`, 24 | 'Content-Type': 'application/json' 25 | } 26 | }); 27 | } 28 | return next.handle(request); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /frontend/src/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, '../coverage'), 20 | reports: ['html', 'lcovonly', 'text-summary'], 21 | fixWebpackSourcePaths: true 22 | }, 23 | reporters: ['progress', 'kjhtml'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ['Chrome'], 29 | singleRun: false 30 | }); 31 | }; -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Lin Zhu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # Shop 2 | 3 | This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 7.1.4. 4 | 5 | ## Development server 6 | 7 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. 8 | 9 | ## Code scaffolding 10 | 11 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. 12 | 13 | ## Build 14 | 15 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build. 16 | 17 | ## Running unit tests 18 | 19 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). 20 | 21 | ## Running end-to-end tests 22 | 23 | Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/). 24 | 25 | ## Further help 26 | 27 | To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md). 28 | -------------------------------------------------------------------------------- /backend/src/main/java/me/zhulin/shopapi/enums/ResultEnum.java: -------------------------------------------------------------------------------- 1 | package me.zhulin.shopapi.enums; 2 | 3 | import lombok.Getter; 4 | 5 | /** 6 | * Created By Zhu Lin on 3/10/2018. 7 | */ 8 | 9 | @Getter 10 | public enum ResultEnum { 11 | 12 | PARAM_ERROR(1, "Parameter Error!"), 13 | PRODUCT_NOT_EXIST(10, "Product does not exit!"), 14 | PRODUCT_NOT_ENOUGH(11, "Not enough products in stock!"), 15 | PRODUCT_STATUS_ERROR(12, "Status is incorrect!"), 16 | PRODUCT_OFF_SALE(13,"Product is off sale!"), 17 | PRODUCT_NOT_IN_CART(14,"Product is not in the cart!"), 18 | CART_CHECKOUT_SUCCESS(20, "Checkout successfully! "), 19 | 20 | CATEGORY_NOT_FOUND(30, "Category does not exit!"), 21 | 22 | ORDER_NOT_FOUND(60, "OrderMain is not exit!"), 23 | ORDER_STATUS_ERROR(61, "Status is not correct"), 24 | 25 | 26 | VALID_ERROR(50, "Wrong information"), 27 | USER_NOT_FOUNT(40,"User is not found!") 28 | ; 29 | 30 | private Integer code; 31 | 32 | private String message; 33 | 34 | ResultEnum(Integer code, String message) { 35 | this.code = code; 36 | this.message = message; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /backend/src/main/java/me/zhulin/shopapi/service/ProductService.java: -------------------------------------------------------------------------------- 1 | package me.zhulin.shopapi.service; 2 | 3 | 4 | import me.zhulin.shopapi.entity.ProductInfo; 5 | import org.springframework.data.domain.Page; 6 | import org.springframework.data.domain.Pageable; 7 | 8 | /** 9 | * Created By Zhu Lin on 3/10/2018. 10 | */ 11 | public interface ProductService { 12 | 13 | ProductInfo findOne(String productId); 14 | 15 | // All selling products 16 | Page findUpAll(Pageable pageable); 17 | // All products 18 | Page findAll(Pageable pageable); 19 | // All products in a category 20 | Page findAllInCategory(Integer categoryType, Pageable pageable); 21 | 22 | // increase stock 23 | void increaseStock(String productId, int amount); 24 | 25 | //decrease stock 26 | void decreaseStock(String productId, int amount); 27 | 28 | ProductInfo offSale(String productId); 29 | 30 | ProductInfo onSale(String productId); 31 | 32 | ProductInfo update(ProductInfo productInfo); 33 | ProductInfo save(ProductInfo productInfo); 34 | 35 | void delete(String productId); 36 | 37 | 38 | } 39 | -------------------------------------------------------------------------------- /backend/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | ## Spring DATASOURCE (DataSourceAutoConfiguration & DataSourceProperties) 2 | spring: 3 | datasource: 4 | driver-class-name: org.postgresql.Driver 5 | username: postgres 6 | password: root 7 | url: jdbc:postgresql://localhost/postgres 8 | platform: postgres 9 | initialization-mode: always 10 | continue-on-error: true 11 | jpa: 12 | show-sql: false 13 | # generate-ddl: false 14 | hibernate: 15 | ddl-auto: create 16 | database: postgresql 17 | properties: 18 | hibernate: 19 | temp: 20 | use_jdbc_metadata_defaults: false; 21 | database-platform: org.hibernate.dialect.PostgreSQL9Dialect 22 | # dialect: org.hibernate.dialect.MySQL5Dialect 23 | 24 | # jackson: 25 | # default-property-inclusion: non_null 26 | # redis: 27 | # host: localhost 28 | # port: 6379 29 | # freemarker: 30 | # cache: false 31 | 32 | 33 | 34 | 35 | queries: 36 | users-query: select email, password, active from users where email=? 37 | roles-query: select email, role from users where email=? 38 | server: 39 | servlet: 40 | contextPath: /api 41 | 42 | jwtSecret: me.zhulin 43 | jwtExpiration: 86400 44 | -------------------------------------------------------------------------------- /backend/src/main/resources/application-docker.yml: -------------------------------------------------------------------------------- 1 | ## Spring DATASOURCE (DataSourceAutoConfiguration & DataSourceProperties) 2 | spring: 3 | datasource: 4 | driver-class-name: org.postgresql.Driver 5 | username: postgres 6 | password: root 7 | url: jdbc:postgresql://db:5432/postgres 8 | platform: postgres 9 | initialization-mode: always 10 | continue-on-error: true 11 | jpa: 12 | show-sql: false 13 | # generate-ddl: false 14 | hibernate: 15 | ddl-auto: create 16 | database: postgresql 17 | properties: 18 | hibernate: 19 | temp: 20 | use_jdbc_metadata_defaults: false; 21 | database-platform: org.hibernate.dialect.PostgreSQL9Dialect 22 | # dialect: org.hibernate.dialect.MySQL5Dialect 23 | 24 | # jackson: 25 | # default-property-inclusion: non_null 26 | # redis: 27 | # host: localhost 28 | # port: 6379 29 | # freemarker: 30 | # cache: false 31 | 32 | 33 | 34 | 35 | queries: 36 | users-query: select email, password, active from users where email=? 37 | roles-query: select email, role from users where email=? 38 | server: 39 | servlet: 40 | contextPath: /api 41 | 42 | jwtSecret: me.zhulin 43 | jwtExpiration: 86400 44 | -------------------------------------------------------------------------------- /backend/src/main/java/me/zhulin/shopapi/security/JWT/JwtEntryPoint.java: -------------------------------------------------------------------------------- 1 | package me.zhulin.shopapi.security.JWT; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.http.HttpStatus; 6 | import org.springframework.security.core.AuthenticationException; 7 | import org.springframework.security.web.AuthenticationEntryPoint; 8 | import org.springframework.stereotype.Component; 9 | 10 | import javax.servlet.http.HttpServletRequest; 11 | import javax.servlet.http.HttpServletResponse; 12 | import java.io.IOException; 13 | 14 | /** 15 | * Created By Zhu Lin on 1/1/2019. 16 | */ 17 | @Component 18 | public class JwtEntryPoint implements AuthenticationEntryPoint { 19 | 20 | 21 | private static final Logger logger = LoggerFactory.getLogger(JwtEntryPoint.class); 22 | 23 | // called if authentication failed 24 | @Override 25 | public void commence(HttpServletRequest request, 26 | HttpServletResponse response, 27 | AuthenticationException e) 28 | throws IOException { 29 | 30 | logger.error("Unauthorized error. Message - {}", e.getMessage()); 31 | response.setStatus(HttpStatus.UNAUTHORIZED.value()); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /frontend/src/app/pages/card/card.component.html: -------------------------------------------------------------------------------- 1 |

{{title}}

2 |
3 |
4 |
5 | {{productInfo.productName}} 6 |
7 |

{{productInfo.productName}}

8 |
9 |

Description: {{productInfo.productDescription}}

10 |

Price: {{productInfo.productPrice | currency}}

11 |

Stock: {{productInfo.productStock}}

12 |
13 |
14 | Get It! 17 | Unavailable 18 |
19 |
20 |
21 |
22 | 23 | -------------------------------------------------------------------------------- /frontend/src/app/services/order.service.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | import {HttpClient} from "@angular/common/http"; 3 | import {catchError} from "rxjs/operators"; 4 | import {Observable, of} from "rxjs"; 5 | import {Order} from "../models/Order"; 6 | import {apiUrl} from "../../environments/environment"; 7 | 8 | @Injectable({ 9 | providedIn: 'root' 10 | }) 11 | export class OrderService { 12 | 13 | private orderUrl = `${apiUrl}/order`; 14 | 15 | constructor(private http: HttpClient) { 16 | } 17 | 18 | getPage(page = 1, size = 10): Observable { 19 | return this.http.get(`${this.orderUrl}?page=${page}&size=${size}`).pipe(); 20 | } 21 | 22 | show(id): Observable { 23 | return this.http.get(`${this.orderUrl}/${id}`).pipe( 24 | catchError(_ => of(null)) 25 | ); 26 | } 27 | 28 | cancel(id): Observable { 29 | return this.http.patch(`${this.orderUrl}/cancel/${id}`, null).pipe( 30 | catchError(_ => of(null)) 31 | ); 32 | } 33 | 34 | finish(id): Observable { 35 | return this.http.patch(`${this.orderUrl}/finish/${id}`, null).pipe( 36 | catchError(_ => of(null)) 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /backend/src/main/java/me/zhulin/shopapi/entity/Cart.java: -------------------------------------------------------------------------------- 1 | package me.zhulin.shopapi.entity; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnore; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | 7 | import javax.persistence.*; 8 | import javax.validation.constraints.NotNull; 9 | import java.io.Serializable; 10 | import java.util.HashSet; 11 | import java.util.Set; 12 | 13 | /** 14 | * Created By Zhu Lin on 1/2/2019. 15 | */ 16 | @Data 17 | @Entity 18 | @NoArgsConstructor 19 | public class Cart implements Serializable { 20 | @Id 21 | @NotNull 22 | @GeneratedValue(strategy = GenerationType.AUTO) 23 | private long cartId; 24 | 25 | @OneToOne(fetch = FetchType.LAZY) 26 | @MapsId 27 | @JsonIgnore 28 | // @JoinColumn(name = "email", referencedColumnName = "email") 29 | private User user; 30 | 31 | @OneToMany(cascade = CascadeType.ALL, 32 | fetch = FetchType.LAZY, orphanRemoval = true, 33 | mappedBy = "cart") 34 | private Set products = new HashSet<>(); 35 | 36 | @Override 37 | public String toString() { 38 | return "Cart{" + 39 | "cartId=" + cartId + 40 | ", products=" + products + 41 | '}'; 42 | } 43 | 44 | public Cart(User user) { 45 | this.user = user; 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /frontend/dist 5 | /frontend/tmp 6 | /frontend/out-tsc 7 | 8 | # dependencies 9 | /frontend/node* 10 | /frontend/node_modules 11 | 12 | # IDEs and editors 13 | *.iml 14 | 15 | 16 | # profiling files 17 | chrome-profiler-events.json 18 | speed-measure-plugin.json 19 | 20 | # IDEs and editors 21 | .idea 22 | .project 23 | .classpath 24 | .c9/ 25 | *.launch 26 | .settings/ 27 | *.sublime-workspace 28 | 29 | # IDE - VSCode 30 | .vscode/* 31 | !.vscode/settings.json 32 | !.vscode/tasks.json 33 | !.vscode/launch.json 34 | !.vscode/extensions.json 35 | 36 | # misc 37 | /frontend/.sass-cache 38 | /frontend/connect.lock 39 | /frontend/coverage 40 | /frontend/libpeerconnection.log 41 | npm-debug.log 42 | yarn-error.log 43 | testem.log 44 | /frontend/typings 45 | 46 | # System Files 47 | .DS_Store 48 | Thumbs.db 49 | 50 | ### backend 51 | 52 | /backend/target/ 53 | !.mvn/wrapper/maven-wrapper.jar 54 | 55 | ### STS ### 56 | .apt_generated 57 | .classpath 58 | .factorypath 59 | .project 60 | .settings 61 | .springBeans 62 | .sts4-cache 63 | 64 | ### IntelliJ IDEA ### 65 | .idea 66 | *.iws 67 | *.iml 68 | *.ipr 69 | 70 | ### NetBeans ### 71 | /backend/nbproject/private/ 72 | /backend/build/ 73 | /backend/nbbuild/ 74 | /backend/dist/ 75 | /backend/nbdist/ 76 | /backend/.nb-gradle/ 77 | -------------------------------------------------------------------------------- /backend/src/main/java/me/zhulin/shopapi/ShopApiApplication.java: -------------------------------------------------------------------------------- 1 | package me.zhulin.shopapi; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 7 | import org.springframework.security.crypto.password.PasswordEncoder; 8 | import org.springframework.security.crypto.password.Pbkdf2PasswordEncoder; 9 | import org.springframework.web.servlet.config.annotation.CorsRegistry; 10 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 11 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; 12 | 13 | @SpringBootApplication 14 | public class ShopApiApplication { 15 | @Bean 16 | public PasswordEncoder passwordEncoder() { 17 | return new BCryptPasswordEncoder(); 18 | } 19 | 20 | @Bean 21 | public WebMvcConfigurer corsConfigurer() { 22 | return new WebMvcConfigurerAdapter() { 23 | @Override 24 | public void addCorsMappings(CorsRegistry registry) { 25 | registry.addMapping("/**"); 26 | } 27 | }; 28 | } 29 | 30 | public static void main(String[] args) { 31 | SpringApplication.run(ShopApiApplication.class, args); 32 | } 33 | 34 | } 35 | 36 | -------------------------------------------------------------------------------- /frontend/src/app/pages/user-edit/user-detail.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit} from '@angular/core'; 2 | import {UserService} from "../../services/user.service"; 3 | import {User} from "../../models/User"; 4 | import {Router} from "@angular/router"; 5 | import {Observable, Subject} from "rxjs"; 6 | import {Role} from "../../enum/Role"; 7 | 8 | @Component({ 9 | selector: 'app-user-detail', 10 | templateUrl: './user-detail.component.html', 11 | styleUrls: ['./user-detail.component.css'] 12 | }) 13 | export class UserDetailComponent implements OnInit { 14 | 15 | 16 | 17 | 18 | constructor(private userService: UserService, 19 | private router: Router) { 20 | } 21 | 22 | user= new User(); 23 | 24 | 25 | ngOnInit() { 26 | const account = this.userService.currentUserValue.account; 27 | 28 | this.userService.get(account).subscribe( u => { 29 | this.user = u; 30 | this.user.password = ''; 31 | }, e => { 32 | 33 | }); 34 | } 35 | 36 | onSubmit() { 37 | this.userService.update(this.user).subscribe(u => { 38 | this.userService.nameTerms.next(u.name); 39 | let url = '/'; 40 | if (this.user.role != Role.Customer) { 41 | url = '/seller'; 42 | } 43 | this.router.navigateByUrl(url); 44 | }, _ => {}) 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /frontend/src/app/models/productInfo.ts: -------------------------------------------------------------------------------- 1 | import {ProductInOrder} from "./ProductInOrder"; 2 | 3 | export class ProductInfo { 4 | productId: string; 5 | productName: string; 6 | productPrice: number; 7 | productStock: number; 8 | productDescription: string; 9 | productIcon: string; 10 | productStatus: number; // 0: onsale 1: offsale 11 | categoryType: number; 12 | createTime: string; 13 | updateTime: string; 14 | 15 | 16 | constructor(productInOrder?: ProductInOrder) { 17 | if (productInOrder) { 18 | this.productId = productInOrder.productId; 19 | this.productName = productInOrder.productName; 20 | this.productPrice = productInOrder.productPrice; 21 | this.productStock = productInOrder.productStock; 22 | this.productDescription = productInOrder.productDescription; 23 | this.productIcon = productInOrder.productIcon; 24 | this.categoryType = productInOrder.categoryType; 25 | this.productStatus = 0; 26 | } else { 27 | this.productId = ''; 28 | this.productName = ''; 29 | this.productPrice = 20; 30 | this.productStock = 100; 31 | this.productDescription = ''; 32 | this.productIcon = ''; 33 | this.categoryType = 0; 34 | this.productStatus = 0; 35 | } 36 | } 37 | } 38 | 39 | -------------------------------------------------------------------------------- /frontend/src/app/pages/order-detail/order-detail.component.html: -------------------------------------------------------------------------------- 1 |

Order Detail

2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 |
PhotoNameDescriptionPriceQuantitySubtotal
17 | {{item.productName}} 19 | {{item.productName}}{{item.productDescription}}{{item.productPrice | currency}}{{item.count}}{{(item.productPrice * item.count | currency)}}
28 |
Total: {{(order$ | async)?.orderAmount | currency}}
29 | -------------------------------------------------------------------------------- /frontend/src/app/_guards/auth.guard.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | import {ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot} from '@angular/router'; 3 | import {Observable} from 'rxjs'; 4 | import {UserService} from "../services/user.service"; 5 | 6 | @Injectable({ 7 | providedIn: 'root' 8 | }) 9 | export class AuthGuard implements CanActivate { 10 | 11 | 12 | constructor( 13 | private router: Router, 14 | private userService: UserService 15 | ) { 16 | } 17 | 18 | canActivate( 19 | route: ActivatedRouteSnapshot, 20 | state: RouterStateSnapshot): Observable | Promise | boolean { 21 | const currentUser = this.userService.currentUserValue; 22 | if (currentUser) { 23 | // check if route is restricted by role 24 | if (route.data.roles && route.data.roles.indexOf(currentUser.role) === -1) { 25 | console.log(currentUser.role + "fail in " + route.data.roles); 26 | // role not authorised so redirect to home page 27 | this.router.navigate(['/']); 28 | return false; 29 | } 30 | // authorised so return true 31 | return true; 32 | } 33 | 34 | console.log("Need log in"); 35 | // not logged in so redirect to login page with the return url{queryParams: {returnUrl: state.url}} 36 | this.router.navigate(['/login'], {queryParams: {returnUrl: state.url}}); 37 | return false; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /backend/src/main/java/me/zhulin/shopapi/entity/ProductInfo.java: -------------------------------------------------------------------------------- 1 | package me.zhulin.shopapi.entity; 2 | 3 | import lombok.Data; 4 | import org.hibernate.annotations.ColumnDefault; 5 | import org.hibernate.annotations.CreationTimestamp; 6 | import org.hibernate.annotations.DynamicUpdate; 7 | import org.hibernate.annotations.UpdateTimestamp; 8 | 9 | import javax.persistence.Entity; 10 | import javax.persistence.Id; 11 | import javax.validation.constraints.Min; 12 | import javax.validation.constraints.NotNull; 13 | import java.io.Serializable; 14 | import java.math.BigDecimal; 15 | import java.util.Date; 16 | 17 | /** 18 | * Created By Zhu Lin on 3/10/2018. 19 | */ 20 | @Entity 21 | @Data 22 | @DynamicUpdate 23 | public class ProductInfo implements Serializable { 24 | @Id 25 | private String productId; 26 | 27 | /** 名字. */ 28 | @NotNull 29 | private String productName; 30 | 31 | /** 单价. */ 32 | @NotNull 33 | private BigDecimal productPrice; 34 | 35 | /** 库存. */ 36 | @NotNull 37 | @Min(0) 38 | private Integer productStock; 39 | 40 | /** 描述. */ 41 | private String productDescription; 42 | 43 | /** 小图. */ 44 | private String productIcon; 45 | 46 | /** 0: on-sale 1: off-sale */ 47 | 48 | @ColumnDefault("0") 49 | private Integer productStatus; 50 | 51 | 52 | /** 类目编号. */ 53 | @ColumnDefault("0") 54 | private Integer categoryType; 55 | 56 | @CreationTimestamp 57 | private Date createTime; 58 | @UpdateTimestamp 59 | private Date updateTime; 60 | 61 | public ProductInfo() { 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /backend/src/main/java/me/zhulin/shopapi/service/impl/ProductInOrderServiceImpl.java: -------------------------------------------------------------------------------- 1 | package me.zhulin.shopapi.service.impl; 2 | 3 | import me.zhulin.shopapi.entity.ProductInOrder; 4 | import me.zhulin.shopapi.entity.User; 5 | import me.zhulin.shopapi.repository.ProductInOrderRepository; 6 | import me.zhulin.shopapi.service.ProductInOrderService; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.stereotype.Service; 9 | import org.springframework.transaction.annotation.Transactional; 10 | 11 | import java.util.concurrent.atomic.AtomicReference; 12 | 13 | /** 14 | * Created By Zhu Lin on 1/3/2019. 15 | */ 16 | @Service 17 | public class ProductInOrderServiceImpl implements ProductInOrderService { 18 | 19 | @Autowired 20 | ProductInOrderRepository productInOrderRepository; 21 | 22 | @Override 23 | @Transactional 24 | public void update(String itemId, Integer quantity, User user) { 25 | var op = user.getCart().getProducts().stream().filter(e -> itemId.equals(e.getProductId())).findFirst(); 26 | op.ifPresent(productInOrder -> { 27 | productInOrder.setCount(quantity); 28 | productInOrderRepository.save(productInOrder); 29 | }); 30 | 31 | } 32 | 33 | @Override 34 | public ProductInOrder findOne(String itemId, User user) { 35 | var op = user.getCart().getProducts().stream().filter(e -> itemId.equals(e.getProductId())).findFirst(); 36 | AtomicReference res = new AtomicReference<>(); 37 | op.ifPresent(res::set); 38 | return res.get(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shop", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "build": "ng build", 8 | "test": "ng test", 9 | "lint": "ng lint", 10 | "e2e": "ng e2e" 11 | }, 12 | "private": true, 13 | "dependencies": { 14 | "@angular/animations": "~7.1.0", 15 | "@angular/common": "~7.1.0", 16 | "@angular/compiler": "~7.1.0", 17 | "@angular/core": "~7.1.0", 18 | "@angular/forms": "~7.1.0", 19 | "@angular/platform-browser": "~7.1.0", 20 | "@angular/platform-browser-dynamic": "~7.1.0", 21 | "@angular/router": "~7.1.0", 22 | "bootstrap": "^4.0.0", 23 | "core-js": "^2.5.4", 24 | "jquery": "^3.3.1", 25 | "ngx-cookie-service": "^2.1.0", 26 | "popper.js": "^1.14.6", 27 | "rxjs": "~6.3.3", 28 | "tslib": "^1.9.0", 29 | "zone.js": "~0.8.26" 30 | }, 31 | "devDependencies": { 32 | "@angular-devkit/build-angular": "~0.11.0", 33 | "@angular/cli": "~7.1.4", 34 | "@angular/compiler-cli": "~7.1.0", 35 | "@angular/language-service": "~7.1.0", 36 | "@types/jasmine": "~2.8.8", 37 | "@types/jasminewd2": "~2.0.3", 38 | "@types/node": "~8.9.4", 39 | "codelyzer": "~4.5.0", 40 | "jasmine-core": "~2.99.1", 41 | "jasmine-spec-reporter": "~4.2.1", 42 | "karma": "~3.1.1", 43 | "karma-chrome-launcher": "~2.2.0", 44 | "karma-coverage-istanbul-reporter": "~2.0.1", 45 | "karma-jasmine": "~1.1.2", 46 | "karma-jasmine-html-reporter": "^0.2.2", 47 | "protractor": "~5.4.0", 48 | "ts-node": "~7.0.0", 49 | "tslint": "~5.11.0", 50 | "typescript": "~3.1.6" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /frontend/src/app/parts/navigation/navigation.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnDestroy, OnInit} from '@angular/core'; 2 | import {UserService} from "../../services/user.service"; 3 | import {Subscription} from "rxjs"; 4 | import {JwtResponse} from "../../response/JwtResponse"; 5 | import {Router} from "@angular/router"; 6 | import {Role} from "../../enum/Role"; 7 | 8 | @Component({ 9 | selector: 'app-navigation', 10 | templateUrl: './navigation.component.html', 11 | styleUrls: ['./navigation.component.css'] 12 | }) 13 | export class NavigationComponent implements OnInit, OnDestroy { 14 | 15 | 16 | currentUserSubscription: Subscription; 17 | name$; 18 | name: string; 19 | currentUser: JwtResponse; 20 | root = '/'; 21 | Role = Role; 22 | 23 | constructor(private userService: UserService, 24 | private router: Router, 25 | ) { 26 | 27 | } 28 | 29 | 30 | ngOnInit() { 31 | this.name$ = this.userService.name$.subscribe(aName => this.name = aName); 32 | this.currentUserSubscription = this.userService.currentUser.subscribe(user => { 33 | this.currentUser = user; 34 | if (!user || user.role == Role.Customer) { 35 | this.root = '/'; 36 | } else { 37 | this.root = '/seller'; 38 | } 39 | }); 40 | } 41 | 42 | ngOnDestroy(): void { 43 | this.currentUserSubscription.unsubscribe(); 44 | // this.name$.unsubscribe(); 45 | } 46 | 47 | logout() { 48 | this.userService.logout(); 49 | // this.router.navigate(['/login'], {queryParams: {logout: 'true'}} ); 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /backend/src/test/java/me/zhulin/shopapi/service/impl/CategoryServiceImplTest.java: -------------------------------------------------------------------------------- 1 | package me.zhulin.shopapi.service.impl; 2 | 3 | import me.zhulin.shopapi.entity.ProductCategory; 4 | import me.zhulin.shopapi.exception.MyException; 5 | import me.zhulin.shopapi.repository.ProductCategoryRepository; 6 | import org.junit.Test; 7 | import org.junit.runner.RunWith; 8 | import org.mockito.InjectMocks; 9 | import org.mockito.Mock; 10 | import org.mockito.Mockito; 11 | import org.springframework.test.context.junit4.SpringRunner; 12 | 13 | @RunWith(SpringRunner.class) 14 | public class CategoryServiceImplTest { 15 | 16 | @InjectMocks 17 | private CategoryServiceImpl categoryService; 18 | 19 | @Mock 20 | private ProductCategoryRepository productCategoryRepository; 21 | 22 | @Test 23 | public void findByCategoryTypeTest() { 24 | ProductCategory productCategory = new ProductCategory(); 25 | productCategory.setCategoryId(1); 26 | 27 | Mockito.when(productCategoryRepository.findByCategoryType(productCategory.getCategoryId())).thenReturn(productCategory); 28 | 29 | categoryService.findByCategoryType(productCategory.getCategoryId()); 30 | 31 | Mockito.verify(productCategoryRepository, Mockito.times(1)).findByCategoryType(productCategory.getCategoryId()); 32 | } 33 | 34 | @Test(expected = MyException.class) 35 | public void findByCategoryTypeExceptionTest() { 36 | ProductCategory productCategory = new ProductCategory(); 37 | productCategory.setCategoryId(1); 38 | 39 | Mockito.when(productCategoryRepository.findByCategoryType(productCategory.getCategoryId())).thenReturn(null); 40 | 41 | categoryService.findByCategoryType(productCategory.getCategoryId()); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /backend/src/main/java/me/zhulin/shopapi/api/CategoryController.java: -------------------------------------------------------------------------------- 1 | package me.zhulin.shopapi.api; 2 | 3 | 4 | import me.zhulin.shopapi.entity.ProductCategory; 5 | import me.zhulin.shopapi.entity.ProductInfo; 6 | import me.zhulin.shopapi.service.CategoryService; 7 | import me.zhulin.shopapi.service.ProductService; 8 | import me.zhulin.shopapi.vo.response.CategoryPage; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.data.domain.Page; 11 | import org.springframework.data.domain.PageRequest; 12 | import org.springframework.web.bind.annotation.*; 13 | 14 | /** 15 | * Created By Zhu Lin on 3/10/2018. 16 | */ 17 | @RestController 18 | @CrossOrigin 19 | public class CategoryController { 20 | @Autowired 21 | CategoryService categoryService; 22 | @Autowired 23 | ProductService productService; 24 | 25 | 26 | /** 27 | * Show products in category 28 | * 29 | * @param categoryType 30 | * @param page 31 | * @param size 32 | * @return 33 | */ 34 | @GetMapping("/category/{type}") 35 | public CategoryPage showOne(@PathVariable("type") Integer categoryType, 36 | @RequestParam(value = "page", defaultValue = "1") Integer page, 37 | @RequestParam(value = "size", defaultValue = "3") Integer size) { 38 | 39 | ProductCategory cat = categoryService.findByCategoryType(categoryType); 40 | PageRequest request = PageRequest.of(page - 1, size); 41 | Page productInCategory = productService.findAllInCategory(categoryType, request); 42 | var tmp = new CategoryPage("", productInCategory); 43 | tmp.setCategory(cat.getCategoryName()); 44 | return tmp; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /frontend/src/app/pages/login/login.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit} from '@angular/core'; 2 | import {UserService} from "../../services/user.service"; 3 | import {ActivatedRoute, Router} from "@angular/router"; 4 | import {Role} from "../../enum/Role"; 5 | 6 | @Component({ 7 | selector: 'app-login', 8 | templateUrl: './login.component.html', 9 | styleUrls: ['./login.component.css'] 10 | }) 11 | export class LoginComponent implements OnInit { 12 | 13 | isInvalid: boolean; 14 | isLogout: boolean; 15 | submitted = false; 16 | model: any = { 17 | username: '', 18 | password: '', 19 | remembered: false 20 | }; 21 | 22 | returnUrl = '/'; 23 | 24 | constructor(private userService: UserService, 25 | private router: Router, 26 | private route: ActivatedRoute) { 27 | } 28 | 29 | ngOnInit() { 30 | let params = this.route.snapshot.queryParamMap; 31 | this.isLogout = params.has('logout'); 32 | this.returnUrl = params.get('returnUrl'); 33 | } 34 | 35 | onSubmit() { 36 | this.submitted = true; 37 | this.userService.login(this.model).subscribe( 38 | user => { 39 | if (user) { 40 | if (user.role != Role.Customer) { 41 | 42 | this.returnUrl = '/seller'; 43 | } 44 | 45 | this.router.navigateByUrl(this.returnUrl); 46 | } else { 47 | this.isLogout = false; 48 | this.isInvalid = true; 49 | } 50 | 51 | } 52 | ); 53 | } 54 | 55 | fillLoginFields(u, p) { 56 | this.model.username = u; 57 | this.model.password = p; 58 | this.onSubmit(); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /backend/src/main/java/me/zhulin/shopapi/security/JWT/JwtProvider.java: -------------------------------------------------------------------------------- 1 | package me.zhulin.shopapi.security.JWT; 2 | 3 | import io.jsonwebtoken.Jwts; 4 | import io.jsonwebtoken.SignatureAlgorithm; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | import org.springframework.beans.factory.annotation.Value; 8 | import org.springframework.security.core.Authentication; 9 | import org.springframework.security.core.userdetails.UserDetails; 10 | import org.springframework.stereotype.Component; 11 | 12 | import java.util.Date; 13 | 14 | 15 | /** 16 | * Created By Zhu Lin on 1/1/2019. 17 | */ 18 | @Component 19 | public class JwtProvider { 20 | 21 | private static final Logger logger = LoggerFactory.getLogger(JwtProvider.class); 22 | @Value("${jwtSecret}") 23 | private String jwtSecret; 24 | @Value("${jwtExpiration}") 25 | private int jwtExpiration; 26 | 27 | public String generate(Authentication authentication) { 28 | 29 | UserDetails userDetails = (UserDetails) authentication.getPrincipal(); 30 | return Jwts.builder() 31 | .setSubject(userDetails.getUsername()) 32 | .setIssuedAt(new Date()) 33 | .setExpiration(new Date(new Date().getTime() + jwtExpiration * 1000)) 34 | .signWith(SignatureAlgorithm.HS512, jwtSecret) 35 | .compact(); 36 | } 37 | 38 | public boolean validate(String token) { 39 | try { 40 | Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(token); 41 | return true; 42 | } catch (Exception e) { 43 | logger.error("JWT Authentication Failed"); 44 | } 45 | return false; 46 | } 47 | 48 | public String getUserAccount(String token) { 49 | return Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(token) 50 | .getBody().getSubject(); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /frontend/src/app/parts/pagination/pagination.component.html: -------------------------------------------------------------------------------- 1 |
2 |
    3 |
  • 4 | Previous 10 |
  • 11 | 12 |
  • Previous
  • 13 |
    14 | 15 | 16 |
  • 17 | {{ i + 1 }} 22 |
  • 23 | 24 |
  • 28 | 29 |
  • 30 |
    31 |
    32 | 33 |
  • 37 | Next 46 |
  • 47 | 48 |
  • Next
  • 49 |
    50 |
51 |
52 | -------------------------------------------------------------------------------- /frontend/src/app/pages/product-edit/product-edit.component.ts: -------------------------------------------------------------------------------- 1 | import {AfterContentChecked, Component, OnInit} from '@angular/core'; 2 | import {ProductInfo} from "../../models/productInfo"; 3 | import {ProductService} from "../../services/product.service"; 4 | import {ActivatedRoute, Router} from "@angular/router"; 5 | 6 | @Component({ 7 | selector: 'app-product-edit', 8 | templateUrl: './product-edit.component.html', 9 | styleUrls: ['./product-edit.component.css'] 10 | }) 11 | export class ProductEditComponent implements OnInit, AfterContentChecked { 12 | 13 | product = new ProductInfo(); 14 | 15 | constructor(private productService: ProductService, 16 | private route: ActivatedRoute, 17 | private router: Router) { 18 | } 19 | 20 | productId: string; 21 | isEdit = false; 22 | 23 | ngOnInit() { 24 | this.productId = this.route.snapshot.paramMap.get('id'); 25 | if (this.productId) { 26 | this.isEdit = true; 27 | this.productService.getDetail(this.productId).subscribe(prod => this.product = prod); 28 | } 29 | 30 | } 31 | 32 | update() { 33 | this.productService.update(this.product).subscribe(prod => { 34 | if (!prod) throw new Error(); 35 | this.router.navigate(['/seller']); 36 | }, 37 | err => { 38 | }); 39 | 40 | } 41 | 42 | onSubmit() { 43 | if (this.productId) { 44 | this.update(); 45 | } else { 46 | this.add(); 47 | } 48 | } 49 | 50 | add() { 51 | this.productService.create(this.product).subscribe(prod => { 52 | if (!prod) throw new Error; 53 | this.router.navigate(['/']); 54 | }, 55 | e => { 56 | }); 57 | } 58 | 59 | ngAfterContentChecked(): void { 60 | console.log(this.product); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /backend/src/main/java/me/zhulin/shopapi/entity/User.java: -------------------------------------------------------------------------------- 1 | package me.zhulin.shopapi.entity; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnore; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | import org.hibernate.annotations.NaturalId; 7 | 8 | import javax.persistence.*; 9 | import javax.validation.constraints.NotEmpty; 10 | import javax.validation.constraints.NotNull; 11 | import javax.validation.constraints.Size; 12 | import java.io.Serializable; 13 | 14 | /** 15 | * Created By Zhu Lin on 3/12/2018. 16 | */ 17 | @Entity 18 | @Data 19 | @Table(name = "users") 20 | @NoArgsConstructor 21 | public class User implements Serializable { 22 | 23 | private static final long serialVersionUID = 4887904943282174032L; 24 | @Id 25 | @GeneratedValue(strategy = GenerationType.AUTO) 26 | private Long id; 27 | 28 | @NaturalId 29 | @NotEmpty 30 | private String email; 31 | @NotEmpty 32 | @Size(min = 3, message = "Length must be more than 3") 33 | private String password; 34 | @NotEmpty 35 | private String name; 36 | @NotEmpty 37 | private String phone; 38 | @NotEmpty 39 | private String address; 40 | @NotNull 41 | private boolean active; 42 | @NotEmpty 43 | private String role = "ROLE_CUSTOMER"; 44 | 45 | @OneToOne(mappedBy = "user", cascade = CascadeType.ALL, fetch = FetchType.LAZY) 46 | @JsonIgnore // fix bi-direction toString() recursion problem 47 | private Cart cart; 48 | 49 | 50 | 51 | 52 | @Override 53 | public String toString() { 54 | return "User{" + 55 | "id=" + id + 56 | ", email='" + email + '\'' + 57 | ", password='" + password + '\'' + 58 | ", name='" + name + '\'' + 59 | ", phone='" + phone + '\'' + 60 | ", address='" + address + '\'' + 61 | ", active=" + active + 62 | ", role='" + role + '\'' + 63 | '}'; 64 | } 65 | 66 | } 67 | 68 | -------------------------------------------------------------------------------- /backend/src/main/java/me/zhulin/shopapi/service/impl/CategoryServiceImpl.java: -------------------------------------------------------------------------------- 1 | package me.zhulin.shopapi.service.impl; 2 | 3 | 4 | import me.zhulin.shopapi.entity.ProductCategory; 5 | import me.zhulin.shopapi.enums.ResultEnum; 6 | import me.zhulin.shopapi.exception.MyException; 7 | import me.zhulin.shopapi.repository.ProductCategoryRepository; 8 | import me.zhulin.shopapi.service.CategoryService; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.stereotype.Service; 11 | import org.springframework.transaction.annotation.Transactional; 12 | 13 | import java.util.List; 14 | 15 | /** 16 | * Created By Zhu Lin on 3/10/2018. 17 | */ 18 | @Service 19 | public class CategoryServiceImpl implements CategoryService { 20 | @Autowired 21 | ProductCategoryRepository productCategoryRepository; 22 | 23 | 24 | 25 | @Override 26 | public List findAll() { 27 | List res = productCategoryRepository.findAllByOrderByCategoryType(); 28 | // res.sort(Comparator.comparing(ProductCategory::getCategoryType)); 29 | return res; 30 | } 31 | 32 | @Override 33 | public ProductCategory findByCategoryType(Integer categoryType) { 34 | ProductCategory res = productCategoryRepository.findByCategoryType(categoryType); 35 | if(res == null) throw new MyException(ResultEnum.CATEGORY_NOT_FOUND); 36 | return res; 37 | } 38 | 39 | @Override 40 | public List findByCategoryTypeIn(List categoryTypeList) { 41 | List res = productCategoryRepository.findByCategoryTypeInOrderByCategoryTypeAsc(categoryTypeList); 42 | //res.sort(Comparator.comparing(ProductCategory::getCategoryType)); 43 | return res; 44 | } 45 | 46 | @Override 47 | @Transactional 48 | public ProductCategory save(ProductCategory productCategory) { 49 | return productCategoryRepository.save(productCategory); 50 | } 51 | 52 | 53 | 54 | } 55 | -------------------------------------------------------------------------------- /backend/src/test/java/me/zhulin/shopapi/service/impl/ProductInOrderServiceImplTest.java: -------------------------------------------------------------------------------- 1 | package me.zhulin.shopapi.service.impl; 2 | 3 | import me.zhulin.shopapi.entity.Cart; 4 | import me.zhulin.shopapi.entity.ProductInOrder; 5 | import me.zhulin.shopapi.entity.User; 6 | import me.zhulin.shopapi.repository.ProductInOrderRepository; 7 | import org.junit.Before; 8 | import org.junit.Test; 9 | import org.junit.runner.RunWith; 10 | import org.mockito.InjectMocks; 11 | import org.mockito.Mock; 12 | import org.mockito.Mockito; 13 | import org.springframework.test.context.junit4.SpringRunner; 14 | 15 | import java.math.BigDecimal; 16 | import java.util.HashSet; 17 | import java.util.Set; 18 | 19 | import static org.hamcrest.CoreMatchers.is; 20 | import static org.junit.Assert.assertThat; 21 | 22 | @RunWith(SpringRunner.class) 23 | public class ProductInOrderServiceImplTest { 24 | 25 | @Mock 26 | private ProductInOrderRepository productInOrderRepository; 27 | 28 | @InjectMocks 29 | private ProductInOrderServiceImpl productInOrderService; 30 | 31 | private User user; 32 | 33 | private ProductInOrder productInOrder; 34 | 35 | @Before 36 | public void setUp() { 37 | user = new User(); 38 | Cart cart = new Cart(); 39 | 40 | productInOrder = new ProductInOrder(); 41 | productInOrder.setProductId("1"); 42 | 43 | Set set = new HashSet<>(); 44 | set.add(productInOrder); 45 | 46 | cart.setProducts(set); 47 | 48 | user.setCart(cart); 49 | } 50 | 51 | @Test 52 | public void updateTest() { 53 | productInOrderService.update("1", 10, user); 54 | 55 | Mockito.verify(productInOrderRepository, Mockito.times(1)).save(productInOrder); 56 | } 57 | 58 | @Test 59 | public void findOneTest() { 60 | ProductInOrder productInOrderReturn = productInOrderService.findOne("1", user); 61 | 62 | assertThat(productInOrderReturn.getProductId(), is(productInOrder.getProductId())); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /frontend/src/app/pages/order/order.component.html: -------------------------------------------------------------------------------- 1 |

Orders

2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 42 | 43 | 44 |
Order #Customer NameCustomer EmailCustomer phoneShipping AddressTotalOrder DataStatusAction
21 | {{order.orderId}} 22 | {{order.buyerName}}{{order.buyerEmail}}{{order.buyerPhone}}{{order.buyerAddress}}{{order.orderAmount | currency}}{{order.createTime | date}}{{OrderStatus[order.orderStatus]}} 31 | 34 | Show 35 | Cancel 36 | 40 | Finish 41 |
45 | 46 | 47 | -------------------------------------------------------------------------------- /frontend/src/app/pages/product-list/product.list.component.html: -------------------------------------------------------------------------------- 1 |

Products

2 | Add 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 39 | 40 | 41 |
PhotoCodeNameTypeDescriptionPriceStockStatusAction
22 | {{productInfo.productName}} 23 | {{productInfo.productId}}{{productInfo.productName}}{{CategoryType[productInfo.categoryType]}}{{productInfo.productDescription}}{{productInfo.productPrice | currency}}{{productInfo.productStock}}{{ProductStatus[productInfo.productStatus]}} 32 | 33 | Edit 34 | 35 | 37 | Remove 38 |
42 | 43 | -------------------------------------------------------------------------------- /frontend/src/app/pages/product-detail/detail.component.html: -------------------------------------------------------------------------------- 1 |

{{title}}

2 |
3 |
4 |
5 | 6 |
7 |

{{productInfo?.productName}}

8 |
9 |
10 | 11 |

Description: {{productInfo?.productDescription}}

12 |

13 | Price: 14 | 15 |

16 |

Stock: {{productInfo?.productStock}}

17 | 18 | 29 | 30 | 31 |

Subtotal: 32 | 33 |

34 |
35 | 39 | Unavailable 40 |
41 | 42 |
43 |
44 |
45 |
46 | -------------------------------------------------------------------------------- /frontend/src/app/pages/card/card.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnDestroy, OnInit} from '@angular/core'; 2 | // import {prod, products} from '../shared/mockData'; 3 | import {ProductService} from '../../services/product.service'; 4 | import {ActivatedRoute} from '@angular/router'; 5 | import {Subscription} from "rxjs"; 6 | 7 | @Component({ 8 | selector: 'app-card', 9 | templateUrl: './card.component.html', 10 | styleUrls: ['./card.component.css'] 11 | }) 12 | export class CardComponent implements OnInit, OnDestroy { 13 | 14 | 15 | title: string; 16 | page: any; 17 | private paramSub: Subscription; 18 | private querySub: Subscription; 19 | 20 | 21 | constructor(private productService: ProductService, 22 | private route: ActivatedRoute) { 23 | 24 | } 25 | 26 | 27 | ngOnInit() { 28 | this.querySub = this.route.queryParams.subscribe(() => { 29 | this.update(); 30 | }); 31 | this.paramSub = this.route.params.subscribe(() => { 32 | this.update(); 33 | }); 34 | 35 | } 36 | 37 | ngOnDestroy(): void { 38 | this.querySub.unsubscribe(); 39 | this.paramSub.unsubscribe(); 40 | } 41 | 42 | update() { 43 | if (this.route.snapshot.queryParamMap.get('page')) { 44 | const currentPage = +this.route.snapshot.queryParamMap.get('page'); 45 | const size = +this.route.snapshot.queryParamMap.get('size'); 46 | this.getProds(currentPage, size); 47 | } else { 48 | this.getProds(); 49 | } 50 | } 51 | getProds(page: number = 1, size: number = 3) { 52 | if (this.route.snapshot.url.length == 1) { 53 | this.productService.getAllInPage(+page, +size) 54 | .subscribe(page => { 55 | this.page = page; 56 | this.title = 'Get Whatever You Want!'; 57 | }); 58 | } else { // /category/:id 59 | const type = this.route.snapshot.url[1].path; 60 | this.productService.getCategoryInPage(+type, page, size) 61 | .subscribe(categoryPage => { 62 | this.title = categoryPage.category; 63 | this.page = categoryPage.page; 64 | }); 65 | } 66 | 67 | } 68 | 69 | 70 | } 71 | -------------------------------------------------------------------------------- /backend/src/test/java/me/zhulin/shopapi/service/impl/UserServiceImplTest.java: -------------------------------------------------------------------------------- 1 | package me.zhulin.shopapi.service.impl; 2 | 3 | import me.zhulin.shopapi.entity.User; 4 | import me.zhulin.shopapi.exception.MyException; 5 | import me.zhulin.shopapi.repository.CartRepository; 6 | import me.zhulin.shopapi.repository.UserRepository; 7 | import org.junit.Before; 8 | import org.junit.Test; 9 | import org.junit.runner.RunWith; 10 | import org.mockito.InjectMocks; 11 | import org.mockito.Mock; 12 | import org.mockito.Mockito; 13 | import org.springframework.security.crypto.password.PasswordEncoder; 14 | import org.springframework.test.context.junit4.SpringRunner; 15 | 16 | import static org.hamcrest.CoreMatchers.is; 17 | import static org.junit.Assert.assertThat; 18 | import static org.mockito.Mockito.when; 19 | 20 | @RunWith(SpringRunner.class) 21 | public class UserServiceImplTest { 22 | 23 | @InjectMocks 24 | private UserServiceImpl userService; 25 | 26 | @Mock 27 | private UserRepository userRepository; 28 | 29 | @Mock 30 | private PasswordEncoder passwordEncoder; 31 | 32 | @Mock 33 | private CartRepository cartRepository; 34 | 35 | private User user; 36 | 37 | @Before 38 | public void setUp() { 39 | user = new User(); 40 | user.setPassword("password"); 41 | user.setEmail("email@email.com"); 42 | user.setName("Name"); 43 | user.setPhone("Phone Test"); 44 | user.setAddress("Address Test"); 45 | } 46 | 47 | @Test 48 | public void createUserTest() { 49 | when(userRepository.save(user)).thenReturn(user); 50 | 51 | userService.save(user); 52 | 53 | Mockito.verify(userRepository, Mockito.times(2)).save(user); 54 | } 55 | 56 | @Test(expected = MyException.class) 57 | public void createUserExceptionTest() { 58 | userService.save(user); 59 | } 60 | 61 | @Test 62 | public void updateTest() { 63 | User oldUser = new User(); 64 | oldUser.setEmail("email@test.com"); 65 | 66 | when(userRepository.findByEmail(user.getEmail())).thenReturn(oldUser); 67 | when(userRepository.save(oldUser)).thenReturn(oldUser); 68 | 69 | User userResult = userService.update(user); 70 | 71 | assertThat(userResult.getName(), is(oldUser.getName())); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /frontend/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import {BrowserModule} from '@angular/platform-browser'; 2 | import {NgModule} from '@angular/core'; 3 | 4 | import {AppComponent} from './app.component'; 5 | import {NavigationComponent} from './parts/navigation/navigation.component'; 6 | import {CardComponent} from './pages/card/card.component'; 7 | import {PaginationComponent} from './parts/pagination/pagination.component'; 8 | import {AppRoutingModule} from './app-routing.module'; 9 | import {LoginComponent} from './pages/login/login.component'; 10 | import {SignupComponent} from './pages/signup/signup.component'; 11 | import {DetailComponent} from './pages/product-detail/detail.component'; 12 | import {FormsModule} from '@angular/forms'; 13 | import {HTTP_INTERCEPTORS, HttpClientModule} from '@angular/common/http'; 14 | import {CartComponent} from './pages/cart/cart.component'; 15 | import {CookieService} from "ngx-cookie-service"; 16 | import {ErrorInterceptor} from "./_interceptors/error-interceptor.service"; 17 | import {JwtInterceptor} from "./_interceptors/jwt-interceptor.service"; 18 | import {OrderComponent} from './pages/order/order.component'; 19 | import {OrderDetailComponent} from './pages/order-detail/order-detail.component'; 20 | import {ProductListComponent} from './pages/product-list/product.list.component'; 21 | import {UserDetailComponent} from './pages/user-edit/user-detail.component'; 22 | import {ProductEditComponent} from './pages/product-edit/product-edit.component'; 23 | 24 | @NgModule({ 25 | declarations: [ 26 | AppComponent, 27 | NavigationComponent, 28 | CardComponent, 29 | PaginationComponent, 30 | LoginComponent, 31 | SignupComponent, 32 | DetailComponent, 33 | CartComponent, 34 | OrderComponent, 35 | OrderDetailComponent, 36 | ProductListComponent, 37 | UserDetailComponent, 38 | ProductEditComponent, 39 | 40 | ], 41 | imports: [ 42 | BrowserModule, 43 | AppRoutingModule, 44 | FormsModule, 45 | HttpClientModule, 46 | 47 | ], 48 | providers: [CookieService, 49 | {provide: HTTP_INTERCEPTORS, useClass: JwtInterceptor, multi: true}, 50 | {provide: HTTP_INTERCEPTORS, useClass: ErrorInterceptor, multi: true}], 51 | bootstrap: [AppComponent] 52 | }) 53 | export class AppModule { 54 | } 55 | -------------------------------------------------------------------------------- /frontend/src/app/pages/product-list/product.list.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnDestroy, OnInit} from '@angular/core'; 2 | import {UserService} from "../../services/user.service"; 3 | import {ProductService} from "../../services/product.service"; 4 | import {JwtResponse} from "../../response/JwtResponse"; 5 | import {Subscription} from "rxjs"; 6 | import {ActivatedRoute} from "@angular/router"; 7 | import {CategoryType} from "../../enum/CategoryType"; 8 | import {ProductStatus} from "../../enum/ProductStatus"; 9 | import {ProductInfo} from "../../models/productInfo"; 10 | import {Role} from "../../enum/Role"; 11 | 12 | @Component({ 13 | selector: 'app-product.list', 14 | templateUrl: './product.list.component.html', 15 | styleUrls: ['./product.list.component.css'] 16 | }) 17 | export class ProductListComponent implements OnInit, OnDestroy { 18 | 19 | constructor(private userService: UserService, 20 | private productService: ProductService, 21 | private route: ActivatedRoute) { 22 | } 23 | 24 | Role = Role; 25 | currentUser: JwtResponse; 26 | page: any; 27 | CategoryType = CategoryType; 28 | ProductStatus = ProductStatus; 29 | private querySub: Subscription; 30 | 31 | ngOnInit() { 32 | this.querySub = this.route.queryParams.subscribe(() => { 33 | this.update(); 34 | }); 35 | } 36 | 37 | ngOnDestroy(): void { 38 | this.querySub.unsubscribe(); 39 | } 40 | 41 | update() { 42 | if (this.route.snapshot.queryParamMap.get('page')) { 43 | const currentPage = +this.route.snapshot.queryParamMap.get('page'); 44 | const size = +this.route.snapshot.queryParamMap.get('size'); 45 | this.getProds(currentPage, size); 46 | } else { 47 | this.getProds(); 48 | } 49 | } 50 | 51 | getProds(page: number = 1, size: number = 5) { 52 | this.productService.getAllInPage(+page, +size) 53 | .subscribe(page => { 54 | this.page = page; 55 | }); 56 | 57 | } 58 | 59 | 60 | remove(productInfos: ProductInfo[], productInfo) { 61 | this.productService.delelte(productInfo).subscribe(_ => { 62 | productInfos = productInfos.filter(e => e.productId != productInfo); 63 | }, 64 | err => { 65 | }); 66 | } 67 | 68 | 69 | } 70 | -------------------------------------------------------------------------------- /backend/src/main/java/me/zhulin/shopapi/entity/OrderMain.java: -------------------------------------------------------------------------------- 1 | package me.zhulin.shopapi.entity; 2 | 3 | import lombok.Data; 4 | import lombok.NoArgsConstructor; 5 | import org.hibernate.annotations.ColumnDefault; 6 | import org.hibernate.annotations.CreationTimestamp; 7 | import org.hibernate.annotations.DynamicUpdate; 8 | import org.hibernate.annotations.UpdateTimestamp; 9 | 10 | import javax.persistence.*; 11 | import javax.validation.constraints.NotEmpty; 12 | import javax.validation.constraints.NotNull; 13 | import java.io.Serializable; 14 | import java.math.BigDecimal; 15 | import java.time.LocalDateTime; 16 | import java.util.HashSet; 17 | import java.util.Set; 18 | 19 | /** 20 | * OrderMain contains User info and products in the order 21 | * Created By Zhu Lin on 3/14/2018. 22 | */ 23 | @Entity 24 | @Data 25 | @NoArgsConstructor 26 | @DynamicUpdate 27 | public class OrderMain implements Serializable { 28 | private static final long serialVersionUID = -3819883511505235030L; 29 | 30 | @Id 31 | @NotNull 32 | @GeneratedValue(strategy = GenerationType.AUTO) 33 | private Long orderId; 34 | 35 | @OneToMany(cascade = CascadeType.ALL, 36 | fetch = FetchType.LAZY, 37 | mappedBy = "orderMain") 38 | private Set products = new HashSet<>(); 39 | 40 | @NotEmpty 41 | private String buyerEmail; 42 | 43 | @NotEmpty 44 | private String buyerName; 45 | 46 | @NotEmpty 47 | private String buyerPhone; 48 | 49 | @NotEmpty 50 | private String buyerAddress; 51 | 52 | // Total Amount 53 | @NotNull 54 | private BigDecimal orderAmount; 55 | 56 | /** 57 | * default 0: new order. 58 | */ 59 | @NotNull 60 | @ColumnDefault("0") 61 | private Integer orderStatus; 62 | 63 | @CreationTimestamp 64 | private LocalDateTime createTime; 65 | 66 | @UpdateTimestamp 67 | private LocalDateTime updateTime; 68 | 69 | public OrderMain(User buyer) { 70 | this.buyerEmail = buyer.getEmail(); 71 | this.buyerName = buyer.getName(); 72 | this.buyerPhone = buyer.getPhone(); 73 | this.buyerAddress = buyer.getAddress(); 74 | this.orderAmount = buyer.getCart().getProducts().stream().map(item -> item.getProductPrice().multiply(new BigDecimal(item.getCount()))) 75 | .reduce(BigDecimal::add) 76 | .orElse(new BigDecimal(0)); 77 | this.orderStatus = 0; 78 | 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /frontend/src/app/pages/order/order.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnDestroy, OnInit} from '@angular/core'; 2 | import {HttpClient} from "@angular/common/http"; 3 | import {OrderService} from "../../services/order.service"; 4 | import {Order} from "../../models/Order"; 5 | import {OrderStatus} from "../../enum/OrderStatus"; 6 | import {UserService} from "../../services/user.service"; 7 | import {JwtResponse} from "../../response/JwtResponse"; 8 | import {Subscription} from "rxjs"; 9 | import {ActivatedRoute} from "@angular/router"; 10 | import {Role} from "../../enum/Role"; 11 | 12 | @Component({ 13 | selector: 'app-order', 14 | templateUrl: './order.component.html', 15 | styleUrls: ['./order.component.css'] 16 | }) 17 | export class OrderComponent implements OnInit, OnDestroy { 18 | 19 | page: any; 20 | OrderStatus = OrderStatus; 21 | currentUser: JwtResponse; 22 | Role = Role; 23 | constructor(private httpClient: HttpClient, 24 | private orderService: OrderService, 25 | private userService: UserService, 26 | private route: ActivatedRoute 27 | ) { 28 | } 29 | 30 | querySub: Subscription; 31 | 32 | ngOnInit() { 33 | this.currentUser = this.userService.currentUserValue; 34 | this.querySub = this.route.queryParams.subscribe(() => { 35 | this.update(); 36 | }); 37 | 38 | } 39 | 40 | update() { 41 | let nextPage = 1; 42 | let size = 10; 43 | if (this.route.snapshot.queryParamMap.get('page')) { 44 | nextPage = +this.route.snapshot.queryParamMap.get('page'); 45 | size = +this.route.snapshot.queryParamMap.get('size'); 46 | } 47 | this.orderService.getPage(nextPage, size).subscribe(page => this.page = page, _ => { 48 | console.log("Get Orde Failed") 49 | }); 50 | } 51 | 52 | 53 | cancel(order: Order) { 54 | this.orderService.cancel(order.orderId).subscribe(res => { 55 | if (res) { 56 | order.orderStatus = res.orderStatus; 57 | } 58 | }); 59 | } 60 | 61 | finish(order: Order) { 62 | this.orderService.finish(order.orderId).subscribe(res => { 63 | if (res) { 64 | order.orderStatus = res.orderStatus; 65 | } 66 | }) 67 | } 68 | 69 | ngOnDestroy(): void { 70 | this.querySub.unsubscribe(); 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /frontend/src/app/pages/cart/cart.component.html: -------------------------------------------------------------------------------- 1 |

My Cart

2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 24 | 26 | 27 | 28 | 40 | 41 | 42 | 45 | 46 | 47 | 48 | 49 |
PhotoNamePriceQuantitySubtotalAction
20 | {{productInOrder.productName}} 23 | {{productInOrder.productName}}{{productInOrder.productPrice | currency}} 29 | 30 | 38 | 39 | {{productInOrder.productPrice * productInOrder.count|currency}} 43 | Remove 44 |
50 | 51 |
52 |
Total: {{total | currency}}
53 | 54 |
55 | 56 |

Cart is empty. Go to get something! :)

57 |
58 | 59 | 60 | -------------------------------------------------------------------------------- /frontend/src/app/pages/product-detail/detail.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit} from '@angular/core'; 2 | import {ProductService} from '../../services/product.service'; 3 | import {ActivatedRoute, Router} from '@angular/router'; 4 | import {CartService} from '../../services/cart.service'; 5 | import {CookieService} from 'ngx-cookie-service'; 6 | import {ProductInOrder} from '../../models/ProductInOrder'; 7 | import {ProductInfo} from '../../models/productInfo'; 8 | 9 | @Component({ 10 | selector: 'app-detail', 11 | templateUrl: './detail.component.html', 12 | styleUrls: ['./detail.component.css'] 13 | }) 14 | export class DetailComponent implements OnInit { 15 | title: string; 16 | count: number; 17 | productInfo: ProductInfo; 18 | 19 | constructor( 20 | private productService: ProductService, 21 | private cartService: CartService, 22 | private cookieService: CookieService, 23 | private route: ActivatedRoute, 24 | private router: Router 25 | ) { 26 | } 27 | 28 | ngOnInit() { 29 | this.getProduct(); 30 | this.title = 'Product Detail'; 31 | this.count = 1; 32 | } 33 | 34 | // ngOnChanges(changes: SimpleChanges): void { 35 | // // Called before any other lifecycle hook. Use it to inject dependencies, but avoid any serious work here. 36 | // // Add '${implements OnChanges}' to the class. 37 | // console.log(changes); 38 | // if (this.item.quantity in changes) { 39 | 40 | // } 41 | // } 42 | 43 | getProduct(): void { 44 | const id = this.route.snapshot.paramMap.get('id'); 45 | this.productService.getDetail(id).subscribe( 46 | prod => { 47 | this.productInfo = prod; 48 | }, 49 | _ => console.log('Get Cart Failed') 50 | ); 51 | } 52 | 53 | addToCart() { 54 | this.cartService 55 | .addItem(new ProductInOrder(this.productInfo, this.count)) 56 | .subscribe( 57 | res => { 58 | if (!res) { 59 | console.log('Add Cart failed' + res); 60 | throw new Error(); 61 | } 62 | this.router.navigateByUrl('/cart'); 63 | }, 64 | _ => console.log('Add Cart Failed') 65 | ); 66 | } 67 | 68 | validateCount() { 69 | console.log('Validate'); 70 | const max = this.productInfo.productStock; 71 | if (this.count > max) { 72 | this.count = max; 73 | } else if (this.count < 1) { 74 | this.count = 1; 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /backend/src/main/java/me/zhulin/shopapi/service/impl/UserServiceImpl.java: -------------------------------------------------------------------------------- 1 | package me.zhulin.shopapi.service.impl; 2 | 3 | 4 | import me.zhulin.shopapi.entity.Cart; 5 | import me.zhulin.shopapi.entity.User; 6 | import me.zhulin.shopapi.enums.ResultEnum; 7 | import me.zhulin.shopapi.exception.MyException; 8 | import me.zhulin.shopapi.repository.CartRepository; 9 | import me.zhulin.shopapi.repository.UserRepository; 10 | import me.zhulin.shopapi.service.UserService; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.context.annotation.DependsOn; 13 | import org.springframework.security.crypto.password.PasswordEncoder; 14 | import org.springframework.stereotype.Service; 15 | import org.springframework.transaction.annotation.Transactional; 16 | 17 | import java.util.Collection; 18 | 19 | /** 20 | * Created By Zhu Lin on 3/13/2018. 21 | */ 22 | @Service 23 | @DependsOn("passwordEncoder") 24 | public class UserServiceImpl implements UserService { 25 | @Autowired 26 | private PasswordEncoder passwordEncoder; 27 | @Autowired 28 | UserRepository userRepository; 29 | @Autowired 30 | CartRepository cartRepository; 31 | 32 | @Override 33 | public User findOne(String email) { 34 | return userRepository.findByEmail(email); 35 | } 36 | 37 | @Override 38 | public Collection findByRole(String role) { 39 | return userRepository.findAllByRole(role); 40 | } 41 | 42 | @Override 43 | @Transactional 44 | public User save(User user) { 45 | //register 46 | user.setPassword(passwordEncoder.encode(user.getPassword())); 47 | try { 48 | User savedUser = userRepository.save(user); 49 | 50 | // initial Cart 51 | Cart savedCart = cartRepository.save(new Cart(savedUser)); 52 | savedUser.setCart(savedCart); 53 | return userRepository.save(savedUser); 54 | 55 | } catch (Exception e) { 56 | throw new MyException(ResultEnum.VALID_ERROR); 57 | } 58 | 59 | } 60 | 61 | @Override 62 | @Transactional 63 | public User update(User user) { 64 | User oldUser = userRepository.findByEmail(user.getEmail()); 65 | oldUser.setPassword(passwordEncoder.encode(user.getPassword())); 66 | oldUser.setName(user.getName()); 67 | oldUser.setPhone(user.getPhone()); 68 | oldUser.setAddress(user.getAddress()); 69 | return userRepository.save(oldUser); 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /frontend/src/app/parts/navigation/navigation.component.html: -------------------------------------------------------------------------------- 1 | 64 | -------------------------------------------------------------------------------- /frontend/src/app/pages/login/login.component.html: -------------------------------------------------------------------------------- 1 |

Sign In

2 |
3 | 4 |
5 | Invalid username and password. 6 |
7 |
8 | You have been logged out. 9 |
10 | 11 | 12 |
13 |
14 | 15 | 17 |
18 | Email is required 19 |
20 |
21 | 22 |
23 | 24 | 26 |
27 | Email is required 28 |
29 |
30 | 31 |
32 |
33 | 34 | 35 | Sign Up 36 |
37 |
38 | 39 |
40 | 41 |
42 |
43 | 44 | 45 | Sample Users 46 | 47 | 48 | 51 | 54 | 57 | 58 |
customer1employee1manager1
59 | 60 |
61 | -------------------------------------------------------------------------------- /backend/src/main/java/me/zhulin/shopapi/security/JWT/JwtFilter.java: -------------------------------------------------------------------------------- 1 | package me.zhulin.shopapi.security.JWT; 2 | 3 | import me.zhulin.shopapi.entity.User; 4 | import me.zhulin.shopapi.service.UserService; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; 7 | import org.springframework.security.core.authority.SimpleGrantedAuthority; 8 | import org.springframework.security.core.context.SecurityContextHolder; 9 | import org.springframework.stereotype.Component; 10 | import org.springframework.web.filter.OncePerRequestFilter; 11 | 12 | import javax.servlet.FilterChain; 13 | import javax.servlet.ServletException; 14 | import javax.servlet.http.HttpServletRequest; 15 | import javax.servlet.http.HttpServletResponse; 16 | import java.io.IOException; 17 | import java.util.ArrayList; 18 | 19 | /** 20 | * Created By Zhu Lin on 1/1/2019. 21 | */ 22 | @Component 23 | public class JwtFilter extends OncePerRequestFilter { 24 | @Autowired 25 | private JwtProvider jwtProvider; 26 | @Autowired 27 | private UserService userService; 28 | 29 | @Override 30 | protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException { 31 | String jwt = getToken(httpServletRequest); 32 | if (jwt != null && jwtProvider.validate(jwt)) { 33 | try { 34 | String userAccount = jwtProvider.getUserAccount(jwt); 35 | User user = userService.findOne(userAccount); 36 | // pwd not necessary 37 | // if jwt ok, then authenticate 38 | SimpleGrantedAuthority sga = new SimpleGrantedAuthority(user.getRole()); 39 | ArrayList list = new ArrayList<>(); 40 | list.add(sga); 41 | UsernamePasswordAuthenticationToken auth 42 | = new UsernamePasswordAuthenticationToken(user.getEmail(), null, list); 43 | SecurityContextHolder.getContext().setAuthentication(auth); 44 | 45 | } catch (Exception e) { 46 | logger.error("Set Authentication from JWT failed"); 47 | } 48 | } 49 | filterChain.doFilter(httpServletRequest, httpServletResponse); 50 | } 51 | 52 | private String getToken(HttpServletRequest request) { 53 | String authHeader = request.getHeader("Authorization"); 54 | 55 | if (authHeader != null && authHeader.startsWith("Bearer ")) { 56 | return authHeader.replace("Bearer ", ""); 57 | } 58 | 59 | return null; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /frontend/src/app/pages/user-edit/user-detail.component.html: -------------------------------------------------------------------------------- 1 |

Edit Profiles

2 |
3 |
4 |
5 | 6 | 8 |
9 |
10 | 11 | 12 |
13 |
14 | Name is required. 15 |
16 |
17 | Name must be at least 3 characters long. 18 |
19 |
20 |
21 |
22 | 23 | 24 |
25 |
26 | Password is required. 27 |
28 |
29 | Password must be at least 3 characters long. 30 |
31 |
32 |
33 |
34 | 35 | 36 |
37 |
38 | Phone is required. 39 |
40 |
41 |
42 |
43 | 44 | 45 |
46 |
47 | Address is required. 48 |
49 |
50 |
51 |
52 | 53 |
54 |
55 |
56 | 57 | -------------------------------------------------------------------------------- /frontend/src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {RouterModule, Routes} from '@angular/router'; 3 | import {CardComponent} from './pages/card/card.component'; 4 | import {LoginComponent} from './pages/login/login.component'; 5 | import {SignupComponent} from './pages/signup/signup.component'; 6 | import {DetailComponent} from './pages/product-detail/detail.component'; 7 | import {CartComponent} from './pages/cart/cart.component'; 8 | import {AuthGuard} from "./_guards/auth.guard"; 9 | import {OrderComponent} from "./pages/order/order.component"; 10 | import {OrderDetailComponent} from "./pages/order-detail/order-detail.component"; 11 | import {ProductListComponent} from "./pages/product-list/product.list.component"; 12 | import {UserDetailComponent} from "./pages/user-edit/user-detail.component"; 13 | import {ProductEditComponent} from "./pages/product-edit/product-edit.component"; 14 | import {Role} from "./enum/Role"; 15 | 16 | const routes: Routes = [ 17 | {path: '', redirectTo: '/product', pathMatch: 'full'}, 18 | {path: 'product/:id', component: DetailComponent}, 19 | {path: 'category/:id', component: CardComponent}, 20 | {path: 'product', component: CardComponent}, 21 | {path: 'category', component: CardComponent}, 22 | {path: 'login', component: LoginComponent}, 23 | {path: 'logout', component: LoginComponent}, 24 | {path: 'register', component: SignupComponent}, 25 | {path: 'cart', component: CartComponent}, 26 | {path: 'success', component: SignupComponent}, 27 | {path: 'order/:id', component: OrderDetailComponent, canActivate: [AuthGuard]}, 28 | {path: 'order', component: OrderComponent, canActivate: [AuthGuard]}, 29 | {path: 'seller', redirectTo: 'seller/product', pathMatch: 'full'}, 30 | { 31 | path: 'seller/product', 32 | component: ProductListComponent, 33 | canActivate: [AuthGuard], 34 | data: {roles: [Role.Manager, Role.Employee]} 35 | }, 36 | { 37 | path: 'profile', 38 | component: UserDetailComponent, 39 | canActivate: [AuthGuard] 40 | }, 41 | { 42 | path: 'seller/product/:id/edit', 43 | component: ProductEditComponent, 44 | canActivate: [AuthGuard], 45 | data: {roles: [Role.Manager, Role.Employee]} 46 | }, 47 | { 48 | path: 'seller/product/:id/new', 49 | component: ProductEditComponent, 50 | canActivate: [AuthGuard], 51 | data: {roles: [Role.Employee]} 52 | }, 53 | 54 | ]; 55 | 56 | @NgModule({ 57 | declarations: [], 58 | imports: [ 59 | RouterModule.forRoot(routes)//{onSameUrlNavigation: 'reload'} 60 | ], 61 | exports: [RouterModule] 62 | }) 63 | export class AppRoutingModule { 64 | } 65 | -------------------------------------------------------------------------------- /frontend/src/app/services/product.service.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | import {HttpClient} from '@angular/common/http'; 3 | import {Observable, of} from 'rxjs'; 4 | import {catchError} from 'rxjs/operators'; 5 | import {ProductInfo} from '../models/productInfo'; 6 | import {apiUrl} from '../../environments/environment'; 7 | 8 | @Injectable({ 9 | providedIn: 'root' 10 | }) 11 | export class ProductService { 12 | 13 | private productUrl = `${apiUrl}/product`; 14 | private categoryUrl = `${apiUrl}/category`; 15 | 16 | constructor(private http: HttpClient) { 17 | } 18 | 19 | getAllInPage(page: number, size: number): Observable { 20 | const url = `${this.productUrl}?page=${page}&size=${size}`; 21 | return this.http.get(url) 22 | .pipe( 23 | // tap(_ => console.log(_)), 24 | ) 25 | } 26 | 27 | getCategoryInPage(categoryType: number, page: number, size: number): Observable { 28 | const url = `${this.categoryUrl}/${categoryType}?page=${page}&size=${size}`; 29 | return this.http.get(url).pipe( 30 | // tap(data => console.log(data)) 31 | ); 32 | } 33 | 34 | getDetail(id: String): Observable { 35 | const url = `${this.productUrl}/${id}`; 36 | return this.http.get(url).pipe( 37 | catchError(_ => { 38 | console.log("Get Detail Failed"); 39 | return of(new ProductInfo()); 40 | }) 41 | ); 42 | } 43 | 44 | update(productInfo: ProductInfo): Observable { 45 | const url = `${apiUrl}/seller/product/${productInfo.productId}/edit`; 46 | return this.http.put(url, productInfo); 47 | } 48 | 49 | create(productInfo: ProductInfo): Observable { 50 | const url = `${apiUrl}/seller/product/new`; 51 | return this.http.post(url, productInfo); 52 | } 53 | 54 | 55 | delelte(productInfo: ProductInfo): Observable { 56 | const url = `${apiUrl}/seller/product/${productInfo.productId}/delete`; 57 | return this.http.delete(url); 58 | } 59 | 60 | 61 | /** 62 | * Handle Http operation that failed. 63 | * Let the app continue. 64 | * @param operation - name of the operation that failed 65 | * @param result - optional value to return as the observable result 66 | */ 67 | private handleError(operation = 'operation', result?: T) { 68 | return (error: any): Observable => { 69 | 70 | console.error(error); // log to console instead 71 | 72 | // Let the app keep running by returning an empty result. 73 | return of(result as T); 74 | }; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /frontend/src/app/pages/signup/signup.component.html: -------------------------------------------------------------------------------- 1 |

Sign Up

2 |
3 |
4 |
5 | 6 | 7 |
8 |
9 | Email is required. 10 |
11 |
12 | Invalid Email. 13 |
14 |
15 |
16 |
17 | 18 | 19 |
20 |
21 | Name is required. 22 |
23 |
24 | Name must be at least 3 characters long. 25 |
26 |
27 |
28 |
29 | 30 | 31 |
32 |
33 | Password is required. 34 |
35 |
36 | Password must be at least 3 characters long. 37 |
38 |
39 |
40 |
41 | 42 | 43 |
44 |
45 | Phone is required. 46 |
47 |
48 |
49 |
50 | 51 | 52 |
53 |
54 | Address is required. 55 |
56 |
57 |
58 |
59 | 60 |
61 |
62 |
63 | 64 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Online Shop Application 3 | [![License](https://img.shields.io/badge/license-MIT-green)](./LICENSE) 4 | 5 | #### A full-stack Online Shop web application using Spring Boot 2 and Angular 7. 6 | This is a Single Page Appliaction with client-side rendering. It includes [backend](https://github.com/zhulinn/SpringBoot-Angular7-ShoppingCart/tree/backend) and [frontend](https://github.com/zhulinn/SpringBoot-Angular7-ShoppingCart/tree/frontend) two seperate projects on different branches. 7 | The frontend client makes API calls to the backend server when it is running. 8 | > This project is based on my previous project [Online-Shopping-Store](https://github.com/zhulinn/Online-Shopping-Store), which uses FreeMarker as template engine for server-side rendering. 9 | > 10 | #### Live Demo: [https://springboot-angular-shop.herokuapp.com/](https://springboot-angular-shop.herokuapp.com/) Heroku has removed free tier on Postgres, the demo is no longer working...:( 11 | 12 | For Heroku application repo cloning, please check [Angular7-SpringBoot-hybrid-project](https://github.com/zhulinn/Angular7-SpringBoot-hybrid-project). 13 | 14 | ## Screenshot 15 | ![](https://raw.githubusercontent.com/zhulinn/blog/hexo/source/uploads/post_pics/spring-angular/cart.png) 16 | 17 | ## Features 18 | - REST API 19 | - Docker 20 | - Docker Compose 21 | - JWT authentication 22 | - Cookie based visitors' shopping cart 23 | - Persistent customers' shopping cart 24 | - Cart & order management 25 | - Checkout 26 | - Catalogue 27 | - Order management 28 | - Pagination 29 | ## Technology Stacks 30 | **Backend** 31 | - Java 11 32 | - Spring Boot 2.2 33 | - Spring Security 34 | - JWT Authentication 35 | - Spring Data JPA 36 | - Hibernate 37 | - PostgreSQL 38 | - Maven 39 | 40 | **Frontend** 41 | - Angular 7 42 | - Angular CLI 43 | - Bootstrap 44 | 45 | ## Database Schema 46 | 47 | ![](https://raw.githubusercontent.com/zhulinn/blog/hexo/source/uploads/post_pics/spring-angular/db.png) 48 | 49 | ## How to Run 50 | 51 | Start the backend server before the frontend client. 52 | 53 | **Backend** 54 | 55 | 1. Install [PostgreSQL](https://www.postgresql.org/download/) 56 | 2. Configure datasource in `application.yml`. 57 | 3. `cd backend`. 58 | 4. Run `mvn install`. 59 | 5. Run `mvn spring-boot:run`. 60 | 6. Spring Boot will import mock data into database by executing `import.sql` automatically. 61 | 7. The backend server is running on [localhost:8080](). 62 | 63 | **Frontend** 64 | 1. Install [Node.js and npm](https://www.npmjs.com/get-npm) 65 | 2. `cd frontend`. 66 | 3. Run `npm install`. 67 | 4. Run `ng serve` 68 | 5. The frontend client is running on [localhost:4200](). 69 | 70 | Note: The backend API url is configured in `src/environments/environment.ts` of the frontend project. It is `localhost:8080/api` by default. 71 | 72 | #### Run in Docker 73 | You can build the image and run the container with Docker. 74 | 1. Build backend project 75 | ```bash 76 | cd backend 77 | mvn package 78 | ``` 79 | 2. Build fontend project 80 | ```bash 81 | cd frontend 82 | npm install 83 | ng build --prod 84 | ``` 85 | 3. Build images and run containers 86 | ```bash 87 | docker-compose up --build 88 | ``` 89 | 90 | -------------------------------------------------------------------------------- /frontend/src/app/mockData.ts: -------------------------------------------------------------------------------- 1 | import {ProductInfo} from './models/productInfo'; 2 | 3 | export const products: ProductInfo[] = [ 4 | { 5 | productId: 'B0001', 6 | productName: 'Core Java', 7 | productPrice: 30.00, 8 | productStock: 96, 9 | productDescription: 'Books for learning Java', 10 | productIcon: 'https://images-na.ssl-images-amazon.com/images/I/41f6Rd6ZEPL._SX363_BO1,204,203,200_.jpg', 11 | productStatus: 0, 12 | categoryType: 0, 13 | createTime: '2018-03-10T11:44:25.000+0000', 14 | updateTime: '2018-03-10T11:44:25.000+0000' 15 | }, 16 | { 17 | productId: 'B0002', 18 | productName: 'Spring In Action', 19 | productPrice: 20.00, 20 | productStock: 195, 21 | productDescription: 'Learn Spring', 22 | productIcon: 'https://images-na.ssl-images-amazon.com/images/I/51gHy16h5TL._SX397_BO1,204,203,200_.jpg', 23 | productStatus: 0, 24 | categoryType: 0, 25 | createTime: '2018-03-10T15:35:43.000+0000', 26 | updateTime: '2018-03-10T15:35:43.000+0000' 27 | }, 28 | { 29 | productId: 'B0001', 30 | productName: 'Core Java', 31 | productPrice: 30.00, 32 | productStock: 96, 33 | productDescription: 'Books for learning Java', 34 | productIcon: 'https://images-na.ssl-images-amazon.com/images/I/41f6Rd6ZEPL._SX363_BO1,204,203,200_.jpg', 35 | productStatus: 1, 36 | categoryType: 0, 37 | createTime: '2018-03-10T11:44:25.000+0000', 38 | updateTime: '2018-03-10T11:44:25.000+0000' 39 | }, 40 | { 41 | productId: 'B0002', 42 | productName: 'Spring In Action', 43 | productPrice: 20.00, 44 | productStock: 195, 45 | productDescription: 'Learn Spring', 46 | productIcon: 'https://images-na.ssl-images-amazon.com/images/I/51gHy16h5TL._SX397_BO1,204,203,200_.jpg', 47 | productStatus: 0, 48 | categoryType: 0, 49 | createTime: '2018-03-10T15:35:43.000+0000', 50 | updateTime: '2018-03-10T15:35:43.000+0000' 51 | }, { 52 | productId: 'B0001', 53 | productName: 'Core Java', 54 | productPrice: 30.00, 55 | productStock: 96, 56 | productDescription: 'Books for learning Java', 57 | productIcon: 'https://images-na.ssl-images-amazon.com/images/I/41f6Rd6ZEPL._SX363_BO1,204,203,200_.jpg', 58 | productStatus: 1, 59 | categoryType: 0, 60 | createTime: '2018-03-10T11:44:25.000+0000', 61 | updateTime: '2018-03-10T11:44:25.000+0000' 62 | }, 63 | { 64 | productId: 'B0002', 65 | productName: 'Spring In Action', 66 | productPrice: 20.00, 67 | productStock: 195, 68 | productDescription: 'Learn Spring', 69 | productIcon: 'https://images-na.ssl-images-amazon.com/images/I/51gHy16h5TL._SX397_BO1,204,203,200_.jpg', 70 | productStatus: 0, 71 | categoryType: 0, 72 | createTime: '2018-03-10T15:35:43.000+0000', 73 | updateTime: '2018-03-10T15:35:43.000+0000' 74 | }]; 75 | 76 | export const prod: ProductInfo = { 77 | productId: 'B0002', 78 | productName: 'Spring In Action', 79 | productPrice: 20.00, 80 | productStock: 195, 81 | productDescription: 'Learn Spring', 82 | productIcon: 'https://images-na.ssl-images-amazon.com/images/I/51gHy16h5TL._SX397_BO1,204,203,200_.jpg', 83 | productStatus: 0, 84 | categoryType: 0, 85 | createTime: '2018-03-10T15:35:43.000+0000', 86 | updateTime: '2018-03-10T15:35:43.000+0000' 87 | }; 88 | 89 | -------------------------------------------------------------------------------- /frontend/src/app/services/user.service.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | import {HttpClient} from '@angular/common/http'; 3 | import {apiUrl} from '../../environments/environment'; 4 | import {BehaviorSubject, Observable, of, Subject} from 'rxjs'; 5 | import {catchError, tap} from 'rxjs/operators'; 6 | import {JwtResponse} from '../response/JwtResponse'; 7 | import {CookieService} from 'ngx-cookie-service'; 8 | import {User} from "../models/User"; 9 | 10 | @Injectable({ 11 | providedIn: 'root' 12 | }) 13 | export class UserService { 14 | 15 | private currentUserSubject: BehaviorSubject; 16 | public currentUser: Observable; 17 | public nameTerms = new Subject(); 18 | public name$ = this.nameTerms.asObservable(); 19 | constructor(private http: HttpClient, 20 | private cookieService: CookieService) { 21 | const memo = localStorage.getItem('currentUser'); 22 | this.currentUserSubject = new BehaviorSubject(JSON.parse(memo)); 23 | this.currentUser = this.currentUserSubject.asObservable(); 24 | cookieService.set('currentUser', memo); 25 | } 26 | 27 | get currentUserValue() { 28 | return this.currentUserSubject.value; 29 | } 30 | 31 | 32 | login(loginForm): Observable { 33 | const url = `${apiUrl}/login`; 34 | return this.http.post(url, loginForm).pipe( 35 | tap(user => { 36 | if (user && user.token) { 37 | this.cookieService.set('currentUser', JSON.stringify(user)); 38 | if (loginForm.remembered) { 39 | localStorage.setItem('currentUser', JSON.stringify(user)); 40 | } 41 | console.log((user.name)); 42 | this.nameTerms.next(user.name); 43 | this.currentUserSubject.next(user); 44 | return user; 45 | } 46 | }), 47 | catchError(this.handleError('Login Failed', null)) 48 | ); 49 | } 50 | 51 | logout() { 52 | this.currentUserSubject.next(null); 53 | localStorage.removeItem('currentUser'); 54 | this.cookieService.delete('currentUser'); 55 | } 56 | 57 | signUp(user: User): Observable { 58 | const url = `${apiUrl}/register`; 59 | return this.http.post(url, user); 60 | } 61 | 62 | update(user: User): Observable { 63 | const url = `${apiUrl}/profile`; 64 | return this.http.put(url, user); } 65 | 66 | get(email: string): Observable { 67 | const url = `${apiUrl}/profile/${email}`; 68 | return this.http.get(url); 69 | } 70 | 71 | /** 72 | * Handle Http operation that failed. 73 | * Let the app continue. 74 | * @param operation - name of the operation that failed 75 | * @param result - optional value to return as the observable result 76 | */ 77 | private handleError(operation = 'operation', result?: T) { 78 | return (error: any): Observable => { 79 | 80 | console.log(error); // log to console instead 81 | 82 | // Let the app keep running by returning an empty result. 83 | return of(result as T); 84 | }; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /backend/src/main/java/me/zhulin/shopapi/api/ProductController.java: -------------------------------------------------------------------------------- 1 | package me.zhulin.shopapi.api; 2 | 3 | import me.zhulin.shopapi.entity.ProductInfo; 4 | import me.zhulin.shopapi.service.CategoryService; 5 | import me.zhulin.shopapi.service.ProductService; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.data.domain.Page; 8 | import org.springframework.data.domain.PageRequest; 9 | import org.springframework.http.ResponseEntity; 10 | import org.springframework.validation.BindingResult; 11 | import org.springframework.web.bind.annotation.*; 12 | 13 | import javax.validation.Valid; 14 | 15 | /** 16 | * Created By Zhu Lin on 3/10/2018. 17 | */ 18 | @CrossOrigin 19 | @RestController 20 | public class ProductController { 21 | @Autowired 22 | CategoryService categoryService; 23 | @Autowired 24 | ProductService productService; 25 | 26 | /** 27 | * Show All Categories 28 | */ 29 | 30 | @GetMapping("/product") 31 | public Page findAll(@RequestParam(value = "page", defaultValue = "1") Integer page, 32 | @RequestParam(value = "size", defaultValue = "3") Integer size) { 33 | PageRequest request = PageRequest.of(page - 1, size); 34 | return productService.findAll(request); 35 | } 36 | 37 | @GetMapping("/product/{productId}") 38 | public ProductInfo showOne(@PathVariable("productId") String productId) { 39 | 40 | ProductInfo productInfo = productService.findOne(productId); 41 | 42 | // // Product is not available 43 | // if (productInfo.getProductStatus().equals(ProductStatusEnum.DOWN.getCode())) { 44 | // productInfo = null; 45 | // } 46 | 47 | return productInfo; 48 | } 49 | 50 | @PostMapping("/seller/product/new") 51 | public ResponseEntity create(@Valid @RequestBody ProductInfo product, 52 | BindingResult bindingResult) { 53 | ProductInfo productIdExists = productService.findOne(product.getProductId()); 54 | if (productIdExists != null) { 55 | bindingResult 56 | .rejectValue("productId", "error.product", 57 | "There is already a product with the code provided"); 58 | } 59 | if (bindingResult.hasErrors()) { 60 | return ResponseEntity.badRequest().body(bindingResult); 61 | } 62 | return ResponseEntity.ok(productService.save(product)); 63 | } 64 | 65 | @PutMapping("/seller/product/{id}/edit") 66 | public ResponseEntity edit(@PathVariable("id") String productId, 67 | @Valid @RequestBody ProductInfo product, 68 | BindingResult bindingResult) { 69 | if (bindingResult.hasErrors()) { 70 | return ResponseEntity.badRequest().body(bindingResult); 71 | } 72 | if (!productId.equals(product.getProductId())) { 73 | return ResponseEntity.badRequest().body("Id Not Matched"); 74 | } 75 | 76 | return ResponseEntity.ok(productService.update(product)); 77 | } 78 | 79 | @DeleteMapping("/seller/product/{id}/delete") 80 | public ResponseEntity delete(@PathVariable("id") String productId) { 81 | productService.delete(productId); 82 | return ResponseEntity.ok().build(); 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /backend/src/main/java/me/zhulin/shopapi/api/test.http: -------------------------------------------------------------------------------- 1 | GET {{host-url}}/product 2 | 3 | ### 4 | 5 | GET {{host-url}}/cart 6 | 7 | ### 8 | 9 | POST {{host-url}}/cart 10 | Content-Type: application/json 11 | 12 | { 13 | "productId": "B0001", 14 | "quantity": 4 15 | } 16 | 17 | ### 18 | 19 | POST {{host-url}}/login 20 | Content-Type: application/json 21 | 22 | { 23 | "username": "manager1@email.com", 24 | "password": "123", 25 | "rem": true 26 | } 27 | 28 | ### 29 | 30 | GET {{host-url}}/cart 31 | Content-Type: application/json 32 | Authorization: Bearer {{token}} 33 | 34 | 35 | ### 36 | 37 | POST {{host-url}}/cart 38 | Content-Type: application/json 39 | Authorization: Bearer {{token}} 40 | 41 | [ 42 | ] 43 | 44 | ### 45 | 46 | PUT {{host-url}}/cart/B0002 47 | Content-Type: application/json 48 | Authorization: Bearer {{token}} 49 | 50 | 1 51 | 52 | ### 53 | POST {{host-url}}/cart/add 54 | Content-Type: application/json 55 | Authorization: Bearer {{token}} 56 | 57 | { 58 | "quantity": 4, 59 | "productId": "B0002" 60 | } 61 | 62 | ### 63 | 64 | DELETE {{host-url}}/cart/B0002 65 | Content-Type: application/json 66 | Authorization: Bearer {{token}} 67 | 68 | ### 69 | 70 | POST {{host-url}}/cart/checkout 71 | Content-Type: application/json 72 | Authorization: Bearer {{token}} 73 | 74 | 75 | ### 76 | 77 | GET {{host-url}}/order/finish/2147483643 78 | Authorization: Bearer {{token}} 79 | 80 | ### 81 | 82 | GET {{host-url}}/order/2147483643 83 | Authorization: Bearer {{token}} 84 | 85 | ### 86 | 87 | GET {{host-url}}/profile/manager1@email.com 88 | Authorization: Bearer {{token}} 89 | 90 | 91 | 92 | ### 93 | 94 | PUT {{host-url}}/profile 95 | Content-Type: application/json 96 | Authorization: Bearer {{token}} 97 | 98 | { 99 | "id": 2147483641, 100 | "email": "customer1@email.com", 101 | "password": "1234", 102 | "name": "customer1", 103 | "phone": "6789", 104 | "address": "3200 West Road", 105 | "active": false, 106 | "role": "ROLE_CUSTOMER", 107 | "authorities": [ 108 | { 109 | "authority": "ROLE_CUSTOMER" 110 | } 111 | ] 112 | } 113 | 114 | ### 115 | 116 | GET {{host-url}}/product/B0003 117 | 118 | Content-Type: application/json 119 | Authorization: Bearer {{token}} 120 | 121 | ### 122 | 123 | PUT {{host-url}}/seller/product/B0002/edit 124 | Content-Type: application/json 125 | Authorization: Bearer {{token}} 126 | 127 | { 128 | "productId": "B0002", 129 | "productName": "Spring In Action", 130 | "productPrice": 20.00, 131 | "productStock": 195, 132 | "productDescription": "Learn Spring", 133 | "productIcon": "https://images-na.ssl-images-amazon.com/images/I/51gHy16h5TL._SX397_BO1,204,203,200_.jpg", 134 | "productStatus": 1, 135 | "categoryType": 0, 136 | "updateTime": "2019-01-05T03:43:29.047+0000" 137 | } 138 | 139 | ### 140 | 141 | POST {{host-url}}/seller/product/new 142 | Content-Type: application/json 143 | Authorization: Bearer {{token}} 144 | 145 | 146 | 147 | { 148 | "productId": "B0022", 149 | "productName": "Spring In Action", 150 | "productPrice": 20.00, 151 | "productStock": 195, 152 | "productDescription": "Learn Spring", 153 | "productIcon": "https://images-na.ssl-images-amazon.com/images/I/51gHy16h5TL._SX397_BO1,204,203,200_.jpg", 154 | "productStatus": 0, 155 | "categoryType": 1 156 | } 157 | 158 | ### 159 | -------------------------------------------------------------------------------- /backend/src/main/java/me/zhulin/shopapi/api/OrderController.java: -------------------------------------------------------------------------------- 1 | package me.zhulin.shopapi.api; 2 | 3 | 4 | import me.zhulin.shopapi.entity.OrderMain; 5 | import me.zhulin.shopapi.entity.ProductInOrder; 6 | import me.zhulin.shopapi.service.OrderService; 7 | import me.zhulin.shopapi.service.UserService; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.data.domain.Page; 10 | import org.springframework.data.domain.PageRequest; 11 | import org.springframework.http.HttpStatus; 12 | import org.springframework.http.ResponseEntity; 13 | import org.springframework.security.core.Authentication; 14 | import org.springframework.security.core.authority.SimpleGrantedAuthority; 15 | import org.springframework.web.bind.annotation.*; 16 | 17 | import java.util.Collection; 18 | 19 | /** 20 | * Created By Zhu Lin on 3/14/2018. 21 | */ 22 | @RestController 23 | @CrossOrigin 24 | public class OrderController { 25 | @Autowired 26 | OrderService orderService; 27 | @Autowired 28 | UserService userService; 29 | 30 | @GetMapping("/order") 31 | public Page orderList(@RequestParam(value = "page", defaultValue = "1") Integer page, 32 | @RequestParam(value = "size", defaultValue = "10") Integer size, 33 | Authentication authentication) { 34 | PageRequest request = PageRequest.of(page - 1, size); 35 | Page orderPage; 36 | if (authentication.getAuthorities().contains(new SimpleGrantedAuthority("ROLE_CUSTOMER"))) { 37 | orderPage = orderService.findByBuyerEmail(authentication.getName(), request); 38 | } else { 39 | orderPage = orderService.findAll(request); 40 | } 41 | return orderPage; 42 | } 43 | 44 | 45 | @PatchMapping("/order/cancel/{id}") 46 | public ResponseEntity cancel(@PathVariable("id") Long orderId, Authentication authentication) { 47 | OrderMain orderMain = orderService.findOne(orderId); 48 | if (!authentication.getName().equals(orderMain.getBuyerEmail()) && authentication.getAuthorities().contains(new SimpleGrantedAuthority("ROLE_CUSTOMER"))) { 49 | 50 | return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); 51 | } 52 | return ResponseEntity.ok(orderService.cancel(orderId)); 53 | } 54 | 55 | @PatchMapping("/order/finish/{id}") 56 | public ResponseEntity finish(@PathVariable("id") Long orderId, Authentication authentication) { 57 | if (authentication.getAuthorities().contains(new SimpleGrantedAuthority("ROLE_CUSTOMER"))) { 58 | return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); 59 | } 60 | return ResponseEntity.ok(orderService.finish(orderId)); 61 | } 62 | 63 | @GetMapping("/order/{id}") 64 | public ResponseEntity show(@PathVariable("id") Long orderId, Authentication authentication) { 65 | boolean isCustomer = authentication.getAuthorities().contains(new SimpleGrantedAuthority("ROLE_CUSTOMER")); 66 | OrderMain orderMain = orderService.findOne(orderId); 67 | if (isCustomer && !authentication.getName().equals(orderMain.getBuyerEmail())) { 68 | return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); 69 | } 70 | 71 | Collection items = orderMain.getProducts(); 72 | return ResponseEntity.ok(orderMain); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /backend/src/main/java/me/zhulin/shopapi/api/CartController.java: -------------------------------------------------------------------------------- 1 | package me.zhulin.shopapi.api; 2 | 3 | 4 | import me.zhulin.shopapi.entity.Cart; 5 | import me.zhulin.shopapi.entity.ProductInOrder; 6 | import me.zhulin.shopapi.entity.User; 7 | import me.zhulin.shopapi.form.ItemForm; 8 | import me.zhulin.shopapi.repository.ProductInOrderRepository; 9 | import me.zhulin.shopapi.service.CartService; 10 | import me.zhulin.shopapi.service.ProductInOrderService; 11 | import me.zhulin.shopapi.service.ProductService; 12 | import me.zhulin.shopapi.service.UserService; 13 | import org.springframework.beans.factory.annotation.Autowired; 14 | import org.springframework.http.ResponseEntity; 15 | import org.springframework.transaction.annotation.Transactional; 16 | import org.springframework.web.bind.annotation.*; 17 | 18 | import java.security.Principal; 19 | import java.util.Collection; 20 | import java.util.Collections; 21 | 22 | /** 23 | * Created By Zhu Lin on 3/11/2018. 24 | */ 25 | @CrossOrigin 26 | @RestController 27 | @RequestMapping("/cart") 28 | public class CartController { 29 | @Autowired 30 | CartService cartService; 31 | @Autowired 32 | UserService userService; 33 | @Autowired 34 | ProductService productService; 35 | @Autowired 36 | ProductInOrderService productInOrderService; 37 | @Autowired 38 | ProductInOrderRepository productInOrderRepository; 39 | 40 | @PostMapping("") 41 | public ResponseEntity mergeCart(@RequestBody Collection productInOrders, Principal principal) { 42 | User user = userService.findOne(principal.getName()); 43 | try { 44 | cartService.mergeLocalCart(productInOrders, user); 45 | } catch (Exception e) { 46 | ResponseEntity.badRequest().body("Merge Cart Failed"); 47 | } 48 | return ResponseEntity.ok(cartService.getCart(user)); 49 | } 50 | 51 | @GetMapping("") 52 | public Cart getCart(Principal principal) { 53 | User user = userService.findOne(principal.getName()); 54 | return cartService.getCart(user); 55 | } 56 | 57 | 58 | @PostMapping("/add") 59 | public boolean addToCart(@RequestBody ItemForm form, Principal principal) { 60 | var productInfo = productService.findOne(form.getProductId()); 61 | try { 62 | mergeCart(Collections.singleton(new ProductInOrder(productInfo, form.getQuantity())), principal); 63 | } catch (Exception e) { 64 | return false; 65 | } 66 | return true; 67 | } 68 | 69 | @PutMapping("/{itemId}") 70 | public ProductInOrder modifyItem(@PathVariable("itemId") String itemId, @RequestBody Integer quantity, Principal principal) { 71 | User user = userService.findOne(principal.getName()); 72 | productInOrderService.update(itemId, quantity, user); 73 | return productInOrderService.findOne(itemId, user); 74 | } 75 | 76 | @DeleteMapping("/{itemId}") 77 | public void deleteItem(@PathVariable("itemId") String itemId, Principal principal) { 78 | User user = userService.findOne(principal.getName()); 79 | cartService.delete(itemId, user); 80 | // flush memory into DB 81 | } 82 | 83 | 84 | @PostMapping("/checkout") 85 | public ResponseEntity checkout(Principal principal) { 86 | User user = userService.findOne(principal.getName());// Email as username 87 | cartService.checkout(user); 88 | return ResponseEntity.ok(null); 89 | } 90 | 91 | 92 | } 93 | -------------------------------------------------------------------------------- /backend/src/main/java/me/zhulin/shopapi/api/UserController.java: -------------------------------------------------------------------------------- 1 | package me.zhulin.shopapi.api; 2 | 3 | import me.zhulin.shopapi.entity.User; 4 | import me.zhulin.shopapi.security.JWT.JwtProvider; 5 | import me.zhulin.shopapi.service.UserService; 6 | import me.zhulin.shopapi.vo.request.LoginForm; 7 | import me.zhulin.shopapi.vo.response.JwtResponse; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.http.HttpStatus; 10 | import org.springframework.http.ResponseEntity; 11 | import org.springframework.security.authentication.AuthenticationManager; 12 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; 13 | import org.springframework.security.core.Authentication; 14 | import org.springframework.security.core.AuthenticationException; 15 | import org.springframework.security.core.context.SecurityContextHolder; 16 | import org.springframework.security.core.userdetails.UserDetails; 17 | import org.springframework.web.bind.annotation.*; 18 | 19 | import java.security.Principal; 20 | 21 | /** 22 | * Created By Zhu Lin on 1/1/2019. 23 | */ 24 | @CrossOrigin 25 | @RestController 26 | public class UserController { 27 | 28 | @Autowired 29 | UserService userService; 30 | 31 | 32 | @Autowired 33 | JwtProvider jwtProvider; 34 | 35 | @Autowired 36 | AuthenticationManager authenticationManager; 37 | 38 | @PostMapping("/login") 39 | public ResponseEntity login(@RequestBody LoginForm loginForm) { 40 | // throws Exception if authentication failed 41 | 42 | try { 43 | Authentication authentication = authenticationManager.authenticate( 44 | new UsernamePasswordAuthenticationToken(loginForm.getUsername(), loginForm.getPassword())); 45 | SecurityContextHolder.getContext().setAuthentication(authentication); 46 | String jwt = jwtProvider.generate(authentication); 47 | UserDetails userDetails = (UserDetails) authentication.getPrincipal(); 48 | User user = userService.findOne(userDetails.getUsername()); 49 | return ResponseEntity.ok(new JwtResponse(jwt, user.getEmail(), user.getName(), user.getRole())); 50 | } catch (AuthenticationException e) { 51 | return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); 52 | } 53 | } 54 | 55 | 56 | @PostMapping("/register") 57 | public ResponseEntity save(@RequestBody User user) { 58 | try { 59 | return ResponseEntity.ok(userService.save(user)); 60 | } catch (Exception e) { 61 | return ResponseEntity.badRequest().build(); 62 | } 63 | } 64 | 65 | @PutMapping("/profile") 66 | public ResponseEntity update(@RequestBody User user, Principal principal) { 67 | 68 | try { 69 | if (!principal.getName().equals(user.getEmail())) throw new IllegalArgumentException(); 70 | return ResponseEntity.ok(userService.update(user)); 71 | } catch (Exception e) { 72 | return ResponseEntity.badRequest().build(); 73 | } 74 | } 75 | 76 | @GetMapping("/profile/{email}") 77 | public ResponseEntity getProfile(@PathVariable("email") String email, Principal principal) { 78 | if (principal.getName().equals(email)) { 79 | return ResponseEntity.ok(userService.findOne(email)); 80 | } else { 81 | return ResponseEntity.badRequest().build(); 82 | } 83 | 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /frontend/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "codelyzer" 4 | ], 5 | "rules": { 6 | "arrow-return-shorthand": true, 7 | "callable-types": true, 8 | "class-name": true, 9 | "comment-format": [ 10 | true, 11 | "check-space" 12 | ], 13 | "curly": true, 14 | "deprecation": { 15 | "severity": "warn" 16 | }, 17 | "eofline": true, 18 | "forin": true, 19 | "import-blacklist": [ 20 | true, 21 | "rxjs/Rx" 22 | ], 23 | "import-spacing": true, 24 | "indent": [ 25 | true, 26 | "spaces" 27 | ], 28 | "interface-over-type-literal": true, 29 | "label-position": true, 30 | "max-line-length": [ 31 | true, 32 | 140 33 | ], 34 | "member-access": false, 35 | "member-ordering": [ 36 | true, 37 | { 38 | "order": [ 39 | "static-field", 40 | "instance-field", 41 | "static-method", 42 | "instance-method" 43 | ] 44 | } 45 | ], 46 | "no-arg": true, 47 | "no-bitwise": true, 48 | "no-console": [ 49 | true, 50 | "debug", 51 | "info", 52 | "time", 53 | "timeEnd", 54 | "trace" 55 | ], 56 | "no-construct": true, 57 | "no-debugger": true, 58 | "no-duplicate-super": true, 59 | "no-empty": false, 60 | "no-empty-interface": true, 61 | "no-eval": true, 62 | "no-inferrable-types": [ 63 | true, 64 | "ignore-params" 65 | ], 66 | "no-misused-new": true, 67 | "no-non-null-assertion": true, 68 | "no-redundant-jsdoc": true, 69 | "no-shadowed-variable": true, 70 | "no-string-literal": false, 71 | "no-string-throw": true, 72 | "no-switch-case-fall-through": true, 73 | "no-trailing-whitespace": true, 74 | "no-unnecessary-initializer": true, 75 | "no-unused-expression": true, 76 | "no-use-before-declare": true, 77 | "no-var-keyword": true, 78 | "object-literal-sort-keys": false, 79 | "one-line": [ 80 | true, 81 | "check-open-brace", 82 | "check-catch", 83 | "check-else", 84 | "check-whitespace" 85 | ], 86 | "prefer-const": true, 87 | "quotemark": [ 88 | true, 89 | "single" 90 | ], 91 | "radix": true, 92 | "semicolon": [ 93 | true, 94 | "always" 95 | ], 96 | "triple-equals": [ 97 | true, 98 | "allow-null-check" 99 | ], 100 | "typedef-whitespace": [ 101 | true, 102 | { 103 | "call-signature": "nospace", 104 | "index-signature": "nospace", 105 | "parameter": "nospace", 106 | "property-declaration": "nospace", 107 | "variable-declaration": "nospace" 108 | } 109 | ], 110 | "unified-signatures": true, 111 | "variable-name": false, 112 | "whitespace": [ 113 | true, 114 | "check-branch", 115 | "check-decl", 116 | "check-operator", 117 | "check-separator", 118 | "check-type" 119 | ], 120 | "no-output-on-prefix": true, 121 | "use-input-property-decorator": true, 122 | "use-output-property-decorator": true, 123 | "use-host-property-decorator": true, 124 | "no-input-rename": true, 125 | "no-output-rename": true, 126 | "use-life-cycle-interface": true, 127 | "use-pipe-transform-interface": true, 128 | "component-class-suffix": true, 129 | "directive-class-suffix": true 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /backend/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 2.2.0.BUILD-SNAPSHOT 9 | 10 | 11 | me.zhulin 12 | shop-api 13 | 0.0.1-SNAPSHOT 14 | shop-api 15 | Online Shopping API Server 16 | 17 | 18 | 11 19 | 20 | 21 | 22 | 23 | org.springframework.boot 24 | spring-boot-starter-data-jpa 25 | 26 | 27 | org.springframework.boot 28 | spring-boot-starter-web 29 | 30 | 31 | org.springframework.boot 32 | spring-boot-starter-security 33 | 34 | 35 | org.postgresql 36 | postgresql 37 | runtime 38 | 39 | 40 | org.projectlombok 41 | lombok 42 | true 43 | 44 | 45 | org.springframework.boot 46 | spring-boot-starter-test 47 | test 48 | 49 | 50 | 51 | io.jsonwebtoken 52 | jjwt 53 | 0.9.1 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | org.springframework.boot 62 | spring-boot-maven-plugin 63 | 64 | 65 | 66 | 67 | 68 | 69 | spring-snapshots 70 | Spring Snapshots 71 | https://repo.spring.io/snapshot 72 | 73 | true 74 | 75 | 76 | 77 | spring-milestones 78 | Spring Milestones 79 | https://repo.spring.io/milestone 80 | 81 | 82 | 83 | 84 | spring-snapshots 85 | Spring Snapshots 86 | https://repo.spring.io/snapshot 87 | 88 | true 89 | 90 | 91 | 92 | spring-milestones 93 | Spring Milestones 94 | https://repo.spring.io/milestone 95 | 96 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /backend/src/main/java/me/zhulin/shopapi/security/SpringSecurityConfig.java: -------------------------------------------------------------------------------- 1 | package me.zhulin.shopapi.security; 2 | 3 | import me.zhulin.shopapi.security.JWT.JwtEntryPoint; 4 | import me.zhulin.shopapi.security.JWT.JwtFilter; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.beans.factory.annotation.Qualifier; 7 | import org.springframework.beans.factory.annotation.Value; 8 | import org.springframework.context.annotation.Bean; 9 | import org.springframework.context.annotation.Configuration; 10 | import org.springframework.context.annotation.DependsOn; 11 | import org.springframework.security.authentication.AuthenticationManager; 12 | import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; 13 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 14 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 15 | import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 16 | import org.springframework.security.config.http.SessionCreationPolicy; 17 | import org.springframework.security.crypto.password.PasswordEncoder; 18 | import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; 19 | 20 | import javax.sql.DataSource; 21 | 22 | /** 23 | * Created By Zhu Lin on 1/1/2019. 24 | */ 25 | @Configuration 26 | @EnableWebSecurity 27 | @DependsOn("passwordEncoder") 28 | public class SpringSecurityConfig extends WebSecurityConfigurerAdapter{ 29 | 30 | @Autowired 31 | JwtFilter jwtFilter; 32 | @Autowired 33 | private JwtEntryPoint accessDenyHandler; 34 | @Autowired 35 | private PasswordEncoder passwordEncoder; 36 | 37 | @Autowired 38 | @Qualifier("dataSource") 39 | DataSource dataSource; 40 | 41 | @Value("${spring.queries.users-query}") 42 | private String usersQuery; 43 | 44 | @Value("${spring.queries.roles-query}") 45 | private String rolesQuery; 46 | 47 | @Override 48 | public void configure(AuthenticationManagerBuilder auth) throws Exception{ 49 | auth 50 | .jdbcAuthentication() 51 | .usersByUsernameQuery(usersQuery) 52 | .authoritiesByUsernameQuery(rolesQuery) 53 | .dataSource(dataSource) 54 | .passwordEncoder(passwordEncoder); 55 | } 56 | 57 | @Bean 58 | @Override 59 | public AuthenticationManager authenticationManagerBean() throws Exception { 60 | return super.authenticationManagerBean(); 61 | } 62 | 63 | @Override 64 | protected void configure(HttpSecurity http) throws Exception{ 65 | http.cors().and().csrf().disable() 66 | .authorizeRequests() 67 | 68 | .antMatchers("/profile/**").authenticated() 69 | .antMatchers("/cart/**").access("hasAnyRole('CUSTOMER')") 70 | .antMatchers("/order/finish/**").access("hasAnyRole('EMPLOYEE', 'MANAGER')") 71 | .antMatchers("/order/**").authenticated() 72 | .antMatchers("/profiles/**").authenticated() 73 | .antMatchers("/seller/product/new").access("hasAnyRole('MANAGER')") 74 | .antMatchers("/seller/**/delete").access("hasAnyRole( 'MANAGER')") 75 | .antMatchers("/seller/**").access("hasAnyRole('EMPLOYEE', 'MANAGER')") 76 | .anyRequest().permitAll() 77 | 78 | .and() 79 | .exceptionHandling().authenticationEntryPoint(accessDenyHandler) 80 | .and() 81 | .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); 82 | 83 | http.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class); 84 | } 85 | 86 | } 87 | -------------------------------------------------------------------------------- /frontend/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE9, IE10, IE11, and Chrome <55 requires all of the following polyfills. 22 | * This also includes Android Emulators with older versions of Chrome and Google Search/Googlebot 23 | */ 24 | 25 | // import 'core-js/es6/symbol'; 26 | // import 'core-js/es6/object'; 27 | // import 'core-js/es6/function'; 28 | // import 'core-js/es6/parse-int'; 29 | // import 'core-js/es6/parse-float'; 30 | // import 'core-js/es6/number'; 31 | // import 'core-js/es6/math'; 32 | // import 'core-js/es6/string'; 33 | // import 'core-js/es6/date'; 34 | // import 'core-js/es6/array'; 35 | // import 'core-js/es6/regexp'; 36 | // import 'core-js/es6/map'; 37 | // import 'core-js/es6/weak-map'; 38 | // import 'core-js/es6/set'; 39 | 40 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 41 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 42 | 43 | /** IE10 and IE11 requires the following for the Reflect API. */ 44 | // import 'core-js/es6/reflect'; 45 | 46 | /** 47 | * Web Animations `@angular/platform-browser/animations` 48 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 49 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 50 | */ 51 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 52 | 53 | /** 54 | * By default, zone.js will patch all possible macroTask and DomEvents 55 | * user can disable parts of macroTask/DomEvents patch by setting following flags 56 | * because those flags need to be set before `zone.js` being loaded, and webpack 57 | * will put import in the top of bundle, so user need to create a separate file 58 | * in this directory (for example: zone-flags.ts), and put the following flags 59 | * into that file, and then add the following code before importing zone.js. 60 | * import './zone-flags.ts'; 61 | * 62 | * The flags allowed in zone-flags.ts are listed here. 63 | * 64 | * The following flags will work for all browsers. 65 | * 66 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 67 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 68 | * (window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 69 | * 70 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 71 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 72 | * 73 | * (window as any).__Zone_enable_cross_context_check = true; 74 | * 75 | */ 76 | 77 | /*************************************************************************************************** 78 | * Zone JS is required by default for Angular itself. 79 | */ 80 | import 'zone.js/dist/zone'; // Included with Angular CLI. 81 | 82 | 83 | /*************************************************************************************************** 84 | * APPLICATION IMPORTS 85 | */ 86 | -------------------------------------------------------------------------------- /backend/src/main/java/me/zhulin/shopapi/service/impl/CartServiceImpl.java: -------------------------------------------------------------------------------- 1 | package me.zhulin.shopapi.service.impl; 2 | 3 | 4 | import me.zhulin.shopapi.entity.Cart; 5 | import me.zhulin.shopapi.entity.OrderMain; 6 | import me.zhulin.shopapi.entity.ProductInOrder; 7 | import me.zhulin.shopapi.entity.User; 8 | import me.zhulin.shopapi.enums.ResultEnum; 9 | import me.zhulin.shopapi.exception.MyException; 10 | import me.zhulin.shopapi.repository.CartRepository; 11 | import me.zhulin.shopapi.repository.OrderRepository; 12 | import me.zhulin.shopapi.repository.ProductInOrderRepository; 13 | import me.zhulin.shopapi.repository.UserRepository; 14 | import me.zhulin.shopapi.service.CartService; 15 | import me.zhulin.shopapi.service.ProductService; 16 | import me.zhulin.shopapi.service.UserService; 17 | import org.springframework.beans.factory.annotation.Autowired; 18 | import org.springframework.stereotype.Service; 19 | import org.springframework.transaction.annotation.Transactional; 20 | 21 | import java.util.Collection; 22 | import java.util.Optional; 23 | import java.util.Set; 24 | 25 | /** 26 | * Created By Zhu Lin on 3/11/2018. 27 | */ 28 | @Service 29 | public class CartServiceImpl implements CartService { 30 | @Autowired 31 | ProductService productService; 32 | @Autowired 33 | OrderRepository orderRepository; 34 | @Autowired 35 | UserRepository userRepository; 36 | 37 | @Autowired 38 | ProductInOrderRepository productInOrderRepository; 39 | @Autowired 40 | CartRepository cartRepository; 41 | @Autowired 42 | UserService userService; 43 | 44 | @Override 45 | public Cart getCart(User user) { 46 | return user.getCart(); 47 | } 48 | 49 | @Override 50 | @Transactional 51 | public void mergeLocalCart(Collection productInOrders, User user) { 52 | Cart finalCart = user.getCart(); 53 | productInOrders.forEach(productInOrder -> { 54 | Set set = finalCart.getProducts(); 55 | Optional old = set.stream().filter(e -> e.getProductId().equals(productInOrder.getProductId())).findFirst(); 56 | ProductInOrder prod; 57 | if (old.isPresent()) { 58 | prod = old.get(); 59 | prod.setCount(productInOrder.getCount() + prod.getCount()); 60 | } else { 61 | prod = productInOrder; 62 | prod.setCart(finalCart); 63 | finalCart.getProducts().add(prod); 64 | } 65 | productInOrderRepository.save(prod); 66 | }); 67 | cartRepository.save(finalCart); 68 | 69 | } 70 | 71 | @Override 72 | @Transactional 73 | public void delete(String itemId, User user) { 74 | if(itemId.equals("") || user == null) { 75 | throw new MyException(ResultEnum.ORDER_STATUS_ERROR); 76 | } 77 | 78 | var op = user.getCart().getProducts().stream().filter(e -> itemId.equals(e.getProductId())).findFirst(); 79 | op.ifPresent(productInOrder -> { 80 | productInOrder.setCart(null); 81 | productInOrderRepository.deleteById(productInOrder.getId()); 82 | }); 83 | } 84 | 85 | @Override 86 | @Transactional 87 | public void checkout(User user) { 88 | // Creat an order 89 | OrderMain order = new OrderMain(user); 90 | orderRepository.save(order); 91 | 92 | // clear cart's foreign key & set order's foreign key& decrease stock 93 | user.getCart().getProducts().forEach(productInOrder -> { 94 | productInOrder.setCart(null); 95 | productInOrder.setOrderMain(order); 96 | productService.decreaseStock(productInOrder.getProductId(), productInOrder.getCount()); 97 | productInOrderRepository.save(productInOrder); 98 | }); 99 | 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /backend/src/main/java/me/zhulin/shopapi/entity/ProductInOrder.java: -------------------------------------------------------------------------------- 1 | package me.zhulin.shopapi.entity; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnore; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | 7 | import javax.persistence.*; 8 | import javax.validation.constraints.Min; 9 | import javax.validation.constraints.NotEmpty; 10 | import javax.validation.constraints.NotNull; 11 | import java.math.BigDecimal; 12 | import java.util.Objects; 13 | 14 | /** 15 | * Created By Zhu Lin on 3/14/2018. 16 | */ 17 | @Entity 18 | @Data 19 | @NoArgsConstructor 20 | public class ProductInOrder { 21 | @Id 22 | @GeneratedValue(strategy = GenerationType.AUTO) 23 | private Long id; 24 | 25 | @ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.REMOVE) 26 | // @JoinColumn(name = "cart_id") 27 | @JsonIgnore 28 | private Cart cart; 29 | 30 | @ManyToOne(fetch = FetchType.LAZY) 31 | @JoinColumn(name = "order_id") 32 | @JsonIgnore 33 | private OrderMain orderMain; 34 | 35 | 36 | @NotEmpty 37 | private String productId; 38 | 39 | /** 40 | * 名字. 41 | */ 42 | @NotEmpty 43 | private String productName; 44 | 45 | /** 46 | * 描述. 47 | */ 48 | @NotNull 49 | private String productDescription; 50 | 51 | /** 52 | * 小图. 53 | */ 54 | private String productIcon; 55 | 56 | /** 57 | * 类目编号. 58 | */ 59 | @NotNull 60 | private Integer categoryType; 61 | 62 | /** 63 | * 单价. 64 | */ 65 | @NotNull 66 | private BigDecimal productPrice; 67 | 68 | /** 69 | * 库存. 70 | */ 71 | @Min(0) 72 | private Integer productStock; 73 | 74 | @Min(1) 75 | private Integer count; 76 | 77 | 78 | public ProductInOrder(ProductInfo productInfo, Integer quantity) { 79 | this.productId = productInfo.getProductId(); 80 | this.productName = productInfo.getProductName(); 81 | this.productDescription = productInfo.getProductDescription(); 82 | this.productIcon = productInfo.getProductIcon(); 83 | this.categoryType = productInfo.getCategoryType(); 84 | this.productPrice = productInfo.getProductPrice(); 85 | this.productStock = productInfo.getProductStock(); 86 | this.count = quantity; 87 | } 88 | 89 | @Override 90 | public String toString() { 91 | return "ProductInOrder{" + 92 | "id=" + id + 93 | ", productId='" + productId + '\'' + 94 | ", productName='" + productName + '\'' + 95 | ", productDescription='" + productDescription + '\'' + 96 | ", productIcon='" + productIcon + '\'' + 97 | ", categoryType=" + categoryType + 98 | ", productPrice=" + productPrice + 99 | ", productStock=" + productStock + 100 | ", count=" + count + 101 | '}'; 102 | } 103 | 104 | @Override 105 | public boolean equals(Object o) { 106 | if (this == o) return true; 107 | if (o == null || getClass() != o.getClass()) return false; 108 | if (!super.equals(o)) return false; 109 | ProductInOrder that = (ProductInOrder) o; 110 | return Objects.equals(id, that.id) && 111 | Objects.equals(productId, that.productId) && 112 | Objects.equals(productName, that.productName) && 113 | Objects.equals(productDescription, that.productDescription) && 114 | Objects.equals(productIcon, that.productIcon) && 115 | Objects.equals(categoryType, that.categoryType) && 116 | Objects.equals(productPrice, that.productPrice); 117 | } 118 | 119 | @Override 120 | public int hashCode() { 121 | 122 | return Objects.hash(super.hashCode(), id, productId, productName, productDescription, productIcon, categoryType, productPrice); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /backend/src/main/java/me/zhulin/shopapi/service/impl/OrderServiceImpl.java: -------------------------------------------------------------------------------- 1 | package me.zhulin.shopapi.service.impl; 2 | 3 | 4 | import me.zhulin.shopapi.entity.OrderMain; 5 | import me.zhulin.shopapi.entity.ProductInOrder; 6 | import me.zhulin.shopapi.entity.ProductInfo; 7 | import me.zhulin.shopapi.enums.OrderStatusEnum; 8 | import me.zhulin.shopapi.enums.ResultEnum; 9 | import me.zhulin.shopapi.exception.MyException; 10 | import me.zhulin.shopapi.repository.OrderRepository; 11 | import me.zhulin.shopapi.repository.ProductInOrderRepository; 12 | import me.zhulin.shopapi.repository.ProductInfoRepository; 13 | import me.zhulin.shopapi.repository.UserRepository; 14 | import me.zhulin.shopapi.service.OrderService; 15 | import me.zhulin.shopapi.service.ProductService; 16 | import org.springframework.beans.factory.annotation.Autowired; 17 | import org.springframework.data.domain.Page; 18 | import org.springframework.data.domain.Pageable; 19 | import org.springframework.stereotype.Service; 20 | import org.springframework.transaction.annotation.Transactional; 21 | 22 | /** 23 | * Created By Zhu Lin on 3/14/2018. 24 | */ 25 | @Service 26 | public class OrderServiceImpl implements OrderService { 27 | @Autowired 28 | OrderRepository orderRepository; 29 | @Autowired 30 | UserRepository userRepository; 31 | @Autowired 32 | ProductInfoRepository productInfoRepository; 33 | @Autowired 34 | ProductService productService; 35 | @Autowired 36 | ProductInOrderRepository productInOrderRepository; 37 | 38 | @Override 39 | public Page findAll(Pageable pageable) { 40 | return orderRepository.findAllByOrderByOrderStatusAscCreateTimeDesc(pageable); 41 | } 42 | 43 | @Override 44 | public Page findByStatus(Integer status, Pageable pageable) { 45 | return orderRepository.findAllByOrderStatusOrderByCreateTimeDesc(status, pageable); 46 | } 47 | 48 | @Override 49 | public Page findByBuyerEmail(String email, Pageable pageable) { 50 | return orderRepository.findAllByBuyerEmailOrderByOrderStatusAscCreateTimeDesc(email, pageable); 51 | } 52 | 53 | @Override 54 | public Page findByBuyerPhone(String phone, Pageable pageable) { 55 | return orderRepository.findAllByBuyerPhoneOrderByOrderStatusAscCreateTimeDesc(phone, pageable); 56 | } 57 | 58 | @Override 59 | public OrderMain findOne(Long orderId) { 60 | OrderMain orderMain = orderRepository.findByOrderId(orderId); 61 | if(orderMain == null) { 62 | throw new MyException(ResultEnum.ORDER_NOT_FOUND); 63 | } 64 | return orderMain; 65 | } 66 | 67 | @Override 68 | @Transactional 69 | public OrderMain finish(Long orderId) { 70 | OrderMain orderMain = findOne(orderId); 71 | if(!orderMain.getOrderStatus().equals(OrderStatusEnum.NEW.getCode())) { 72 | throw new MyException(ResultEnum.ORDER_STATUS_ERROR); 73 | } 74 | 75 | orderMain.setOrderStatus(OrderStatusEnum.FINISHED.getCode()); 76 | orderRepository.save(orderMain); 77 | return orderRepository.findByOrderId(orderId); 78 | } 79 | 80 | @Override 81 | @Transactional 82 | public OrderMain cancel(Long orderId) { 83 | OrderMain orderMain = findOne(orderId); 84 | if(!orderMain.getOrderStatus().equals(OrderStatusEnum.NEW.getCode())) { 85 | throw new MyException(ResultEnum.ORDER_STATUS_ERROR); 86 | } 87 | 88 | orderMain.setOrderStatus(OrderStatusEnum.CANCELED.getCode()); 89 | orderRepository.save(orderMain); 90 | 91 | // Restore Stock 92 | Iterable products = orderMain.getProducts(); 93 | for(ProductInOrder productInOrder : products) { 94 | ProductInfo productInfo = productInfoRepository.findByProductId(productInOrder.getProductId()); 95 | if(productInfo != null) { 96 | productService.increaseStock(productInOrder.getProductId(), productInOrder.getCount()); 97 | } 98 | } 99 | return orderRepository.findByOrderId(orderId); 100 | 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /backend/src/test/java/me/zhulin/shopapi/service/impl/CartServiceImplTest.java: -------------------------------------------------------------------------------- 1 | package me.zhulin.shopapi.service.impl; 2 | 3 | import me.zhulin.shopapi.entity.Cart; 4 | import me.zhulin.shopapi.entity.ProductInOrder; 5 | import me.zhulin.shopapi.entity.User; 6 | import me.zhulin.shopapi.exception.MyException; 7 | import me.zhulin.shopapi.repository.CartRepository; 8 | import me.zhulin.shopapi.repository.OrderRepository; 9 | import me.zhulin.shopapi.repository.ProductInOrderRepository; 10 | import me.zhulin.shopapi.service.ProductService; 11 | import org.junit.Before; 12 | import org.junit.Test; 13 | import org.junit.runner.RunWith; 14 | import org.mockito.InjectMocks; 15 | import org.mockito.Mock; 16 | import org.mockito.Mockito; 17 | import org.springframework.test.context.junit4.SpringRunner; 18 | 19 | import java.math.BigDecimal; 20 | import java.util.HashSet; 21 | import java.util.Set; 22 | 23 | @RunWith(SpringRunner.class) 24 | public class CartServiceImplTest { 25 | 26 | @InjectMocks 27 | private CartServiceImpl cartService; 28 | 29 | @Mock 30 | private ProductService productService; 31 | 32 | @Mock 33 | private ProductInOrderRepository productInOrderRepository; 34 | 35 | @Mock 36 | private CartRepository cartRepository; 37 | 38 | @Mock 39 | private OrderRepository orderRepository; 40 | 41 | private User user; 42 | 43 | private ProductInOrder productInOrder; 44 | 45 | private Set set; 46 | 47 | private Cart cart; 48 | 49 | @Before 50 | public void setUp() { 51 | user = new User(); 52 | cart = new Cart(); 53 | 54 | user.setEmail("email@email.com"); 55 | user.setName("Name"); 56 | user.setPhone("Phone Test"); 57 | user.setAddress("Address Test"); 58 | 59 | productInOrder = new ProductInOrder(); 60 | productInOrder.setProductId("1"); 61 | productInOrder.setCount(10); 62 | productInOrder.setProductPrice(BigDecimal.valueOf(1)); 63 | 64 | set = new HashSet<>(); 65 | set.add(productInOrder); 66 | 67 | cart.setProducts(set); 68 | 69 | user.setCart(cart); 70 | } 71 | 72 | @Test 73 | public void mergeLocalCartTest() { 74 | cartService.mergeLocalCart(set, user); 75 | 76 | Mockito.verify(cartRepository, Mockito.times(1)).save(cart); 77 | Mockito.verify(productInOrderRepository, Mockito.times(1)).save(productInOrder); 78 | } 79 | 80 | @Test 81 | public void mergeLocalCartTwoProductTest() { 82 | ProductInOrder productInOrder2 = new ProductInOrder(); 83 | productInOrder2.setProductId("2"); 84 | productInOrder2.setCount(10); 85 | 86 | user.getCart().getProducts().add(productInOrder2); 87 | 88 | cartService.mergeLocalCart(set, user); 89 | 90 | Mockito.verify(cartRepository, Mockito.times(1)).save(cart); 91 | Mockito.verify(productInOrderRepository, Mockito.times(1)).save(productInOrder); 92 | Mockito.verify(productInOrderRepository, Mockito.times(1)).save(productInOrder2); 93 | } 94 | 95 | @Test 96 | public void mergeLocalCartNoProductTest() { 97 | user.getCart().setProducts(new HashSet<>()); 98 | 99 | cartService.mergeLocalCart(set, user); 100 | 101 | Mockito.verify(cartRepository, Mockito.times(1)).save(cart); 102 | Mockito.verify(productInOrderRepository, Mockito.times(1)).save(productInOrder); 103 | } 104 | 105 | @Test 106 | public void deleteTest() { 107 | cartService.delete("1", user); 108 | 109 | Mockito.verify(productInOrderRepository, Mockito.times(1)).deleteById(productInOrder.getId()); 110 | } 111 | 112 | @Test(expected = MyException.class) 113 | public void deleteNoProductTest() { 114 | cartService.delete("", user); 115 | } 116 | 117 | @Test(expected = MyException.class) 118 | public void deleteNoUserTest() { 119 | cartService.delete("1", null); 120 | } 121 | 122 | @Test 123 | public void checkoutTest() { 124 | cartService.checkout(user); 125 | 126 | Mockito.verify(productInOrderRepository, Mockito.times(1)).save(productInOrder); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /frontend/angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "shop": { 7 | "root": "", 8 | "sourceRoot": "src", 9 | "projectType": "application", 10 | "prefix": "app", 11 | "schematics": {}, 12 | "architect": { 13 | "build": { 14 | "builder": "@angular-devkit/build-angular:browser", 15 | "options": { 16 | "outputPath": "dist/shop", 17 | "index": "src/index.html", 18 | "main": "src/main.ts", 19 | "polyfills": "src/polyfills.ts", 20 | "tsConfig": "src/tsconfig.app.json", 21 | "assets": [ 22 | "src/favicon.ico", 23 | "src/assets" 24 | ], 25 | "styles": [ 26 | "src/styles.css" 27 | ], 28 | "scripts": [ 29 | "node_modules/jquery/dist/jquery.min.js", 30 | "node_modules/bootstrap/dist/js/bootstrap.min.js" 31 | ] 32 | }, 33 | "configurations": { 34 | "production": { 35 | "fileReplacements": [ 36 | { 37 | "replace": "src/environments/environment.ts", 38 | "with": "src/environments/environment.prod.ts" 39 | } 40 | ], 41 | "optimization": true, 42 | "outputHashing": "all", 43 | "sourceMap": false, 44 | "extractCss": true, 45 | "namedChunks": false, 46 | "aot": true, 47 | "extractLicenses": true, 48 | "vendorChunk": false, 49 | "buildOptimizer": true, 50 | "budgets": [ 51 | { 52 | "type": "initial", 53 | "maximumWarning": "2mb", 54 | "maximumError": "5mb" 55 | } 56 | ] 57 | } 58 | } 59 | }, 60 | "serve": { 61 | "builder": "@angular-devkit/build-angular:dev-server", 62 | "options": { 63 | "browserTarget": "shop:build" 64 | }, 65 | "configurations": { 66 | "production": { 67 | "browserTarget": "shop:build:production" 68 | } 69 | } 70 | }, 71 | "extract-i18n": { 72 | "builder": "@angular-devkit/build-angular:extract-i18n", 73 | "options": { 74 | "browserTarget": "shop:build" 75 | } 76 | }, 77 | "test": { 78 | "builder": "@angular-devkit/build-angular:karma", 79 | "options": { 80 | "main": "src/test.ts", 81 | "polyfills": "src/polyfills.ts", 82 | "tsConfig": "src/tsconfig.spec.json", 83 | "karmaConfig": "src/karma.conf.js", 84 | "styles": [ 85 | "src/styles.css" 86 | ], 87 | "scripts": [], 88 | "assets": [ 89 | "src/favicon.ico", 90 | "src/assets" 91 | ] 92 | } 93 | }, 94 | "lint": { 95 | "builder": "@angular-devkit/build-angular:tslint", 96 | "options": { 97 | "tsConfig": [ 98 | "src/tsconfig.app.json", 99 | "src/tsconfig.spec.json" 100 | ], 101 | "exclude": [ 102 | "**/node_modules/**" 103 | ] 104 | } 105 | } 106 | } 107 | }, 108 | "shop-e2e": { 109 | "root": "e2e/", 110 | "projectType": "application", 111 | "prefix": "", 112 | "architect": { 113 | "e2e": { 114 | "builder": "@angular-devkit/build-angular:protractor", 115 | "options": { 116 | "protractorConfig": "e2e/protractor.conf.js", 117 | "devServerTarget": "shop:serve" 118 | }, 119 | "configurations": { 120 | "production": { 121 | "devServerTarget": "shop:serve:production" 122 | } 123 | } 124 | }, 125 | "lint": { 126 | "builder": "@angular-devkit/build-angular:tslint", 127 | "options": { 128 | "tsConfig": "e2e/tsconfig.e2e.json", 129 | "exclude": [ 130 | "**/node_modules/**" 131 | ] 132 | } 133 | } 134 | } 135 | } 136 | }, 137 | "defaultProject": "shop", 138 | 139 | } 140 | -------------------------------------------------------------------------------- /frontend/src/app/pages/cart/cart.component.ts: -------------------------------------------------------------------------------- 1 | import {AfterContentChecked, Component, OnDestroy, OnInit} from '@angular/core'; 2 | import {CartService} from '../../services/cart.service'; 3 | import {Subject, Subscription} from 'rxjs'; 4 | import {UserService} from '../../services/user.service'; 5 | import {JwtResponse} from '../../response/JwtResponse'; 6 | import {ProductInOrder} from '../../models/ProductInOrder'; 7 | import {debounceTime, switchMap} from 'rxjs/operators'; 8 | import {ActivatedRoute, Router} from '@angular/router'; 9 | import {Role} from '../../enum/Role'; 10 | 11 | @Component({ 12 | selector: 'app-cart', 13 | templateUrl: './cart.component.html', 14 | styleUrls: ['./cart.component.css'] 15 | }) 16 | export class CartComponent implements OnInit, OnDestroy, AfterContentChecked { 17 | 18 | constructor(private cartService: CartService, 19 | private userService: UserService, 20 | private router: Router) { 21 | this.userSubscription = this.userService.currentUser.subscribe(user => this.currentUser = user); 22 | } 23 | 24 | productInOrders = []; 25 | total = 0; 26 | currentUser: JwtResponse; 27 | userSubscription: Subscription; 28 | 29 | private updateTerms = new Subject(); 30 | sub: Subscription; 31 | 32 | static validateCount(productInOrder) { 33 | const max = productInOrder.productStock; 34 | if (productInOrder.count > max) { 35 | productInOrder.count = max; 36 | } else if (productInOrder.count < 1) { 37 | productInOrder.count = 1; 38 | } 39 | console.log(productInOrder.count); 40 | } 41 | 42 | ngOnInit() { 43 | this.cartService.getCart().subscribe(prods => { 44 | this.productInOrders = prods; 45 | }); 46 | 47 | this.sub = this.updateTerms.pipe( 48 | // wait 300ms after each keystroke before considering the term 49 | debounceTime(300), 50 | // 51 | // ignore new term if same as previous term 52 | // Same Object Reference, not working here 53 | // distinctUntilChanged((p: ProductInOrder, q: ProductInOrder) => p.count === q.count), 54 | // 55 | // switch to new search observable each time the term changes 56 | switchMap((productInOrder: ProductInOrder) => this.cartService.update(productInOrder)) 57 | ).subscribe(prod => { 58 | if (prod) { throw new Error(); } 59 | }, 60 | _ => console.log('Update Item Failed')); 61 | } 62 | 63 | ngOnDestroy() { 64 | if (!this.currentUser) { 65 | this.cartService.storeLocalCart(); 66 | } 67 | this.userSubscription.unsubscribe(); 68 | } 69 | 70 | ngAfterContentChecked() { 71 | this.total = this.productInOrders.reduce( 72 | (prev, cur) => prev + cur.count * cur.productPrice, 0); 73 | } 74 | 75 | addOne(productInOrder) { 76 | productInOrder.count++; 77 | CartComponent.validateCount(productInOrder); 78 | if (this.currentUser) { this.updateTerms.next(productInOrder); } 79 | } 80 | 81 | minusOne(productInOrder) { 82 | productInOrder.count--; 83 | CartComponent.validateCount(productInOrder); 84 | if (this.currentUser) { this.updateTerms.next(productInOrder); } 85 | } 86 | 87 | onChange(productInOrder) { 88 | CartComponent.validateCount(productInOrder); 89 | if (this.currentUser) { this.updateTerms.next(productInOrder); } 90 | } 91 | 92 | 93 | remove(productInOrder: ProductInOrder) { 94 | this.cartService.remove(productInOrder).subscribe( 95 | success => { 96 | this.productInOrders = this.productInOrders.filter(e => e.productId !== productInOrder.productId); 97 | console.log('Cart: ' + this.productInOrders); 98 | }, 99 | _ => console.log('Remove Cart Failed')); 100 | } 101 | 102 | checkout() { 103 | if (!this.currentUser) { 104 | this.router.navigate(['/login'], {queryParams: {returnUrl: this.router.url}}); 105 | } else if (this.currentUser.role !== Role.Customer) { 106 | this.router.navigate(['/seller']); 107 | } else { 108 | this.cartService.checkout().subscribe( 109 | _ => { 110 | this.productInOrders = []; 111 | }, 112 | error1 => { 113 | console.log('Checkout Cart Failed'); 114 | }); 115 | this.router.navigate(['/']); 116 | } 117 | 118 | } 119 | } 120 | 121 | -------------------------------------------------------------------------------- /frontend/src/app/services/cart.service.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from '@angular/core'; 2 | import {HttpClient} from '@angular/common/http'; 3 | 4 | import {apiUrl} from '../../environments/environment'; 5 | import {CookieService} from 'ngx-cookie-service'; 6 | import {BehaviorSubject, Observable, of} from 'rxjs'; 7 | import {catchError, map, tap} from 'rxjs/operators'; 8 | import {UserService} from './user.service'; 9 | import {Cart} from '../models/Cart'; 10 | import {Item} from '../models/Item'; 11 | import {JwtResponse} from '../response/JwtResponse'; 12 | import {ProductInOrder} from '../models/ProductInOrder'; 13 | 14 | @Injectable({ 15 | providedIn: 'root' 16 | }) 17 | export class CartService { 18 | 19 | 20 | private cartUrl = `${apiUrl}/cart`; 21 | 22 | localMap = {}; 23 | 24 | 25 | private itemsSubject: BehaviorSubject; 26 | private totalSubject: BehaviorSubject; 27 | public items: Observable; 28 | public total: Observable; 29 | 30 | 31 | private currentUser: JwtResponse; 32 | 33 | constructor(private http: HttpClient, 34 | private cookieService: CookieService, 35 | private userService: UserService) { 36 | this.itemsSubject = new BehaviorSubject(null); 37 | this.items = this.itemsSubject.asObservable(); 38 | this.totalSubject = new BehaviorSubject(null); 39 | this.total = this.totalSubject.asObservable(); 40 | this.userService.currentUser.subscribe(user => this.currentUser = user); 41 | 42 | 43 | } 44 | 45 | private getLocalCart(): ProductInOrder[] { 46 | if (this.cookieService.check('cart')) { 47 | this.localMap = JSON.parse(this.cookieService.get('cart')); 48 | return Object.values(this.localMap); 49 | } else { 50 | this.localMap = {}; 51 | return []; 52 | } 53 | } 54 | 55 | getCart(): Observable { 56 | const localCart = this.getLocalCart(); 57 | if (this.currentUser) { 58 | if (localCart.length > 0) { 59 | return this.http.post(this.cartUrl, localCart).pipe( 60 | tap(_ => { 61 | this.clearLocalCart(); 62 | }), 63 | map(cart => cart.products), 64 | catchError(_ => of([])) 65 | ); 66 | } else { 67 | return this.http.get(this.cartUrl).pipe( 68 | map(cart => cart.products), 69 | catchError(_ => of([])) 70 | ); 71 | } 72 | } else { 73 | return of(localCart); 74 | } 75 | } 76 | 77 | addItem(productInOrder): Observable { 78 | if (!this.currentUser) { 79 | if (this.cookieService.check('cart')) { 80 | this.localMap = JSON.parse(this.cookieService.get('cart')); 81 | } 82 | if (!this.localMap[productInOrder.productId]) { 83 | this.localMap[productInOrder.productId] = productInOrder; 84 | } else { 85 | this.localMap[productInOrder.productId].count += productInOrder.count; 86 | } 87 | this.cookieService.set('cart', JSON.stringify(this.localMap)); 88 | return of(true); 89 | } else { 90 | const url = `${this.cartUrl}/add`; 91 | return this.http.post(url, { 92 | 'quantity': productInOrder.count, 93 | 'productId': productInOrder.productId 94 | }); 95 | } 96 | } 97 | 98 | update(productInOrder): Observable { 99 | 100 | if (this.currentUser) { 101 | const url = `${this.cartUrl}/${productInOrder.productId}`; 102 | return this.http.put(url, productInOrder.count); 103 | } 104 | } 105 | 106 | 107 | remove(productInOrder) { 108 | if (!this.currentUser) { 109 | delete this.localMap[productInOrder.productId]; 110 | return of(null); 111 | } else { 112 | const url = `${this.cartUrl}/${productInOrder.productId}`; 113 | return this.http.delete(url).pipe( ); 114 | } 115 | } 116 | 117 | 118 | checkout(): Observable { 119 | const url = `${this.cartUrl}/checkout`; 120 | return this.http.post(url, null).pipe(); 121 | } 122 | 123 | storeLocalCart() { 124 | this.cookieService.set('cart', JSON.stringify(this.localMap)); 125 | } 126 | 127 | clearLocalCart() { 128 | console.log('clear local cart'); 129 | this.cookieService.delete('cart'); 130 | this.localMap = {}; 131 | } 132 | 133 | } 134 | -------------------------------------------------------------------------------- /frontend/src/app/pages/product-edit/product-edit.component.html: -------------------------------------------------------------------------------- 1 |

Edit Product

2 |
3 |
4 | 5 |
6 | 7 | 10 |
11 |
12 | Name is required. 13 |
14 |
15 |
16 | 17 |
18 | 19 | 21 |
22 | 23 | 24 |
25 | 26 | 29 |
30 |
31 |
32 | Name is required. 33 |
34 |
35 | 36 | 37 |
38 | 39 | 47 |
48 | 49 | 50 |
51 | 52 | 55 |
56 | 57 |
58 | 59 | 66 |
67 |
68 | Price is required. 69 |
70 |
71 |
72 | 73 |
74 | 75 | 82 |
83 |
84 | Stock is required. 85 |
86 |
87 |
88 | 89 |
90 | 91 | 96 |
97 |
98 | 99 |
100 |
101 | 102 | 103 |
104 | -------------------------------------------------------------------------------- /backend/src/main/java/me/zhulin/shopapi/service/impl/ProductServiceImpl.java: -------------------------------------------------------------------------------- 1 | package me.zhulin.shopapi.service.impl; 2 | 3 | 4 | import me.zhulin.shopapi.entity.ProductInfo; 5 | import me.zhulin.shopapi.enums.ProductStatusEnum; 6 | import me.zhulin.shopapi.enums.ResultEnum; 7 | import me.zhulin.shopapi.exception.MyException; 8 | import me.zhulin.shopapi.repository.ProductInfoRepository; 9 | import me.zhulin.shopapi.service.CategoryService; 10 | import me.zhulin.shopapi.service.ProductService; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.data.domain.Page; 13 | import org.springframework.data.domain.Pageable; 14 | import org.springframework.stereotype.Service; 15 | import org.springframework.transaction.annotation.Transactional; 16 | 17 | /** 18 | * Created By Zhu Lin on 3/10/2018. 19 | */ 20 | @Service 21 | public class ProductServiceImpl implements ProductService { 22 | 23 | @Autowired 24 | ProductInfoRepository productInfoRepository; 25 | 26 | @Autowired 27 | CategoryService categoryService; 28 | 29 | @Override 30 | public ProductInfo findOne(String productId) { 31 | 32 | ProductInfo productInfo = productInfoRepository.findByProductId(productId); 33 | return productInfo; 34 | } 35 | 36 | @Override 37 | public Page findUpAll(Pageable pageable) { 38 | return productInfoRepository.findAllByProductStatusOrderByProductIdAsc(ProductStatusEnum.UP.getCode(),pageable); 39 | } 40 | 41 | @Override 42 | public Page findAll(Pageable pageable) { 43 | return productInfoRepository.findAllByOrderByProductId(pageable); 44 | } 45 | 46 | @Override 47 | public Page findAllInCategory(Integer categoryType, Pageable pageable) { 48 | return productInfoRepository.findAllByCategoryTypeOrderByProductIdAsc(categoryType, pageable); 49 | } 50 | 51 | @Override 52 | @Transactional 53 | public void increaseStock(String productId, int amount) { 54 | ProductInfo productInfo = findOne(productId); 55 | if (productInfo == null) throw new MyException(ResultEnum.PRODUCT_NOT_EXIST); 56 | 57 | int update = productInfo.getProductStock() + amount; 58 | productInfo.setProductStock(update); 59 | productInfoRepository.save(productInfo); 60 | } 61 | 62 | @Override 63 | @Transactional 64 | public void decreaseStock(String productId, int amount) { 65 | ProductInfo productInfo = findOne(productId); 66 | if (productInfo == null) throw new MyException(ResultEnum.PRODUCT_NOT_EXIST); 67 | 68 | int update = productInfo.getProductStock() - amount; 69 | if(update <= 0) throw new MyException(ResultEnum.PRODUCT_NOT_ENOUGH ); 70 | 71 | productInfo.setProductStock(update); 72 | productInfoRepository.save(productInfo); 73 | } 74 | 75 | @Override 76 | @Transactional 77 | public ProductInfo offSale(String productId) { 78 | ProductInfo productInfo = findOne(productId); 79 | if (productInfo == null) throw new MyException(ResultEnum.PRODUCT_NOT_EXIST); 80 | 81 | if (productInfo.getProductStatus() == ProductStatusEnum.DOWN.getCode()) { 82 | throw new MyException(ResultEnum.PRODUCT_STATUS_ERROR); 83 | } 84 | 85 | //更新 86 | productInfo.setProductStatus(ProductStatusEnum.DOWN.getCode()); 87 | return productInfoRepository.save(productInfo); 88 | } 89 | 90 | @Override 91 | @Transactional 92 | public ProductInfo onSale(String productId) { 93 | ProductInfo productInfo = findOne(productId); 94 | if (productInfo == null) throw new MyException(ResultEnum.PRODUCT_NOT_EXIST); 95 | 96 | if (productInfo.getProductStatus() == ProductStatusEnum.UP.getCode()) { 97 | throw new MyException(ResultEnum.PRODUCT_STATUS_ERROR); 98 | } 99 | 100 | //更新 101 | productInfo.setProductStatus(ProductStatusEnum.UP.getCode()); 102 | return productInfoRepository.save(productInfo); 103 | } 104 | 105 | @Override 106 | public ProductInfo update(ProductInfo productInfo) { 107 | 108 | // if null throw exception 109 | categoryService.findByCategoryType(productInfo.getCategoryType()); 110 | if(productInfo.getProductStatus() > 1) { 111 | throw new MyException(ResultEnum.PRODUCT_STATUS_ERROR); 112 | } 113 | 114 | 115 | return productInfoRepository.save(productInfo); 116 | } 117 | 118 | @Override 119 | public ProductInfo save(ProductInfo productInfo) { 120 | return update(productInfo); 121 | } 122 | 123 | @Override 124 | public void delete(String productId) { 125 | ProductInfo productInfo = findOne(productId); 126 | if (productInfo == null) throw new MyException(ResultEnum.PRODUCT_NOT_EXIST); 127 | productInfoRepository.delete(productInfo); 128 | 129 | } 130 | 131 | 132 | } 133 | -------------------------------------------------------------------------------- /backend/src/test/java/me/zhulin/shopapi/service/impl/ProductServiceImplTest.java: -------------------------------------------------------------------------------- 1 | package me.zhulin.shopapi.service.impl; 2 | 3 | import me.zhulin.shopapi.entity.ProductInfo; 4 | import me.zhulin.shopapi.enums.ProductStatusEnum; 5 | import me.zhulin.shopapi.exception.MyException; 6 | import me.zhulin.shopapi.repository.ProductInfoRepository; 7 | import me.zhulin.shopapi.service.CategoryService; 8 | import org.junit.Before; 9 | import org.junit.Test; 10 | import org.junit.runner.RunWith; 11 | import org.mockito.InjectMocks; 12 | import org.mockito.Mock; 13 | import org.mockito.Mockito; 14 | import org.springframework.test.context.junit4.SpringRunner; 15 | 16 | import static org.mockito.Mockito.when; 17 | 18 | @RunWith(SpringRunner.class) 19 | public class ProductServiceImplTest { 20 | 21 | @InjectMocks 22 | private ProductServiceImpl productService; 23 | 24 | @Mock 25 | private ProductInfoRepository productInfoRepository; 26 | 27 | @Mock 28 | private CategoryService categoryService; 29 | 30 | private ProductInfo productInfo; 31 | 32 | @Before 33 | public void setUp() { 34 | productInfo = new ProductInfo(); 35 | productInfo.setProductId("1"); 36 | productInfo.setProductStock(10); 37 | productInfo.setProductStatus(1); 38 | } 39 | 40 | @Test 41 | public void increaseStockTest() { 42 | when(productInfoRepository.findByProductId(productInfo.getProductId())).thenReturn(productInfo); 43 | 44 | productService.increaseStock("1", 10); 45 | 46 | Mockito.verify(productInfoRepository, Mockito.times(1)).save(productInfo); 47 | } 48 | 49 | @Test(expected = MyException.class) 50 | public void increaseStockExceptionTest() { 51 | productService.increaseStock("1", 10); 52 | } 53 | 54 | @Test 55 | public void decreaseStockTest() { 56 | when(productInfoRepository.findByProductId(productInfo.getProductId())).thenReturn(productInfo); 57 | 58 | productService.decreaseStock("1", 9); 59 | 60 | Mockito.verify(productInfoRepository, Mockito.times(1)).save(productInfo); 61 | } 62 | 63 | @Test(expected = MyException.class) 64 | public void decreaseStockValueLesserEqualTest() { 65 | when(productInfoRepository.findByProductId(productInfo.getProductId())).thenReturn(productInfo); 66 | 67 | productService.decreaseStock("1", 10); 68 | } 69 | 70 | @Test(expected = MyException.class) 71 | public void decreaseStockExceptionTest() { 72 | productService.decreaseStock("1", 10); 73 | } 74 | 75 | @Test 76 | public void offSaleTest() { 77 | productInfo.setProductStatus(ProductStatusEnum.UP.getCode()); 78 | 79 | when(productInfoRepository.findByProductId(productInfo.getProductId())).thenReturn(productInfo); 80 | 81 | productService.offSale("1"); 82 | 83 | Mockito.verify(productInfoRepository, Mockito.times(1)).save(productInfo); 84 | } 85 | 86 | @Test(expected = MyException.class) 87 | public void offSaleStatusDownTest() { 88 | productInfo.setProductStatus(ProductStatusEnum.DOWN.getCode()); 89 | 90 | when(productInfoRepository.findByProductId(productInfo.getProductId())).thenReturn(productInfo); 91 | 92 | productService.offSale("1"); 93 | } 94 | 95 | @Test(expected = MyException.class) 96 | public void offSaleProductNullTest() { 97 | when(productInfoRepository.findByProductId(productInfo.getProductId())).thenReturn(null); 98 | 99 | productService.offSale("1"); 100 | } 101 | 102 | @Test 103 | public void onSaleTest() { 104 | productInfo.setProductStatus(ProductStatusEnum.DOWN.getCode()); 105 | 106 | when(productInfoRepository.findByProductId(productInfo.getProductId())).thenReturn(productInfo); 107 | 108 | productService.onSale("1"); 109 | 110 | Mockito.verify(productInfoRepository, Mockito.times(1)).save(productInfo); 111 | } 112 | 113 | @Test(expected = MyException.class) 114 | public void onSaleStatusUpTest() { 115 | productInfo.setProductStatus(ProductStatusEnum.UP.getCode()); 116 | 117 | when(productInfoRepository.findByProductId(productInfo.getProductId())).thenReturn(productInfo); 118 | 119 | productService.onSale("1"); 120 | } 121 | 122 | @Test(expected = MyException.class) 123 | public void onSaleProductNullTest() { 124 | when(productInfoRepository.findByProductId(productInfo.getProductId())).thenReturn(null); 125 | 126 | productService.offSale("1"); 127 | } 128 | 129 | @Test 130 | public void updateTest() { 131 | productService.update(productInfo); 132 | 133 | Mockito.verify(productInfoRepository, Mockito.times(1)).save(productInfo); 134 | } 135 | 136 | @Test(expected = MyException.class) 137 | public void updateProductStatusBiggerThenOneTest() { 138 | productInfo.setProductStatus(2); 139 | 140 | productService.update(productInfo); 141 | } 142 | 143 | @Test 144 | public void deleteTest() { 145 | when(productInfoRepository.findByProductId(productInfo.getProductId())).thenReturn(productInfo); 146 | 147 | productService.delete("1"); 148 | 149 | Mockito.verify(productInfoRepository, Mockito.times(1)).delete(productInfo); 150 | } 151 | 152 | @Test(expected = MyException.class) 153 | public void deleteProductNullTest() { 154 | productService.delete("1"); 155 | } 156 | } 157 | --------------------------------------------------------------------------------