来点前端

38k 词

来点前后端

ok,最近要搞前端项目练手,来点spring boot

1.0.后端:Spring Boot?这是什么?

找到springboot的教程,开始部署开发环境。

不过我觉得直接从IDEA开局貌似更好?

直接在github上找到Spring的开源项目[点我去看看]
简单瞅一眼,发现有这个框架在IDEA中部署开发的教程,让我看看

Steps:
Within your locally cloned spring-framework working directory:

Precompile spring-oxm with ./gradlew :spring-oxm:compileTestJava
Import into IntelliJ (File -> New -> Project from Existing Sources -> Navigate to directory -> Select build.gradle)
When prompted exclude the spring-aspects module (or after the import via File-> Project Structure -> Modules)

OK,gradle的部署方式,那在IDEA中从已有的远程项目中创建。

欸?好像启动不了一个可运行的示例,我再找找…原来是可以使用Spring Initializr直接创建一个项目啊,看来之前安装的Spring-framework项目如果要启动要使用集成测试,属于是新手误入Boss房了。

好,Spring Initializr生成出来的项目小很多,使用IDEA启动并完成构建之后,我们就有了一个简易的Spring Boot项目了。

1.1.将SpringBoot连接到我的Mysql

Step1.数据库准备

Mysql部分:使用Navicat轻松创建表格定义数据类型,这里我创建了一个test数据库后在库中定义一个user表方便我们后续的类定义以及其他的功能。

**user表格(用户)**:

1
2
3
4
5
6
7
8
9
10
11
12
CREATE TABLE user (
userid INT AUTO_INCREMENT PRIMARY KEY,-- 用户ID,自增主键
username VARCHAR(50) NOT NULL UNIQUE,-- 用户名,唯一且不能为空
password VARCHAR(255) NOT NULL,-- 密码,不能为空
gender ENUM('male', 'female', 'other') NOT NULL, -- 性别,枚举类型:male、female、other
birthdate DATE,-- 出生日期,格式为YYYY-MM-DD
address VARCHAR(255),-- 家庭住址,字符串类型
peopleid VARCHAR(20) NOT NULL,-- 身份证号,字符串类型,不能为空
peoplename VARCHAR(20) NOT NULL,-- 真实姓名,字符串类型,不能为空
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, -- 用户创建时间,默认为当前时间
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP -- 用户信息更新时间,默认为当前时间,更新时自动更新
);

**medical_records表格(病历)**:
这个表一个外键,使其与users表中的userid相关联

1
2
3
4
5
6
7
8
9
10
CREATE TABLE medical_records (
record_id INT AUTO_INCREMENT PRIMARY KEY, -- 主键
userid INT NOT NULL, -- 关联到用户表的userid,注意这里使用INT类型
diagnosis_date DATE NOT NULL, -- 诊断日期
doctor VARCHAR(255) NOT NULL, -- 医生姓名
diagnosis TEXT NOT NULL, -- 诊断结果
remark TEXT, -- 备注信息
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (userid) REFERENCES user(userid) ON DELETE CASCADE
);

在IDEA中,点击窗口右侧的数据库图标,在里面添加我们的数据库,笔者这里用的Mysql,其他厂商的数据库你在下拉列表里找到就行了。
Alt text
点击后填写我们的数据库信息,数据库方面不是本文重点,这里不过多笔墨了。

Step2.配置项目依赖并重新构建

SpringBoot部分:我使用的是gradle构建,因此我在build.gradle中配置必要的依赖项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
plugins {
id 'java'
id 'org.springframework.boot' version '3.4.3'
id 'io.spring.dependency-management' version '1.1.7'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'

java {
toolchain {
languageVersion = JavaLanguageVersion.of(23)
}
}

repositories {
mavenCentral()
}

dependencies {
implementation 'org.springframework.boot:spring-boot-starter'
implementation 'org.springframework.boot:spring-boot-starter-actuator'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'


// MySQL Connector/J
runtimeOnly 'mysql:mysql-connector-java:8.0.33' // 根据具体项目调整版本号

// Spring Data JPA
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'

// Spring Web Starter 依赖
implementation 'org.springframework.boot:spring-boot-starter-web'

//Junit,后面测试连通性用到的依赖
testImplementation('org.springframework.boot:spring-boot-starter-test') {
exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
}
// 确保包含AssertJ
testImplementation 'org.assertj:assertj-core:3.24.2'

}

tasks.named('test') {
useJUnitPlatform()
}

如果飘红,就按照IDEA的引导引入对应的依赖库就行了。

mysql-connector的版本和Mysql版本有关,我的Mysql版本是8.0,所以这里使用8.0的Connector,具体的Connector版本和Mysql版本的对照可以到Mysql官网查看

添加完成依赖之后,运行build.gradle完成构建

Step3.配置数据库连接

application.properties中添加数据库连接配置,当然,如果我们的spring版本比较旧,可能是application.yml之类的文件,这里我贴出我的application.propertiesyml格式的文件不支持直接粘贴进去。

1
2
3
4
5
6
7
spring.datasource.url=jdbc:mysql://localhost:3306/your_database_name?useSSL=false&serverTimezone=UTC
## 自己改your_database_name
spring.datasource.username=your_username
## 自己改your_username
spring.datasource.password=your_password
## 自己改your_password
spring.jpa.hibernate.ddl-auto=update

Step4.创建用户实体类

根据我们刚刚新建的User表创建JPA实体类
Alt text

然后,进入User类,IDEA生成的JPA代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
package com.example.demo.entity;

import jakarta.persistence.*;
import org.hibernate.annotations.ColumnDefault;

import java.time.Instant;
import java.time.LocalDate;

@Entity
@Table(name = "user")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "userid", nullable = false)
private Integer id;

