从 0 到 1 认识 Spring MVC:核心思想与基本用法(下)

文章目录

    • 📕4. 响应
        • ✏️4.1 返回静态页面
        • ✏️4.2 返回数据@ResponseBody​
        • ✏️4.3 返回HTML代码片段​
        • ✏️4.4 返回JSON
        • ✏️4.5 设置状态码
        • ✏️4.6 设置Header(了解)
    • 📕5. 案例练习
        • ✏️5.1 加法计算器
        • ✏️5.2 用户登录
        • ✏️5.3 留言板
    • 📕6. lombok介绍
    • 📕7. 应用分层
        • ✏️7.1 使用三层架构进行分层
        • ✏️7.2 应用分层的好处

上接:从 0 到 1 认识 Spring MVC:核心思想与基本用法(上)

📕4. 响应

在我们前面的代码例子中,都已经设置了响应数据,Http响应结果可以是数据,也可以是静态页面,也可以针对响应设置状态码,Header信息等。

✏️4.1 返回静态页面

创建前端页面 index.html(注意路径)​
在这里插入图片描述

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>Index页面</title>
</head>
<body>Hello,Spring MVC,我是Index页面.
</body>
</html>
@RestController
public class indexController(){@RequestMapping("/index")public String index(){return "/index.html";}
}

在这里插入图片描述
结果却发现, 页面未正确返回, http响应把"/index.html" 当做了http响应正文的数据​ 那Spring MVC如何才能识别出来 index.html 是一个静态页面, 并进行返回呢?​

我们需要把 @RestController 改为 @Controller

@RestController 和 @Controller 有着什么样的关联和区别呢?​

之前我们介绍了MVC模式,后端会返回视图,这是早期时的概念​。随着互联网的发展,目前项目开发流行"前后端分离"模式, Java主要是用来做后端项目的开发,所以也就 不再处理前端相关的内容了。MVC的概念也逐渐发生了变化,View不再返回视图,而是返回显示视图时需要的数据。如果想返回视图的话, 只需要把@ResponseBody 去掉就可以了, 也就是@Controller ​

所以前面使用的@RestController 其实是返回的数据.​ @RestController = @Controller + @ResponseBody
@Controller : 定义一个控制器, Spring 框架启动时加载, 把这个对象交给Spring管理.​
@ResponseBody : 定义返回的数据格式为非视图, 返回一个 text/html 信息​

✏️4.2 返回数据@ResponseBody​
@Controller
@ResponseBody
public class indexController(){@RequestMapping("/index")public String index(){return "/index.html";}
}

加上@ResponseBody 注解, 该方法就会把 “/index.html” 当做一个数据返回给前端.​

在这里插入图片描述

@ResponseBody 既是类注解,又是方法注解​。
如果作用在类上,表示该类的所有方法,返回的都是数据,如果作用在方法上,表示该方法返回的是数据。也就是说: 在类上添加@ResponseBody 就相当于在所有的方法上添加了@ResponseBody 注解。同样,如果类上有@RestController 注解时 : 表示所有的方法上添加了@ResponseBody 注解,也就是当前类下所有的方法返回值做为响应数据​。

✏️4.3 返回HTML代码片段​

后端返回数据时, 如果数据中有HTML代码, 也会被浏览器解析​

@RestMapping("/return/html")
@ReponseBody
public class returnHtml(){public String return(){return "<h1>Hello,HTML~<h1>";}
}

在这里插入图片描述
通过Fiddler观察响应结果, Content-Type 为text/html

响应中的 Content-Type 常见取值有以下几种:​

  1. text/html : body 数据格式是 HTML​
  2. text/css : body 数据格式是 CSS​
  3. application/javascript : body 数据格式是 JavaScript​
  4. application/json : body 数据格式是 JSON​
✏️4.4 返回JSON

Spring MVC 也可以返回JSON​,后端方法返回结果为对象。

@RequestMapping("/m11")public Map<String,String> m11(){Map<String,String> hash = new HashMap<>();hash.put("name","zhuxulong");hash.put("name1","lijiufang");return hash;}

