使用springboot+angular开发一个todolist

后端使用springboot + spring data jpa

  1. Bootstrap your application with Spring Initializr: https://start.spring.io/
  2. select Dependencies then grnreate.

    注册登录模块

    • 实体类
    @Entity
    @Data
    @EntityListeners(AuditingEntityListener.class)
    public class UserRecord {
    
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Integer id;
    
        private String name;
    
        private String email;
    
        private String password;
    
        @CreatedDate
        @Temporal(TemporalType.TIMESTAMP)
        @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone="GMT+8")
        private Date createTime;
    
        @LastModifiedDate
        @Temporal(TemporalType.TIMESTAMP)
        @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone="GMT+8")
        private Date updateTime;
    
    }
    • Repository
    public interface UserRepository extends CrudRepository{
    UserRecord findByName(String name);
    
    UserRecord findByNameAndPassword(String name,String password);
    }
    • Service
    @Service
    public class UserService {
    
    private final UserRepository userRepository;
    
    public UserService(UserRepository userRepository){
        this.userRepository = userRepository;
    }
    
    public List getAllUsers(){
        List userRecords = new ArrayList<>();
        userRepository.findAll().forEach(userRecords::add);
        return userRecords;
    }
    
    public void addUser(UserRecord userRecord){
        userRepository.save(userRecord);
    }
    
    public void updateUser(UserRecord userRecord){
        Optional userRecordOptional = userRepository.findById(userRecord.getId());
        if ( userRecordOptional.isPresent() )
        {
            UserRecord userRecordFind = userRecordOptional.get();
            userRecordFind.setName(userRecord.getName());
            userRecordFind.setEmail(userRecord.getEmail());
            userRepository.save(userRecordFind);
        }
    }
    
    public UserRecord findByName(String name){
       return userRepository.findByName(name);
    }
    
    public UserRecord findByNameAndPassword(String name,String password){
        return userRepository.findByNameAndPassword(name,password);
    }
    }
    • controller
    @Log4j2
    @RestController
    @Tag(name = "LoginController", description = "登录接口")
    public class LoginController{
    
    private final UserService userService;
    
    public LoginController(UserService userService){
        this.userService = userService;
    }
    
    @PostMapping(value="/sign-in")
    @Operation(summary = "登录", description = "登录")
    public Result signIn(@RequestBody UserRecord userRecord){
        String password = DigestUtils.md5DigestAsHex(userRecord.getPassword()
                                                             .getBytes(StandardCharsets.UTF_8));
        UserRecord userRecordFind = userService.findByNameAndPassword(userRecord.getName(),password);
        if ( userRecordFind != null){
            return ResultUtil.getOK(userRecordFind);
        }
        return null;
    }
    
    @PostMapping(value="/sign-up")
    @Operation(summary = "注册", description = "注册")
    public Result signUp(@RequestBody UserRecord userRecord) throws Exception{
        UserRecord userRecordFind = userService.findByName(userRecord.getName());
        if ( userRecordFind != null){
            throw new Exception("user name already exist!");
        }
        String password = DigestUtils.md5DigestAsHex(userRecord.getPassword()
                                                      .getBytes(StandardCharsets.UTF_8));
        userRecord.setPassword(password);
        userService.addUser(userRecord);
        return ResultUtil.getOK();
    }
    }

TODO CRUD模块

  • 实体类
@Entity
@Data
@EntityListeners(AuditingEntityListener.class)
@ToString
public class TodoRecord extends AbstractBaseEntity{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;

@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true, mappedBy = "todoRecord")
private List<TodoRecordItem> todoRecordItems = new ArrayList<>();

private LocalDate todoDay;

@CreatedDate
@Temporal(TemporalType.TIMESTAMP)
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone="GMT+8")
private Date createTime;

@LastModifiedDate
@Temporal(TemporalType.TIMESTAMP)
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone="GMT+8")
private Date updateTime;
}

@Entity
@Data
@EntityListeners(AuditingEntityListener.class)
@ToString(exclude = "todoRecord")
public class TodoRecordItem
{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;

@ManyToOne
@JoinColumn(name = "todo_record_id")
private TodoRecord todoRecord;

private String description;

private Boolean completed = false;

@CreatedDate
@Temporal(TemporalType.TIMESTAMP)
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone="GMT+8")
private Date createTime;

@LastModifiedDate
@Temporal(TemporalType.TIMESTAMP)
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone="GMT+8")
private Date updateTime;
}
  • Repository
public interface TodoRecordRepository extends CrudRepository<TodoRecord,Integer>{}
public interface TodoRecordItemRepository extends CrudRepository<TodoRecordItem,Integer>{}
  • Service