@Column(name = "username", nullable = false, length = 50)
private String username;

@Column(name = "password", nullable = false)
private String password;

@Lob
@Column(name = "gender", nullable = false)
private String gender;

@Column(name = "birthdate")
private LocalDate birthdate;

@Column(name = "address")
private String address;

@Column(name = "pepoleid", nullable = false, length = 20)
private String pepoleid;

@Column(name = "pepolename", nullable = false, length = 20)
private String pepolename;

@ColumnDefault("CURRENT_TIMESTAMP")
@Column(name = "created_at")
private Instant createdAt;

@ColumnDefault("CURRENT_TIMESTAMP")
@Column(name = "updated_at")
private Instant updatedAt;

public Integer getId() {
return id;
}

public void setId(Integer id) {
this.id = id;
}

public String getUsername() {
return username;
}

public void setUsername(String username) {
this.username = username;
}

public String getPassword() {
return password;
}

public void setPassword(String password) {
this.password = password;
}

public String getGender() {
return gender;
}

public void setGender(String gender) {
this.gender = gender;
}

public LocalDate getBirthdate() {
return birthdate;
}

public void setBirthdate(LocalDate birthdate) {
this.birthdate = birthdate;
}

public String getAddress() {
return address;
}

public void setAddress(String address) {
this.address = address;
}

public String getPepoleid() {
return pepoleid;
}

public void setPepoleid(String pepoleid) {
this.pepoleid = (String) pepoleid;
}

public String getPepolename() {
return pepolename;
}

public void setPepolename(String pepolename) {
this.pepolename = pepolename;
}

public Instant getCreatedAt() {
return createdAt;
}

public void setCreatedAt(Instant createdAt) {
this.createdAt = createdAt;
}

public Instant getUpdatedAt() {
return updatedAt;
}

public void setUpdatedAt(Instant updatedAt) {
this.updatedAt = updatedAt;
}

}

然后你会看到@Table(name = "user")这一行中有警告,根据IDEA的提示给他重新分配数据源到我们刚刚创建的user数据库中
Alt text

完成之后你Ctrl+鼠标左键点击这个"user"应该是能触发你IDEA弹出数据库的弹窗的,这样就对了。

另外,你会发现在User类在com.example.demo中,为了方便管理,我们在com.example.demo下创建一个entity包,并将User类移动到这个包中。

做完这些之后的项目结构:
Alt text

对病历表实体进行同样的操作,将MedicalRecord类移动到com.example.demo.entity包中。
MedicalRecord

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
package com.example.demo.entity;

import jakarta.persistence.*;
import org.hibernate.annotations.ColumnDefault;
import org.hibernate.annotations.OnDelete;
import org.hibernate.annotations.OnDeleteAction;

import java.time.Instant;
import java.time.LocalDate;

@Entity
@Table(name = "medical_records")
public class MedicalRecord {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "record_id", nullable = false)
private Integer id;

@ManyToOne(fetch = FetchType.LAZY, optional = false)
@OnDelete(action = OnDeleteAction.CASCADE)
@JoinColumn(name = "userid", nullable = false)
private User userid;

@Column(name = "diagnosis_date", nullable = false)
private LocalDate diagnosisDate;

@Column(name = "doctor", nullable = false)
private String doctor;

@Lob
@Column(name = "diagnosis", nullable = false)
private String diagnosis;

@Lob
@Column(name = "remark")
private String remark;

@ColumnDefault("CURRENT_TIMESTAMP")
@Column(name = "created_at")
private Instant createdAt;

public Integer getId() {
return id;
}

public void setId(Integer id) {
this.id = id;
}

public User getUserid() {
return userid;
}

public void setUserid(User userid) {
this.userid = userid;
}

public LocalDate getDiagnosisDate() {
return diagnosisDate;
}

public void setDiagnosisDate(LocalDate diagnosisDate) {
this.diagnosisDate = diagnosisDate;
}

public String getDoctor() {
return doctor;
}

public void setDoctor(String doctor) {
this.doctor = doctor;
}

public String getDiagnosis() {
return diagnosis;
}