在这里插入图片描述
通过Fiddler观察响应结果, Content-Type 为 application/json

✏️4.5 设置状态码

Spring MVC会根据我们方法的返回结果自动设置响应状态码, 程序员也可以手动指定状态码​。通过Spring MVC的内置对象HttpServletResponse 提供的方法来进行设置​。

@RequestMapping("/m12")public String m12(HttpServletResponse response){response.setStatus(401);return "yes";}

状态码不影响页面的展示
我们通过Proxyman来抓包发现状态码是401
这里是引用

✏️4.6 设置Header(了解)

Http响应报头也会向客户端传递一些附加信息,比如服务程序的名称,请求的资源已移动到新地址等,如: Content-Type,Local等。这些信息通过@RequestMapping 注解的属性来实现。

设置Content-Type​

 @RequestMapping(value = "/m13",produces = "application/json")public String m13(){return "{\"success\":true}";}

如果不设置produces , 方法返回结果为String时, Spring MVC默认返回类型, 是text/html.​

设置其他Header

设置其他Header的话, 需要使用Spring MVC的内置对象HttpServletResponse 提供的方法来进行设置​

@RequestMapping(value = "/m14")public String m14(HttpServletResponse response){response.setHeader("myHeader","hahaha");return "yes";}

void setHeader(String name, String value) 设置一个带有给定的名称和值的 header. 如果 name 已经存在, 则覆盖旧的值.​

在这里插入图片描述
通过Proxyman来查看我们设置的结果

📕5. 案例练习

✏️5.1 加法计算器

需求

输入两个整数, 点击"点击相加"按钮, 显示计算结果

定义接口

请求路径 : calc/sum
请求方式 : GET/POST
接口描述 : 计算两个整数相加

请求参数

参数名​类型​是否必须​备注​
num1​Integer​是​参与计算的第一个数​
num2​Integer​是​参与计算的第二个数​

响应数据

Content-Type : text/html
响应内容 : 计算机计算结果:

前端代码

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title>
</head>
<body>
<form action="calc/sum" method="post"><h1>计算器</h1>数字1:<input name="num1" type="text"><br>数字2:<input name="num2" type="text"><br><input type="submit" value=" 点击相加 ">
</form>
</body></html>

后端代码

package xulong.com;import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;@RestController
@RequestMapping("/calc")
public class CalcController {@RequestMapping("/sum")public String add(Integer num1,Integer num2){if(num1==null||num2==null){return "输入不合法";}return "计算机计算结果:"+(num1+num2);}
}
✏️5.2 用户登录

1.校验接口
需求

用户输入账号和密码, 后端进行校验密码是否正确​

  1. 如果不正确, 前端进行用户告知​
  2. 如果正确, 跳转到首页. 首页显示当前登录用户​
  3. 后续再访问首页, 可以获取到登录用户信息​

定义接口

请求路径 : /user/login
请求方式 : POST
接口描述 : 校验账号密码是否正确

请求参数

参数名​类型​是否必须​备注​
userName​String是​校验的账号​
passwordString​是​校验的密码​

响应数据

Content-Type : text/html
响应内容 : true //账号密码验证成功 false//账号密码验证失败

2.查询接口

定义接口

请求路径 : /user/getLoginUser
请求方式 : GET
接口描述 : 查询当前登录的用户

响应数据

Content-Type : text/html
响应内容 : zhangsan

===================================================================================

后端代码

package xulong.com.Login;import jakarta.servlet.http.HttpSession;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;@RestController
@RequestMapping("/user")
public class LoginController {@RequestMapping("/login")public boolean login(String userName, String password, HttpSession session ){if(!StringUtils.hasLength(userName) || !StringUtils.hasLength(password)){return false;}if("zhangsan".equals(userName)&&"123456".equals(password)){session.setAttribute("username",userName);return true;}return false;}@RequestMapping("/getLoginUser")public String getLoginUser(HttpSession session){String username = (String)session.getAttribute("username");if(StringUtils.hasLength(username)) {return username;}return "";}}