@Service
@Transactional public class TodoRecordService{

    private final TodoRecordRepository todoRecordRepository;

    public TodoRecordService(TodoRecordRepository todoRecordRepository){
        this.todoRecordRepository = todoRecordRepository;
    }

    public List<TodoRecordDto> getAllTodoRecords() throws Exception{
        List<TodoRecord> todoRecords = new ArrayList<>();
        todoRecordRepository.findAll().forEach(todoRecords::add);
        List<TodoRecordDto> todoRecordDtos = todoRecords.stream()
                .map(r -> {
                    TodoRecordDto todoRecordDto = new TodoRecordDto();
                    todoRecordDto.setTodoRecordItems(r.getTodoRecordItems()
                                                             .stream()
                                                             .map(i -> {
                                                                 TodoRecordItemDto todoRecordItemDto = new TodoRecordItemDto();
                                                                 todoRecordItemDto.setDescription(i.getDescription());
                                                                 todoRecordItemDto.setCompleted(i.getCompleted());
                                                                 todoRecordItemDto.setId(i.getId());
                                                                 return todoRecordItemDto;
                                                             })
                                                             .collect(Collectors.toList()));
                    if ( r.getTodoDay() != null )
                    {
                        todoRecordDto.setTodoDay(r.getTodoDay());
                    }

                    todoRecordDto.setId(r.getId());
                    return todoRecordDto;
                })
                .collect(Collectors.toList());

        return todoRecordDtos;
    }

    public void addTodoRecord(TodoRecordDto todoRecordDto) throws Exception{
        TodoRecord todoRecord = new TodoRecord();
        todoRecord.setTodoRecordItems(todoRecordDto.getTodoRecordItems()
                                              .stream()
                                              .map(i -> {
                                                  TodoRecordItem todoRecordItem = new TodoRecordItem();
                                                  todoRecordItem.setDescription(i.getDescription());
                                                  todoRecordItem.setTodoRecord(todoRecord);
                                                  return todoRecordItem;
                                              })
                                              .collect(Collectors.toList()));
        todoRecord.setTodoDay(todoRecordDto.getTodoDay());
        todoRecordRepository.save(todoRecord);
    }