public void setDiagnosis(String diagnosis) {
this.diagnosis = diagnosis;
}

public String getRemark() {
return remark;
}

public void setRemark(String remark) {
this.remark = remark;
}

public Instant getCreatedAt() {
return createdAt;
}

public void setCreatedAt(Instant createdAt) {
this.createdAt = createdAt;
}

}

Step5.编写数据访问层Repository

在项目中src/main/java/com.example.demo/下创建一个repository包,并在其中创建UserRepositoryMedicalRecordRepository接口,代码如下:

UserRepository:

1
2
3
4
5
6
7
8
9
10
package com.example.demo.repository;

import com.example.demo.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface UserRepository extends JpaRepository<User, Integer> {
User findByUsername(String username);
}

MedicalRecordRepository:

1
2
3
4
5
6
7
8
9
10
11
12
package com.example.demo.repository;

import com.example.demo.entity.MedicalRecord;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public interface MedicalRecordRepository extends JpaRepository<MedicalRecord, Integer> {
List<MedicalRecord> findByUserid(Integer userid);
}

注意UserRepositoryMedicalRecordRepositoryInterface而不是Class

Step7.编写测试方法,检测连通性

转到src/test/java/com.example.demo/,注意是test中,创建一个repository包,添加UserRepositoryTest类用来测试,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
package com.example.demo.repository;

import com.example.demo.entity.User;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager;
import org.springframework.test.annotation.Rollback;

import java.time.LocalDate;

import static org.assertj.core.api.Assertions.assertThat;



@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
//↑这句非常重要,JPA单元测试时,会自动切换到内嵌数据库,使用这个语句把它关掉,不然你头想破了都不知道为什么测试连不上数据库
public class UserRepositoryTest {

@Autowired
private UserRepository userRepository;

@Autowired
private TestEntityManager entityManager;

private User testUser;
@BeforeEach
void setUp() {
System.out.println("Setting up test environment...");
// 创建测试用户
testUser = new User();
testUser.setUsername("zzb");
testUser.setPassword("password");
testUser.setGender("male");
testUser.setBirthdate(LocalDate.of(1998, 3, 3));
testUser.setAddress("Example Address");
testUser.setPepolename("zzb");
testUser.setPepoleid("123456789123456789");
// 将测试用户插入到数据库中
entityManager.persist(testUser);
System.out.println("Inserted user ID: " + testUser.getId());
}

@Test
@Rollback(false)//←控制测试创建的数据是否回滚,这里用false,防止出现"数据库连接幻觉"情况
public void whenFindByUsername_thenReturnUser() {
System.out.println("Running whenFindByUsername_thenReturnUser test...");
User foundUser = userRepository.findByUsername(testUser.getUsername());

assertThat(foundUser).isNotNull().usingRecursiveComparison().isEqualTo(testUser);

if (foundUser == null) {
System.out.println("User not found by username.");
} else {
System.out.println("Found user with ID: " + foundUser.getId());
}
}

@Test
public void testSaveUser() {
// 创建新用户并保存
User newUser = new User();
newUser.setUsername("zyy");
newUser.setPassword("newpassword");
newUser.setGender("female"); // 使用String类型的gender
newUser.setBirthdate(LocalDate.of(1993, 3, 3));
newUser.setAddress("New Example Address");
newUser.setPepolename("zyy");
newUser.setPepoleid("12345678912345678X");

User savedUser = userRepository.save(newUser);

// 确认新用户的ID已生成(即成功保存)
assertThat(savedUser.getId()).isNotNull();

// 可选:验证新用户是否可以被找到
User foundUser = userRepository.findByUsername("zyy");
assertThat(foundUser).isNotNull().usingRecursiveComparison().isEqualTo(savedUser);

if (savedUser == null || foundUser == null) {
System.out.println("Failed to save or find the new user.");
} else {
System.out.println("Saved and found user with ID: " + savedUser.getId());
}
}
}

做完这些之后我们的项目结构
Alt text
你可能会发现AutoConfigureTestDatabaselocalDate这两个类没有导入,导入一下就完事了。
Alt text

搞定,重新构建一下,没有报错就可以跑一下测试了

1
./gradlew test

测试完成,没有报错,刷新表之后在Navicat中看到我们测试用例创建的新用户
Alt text
搞定~

Step8:给前端提供功能

对于用户相关的操作,我们提供注册、登录等功能。
其中,能操作所有数据的管理员功能我们写到controller
而普通用户需要用到的操作,我们写到service

首先我们创建一个service包,然后创建UserServiceMedicalRecordService接口

UserService:

1
2
3
4
5
6
7
8
9
10
package com.example.demo.service;

import com.example.demo.entity.User;

public interface UserService {
User validateUser(String username, String password);
User registerUser(User user) throws IllegalArgumentException; // 注册用户
User updateUserInfo(Integer userId, User userDetails) throws IllegalArgumentException; // 修改用户信息
void updatePassword(Integer userId, String oldPassword, String newPassword) throws IllegalArgumentException; // 修改密码
}