前端代码

<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><title>登录页面</title>
</head><body><h1>用户登录</h1>用户名:<input name="userName" type="text" id="userName"><br>密码:<input name="password" type="password" id="password"><br><input type="button" value="登录" onclick="login()"><script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.4/jquery.min.js"></script><script>function login() {$.ajax({type: "post",url: "/user/login",data: {"userName": $("#userName").val(),"password": $("#password").val()},success: function (result) {if (result) {location.href = "/index.html"} else {alert("账号或密码有误.");}}});}</script>
</body></html>
✏️5.3 留言板

需求

  1. 输入留言信息, 点击提交. 后端把数据存储起来.
  2. 页面展示输入的表白墙的信息

接口定义

  1. 获取全部留言
    请求:​GET /message/getList
    响应: JSON 格式​
    浏览器给服务器发送一个 GET /message/getList 这样的请求, 就能返回当前一共有哪些留言 记录. 结果以 json 的格式返回过来.
  1. 发表新留言
    请求: body 也为 JSON 格式.​
    响应: JSON 格式.​
    我们期望浏览器给服务器发送一个 POST /message/publish 这样的请求, 就能把当前的留言提 交给服务器.​

前端代码

<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>留言板</title><style>.container {width: 350px;height: 300px;margin: 0 auto;/* border: 1px black solid; */text-align: center;}.grey {color: grey;}.container .row {width: 350px;height: 40px;display: flex;justify-content: space-between;align-items: center;}.container .row input {width: 260px;height: 30px;}#submit {width: 350px;height: 40px;background-color: orange;color: white;border: none;margin: 10px;border-radius: 5px;font-size: 20px;}</style>
</head><body><div class="container"><h1>留言板</h1><p class="grey">输入后点击提交, 会将信息显示下方空白处</p><div class="row"><span>谁:</span> <input type="text" name="" id="from"></div><div class="row"><span>对谁:</span> <input type="text" name="" id="to"></div><div class="row"><span>说什么:</span> <input type="text" name="" id="say"></div><input type="button" value="提交" id="submit" onclick="submit()"><!-- <div>A 对 B 说: hello</div> --></div><script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.4/jquery.min.js"></script><script>load();function load() {$.ajax({type: "get",url: "/message/getList",success: function (result) {for (var message of result) {var divE = "<div>" + message.from + "对" + message.to + "说:" +message.message + "</div>";$(".container").append(divE);}}});}function submit() {//1. 获取留⾔的内容var from = $('#from').val();var to = $('#to').val();var say = $('#say').val();if (from == '' || to == '' || say == '') {return;}$.ajax({type: "post",url: "/message/publish",data: {from: from,to: to,message: say},success: function (result) {if (result) {//2. 构造节点var divE = "<div>" + from + "对" + to + "说:" + say + "</div>";//3. 把节点添加到⻚⾯上$(".container").append(divE);//4. 清空输⼊框的值$('#from').val("");$('#to').val("");$('#say').val("");} else {alert("发表留⾔失败!");}}});}</script>
</body></html>

后端代码

package xulong.com.Message;import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import java.util.ArrayList;
import java.util.List;@RestController
@RequestMapping("/message")
public class MessageController {List<MessageInfo> list = new ArrayList<>();@RequestMapping("/getList")public List<MessageInfo> getList(){return list;}@RequestMapping("/publish")public boolean punish(@RequestBody MessageInfo messageInfos){if(StringUtils.hasLength(messageInfos.getFrom())&&StringUtils.hasLength(messageInfos.getTo())&&StringUtils.hasLength(messageInfos.getMessage())){list.add(messageInfos);return true;}return false;}
}

📕6. lombok介绍

Lombok是一个Java工具库,通过添加注解的方式,简化Java的开发.​

引入依赖

<dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional>
</dependency>

使用

lombok通过一些注解的方式, 可以帮助我们消除一些冗长代码, 使代码看起来简洁一些​
比如之前的Person对象 就可以改为​

@Data
public class Person {private int id;private String name;private String password;
}

