使用springboot+angular开发一个todolist
后端使用springboot + spring data jpa
- Bootstrap your application with Spring Initializr: https://start.spring.io/

-
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 ListgetAllUsers(){ 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框架
- 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 - 登录注册页面
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']);
}
}
- 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镜像
- 将前端打包后的文件复制到resource/static目录下
- 引入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>
- 开启localhost/127.0.0.1端口2375的监听

使用springdoc-openapi管理文档
- 引入dependency
< dependency>
< groupId>org.springdoc< /groupId>
< artifactId>springdoc-openapi-ui< /artifactId>
< version>1.6.11< /version>
< /dependency>
- 配置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);
}
}