MedicalRecordService:

1
2
3
4
5
6
7
8
9
10
package com.example.demo.service;

import com.example.demo.entity.MedicalRecord;

import java.util.List;

public interface MedicalRecordService {
List<MedicalRecord> getMedicalRecordsByUserId(Integer userId);
}

然后,我们用再在service内创建实现其功能的包impl,在service.impl内创建实现这个接口的类UserServiceImplMedicalRecordServiceImpl

UserServiceImpl:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
package com.example.demo.service.impl;

import com.example.demo.entity.User;
import com.example.demo.repository.UserRepository;
import com.example.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.Optional;

@Service
public class UserServiceImpl implements UserService {

@Autowired
private UserRepository userRepository;

@Override
public User validateUser(String username, String password) {
User user = userRepository.findByUsername(username);
if (user != null && user.getPassword().equals(password)) {
return user;
}
return null;
}

@Override
public User registerUser(User user) throws IllegalArgumentException {
// 检查用户名是否已存在
if (userRepository.findByUsername(user.getUsername()) != null) {
throw new IllegalArgumentException("用户名已存在");
}
// 检查密码长度
if (user.getPassword().length() < 6 || user.getPassword().length() > 255) {
throw new IllegalArgumentException("密码长度必须在 6 到 255 个字符之间");
}
// 保存用户
return userRepository.save(user);
}

@Override
public User updateUserInfo(Integer userId, User userDetails) throws IllegalArgumentException {
Optional<User> optionalUser = userRepository.findById(userId);
if (optionalUser.isPresent()) {
User user = optionalUser.get();
user.setUsername(userDetails.getUsername());
user.setGender(userDetails.getGender());
user.setBirthdate(userDetails.getBirthdate());
user.setAddress(userDetails.getAddress());
user.setPepoleid(userDetails.getPepoleid());
user.setPepolename(userDetails.getPepolename());
return userRepository.save(user);
} else {
throw new IllegalArgumentException("用户不存在");
}
}

@Override
public void updatePassword(Integer userId, String oldPassword, String newPassword) throws IllegalArgumentException {
Optional<User> optionalUser = userRepository.findById(userId);
if (optionalUser.isPresent()) {
User user = optionalUser.get();
// 验证旧密码
if (!user.getPassword().equals(oldPassword)) {
throw new IllegalArgumentException("旧密码错误");
}
// 更新密码
user.setPassword(newPassword);
userRepository.save(user);
} else {
throw new IllegalArgumentException("用户不存在");
}
}
}

MedicalRecordServiceImpl:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.example.demo.service.impl;

import com.example.demo.entity.MedicalRecord;
import com.example.demo.repository.MedicalRecordRepository;
import com.example.demo.service.MedicalRecordService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class MedicalRecordServiceImpl implements MedicalRecordService {

@Autowired
private MedicalRecordRepository medicalRecordRepository;

@Override
public List<MedicalRecord> getMedicalRecordsByUserId(Integer userId) {
return medicalRecordRepository.findByUserid(userId);
}
}

对于查询所有账户的信息,对账户信息的增删改查等管理员权限的功能,我们写在controller包的UserController类中,当然,为了调用方便,我们也在这个类中应用UserService的方法。

UserController:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95

package com.example.demo.controller;

import com.example.demo.entity.User;
import com.example.demo.repository.UserRepository;
import com.example.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.Optional;

@RestController
@RequestMapping("/api/users")
public class UserController {

//调用UserService接口,登录的验证使用UserService
@Autowired
private UserService userService;

// 登录接口
@PostMapping("/login")

public ResponseEntity<?> login(@RequestBody User loginRequest) {
// 调用 UserService 验证用户
User user = userService.validateUser(loginRequest.getUsername(), loginRequest.getPassword());
if (user != null) {
// 登录成功,返回用户信息
return ResponseEntity.ok(user);
} else {
// 登录失败,返回错误信息
return ResponseEntity.badRequest().body("用户名或密码错误");
}
}

//调用UserRepository接口,对账户的控制函数都实现于本文件内。
@Autowired
private UserRepository userRepository;

// 获取所有用户
@GetMapping
public List<User> getAllUsers() {
return userRepository.findAll();
}

// 根据ID获取用户
@GetMapping("/{id}")
public ResponseEntity<User> getUserById(@PathVariable(value = "id") Integer userId) {
Optional<User> user = userRepository.findById(userId);
if (user.isPresent()) {
return ResponseEntity.ok().body(user.get());
} else {
return ResponseEntity.notFound().build();
}
}

// 创建新用户
@PostMapping
public User createUser(@RequestBody User newUser) {
return userRepository.save(newUser);
}

// 更新用户信息
@PutMapping("/{id}")
public ResponseEntity<User> updateUser(@PathVariable(value = "id") Integer userId, @RequestBody User userDetails) {
Optional<User> optionalUser = userRepository.findById(userId);
if (optionalUser.isPresent()) {
User user = optionalUser.get();
user.setUsername(userDetails.getUsername());
user.setPassword(userDetails.getPassword());
user.setGender(userDetails.getGender());
user.setBirthdate(userDetails.getBirthdate());
user.setAddress(userDetails.getAddress());
user.setPepoleid(userDetails.getPepoleid());
user.setPepolename(userDetails.getPepolename());
final User updatedUser = userRepository.save(user);
return ResponseEntity.ok(updatedUser);
} else {
return ResponseEntity.notFound().build();
}
}

// 删除用户
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteUser(@PathVariable(value = "id") Integer userId) {
Optional<User> user = userRepository.findById(userId);
if (user.isPresent()) {
userRepository.delete(user.get());
return ResponseEntity.ok().build();
} else {
return ResponseEntity.notFound().build();
}
}
}

