添加了swagger接口、重置密码、修改了usercontroller
Change-Id: Ib651fa9b0fe0b220eb8cb88dde2b63d6bf54895e
diff --git a/src/main/java/edu/bjtu/groupone/backend/config/Config.java b/src/main/java/edu/bjtu/groupone/backend/config/Config.java
index cfc3745..6816c05 100644
--- a/src/main/java/edu/bjtu/groupone/backend/config/Config.java
+++ b/src/main/java/edu/bjtu/groupone/backend/config/Config.java
@@ -28,7 +28,14 @@
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authInterceptor)
- .addPathPatterns("/**");
+ .addPathPatterns("/**")
+ .excludePathPatterns(
+ // 放行 swagger-ui 的所有静态资源和 API 文档
+ "/swagger-ui/**",
+ "/swagger-ui.html",
+ "/v3/api-docs/**",
+ "/favicon.ico"
+ );
}
// 其它 CORS、静态资源、Swagger 配置保持不动
}
diff --git a/src/main/java/edu/bjtu/groupone/backend/config/SwaggerConfig.java b/src/main/java/edu/bjtu/groupone/backend/config/SwaggerConfig.java
index 042c278..8d85d7a 100644
--- a/src/main/java/edu/bjtu/groupone/backend/config/SwaggerConfig.java
+++ b/src/main/java/edu/bjtu/groupone/backend/config/SwaggerConfig.java
@@ -1,3 +1,4 @@
+// src/main/java/edu/bjtu/groupone/backend/config/SwaggerConfig.java
package edu.bjtu.groupone.backend.config;
import org.springframework.context.annotation.Bean;
@@ -5,35 +6,33 @@
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
+import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
+import java.util.Collections;
@Configuration
-//@EnableOpenApi
public class SwaggerConfig {
@Bean
- public Docket docket(){
- return new Docket(DocumentationType.OAS_30)
+ public Docket apiDocket() {
+ return new Docket(DocumentationType.OAS_30) // 使用 OpenAPI 3.0
.apiInfo(apiInfo())
- .enable(true)
- .groupName("CJB")
.select()
- .apis(RequestHandlerSelectors.basePackage("com.example.eliteedu_prism.controller"))
- .paths(PathSelectors.ant("/controller/**"))
+ .apis(RequestHandlerSelectors.basePackage("edu.bjtu.groupone.backend.controller"))
+ .paths(PathSelectors.any())
.build();
}
-
- @SuppressWarnings("all")
- public ApiInfo apiInfo(){
+ private ApiInfo apiInfo() {
return new ApiInfo(
- "zrj's api",
- "redis project",
+ "PtSite 后端 API 文档",
+ "登录 / 注册 / 重置密码 等接口",
"v1.0",
- "2261839618@qq.com", //开发者团队的邮箱
- "ZRJ",
- "Apache 2.0", //许可证
- "http://www.apache.org/licenses/LICENSE-2.0" //许可证链接
+ null,
+ new Contact("Team1 后端", "", "1697232065@qq.com"),
+ "Apache 2.0",
+ "http://www.apache.org/licenses/LICENSE-2.0",
+ Collections.emptyList()
);
}
-}
\ No newline at end of file
+}
diff --git a/src/main/java/edu/bjtu/groupone/backend/controller/UserController.java b/src/main/java/edu/bjtu/groupone/backend/controller/UserController.java
index 7ac29f1..0f32a74 100644
--- a/src/main/java/edu/bjtu/groupone/backend/controller/UserController.java
+++ b/src/main/java/edu/bjtu/groupone/backend/controller/UserController.java
@@ -1,99 +1,137 @@
+// src/main/java/edu/bjtu/groupone/backend/controller/UserController.java
package edu.bjtu.groupone.backend.controller;
import edu.bjtu.groupone.backend.model.Result;
import edu.bjtu.groupone.backend.model.User;
import edu.bjtu.groupone.backend.service.UserService;
import edu.bjtu.groupone.backend.utils.JwtUtils;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
-import java.time.LocalDateTime;
-import java.time.format.DateTimeFormatter;
-import java.util.HashMap;
import java.util.Map;
@CrossOrigin
-@Slf4j
+@Tag(name = "用户相关接口") //输入http://localhost:8080/swagger-ui/index.html或http://localhost:8080/v3/api-docs/可查看接口文档
@RestController
+@Slf4j
public class UserController {
@Autowired
private UserService userService;
- /** 登录:接收表单字段 identificationNumber、password */
+ /** 邮箱+密码 登录 */
+ @Operation(summary = "用户登录", description = "使用邮箱和密码进行登录,成功后返回 JWT token")
@PostMapping("/login")
- public Result login(
- @RequestParam Integer identificationNumber,
- @RequestParam String password
- ) {
+ public Result login(@RequestBody Map<String,String> body) {
+ String email = body.get("email");
+ String password = body.get("password");
+ if (email == null || password == null) {
+ return Result.error("参数不完整");
+ }
User u = new User();
- u.setIdentificationNumber(identificationNumber);
+ u.setEmail(email);
u.setPassword(password);
User user = userService.login(u);
if (user != null) {
- Map<String, Object> payload = new HashMap<>();
- payload.put("id", user.getUserId());
- payload.put("username", user.getUsername());
- payload.put("identificationNumber", user.getIdentificationNumber());
+ var payload = Map.<String,Object>of(
+ "id", user.getUserId(),
+ "username", user.getUsername(),
+ "email", user.getEmail()
+ );
String token = JwtUtils.generateJwt(payload);
-
- // 打印到控制台
- log.info("【登录成功】生成的 JWT token = {}", token);
return Result.success(token);
}
- return Result.error("用户名或密码错误");
+ return Result.error("邮箱或密码错误");
}
- /** 注册:接收表单字段 username、email、password、verificationCode、identificationNumber、inviteCode */
+ /** 注册 */
+ @Operation(summary = "用户注册", description = "注册新用户,必须提供用户名、邮箱、验证码、密码和身份证号码")
@PostMapping("/register")
- public Result register(
- @RequestParam String username,
- @RequestParam String email,
- @RequestParam String password,
- @RequestParam String verificationCode,
- @RequestParam Integer identificationNumber
- ) {
-
- // 2. 验证邮箱验证码
- if (!userService.verifyCode(email, verificationCode)) {
+ public Result register(@RequestBody Map<String,String> body) {
+ String username = body.get("username");
+ String email = body.get("email");
+ String code = body.get("verificationCode");
+ String pwd = body.get("password");
+ String idNumStr = body.get("identificationNumber");
+ if (email==null||code==null||pwd==null||idNumStr==null) {
+ return Result.error("参数不完整");
+ }
+ int idNum = Integer.parseInt(idNumStr);
+ if (!userService.verifyCode(email, code)) {
return Result.error("验证码错误或已过期");
}
- // 3. 检查邮箱唯一
if (userService.isEmailExists(email)) {
return Result.error("邮箱已被注册");
}
- // 4. 构造用户实体并设置所有非空字段
+ if (username!=null && userService.isUsernameExists(username)) {
+ return Result.error("用户名已被占用");
+ }
+ if (idNumStr == null || idNumStr.isBlank()) {
+ return Result.error("身份证号码不能为空");
+ }
+ if (idNumStr.length() !=8){
+ return Result.error("身份证号码长度不正确,应为8位");
+ }
+ if (userService.isIdentificationNumberExists(idNum)) {
+ return Result.error("身份证号码已被注册");
+ }
+
User newUser = new User();
newUser.setUsername(username);
newUser.setEmail(email);
- newUser.setPassword(password);
- newUser.setIdentificationNumber(identificationNumber);
- // registrationDate 格式要和你的 Mapper SQL 对应,如果是 String 存储:
- newUser.setRegistrationDate(
- LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))
- );
- // 5. 插入到数据库
+ newUser.setPassword(pwd);
+ newUser.setIdentificationNumber(idNum);
userService.register(newUser);
-
return Result.success("注册成功");
}
- /** 发送邮箱验证码:接收表单字段 email、inviteCode */
+ /** 发送注册验证码 */
+ @Operation(summary = "发送注册验证码", description = "向指定邮箱发送注册验证码")
@PostMapping("/sendVerification")
- public Result sendVerification(
- @RequestParam String email
- ) {
-
+ public Result sendVerification(@RequestBody Map<String,String> p) {
+ String email = p.get("email");
+ if (email==null) {
+ return Result.error("邮箱不能为空");
+ }
userService.sendVerificationCode(email);
return Result.success("验证码已发送");
}
+ @Operation(summary = "发送重置密码验证码", description = "向指定邮箱发送重置密码的验证码")
+ @PostMapping("/sendResetCode")
+ public Result sendResetCode(@RequestBody Map<String, String> body) {
+ String email = body.get("email");
+ userService.sendResetCode(email);
+ return Result.success("重置验证码已发送");
+ }
+
+ @Operation(summary = "重置密码", description = "根据邮箱和验证码重置密码")
+ @PostMapping("/resetPassword")
+ public Result resetPassword(@RequestBody Map<String, String> body) {
+ String email = body.get("email");
+ String code = body.get("code");
+ String newPassword = body.get("newPassword");
+ boolean ok = userService.resetPassword(email, code, newPassword);
+ return ok
+ ? Result.success("密码已重置")
+ : Result.error("验证码错误或已过期");
+ }
+
+ /** 前端受保护页面获取当前用户信息 */
+ @Operation(summary = "获取当前用户信息", description = "需要提供有效的 JWT token")
@GetMapping("/api/me")
public Result me(@RequestHeader("token") String token) {
- // 从 token 解析出用户信息,JwtUtils.parseJwt 返回一个 Map
var claims = JwtUtils.parseJwt(token);
- // 你可以根据需要返回完整 User,也可以直接返回用户名
- return Result.success(claims);
+ if (claims == null) {
+ return Result.error("无效的 token");
+ }
+ var info = Map.<String,Object>of(
+ "username", claims.get("username"),
+ "token", token
+ );
+ return Result.success(info);
}
}
diff --git a/src/main/java/edu/bjtu/groupone/backend/interceptor/Interceptor.java b/src/main/java/edu/bjtu/groupone/backend/interceptor/Interceptor.java
index f00a646..09c65c9 100644
--- a/src/main/java/edu/bjtu/groupone/backend/interceptor/Interceptor.java
+++ b/src/main/java/edu/bjtu/groupone/backend/interceptor/Interceptor.java
@@ -23,15 +23,17 @@
String url = req.getRequestURI();
log.info("Request URI: {}", url);
// 放行无需鉴权的路径:登录、注册、发码、静态资源、HTML 页面
- if (url.equals("/") ||
+ if (url.contains("sendResetCode") || // 发送重置验证码
+ url.contains("resetPassword") ||
+ url.equals("/") ||
url.endsWith(".html") ||
url.contains("login") ||
url.contains("register") ||
url.equals("/api/me") || // 放行用户信息接口
url.equals("/error") || // 放行错误页面
url.contains("sendVerification") ||
- url.startsWith("/swagger-ui") ||
- url.startsWith("/v3/api-docs") ||
+ url.startsWith("/swagger-ui/") ||
+ url.startsWith("/v3/api-docs/") ||
url.startsWith("/favicon.ico") ||
url.startsWith("/static/")) {
return true;
diff --git a/src/main/java/edu/bjtu/groupone/backend/mapper/UserMapper.java b/src/main/java/edu/bjtu/groupone/backend/mapper/UserMapper.java
index 3cbf2c1..b1bda89 100644
--- a/src/main/java/edu/bjtu/groupone/backend/mapper/UserMapper.java
+++ b/src/main/java/edu/bjtu/groupone/backend/mapper/UserMapper.java
@@ -6,7 +6,7 @@
@Mapper
public interface UserMapper {
- @Select("select * from user where identification_number=#{identificationNumber} and password=#{password}")
+ @Select("select * from user where email=#{email} and password=#{password}")
User login(User user);
@Select("SELECT * FROM user WHERE username = #{username}")
@@ -19,4 +19,11 @@
"VALUES(#{username}, #{email}, #{password}, #{registrationDate}, #{identificationNumber})")
@Options(useGeneratedKeys = true, keyProperty = "userId")
void insertUser(User user);
+
+ @Update("UPDATE `user` SET password = #{password} WHERE email = #{email}")
+ int updatePasswordByEmail(@Param("email") String email,
+ @Param("password") String password);
+
+ @Select("SELECT COUNT(*) FROM user WHERE identification_number = #{identificationNumber}")
+ int countByIdentificationNumber(int identificationNumber);
}
diff --git a/src/main/java/edu/bjtu/groupone/backend/service/UserService.java b/src/main/java/edu/bjtu/groupone/backend/service/UserService.java
index 662dafe..37831ec 100644
--- a/src/main/java/edu/bjtu/groupone/backend/service/UserService.java
+++ b/src/main/java/edu/bjtu/groupone/backend/service/UserService.java
@@ -1,15 +1,17 @@
+// src/main/java/edu/bjtu/groupone/backend/service/UserService.java
package edu.bjtu.groupone.backend.service;
-
import edu.bjtu.groupone.backend.model.User;
public interface UserService {
-
User login(User user);
-
boolean isUsernameExists(String username);
boolean isEmailExists(String email);
void register(User user);
void sendVerificationCode(String email);
boolean verifyCode(String email, String code);
+ boolean isIdentificationNumberExists(int idNum);
+ // 新增重置密码相关
+ void sendResetCode(String email);
+ boolean resetPassword(String email, String code, String newPassword);
}
diff --git a/src/main/java/edu/bjtu/groupone/backend/service/impl/UserServImpl.java b/src/main/java/edu/bjtu/groupone/backend/service/impl/UserServImpl.java
index 828b78d..27ba52f 100644
--- a/src/main/java/edu/bjtu/groupone/backend/service/impl/UserServImpl.java
+++ b/src/main/java/edu/bjtu/groupone/backend/service/impl/UserServImpl.java
@@ -20,6 +20,7 @@
@Autowired
private EmailUtil emailUtil;
private final ConcurrentHashMap<String, String> emailCodes = new ConcurrentHashMap<>();
+ private final ConcurrentHashMap<String, String> resetCodes = new ConcurrentHashMap<>();
private static final long CODE_EXPIRE_MINUTES = 5;
@Override
@@ -38,6 +39,11 @@
}
@Override
+ public boolean isIdentificationNumberExists(int idNum) {
+ return userMapper.countByIdentificationNumber(idNum) > 0;
+ }
+
+ @Override
public void register(User user) {
// 如果前端未提供用户名,可生成默认
String username = user.getUsername();
@@ -78,6 +84,36 @@
return System.currentTimeMillis() <= expireTime && code.equals(inputCode);
}
+
+ @Override
+ public void sendResetCode(String email) {
+ if (!isEmailExists(email)) {
+ throw new IllegalArgumentException("该邮箱尚未注册");
+ }
+ String code = generateRandomCode();
+ emailUtil.sendVerificationEmail(email, code);
+ long expireAt = System.currentTimeMillis() + CODE_EXPIRE_MINUTES * 60 * 1000;
+ resetCodes.put(email, code + "|" + expireAt);
+ }
+
+ @Override
+ public boolean resetPassword(String email, String code, String newPassword) {
+ String stored = resetCodes.get(email);
+ if (stored == null) return false;
+ String[] p = stored.split("\\|");
+ long exp = Long.parseLong(p[1]);
+ if (System.currentTimeMillis() > exp || !p[0].equals(code)) {
+ return false;
+ }
+ int cnt = userMapper.updatePasswordByEmail(email, newPassword);
+ if (cnt == 1) {
+ resetCodes.remove(email);
+ return true;
+ }
+ return false;
+ }
+
+
private String generateRandomCode() {
return String.format("%06d", new Random().nextInt(1_000000));
}
diff --git a/src/main/resources/static/home.html b/src/main/resources/static/home.html
index 5cac114..507e3ad 100644
--- a/src/main/resources/static/home.html
+++ b/src/main/resources/static/home.html
@@ -9,22 +9,20 @@
<div id="userInfo"></div>
<script>
(async () => {
- const token = localStorage.getItem('token'); // 从 localStorage 获取 token
- if (!token) return window.location.href = 'login.html'; // 如果 token 不存在,重定向到登录页面
-
- const res = await fetch('/api/me', {
- headers: { 'token': token } // 使用 token 请求用户信息
- });
-
- const json = await res.json(); // 解析响应数据
- if (json.code !== 0) {
- return window.location.href = 'login.html'; // 如果获取用户信息失败,重定向到登录页面
+ const token = localStorage.getItem('token');
+ if (!token) {
+ return window.location.href = 'login.html';
}
-
- // 输出当前用户和 token
- document.getElementById('userInfo')
- .innerText = '当前用户:' + json.data.username + ',Token:' + json.data; // 显示用户名和 token
+ const res = await fetch('/api/me', {
+ headers: { 'token': token }
+ });
+ const json = await res.json();
+ if (json.code !== 0) {
+ return window.location.href = 'login.html';
+ }
+ document.getElementById('userInfo').innerText =
+ `当前用户:${json.data.username},Token:${json.data.token}`;
})();
</script>
</body>
-</html>
\ No newline at end of file
+</html>
diff --git a/src/main/resources/static/login.html b/src/main/resources/static/login.html
index 993941d..f7d02c7 100644
--- a/src/main/resources/static/login.html
+++ b/src/main/resources/static/login.html
@@ -8,31 +8,38 @@
<body>
<h2>用户登录</h2>
<form id="loginForm">
- <label>身份证号:</label>
- <input type="number" name="identificationNumber" required>
- <label>密码:</label>
- <input type="password" name="password" required>
+ <div>
+ <label>邮箱:</label>
+ <input type="email" name="email" required>
+ </div>
+ <div>
+ <label>密码:</label>
+ <input type="password" name="password" required>
+ </div>
<button type="submit">登录</button>
</form>
++ <p><a href="reset.html">忘记密码?</a></p>
+
<script>
- document.getElementById('loginForm')
- .addEventListener('submit', async e => {
- e.preventDefault();
- const form = new URLSearchParams(new FormData(e.target));
- const res = await fetch('/login', {
- method: 'POST',
- body: form
- });
- const json = await res.json();
- if (res.ok && json.code === 0) {
- // 保存 token
- localStorage.setItem('token', json.data);
- // 跳转到受保护的页面
- window.location.href = 'home.html';
- } else {
- alert(json.msg);
- }
+ document.getElementById('loginForm').addEventListener('submit', async e => {
+ e.preventDefault();
+ const data = {
+ email: e.target.email.value,
+ password: e.target.password.value
+ };
+ const res = await fetch('/login', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(data)
});
+ const json = await res.json();
+ if (res.ok && json.code === 0) {
+ localStorage.setItem('token', json.data);
+ window.location.href = 'home.html';
+ } else {
+ alert(json.msg || json.error);
+ }
+ });
</script>
</body>
</html>
diff --git a/src/main/resources/static/register.html b/src/main/resources/static/register.html
index 549c790..72fc1b8 100644
--- a/src/main/resources/static/register.html
+++ b/src/main/resources/static/register.html
@@ -1,4 +1,3 @@
-<!-- src/main/resources/static/register.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
@@ -8,44 +7,65 @@
<body>
<h2>用户注册</h2>
<form id="regForm">
- <label>用户名:</label>
- <input type="text" name="username" required>
- <label>邮箱:</label>
- <input type="email" name="email" required>
- <button type="button" id="sendCode">发送验证码</button>
- <label>邮箱验证码:</label>
- <input type="text" name="verificationCode" required>
- <label>密码:</label>
- <input type="password" name="password" required>
- <label>身份证号(8 位数字):</label>
- <input type="number" name="identificationNumber" required>
+ <div>
+ <label>用户名:</label>
+ <input type="text" name="username" required>
+ </div>
+ <div>
+ <label>邮箱:</label>
+ <input type="email" name="email" required>
+ <button type="button" id="sendCode">发送验证码</button>
+ </div>
+ <div>
+ <label>邮箱验证码:</label>
+ <input type="text" name="verificationCode" required>
+ </div>
+ <div>
+ <label>密码:</label>
+ <input type="password" name="password" required>
+ </div>
+ <div>
+ <label>身份证号(8 位):</label>
+ <input type="text" name="identificationNumber" required pattern="\d{8}" maxlength="8">
+ </div>
<button type="submit">注册</button>
</form>
+
<script>
- document.getElementById('sendCode')
- .addEventListener('click', async () => {
- const form = new URLSearchParams();
- form.set('email', document.querySelector('[name=email]').value);
- const res = await fetch('/sendVerification', {
- method: 'POST',
- body: form
- });
- alert((await res.json()).msg);
+ // 发送注册验证码
+ document.getElementById('sendCode').addEventListener('click', async () => {
+ const email = document.querySelector('[name=email]').value;
+ if (!email) { alert('请先输入邮箱'); return; }
+ const res = await fetch('/sendVerification', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ email })
});
- document.getElementById('regForm')
- .addEventListener('submit', async e => {
- e.preventDefault();
- const form = new URLSearchParams(new FormData(e.target));
- const res = await fetch('/register', {
- method: 'POST',
- body: form
- });
- const json = await res.json();
- alert(json.msg);
- if (res.ok && json.code===0) {
- window.location.href='login.html';
- }
+ const json = await res.json();
+ alert(json.msg || json.error);
+ });
+
+ // 提交注册
+ document.getElementById('regForm').addEventListener('submit', async e => {
+ e.preventDefault();
+ const data = {
+ username: e.target.username.value,
+ email: e.target.email.value,
+ verificationCode: e.target.verificationCode.value,
+ password: e.target.password.value,
+ identificationNumber: e.target.identificationNumber.value
+ };
+ const res = await fetch('/register', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(data)
});
+ const json = await res.json();
+ alert(json.msg || json.error);
+ if (res.ok && json.code === 0) {
+ window.location.href = 'login.html';
+ }
+ });
</script>
</body>
</html>
diff --git a/src/main/resources/static/reset.html b/src/main/resources/static/reset.html
new file mode 100644
index 0000000..7a87ccf
--- /dev/null
+++ b/src/main/resources/static/reset.html
@@ -0,0 +1,61 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+ <meta charset="UTF-8">
+ <title>重置密码</title>
+</head>
+<body>
+<h2>重置密码</h2>
+<form id="resetForm">
+ <div>
+ <label>邮箱:</label>
+ <input type="email" name="email" required>
+ <button type="button" id="sendResetCode">获取重置验证码</button>
+ </div>
+ <div>
+ <label>验证码:</label>
+ <input type="text" name="code" required>
+ </div>
+ <div>
+ <label>新密码:</label>
+ <input type="password" name="newPassword" required>
+ </div>
+ <button type="submit">重置密码</button>
+</form>
+
+<script>
+ // 发送重置验证码
+ document.getElementById('sendResetCode').addEventListener('click', async () => {
+ const email = document.querySelector('[name=email]').value;
+ if (!email) { alert('请先输入邮箱'); return; }
+ const res = await fetch('/sendResetCode', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ email })
+ });
+ const json = await res.json();
+ alert(json.msg || json.error);
+ });
+
+ // 提交重置请求
+ document.getElementById('resetForm').addEventListener('submit', async e => {
+ e.preventDefault();
+ const data = {
+ email: e.target.email.value,
+ code: e.target.code.value,
+ newPassword: e.target.newPassword.value
+ };
+ const res = await fetch('/resetPassword', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(data)
+ });
+ const json = await res.json();
+ alert(json.msg || json.error);
+ if (res.ok && json.code === 0) {
+ window.location.href = 'login.html';
+ }
+ });
+</script>
+</body>
+</html>