@Data 注解会帮助我们自动一些方法, 包含getter/setter, equals, toString等

更多使用

如果觉得@Data比较粗暴(生成方法太多), lombok也提供了一些更精细粒度的注解​​

注解​作用​
@Getter ​自动添加 getter 方法​
@Setter ​自动添加 setter 方法​
@ToString ​自动添加 toString 方法​
@EqualsAndHashCode ​自动添加 equals 和 hashCode 方法​
@NoArgsConstructor ​自动添加无参构造方法​
@AllArgsConstructor ​自动添加全属性构造方法,顺序按照属性的定义顺序​
@NonNull ​属性不能为 null​
@RequiredArgsConstructor​自动添加必需属性的构造方法,final + @NonNull 的属性为 必需 ​

@Data = @Getter + @Setter + @ToString + @EqualsAndHashCode + @RequiredArgsConstructor + @NoArgsConstructor ​

📕7. 应用分层

通过上面的练习,我们学习了Spring MVC简单功能的开发,但是我们也发现了一些问题​,目前我们程序的代码有点"杂乱",然而当前只是"一点点功能"的开发。如果我们把整个项目功能完成呢? 代码会更加的"杂乱无章"(文件乱, 代码内容乱)​。也基于此, 咱们接下来学习应用分层.​

为什么需要应用分层?​

在最开始的时候,为了让项目快速上线,我们通常是不考虑分层的。但是随着业务越来越复杂,大量的代码混在一起,会出现逻辑不清晰、各模块相互依赖、代码扩展性差、改动一处就牵一发而动全身等问题。所以学习对项目进行分层就是我们程序员的必修课了。

类似公司的组织架构
公司初创阶段, 一个人身兼数职, 既做财务, 又做人事, 还有行政.​
随着公司的逐渐壮大, 会把岗位进行细分, 划分为财务部门, 人事部门, 行政部门等. 各个部门内部还会 再进行细分.​
项目开发也是类似, 最开始功能简单时, 我们前后端放在一起开发, 随着项目功能的复杂, 我们分为前 端和后端不同的团队, 甚至更细粒度的团队. 后端开发也会根据功能再进行细分. MVC就是其中的一种 拆分方式.​
但是随着后端人员不再涉及前端, 后端开发又有了新的分层方式.​

✏️7.1 使用三层架构进行分层

之前介绍的 “MVC”,就是把整体的系统分成了 Model(模型),View(视图)和Controller (控制器)三个层次,也就是将用户视图和业务处理隔离开,并且通过控制器连接起来,很好地实现了表现和逻辑的解耦,是一种标准的软件分层架构。

在这里插入图片描述

目前现在更主流的开发方式是 “前后端分离” 的方式,后端开发工程师不再需要关注前端的实现, 所以对 于Java后端开发者,又有了一种新的分层架构:把整体架构分为表现层、业务逻辑层和数据层。这种分层方式也称之为"三层架构"。

  1. 表现层: 就是展示数据结果和接受用户指令的,是最靠近用户的一层。​
  2. 业务逻辑层:负责处理业务逻辑,里面有复杂业务的具体实现。
  3. 数据层:负责存储和管理与应用程序相关的数据​

按照上面的层次划分, Spring MVC 站在后端开发人员的角度上, 也进行了支持, 把上面的代码划分为三 个部分: ​
在这里插入图片描述

在这里插入图片描述

✏️7.2 应用分层的好处
  1. 降低层与层之间的依赖, 结构更加的明确, 利于各层逻辑的复用​
  2. 开发人员可以只关注整个结构中的其中某一层, 极大地降低了维护成本和维护时间​
  3. 可以很容易的用新的实现来替换原有层次的实现
  4. 有利于标准化

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/pingmian/91874.shtml
繁体地址,请注明出处:http://hk.pswp.cn/pingmian/91874.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Python-初学openCV——图像预处理(五)——梯度处理、边缘检测、图像轮廓