注意添加完成后重新构建一下。

2.0.前端:VUE?这是什么?

好吧,找一下发现VUE是JavaScript的框架,那还是要Node.js的运行环境,装就完事了。node.js的安装还是很简单的,笔者是Windows11环境,使用pnpm安装就完事了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Download and install fnm:
winget install Schniz.fnm

# Download and install Node.js:
fnm install 22

# Verify the Node.js version:
node -v # Should print "v22.14.0".

# Download and install pnpm:
corepack enable pnpm

# Verify pnpm version:
pnpm -v

ok,安装完成,node -v能出现版本号了。

Vite:强大的Vue构建工具

OK,导师说装个Vite更方便,问一下AI这玩意干啥的。

Generated from Qwen:
Vite 是一个现代化的前端构建工具,旨在显著提升前端开发体验。它主要由两大部分组成:一个是开发服务器,另一个是用于生产环境的构建指令。

装就完事了

1
npm install -g create-vite

OK装好之后就可以用create-vite来方便地创建一个Demo了。

创建并进入一个我们打算存放Demo的地方,然后运行

1
2
3
4
5
6
7
8
9
10
create-vite vue_demo --template vue
Scaffolding project in

vue_demo...

Done. Now run:

cd vue_demo
npm install
npm run dev

OK,接下来按照指引运行一下这三个命令,就能运行我们的Demo了。


安装和配置必要的组件库

当然,此时的Demo还是空的,我们还需要安装一些必要的组件库,比如axios、vue-router、Element Plus等,才能满足我们对页面的各种各样的需求。

axios

安装 Axios:

Axios用于发送Http请求给后端API。

1
npm install axios
配置 Axios:

我们还需要创建一个文件来封装它,以便于管理和维护。

  1. 创建一个新的文件 src/api/index.js
1
2
3
4
5
6
7
8
9
import axios from 'axios';

const instance = axios.create({
baseURL: 'http://localhost:8080/api/', // 设置默认的API地址
timeout: 5000, // 请求超时时间
headers: {'Content-Type': 'application/json'}
});

export default instance;
  1. 创建 axios 实例

src目录下创建一个utils文件夹,并在其中创建一个http.js文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

import axios from 'axios';

const http = axios.create({
baseURL: 'https://api.example.com', // 我们的 API 地址
timeout: 5000, // 请求超时时间
});

// 请求拦截器
http.interceptors.request.use(
(config) => {
// 在发送请求之前做些什么
return config;
},
(error) => {
// 对请求错误做些什么
return Promise.reject(error);
}
);

// 响应拦截器
http.interceptors.response.use(
(response) => {
// 对响应数据做些什么
return response.data;
},
(error) => {
// 对响应错误做些什么
return Promise.reject(error);
}
);

export default http;
  1. 在需要调用 API 的地方引入并使用这个实例:
1
import http from '@/utils/http';

vue-router

安装 Vue Router:

Vue Router用于管理页面路由,也就是我们在网页中经常用到的页面跳转功能。

1
npm install vue-router
配置 Vue Router:

接下来,我们为 Vue 应用程序设置路由管理。

  1. 创建路由配置文件 src/router/index.js

这里病历页面使用/medical-records/:userId的方式进行访问。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import { createRouter, createWebHistory } from 'vue-router';

const routes = [
{
path: '/',
name: 'Home',
component: () => import('@/views/Home.vue'),
},
{
path: '/login',
name: 'Login',
component: () => import('@/views/Login.vue'),
},
{
path: '/register',
name: 'Register',
component: () => import('@/views/Register.vue'),
},
{
path: '/medical-records/:userId',
name: 'MedicalRecords',
component: () => import('@/views/MedicalRecords.vue'),
}
];

const router = createRouter({
history: createWebHistory(),
routes,
});

export default router;
  1. main.jsmain.ts 中引入并使用路由:
1
2
3
4
5
6
7
8
9
10
import { createApp } from 'vue';
import App from './App.vue';
import router from './router';
import ElementPlus from 'element-plus';
import 'element-plus/dist/index.css';