    public void deleteTodoRecord(Integer id){
        todoRecordRepository.deleteById(id);
    }
}
  • Controller

    
    @RestController
    @Tag(name = "TodoRecordController", description = "清单接口")
    public class TodoRecordController{
    
    private final TodoRecordService todoRecordService;
    
    private final TodoRecordItemService todoRecordItemService;
    
    public TodoRecordController(TodoRecordService todoRecordService,TodoRecordItemService todoRecordItemService){
        this.todoRecordService = todoRecordService;
        this.todoRecordItemService = todoRecordItemService;
    }
    
    @EnableFilter
    @GetMapping("/getAllTodoRecord")
    @Operation(summary = "查询所有清单", description = "查询所有清单",security = { @SecurityRequirement(name = "TENANT-ID") })
    public Result getAllTodoRecords() throws Exception{
        List<TodoRecordDto> allTodoRecords = todoRecordService.getAllTodoRecords();
        return ResultUtil.getOK(allTodoRecords);
    }
    
    @PostMapping(value="/add-todoRecord")
    @Operation(summary = "创建清单", description = "创建清单",security = { @SecurityRequirement(name = "TENANT-ID") })
    public Result addTodoRecord(@RequestBody TodoRecordDto todoRecordDto) throws Exception{
        todoRecordService.addTodoRecord(todoRecordDto);
        return ResultUtil.getOK();
    }
    
    @DeleteMapping(value="/todoRecord/{id}")
    @Operation(summary = "删除清单", description = "删除清单")
    public Result deleteTodoRecord(@PathVariable("id") Integer id){
        todoRecordService.deleteTodoRecord(id);
        return ResultUtil.getOK();
    }
    
    @PostMapping(value="/update-todoRecordItem")
    @Operation(summary = "更新单个任务", description = "更新单个任务")
    public Result updateTodoRecord(@RequestBody TodoRecordItem todoRecordItem){
        todoRecordItemService.updateTodoRecordItem(todoRecordItem);
        return ResultUtil.getOK();
    }
    }
    ```

前端使用angular框架

  1. install angular cli
    npm install -g @angular/cli
    ng generate --help

    使用angular cli创建项目,组件,service,打包等

    ng new my-first-project
    cd my-first-project
    ng serve
    ng g c add-todo
    ng build clean
    ng build --configuration=production
  2. 登录注册页面
    login.component.html
< div class="main-wrapper" fxLayout="row" fxLayoutAlign="center center">
  < mat-card class="box">
    < mat-card-header>
      < mat-card-title>Login< /mat-card-title>
    < /mat-card-header>
    < form class="example-form" [formGroup]="loginForm"  (submit)="login()">
      < mat-card-content>
        < mat-form-field class="example-full-width">
          < input matInput placeholder="Username" formControlName="name" type="text">
          < mat-error>This field is mandatory.
        < /mat-form-field>

        < mat-form-field class="example-full-width">
          < input matInput placeholder="Password" formControlName="password" type="password">
          < mat-error>This field is mandatory.< /mat-error>
        < /mat-form-field>
      < /mat-card-content>
      < button mat-stroked-button color="accent" class="btn-block"  [disabled]="loginForm.invalid">Login< /button>
    < /form>
  < /mat-card>
< /div>

register.component.html

< div class="main-wrapper" fxLayout="row" fxLayoutAlign="center center">
  < mat-card class="box">
    < mat-card-header>
      < mat-card-title>Register< /mat-card-title>
    < /mat-card-header>
    < form class="example-form" [formGroup]="form"  (submit)="register()">
      < mat-card-content>
        < mat-form-field class="example-full-width">
          < input  formControlName="name" matInput placeholder="Username">
          < mat-error>This field is mandatory.< /mat-error>
        < /mat-form-field>
        < mat-form-field class="example-full-width">
          < input formControlName="email" matInput placeholder="Email">
          < mat-error>Invalid email address.< /mat-error>
        < /mat-form-field>
        < mat-form-field class="example-full-width">
          < input formControlName="password" matInput placeholder="Password" type="password">
          < mat-error>This field is mandatory.< /mat-error>
        < /mat-form-field>
      < /mat-card-content>
      < button mat-stroked-button color="accent" class="btn-block"  [disabled]="form.invalid">Register< /button>
    < /form>
  < /mat-card>
< /div>

login.component.ts

@Component({
  selector: 'app-login',
  templateUrl: './login.component.html',
  styleUrls: ['./login.component.css']
})
export class LoginComponent implements OnInit {

  public loginForm: FormGroup = new FormGroup({
    name: new FormControl('',Validators.required),
    password: new FormControl('', Validators.required),
  });

  constructor(private registerService:RegisterService, private notificationService: NotificationService,
    private router: Router,private tokenStorageService: TokenStorageService) { }

  ngOnInit(): void {
  }

  login() {
    if (this.loginForm.valid) {
      this.registerService.login(this.loginForm.value).subscribe((response:Result)=>{
        console.log(response);
        if(response.code == 0){
          this.tokenStorageService.saveUser(response.data);
          this.notificationService.success('login successfully');
          this.goTodo();
        }else{
          this.notificationService.success('login fail');
        }

      });
    }
  }

  goTodo() {
    this.router.navigate(['app']).then(()=>{
      window.location.reload();
    });
    this.router.navigate(['todo']);
  }

}

register.component.ts

@Component({
  selector: 'app-register',
  templateUrl: './register.component.html',
  styleUrls: ['./register.component.css']
})
export class RegisterComponent implements OnInit {

  constructor(private registerService:RegisterService, private notificationService: NotificationService,
    private router: Router) { }

  ngOnInit(): void {
  }

  public form: FormGroup = new FormGroup({
    id : new FormControl(null),
    name: new FormControl('',Validators.required),
    email: new FormControl('', Validators.email),
    password: new FormControl('', Validators.required),
  });

  register(){
    if (this.form.valid) {
      this.registerService.register(this.form.value).subscribe((result:Result)=>{
        console.log(result);
        if(result.code == 0){
          this.notificationService.success('register successfully, please login');
          this.goToLogin();
        }else{
          this.notificationService.warn(result.msg);
        }
      });
    }
  }

  goToLogin() {
    this.router.navigate(['login']);
  }
}
  1. TODO list页面
    todo.component.html
< div class="content">
  < div fxLayout="row wrap" fxLayoutGap="16px grid">
    < div fxFlex="25%" fxFlex.xs="100%" fxFlex.sm="33%" *ngFor="let todoRecord of todoRecords">
      < mat-card class="mat-elevation-z4 backgroundImage">
        < mat-card-header>
          < mat-card-title>{{todoRecord.todoDay}}
        < /mat-card-header>
        < mat-card-content>
          < mat-selection-list>
            < mat-list-option  *ngFor="let todoRecordItem of todoRecord.todoRecordItems" [selected]="todoRecordItem.completed" (click)="updateCompleted(todoRecordItem)">
              < p  [ngStyle]="{'text-decoration':todoRecordItem.completed ? 'line-through' : '' }">
                {{todoRecordItem.description}}
              < /p>
            < /mat-list-option>
          < /mat-selection-list>
        < /mat-card-content>
        < mat-card-actions>
          < button mat-button (click)="deleteTodo(todoRecord.id)">< mat-icon>clear< /mat-icon>< /button>
        < /mat-card-actions>
      < /mat-card>
    < /div>
  < /div>
  < /div>

todo.component.ts

@Component({
  selector: 'app-todo',
  templateUrl: './todo.component.html',
  styleUrls: ['./todo.component.css'],
  providers: [TodoService]
})
export class TodoComponent implements OnInit {

  todoRecords?: TodoRecord[];
  desc = '';
  result!: Result;
  constructor(private todoService:TodoService) { }

  ngOnInit() {
    this.getTodos();
  }

  getTodos(){
    this.todoService.getTodos().subscribe((result)=>{
      console.log(result);
      this.todoRecords=result.data;
      console.log(this.todoRecords);
    })
  }

  deleteTodo(id: any){
    this.todoService.delteTodos(id).subscribe(response=>{
      this.getTodos();
    });
  }

  updateCompleted(todo: Todo) {
    this.todoService.updateCompleted(todo).subscribe(response=>{
      this.getTodos();
    });
  }
}

请求后端todo.service.ts

const baseUrl = 'http://localhost:8080/';

@Injectable({
  providedIn: 'root'
})
export class TodoService {

  todos: Todo[] = [];

  constructor(private http: HttpClient,private tokenStorageService:TokenStorageService) { }

  addTodo(todo: any){
    const user = this.tokenStorageService.getUser();
    const headers= new HttpHeaders({'TENANT-ID': user.id.toString()});
    return this.http.post(baseUrl + 'add-todoRecord', todo,{'headers':headers}).subscribe(response=>{
      console.log(response);
    });
  }

  getTodos():Observable{
    const user = this.tokenStorageService.getUser();
    const headers= new HttpHeaders({'TENANT-ID': user.id.toString()});
    return this.http.get(baseUrl + 'getAllTodoRecord/',{'headers':headers});
  }

  delteTodos(id:number){
    return this.http.delete(baseUrl + 'todoRecord/' + id);
  }

  updateCompleted(todo: Todo) {
    return this.http.post(baseUrl + 'update-todoRecordItem', todo);
  }
}

数据库使用postgresql

POM文件配置

< dependency>
    < groupId>org.postgresql< /groupId>
    < artifactId>postgresql< /artifactId>
    < scope>runtime< /scope>
< /dependency>

application.properties文件配置

spring.datasource.hikari.connectionTimeout=20000
spring.datasource.hikari.maximumPoolSize=5

spring.datasource.url=jdbc:postgresql://localhost:5432/test
spring.datasource.username=postgres
spring.datasource.password=password

spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true

前后端合并部署打包成docker镜像

  1. 将前端打包后的文件复制到resource/static目录下
  2. 引入dockerfile-maven-plugin插件,配置自己的镜像仓库地址
< plugin>
    < groupId>com.spotify< /groupId>
    < artifactId>dockerfile-maven-plugin< /artifactId>
    < version>1.4.8< /version>
    < executions>
        < execution>
            < id>default< /id>
            < goals>
                < goal>build< /goal>
                < goal>push< /goal>
            < /goals>
        < /execution>
    < /executions>
    < configuration>
        < repository>yourRepository/todo< /repository>
        < tag>${project.version}< /tag>
    < /configuration>
< /plugin>
  1. 开启localhost/127.0.0.1端口2375的监听

使用springdoc-openapi管理文档

  1. 引入dependency
< dependency>
    < groupId>org.springdoc< /groupId>
    < artifactId>springdoc-openapi-ui< /artifactId>
    < version>1.6.11< /version>
< /dependency>
  1. 配置SpringDoc
@Configuration
public class SpringDocConfig
{
    @Bean
    public OpenAPI myOpenAPI()
    {
        return new OpenAPI().components(new Components()
                             .addSecuritySchemes("TENANT-ID", securityScheme("TENANT-ID"))
        ).info(new Info().title("SpringDoc")
                                          .description("测试 SpringDoc")
                                          .version("v1.0.0"));
    }

    private SecurityScheme securityScheme(String name) {
        return new io.swagger.v3.oas.models.security.SecurityScheme()
                .type(io.swagger.v3.oas.models.security.SecurityScheme.Type.APIKEY)
                .in(io.swagger.v3.oas.models.security.SecurityScheme.In.HEADER)
                .name(name);
    }
}
  1. 启动项目访问页面:http://localhost:8080/swagger-ui/index.html