目录 一、图像梯度处理 1、垂直边缘提取 2、Sobel算子 3、Laplacian算子 二、图像边缘检测 1、高斯滤波 2、计算图像的梯度、方向 3、非极大值抑制 4、双阈值筛选 三、绘制图像轮廓 1、概念 2、寻找轮廓 3、绘制轮廓 一、图像梯度处理 还记得高数中的一阶导数求极值…

【Redis】安装Redis,通用命令,常用数据结构,单线程模型

目录 一.在Ubuntu系统安装Redis 二. redis客户端介绍 三. 全局命令 3.1.GET和SET命令 3.2.KEYS&#xff08;生产环境禁止使用&#xff09; 3.3.EXISTS 3.4.DEL 3.5.EXPIRE 3.6.TTL 3.6.1.Redis的过期策略 3.6.2.基于优先级队列/堆的实现去实现定时器 3.6.3.定时器&a…

ubuntu22.04系统实践 linux基础入门命令(三) 用户管理命令

以下有免费的4090云主机提供ubuntu22.04系统的其他入门实践操作 地址&#xff1a;星宇科技 | GPU服务器 高性能云主机 云服务器-登录 相关兑换码星宇社区---4090算力卡免费体验、共享开发社区-CSDN博客 之所以推荐给大家使用&#xff0c;是因为上面的云主机目前是免费使用的…

DPDK中的TCP头部处理

1. TCP头部结构 TCP头部通常为20字节&#xff08;不含可选字段&#xff09;&#xff0c;每个字段占据固定的字节位置。以下是TCP头部的结构&#xff0c;按字节位置逐一说明&#xff1a;0 1 2 30 1 2 3 4 5 6 7 8 9 0 1 …

开源在线客服系统Chatwoot配置文件

参考&#xff1a; https://developers.chatwoot.com/self-hosted/deployment/dockerhttps://developers.chatwoot.com/self-hosted/deployment/docker 1、.env 配置文件 # Learn about the various environment variables at # https://www.chatwoot.com/docs/self-hosted/co…

PHP进阶语法详解:命名空间、类型转换与文件操作

掌握了PHP面向对象编程的基础后&#xff0c;就可以深入学习命名空间、类型转换、文档注释、序列化以及文件操作等重要概念。 1、命名空间&#xff08;Namespace&#xff09; 命名空间是PHP 5.3引入的重要特性&#xff0c;它解决了类名、函数名和常量名冲突的问题&#xff0c;使…

Webpack 搭建 Vue3 脚手架详细步骤

创建一个新的 Vue 项目 1&#xff09;初始化项目目录 新建一个文件夹&#xff0c;或者使用以下指令 mkdir webpack-vue_demo cd webpack-vue_demo2&#xff09;初始化 npm 项目 npm init -y3&#xff09;安装 vue 和 webpack 相关依赖 npm install vue vue-loader vue-template…

【Git 误操作恢复指南】

Git 误操作恢复指南 适用场景&#xff1a;git reset --hard 误操作后的紧急恢复 风险等级&#xff1a;&#x1f534; 高风险 - 可能导致代码丢失 恢复成功率&#xff1a;95%&#xff08;CI/CD 环境下&#xff09; &#x1f6a8; 紧急情况概述 问题描述 在项目开发过程中&am…

Go语言 逃 逸 分 析

逃逸分析是什么 逃逸分析是编译器用于决定变量分配到堆上还是栈上的一种行为。一个变量是在堆上分配&#xff0c;还是在栈上分配&#xff0c;是经过编译器的逃逸分析之后得出的“结论”。Go 语言里编译器的逃逸分析&#xff1a;它是编译器执行静态代码分析后&#xff0c…

LeetCode算法日记 - Day 1: 移动零、复写零

目录 1. 移动零 1.1 思路解析 1.2 代码实现 2. 复写零 2.1 思路解析 2.2 代码实现 1. 移动零 283. 移动零 - 力扣&#xff08;LeetCode&#xff09; 给定一个数组 nums&#xff0c;编写一个函数将所有 0 移动到数组的末尾&#xff0c;同时保持非零元素的相对顺序。 请…

Odoo:免费开源的医疗器械行业解决方案