const app = createApp(App);
app.use(router);//router
app.use(ElementPlus);//el
app.mount('#app');

Element Plus

安装 Element Plus:

最后安装Element Plus用于快速开发UI界面。
顺便再装一个自动引入的插件,这样我们就可以直接使用Element Plus的组件而无需手动导入每个组件。

1
2
npm install element-plus
npm install unplugin-auto-import unplugin-vue-components -D
配置 Element Plus:

对于 Element Plus,我们可以选择完整引入或按需引入。这里推荐使用按需引入以减少打包体积。

修改 vite.config.js 文件:

1
2
3
4
5
6
7
8
9
10
11
12
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import path from 'path'; // 需要引入 path 模块

export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'), // 将 @ 指向 src 目录
},
},
});

这样,你就可以在我们的组件中直接使用 Element Plus 的组件而无需手动导入每个组件。

通过以上步骤,你应该能够在我们的 Vite + Vue 项目中成功配置和使用 axiosvue-routerElement Plus。记得根据实际需求调整配置和代码逻辑。


页面编写

好了,接下来就可以开始写登录界面的代码了。
我们在之前配置 Vue Router的时候已经指定了HomeLoginRegister三个文件的路径,他们是在views目录下的,所以我们要先在根目录src下创建views文件夹,然后在里面创建Home.vueLogin.vueRegister.vue,分别对应首页、登录页和注册页,MedicalRecords.vue则是病历的动态页面。

之后,我们的项目目录应该看起来像这样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

src/
├── assets/
├── components/
├── router/
│ └── index.js
├── utils/
│ └── http.js
├── views/
│ ├── Home.vue
│ ├── Login.vue
│ ├── MedicalRecords.vue
│ └── Register.vue
├── App.vue
└── main.js


Home.vue:

随便写点东西在Home(其实一开始是想在这个界面显示数据库中最近创建的16个用户,不过写完是一坨bug,所以就删掉了,不完整但是无伤大雅)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
<template>
<el-container>
<el-main>
<h1>Welcome to Home Page</h1>
<el-button type="primary" @click="fetchData">Fetch Data</el-button>

<!-- 数据展示 -->
<el-table :data="tableData" v-loading="loading" style="margin-top: 20px">
<el-table-column prop="id" label="ID" width="100" />
<el-table-column prop="title" label="Title" />
</el-table>
</el-main>
</el-container>
</template>

<script>
import http from '@/utils/http';

export default {
name: 'Home',
data() {
return {
tableData: [], // 表格数据
loading: false, // 加载状态
};
},
methods: {
async fetchData() {
this.loading = true;
try {
// 调用 API 获取数据
const response = await http.get('/posts'); // 假设 API 返回一个帖子列表
this.tableData = response;
} catch (error) {
this.$message.error('Failed to fetch data');
console.error(error);
} finally {
this.loading = false;
}
},
},
};
</script>

<style scoped>
h1 {
color: #409EFF;
}

.el-button {
margin-bottom: 20px;
}
</style>

Login.vue:

登录成功之后使用router将UserId传给之后的页面,用于传递用户信息,当然使用会话也很好,有验证机制,这里先用URL保存用户ID

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
<template>
<el-container class="login-container">
<el-main>
<el-card class="login-card">
<h2>用户登录</h2>
<el-form :model="loginForm" :rules="loginRules" ref="loginFormRef" label-width="80px">
<!-- 用户名 -->
<el-form-item label="用户名" prop="username">
<el-input v-model="loginForm.username" placeholder="请输入用户名" />
</el-form-item>

<!-- 密码 -->
<el-form-item label="密码" prop="password">
<el-input
v-model="loginForm.password"
type="password"
placeholder="请输入密码"
show-password
/>
</el-form-item>

<!-- 登录按钮 -->
<el-form-item>
<el-button type="primary" @click="handleLogin">登录</el-button>
<el-button @click="resetForm">重置</el-button>
</el-form-item>
</el-form>
</el-card>
</el-main>
</el-container>
</template>

<script>
import http from '@/utils/http'; // 引入 axios 实例
import { ElMessage } from 'element-plus'; // 引入 Element Plus 的消息提示组件

export default {
name: 'Login',
data() {
return {
loginForm: {
username: '',
password: '',
},
loginRules: {
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 3, max: 50, message: '用户名长度在 3 到 50 个字符', trigger: 'blur' },
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 6, max: 255, message: '密码长度在 6 到 255 个字符', trigger: 'blur' },
],
},
};
},
methods: {
async handleLogin() {
try {
await this.$refs.loginFormRef.validate();
const response = await http.post('/login', this.loginForm);
if (response.code === 200) {
ElMessage.success('登录成功');
const userId = response.data.userId;
localStorage.setItem('user', JSON.stringify(response.data));
this.$router.push({ name: 'MedicalRecords', params: { userId } });
} else {
ElMessage.error(response.message || '登录失败');
}
} catch (error) {
ElMessage.error('登录失败,请检查用户名和密码');
console.error(error);
}
},
resetForm() {
this.$refs.loginFormRef.resetFields();
},
},
};
</script>

<style scoped>
.login-container {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
background-color: #f5f5f5;
}

.login-card {
width: 400px;
padding: 20px;
}

h2 {
text-align: center;
margin-bottom: 20px;
color: #409EFF;
}
</style>

效果是这样
Alt text


Register.vue:

给每个输入都做了判定和单独的提示,如果输入不合法,则不会发送请求,并且会有提示。

其中,比较有创新的地方是

  1. 按照出生日期在前端实时刷新年龄,当前时间减去出生日期,并将计算结果显示在前端。
  2. 限制了出生日期只能选择在今天以前的日期。
  3. 对身份证号有特殊检查,只能是18位,并且前17位必须是数字,最后一位可以是数字或者X。

其他数据的检查稀松平常,看代码吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
<template>
<el-container class="register-container">
<el-main>
<el-card class="register-card">
<h2>用户注册</h2>
<el-form :model="registerForm" :rules="registerRules" ref="registerFormRef" label-width="100px">
<!-- 用户名 -->
<el-form-item label="用户名" prop="username">
<el-input v-model="registerForm.username" placeholder="请输入用户名" />
</el-form-item>

<!-- 密码 -->
<el-form-item label="密码" prop="password">
<el-input v-model="registerForm.password" type="password" placeholder="请输入密码" show-password />
</el-form-item>

<!-- 确认密码 -->
<el-form-item label="确认密码" prop="confirmPassword">
<el-input v-model="registerForm.confirmPassword" type="password" placeholder="请再次输入密码" show-password />
</el-form-item>

<!-- 性别 -->
<el-form-item label="性别" prop="gender">
<el-radio-group v-model="registerForm.gender">
<el-radio label="male"></el-radio>
<el-radio label="female"></el-radio>
<el-radio label="other">其他</el-radio>
</el-radio-group>
</el-form-item>

<!-- 出生日期和年龄 -->
<el-row>
<el-col :span="12">
<el-form-item label="出生日期" prop="birthdate">
<el-date-picker
v-model="registerForm.birthdate"
type="date"
placeholder="选择日期"
@change="calculateAge"
:disabled-date="disabledDate"/>
</el-form-item>


</el-col>
<el-col :span="12">
<el-form-item label="年龄">
<el-input :value="age" disabled></el-input>
</el-form-item>
</el-col>
</el-row>

<!-- 地址 -->
<el-form-item label="地址" prop="address">
<el-input v-model="registerForm.address" placeholder="请输入地址" />
</el-form-item>

<!-- 姓名 -->
<el-form-item label="姓名" prop="realName">
<el-input v-model="registerForm.realName" placeholder="请输入您的真实姓名" />
</el-form-item>

<!-- 身份证号 -->
<el-form-item label="身份证号" prop="idNumber">
<el-input v-model="registerForm.idNumber" placeholder="请输入您的身份证号码" />
</el-form-item>

<!-- 注册按钮 -->
<el-form-item>
<el-button type="primary" @click="handleRegister">注册</el-button>
<el-button @click="resetForm">重置</el-button>
</el-form-item>
</el-form>
</el-card>
</el-main>
</el-container>
</template>

<script>
import http from '@/utils/http'; // 引入 axios 实例
import { ElMessage } from 'element-plus'; // 引入 Element Plus 的消息提示组件

export default {
name: 'Register',
data() {
const validateConfirmPassword = (rule, value, callback) => {
if (value !== this.registerForm.password) {
callback(new Error('两次输入的密码不一致'));
} else {
callback();
}
};

return {
registerForm: {
username: '',
password: '',
confirmPassword: '',
gender: 'male',
birthdate: null,
address: ''
},
age: null, // 新增用于存储计算出来的年龄
registerRules: {
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 3, max: 50, message: '用户名长度在 3 到 50 个字符', trigger: 'blur' },
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 6, max: 255, message: '密码长度在 6 到 255 个字符', trigger: 'blur' },
],
confirmPassword: [
{ required: true, message: '请再次输入密码', trigger: 'blur' },
{ validator: validateConfirmPassword, trigger: 'blur' },
],
gender: [
{ required: true, message: '请选择性别', trigger: 'change' },
],
birthdate: [
{ required: true, message: '请选择出生日期', trigger: 'change' },
],
address: [
{ required: true, message: '请输入地址', trigger: 'blur' },
],
realName: [
{ required: true, message: '请输入您的真实姓名', trigger: 'blur' },
],
idNumber: [
{ required: true, message: '请输入您的身份证号码', trigger: 'blur' },
{ min: 18, max: 18, message: '身份证号码必须是18位', trigger: 'blur' }, // 检查长度
{
pattern: /^[0-9]{17}[0-9X]$/, // 正则表达式,确保只包含数字和最后一位可能是X
message: '身份证号码格式不正确,只能包含数字和最后一位可能是大写字母X',
trigger: 'blur'
}
],
},
};
},
methods: {
disabledDate(time) {
return time.getTime() > Date.now(); // 只能选择今天之前(不含今天)的日期
},
calculateAge() {
if (this.registerForm.birthdate) {
const today = new Date();
const birthDate = new Date(this.registerForm.birthdate);
let years = today.getFullYear() - birthDate.getFullYear();
const monthDiff = today.getMonth() - birthDate.getMonth();

if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) {
years--;
}

this.age = years;
} else {
this.age = null;
}
},
async handleRegister() {
try {
await this.$refs.registerFormRef.validate();
const response = await http.post('/api/users/register', this.registerForm);
if (response.code === 200) {
ElMessage.success('注册成功');
this.$router.push('/login'); // 跳转到登录页面
} else {
ElMessage.error(response.message || '注册失败');
}
} catch (error) {
ElMessage.error(error.response?.data?.message || '注册失败,请检查输入');
console.error(error);
}
},
resetForm() {
this.$refs.registerFormRef.resetFields();
this.age = null; // 重置时也需重置年龄
},
},
};
</script>