开源智造Odoo专家团队深知&#xff0c;作为医疗器械制造商&#xff0c;您的成功取决于制造卓越产品的能力。您必须遵循严密控制的流程&#xff0c;开发和制造出达到最严格质量标准的产品。“开源智造Odoo医疗器械行业解决方案”是为医疗器械制造商设计的全球企业资源规划(ERP)软…

Redis键值对中值的数据结构

前言 前面我们已经介绍了Redis的键值对存储管理的底层数据结构。如果不清楚的同志可以看我前面的博客 Redis数据库存储键值对的底层原理-CSDN博客 下面,我们来看一下Redis键值对中值的数据结构有那些叭 Redis常见的5种数据类型 string …

MySQL自动化安装工具-mysqldeploy

功能 可在linux系统上安装 mysql5.5/5.6/5.7/8.0/8.4 版本的 MySQL&#xff0c;可以初始化多实例 MySQL。 码云: https://gitee.com/hh688/mysqldeploy guithub: https://github.com/hhkens/mysqldeploy 限制 仅在 centos7 环境进行测试&#xff0c;后期可能支持更多系统。 此程…

简要探讨大型语言模型(LLMs)的发展历史

关注大型语言模型(LLMs) 简要探讨语言模型的发展历史 理解Transformer架构的基本元素和注意力机制 了解不同类型的微调方法 语言模型的大小之分 在语言模型领域,“小”和“大”是相对概念。几年前还被视为“巨大”的模型,如今已被认为相当小。该领域发展迅猛,从参数规模为…

Java试题-选择题(2)

Java试题-选择题(2) 题目 下列语句创建对象的总个数是: String s=“a”+“b”+"c”+“d”+"e” A.4 B.2 C.3 D.1 关于下面的程序段的说法正确的是()? File file1=new File(“e:\xxx\yyy\zzz");file1.mkdir(); A.如目录e:\xxx\yyy\不存在,程序会抛出FileN…

揭秘动态测试:软件质量的实战防线

动态测试概述&#xff08;扩展版&#xff09; 目录 动态测试概述&#xff08;扩展版&#xff09; 一、动态测试的定义与重要性 ⚡ 二、动态测试类型 &#x1f50d; &#xff08;一&#xff09;功能测试 &#x1f9e9; &#xff08;二&#xff09;非功能测试 &#x1f4ca…

机器学习①【机器学习的定义以及核心思想、数据集:机器学习的“燃料”(组成和获取)】

文章目录先言一、什么是机器学习1.机器学习的定义以及核心思想2.机器学习的四大类型2.1监督学习&#xff08;Supervised Learning&#xff09;2.2半监督学习&#xff08;Midsupervised Learning&#xff09;2.3无监督学习&#xff08;Unsupervised Learning&#xff09;2.4强化…

GaussDB 数据库架构师(十二) 资源规划

1 硬件和软件要求 1&#xff09;硬件配置示例 硬件配置示例设备类型 设备型号 数量 备注 计算节点 CPU&#xff1a; 2*64 Cores&#xff0c;Kunpeng 920 内存&#xff1a;32*32GB 系统盘&#xff1a;2*960GB SATA SSD 数据盘&#xff1a;24*960GB SATA SSD RAID卡&#x…

Linux系统文件与目录内容检索(Day.2)

一、文件和目录内容检索处理命令1、uniq去重语法uniq [options] [input_file [output_file]]选项选项作用-c进行计数&#xff0c;并删除文件中重复出现的行-d仅显示连续的重复行-u仅显示出现一次的行-i忽略大小写案例1、删除输入文件中的重复行sort input.txt | uniq2、仅显示重…

如何选择一个容易被搜索引擎发现的域名?

在这个数字化时代&#xff0c;域名不仅是企业线上身份的标识&#xff0c;更是影响网站搜索曝光率的关键因素。一个精心挑选的域名能为品牌带来更多自然流量&#xff0c;下面我们就来探讨几个实用技巧。一、简洁易记是王道好域名首先要让人过目不忘。想象一下&#xff0c;当用户…