<style scoped>
.register-container {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
background-color: #f5f5f5;
}

.register-card {
width: 500px;
padding: 20px;
}

h2 {
text-align: center;
margin-bottom: 20px;
color: #409EFF;
}
</style>

效果截图贴这,我主要技术栈是单片机方向的,所以页面主打一个能用就行,不太美观。

Alt text

在酒吧点个炒饭看看
Alt text
好,全防住了

MedicalRecords.vue

通过URL传递的用户ID,对后端进行访问,查询到用户的病历数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
<template>
<div class="medical-records">
<!-- 病历列表 -->
<el-table :data="medicalRecords" style="width: 100%">
<el-table-column prop="diagnosisDate" label="诊断日期" width="180"></el-table-column>
<el-table-column prop="doctor" label="医生" width="180"></el-table-column>
<el-table-column prop="diagnosis" label="诊断"></el-table-column>
<el-table-column prop="remark" label="备注"></el-table-column>

<el-table-column label="操作">
<template slot-scope="scope">
<el-button @click="handleDetail(scope.row)" type="text" size="small">查看详情</el-button>
</template>
</el-table-column>
</el-table>

<!-- 病历详情对话框 -->
<el-dialog title="病历详情" :visible.sync="dialogVisible">
<p>诊断日期: {{ currentRecord.diagnosisDate }}</p>
<p>医生: {{ currentRecord.doctor }}</p>
<p>诊断: {{ currentRecord.diagnosis }}</p>
<p>备注: {{ currentRecord.remark }}</p>
<span slot="footer" class="dialog-footer">
<el-button type="primary" @click="dialogVisible = false">关闭</el-button>
</span>
</el-dialog>
</div>
</template>

<script>
import { useRoute } from 'vue-router';

export default {
data() {
return {
userId: null,
medicalRecords: [], // 动态获取的病历数据
dialogVisible: false,
currentRecord: {}
};
},
created() {
const route = useRoute();
this.userId = route.params.userId; // 从路由参数中获取userId
this.fetchMedicalRecords(); // 页面加载时调用此方法获取病历数据
},
methods: {
fetchMedicalRecords() {
},
handleDetail(record) {
this.currentRecord = record;
this.dialogVisible = true;
}
}
};
</script>

<style scoped>
.medical-records {
padding: 20px;
}
</style>

效果图
Alt text

系统漏洞分析

  1. 后端没有编写对发送过来的表单进行二次审查的功能,所以很容易被攻击输入非法数据甚至是SQL注入,安全性几乎是0。
  2. 用户输入的所有数据都是透明传输,所以用户输入的数据很容易被窃取。

虽然这些系统上的问题都可以通过增加功能进行防御,我都想写,但是时间上来不及了,手上也没有现成的开源模块可以直接塞进去(手上能用的是Python的代码),不过还是在这里简单总结一下,谨留以后效。

结语

至此,我们完成了对用户注册和登录功能的开发,并添加了表单验证和错误处理。现在,用户可以注册他们的账户,并且系统会根据输入的数据进行验证,确保数据的完整性和准确性现在我们了解了怎么在IDEA中用SpringBoot进行数据库方面的开发,另外学了一些前端的知识,也算是前端成功入门了,技能+++++……

本文使用过的AI工具:

Qwen
DeepSeek

在遇到一些技术问题,比如在测试中怎么取消测试用例的回滚,以及怎么防止测试用例访问自己的H2数据库而不是我的Mysql这些问题上,感谢这些AI帮我解决了这些关键问题,顺便还了解了JPA和EntityManager的知识,爽!AI技术进步和开源真好,在AI工具的帮助下,我学东西都快了不少。