试题

1、 服务端记录404的错误

使用拦截器,在请求返回时进行拦截

/**

* 在调用controller具体方法后拦截

*/

@Autowired

private ExceptionLogServicelogService;

@Override

public void postHandle(HttpServletRequest request, HttpServletResponse response, Object object, ModelAndView modelAndView)throws Exception {

int status = response.getStatus();

if (status ==404 && !request.getRequestURI().contains("error")) {

ExceptionLog exceptionLog =new ExceptionLog();

//设置404异常信息

        long happenTime = System.currentTimeMillis();

        exceptionLog.setExcId(UUID.randomUUID().toString());

        exceptionLog.setExcName("404");

        exceptionLog.setOperUri(request.getRequestURI());

        exceptionLog.setOperCreateTime(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));

        log.info("当前的异常是:404");

        logService.insert(exceptionLog);

  }

}
image.png

2、 服务器记录Jackson解析的非法字符的错误。如用户输入的Json信息是

http://localhost:8083/jeecg/user/testValidate
{
"fileid": “”,
"md5": ""
}在服务端记录, 用户输入的fileid中包含非法字符

这个过程就遇到了一个问题:ServletRequest的getReader()和getInputStream()两个方法只能被调用一次,而且不能两个都调用。那么如果Filter中调用了一次,在Controller里面就不能再调用了。HttpServletRequestWrapper把request保存下来,然后通过过滤器把保存下来的request再填充进去,这样就可以多次读取request了。

public class RequestWrapper extends HttpServletRequestWrapper {
    private final String body;

    public RequestWrapper(HttpServletRequest request) {
        super(request);
        StringBuilder stringBuilder = new StringBuilder();
        BufferedReader bufferedReader = null;
        InputStream inputStream = null;
        try {
            inputStream = request.getInputStream();
            if (inputStream != null) {
                bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
                char[] charBuffer = new char[128];
                int bytesRead = -1;
                while ((bytesRead = bufferedReader.read(charBuffer)) > 0) {
                    stringBuilder.append(charBuffer, 0, bytesRead);
                }
            } else {
                stringBuilder.append("");
            }
        } catch (IOException ex) {

        } finally {
            if (inputStream != null) {
                try {
                    inputStream.close();
                }
                catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (bufferedReader != null) {
                try {
                    bufferedReader.close();
                }
                catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        body = stringBuilder.toString();
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {
        final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body.getBytes());
        ServletInputStream servletInputStream = new ServletInputStream() {
            @Override
            public boolean isFinished() {
                return false;
            }
            @Override
            public boolean isReady() {
                return false;
            }
            @Override
            public void setReadListener(ReadListener readListener) {
            }
            @Override
            public int read() throws IOException {
                return byteArrayInputStream.read();
            }
        };
        return servletInputStream;

    }

    @Override
    public BufferedReader getReader() throws IOException {
        return new BufferedReader(new InputStreamReader(this.getInputStream()));
    }

    public String getBody() {
        return this.body;
    }

}
package com.hyd.zcar.cms.Interceptor;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
@Slf4j
@WebFilter(urlPatterns = "/*", filterName = "channelFilter")
public class ChannelFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        //取Body数据
        RequestWrapper requestWrapper = new RequestWrapper(request);
        String body = requestWrapper.getBody();
        filterChain.doFilter(requestWrapper != null ? requestWrapper : request, servletResponse);

        HttpServletResponse response = (HttpServletResponse) servletResponse;

        log.info(body);
        

    }


    @Override
    public void destroy() {

    }
}

3、 所有的用户请求,请求参数等都在服务器被记录

4、 记录的时候,采用2种方式,一种是JPA到本地数据库,一种是Log日志

解决方法:通过aop来对标注了OperLog的注解进行切入,在后置通知中进行用户的操作记录,分别记录到数据库跟本地;当发生异常时,有异常通知来进行异常的记录。

    @Autowired
    private OperationLogService operationLogService;

    @Autowired
    private ExceptionLogService exceptionLogService;

    /**
     * 设置操作日志切入点 记录操作日志 在注解的位置切入代码
     */
    @Pointcut("@annotation(com.hyd.zcar.cms.utils.annotation.OperLog)")
    public void operLogPoinCut() {
    }

    /**
     * 正常返回通知,拦截用户操作日志,连接点正常执行完成后执行, 如果连接点抛出异常,则不会执行
     *
     * @param joinPoint 切入点
     * @param keys      返回结果
     */
    @AfterReturning(value = "operLogPoinCut()", returning = "keys")
    public void saveOperLog(JoinPoint joinPoint, Object keys) {
        // 获取RequestAttributes
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        // 从获取RequestAttributes中获取HttpServletRequest的信息
        HttpServletRequest request = (HttpServletRequest) requestAttributes
                .resolveReference(RequestAttributes.REFERENCE_REQUEST);

        OperationLog operlog = new OperationLog();
        try {
            operlog.setOperId(UUID.randomUUID().toString()); // 主键ID

            // 从切面织入点处通过反射机制获取织入点处的方法
            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
            // 获取切入点所在的方法
            Method method = signature.getMethod();
            // 获取操作
            OperLog opLog = method.getAnnotation(OperLog.class);
            if (opLog != null) {
                String operModul = opLog.operModul();
                String operType = opLog.operType();
                String operDesc = opLog.operDesc();
                operlog.setOperModul(operModul); // 操作模块
                operlog.setOperType(operType); // 操作类型
                operlog.setOperDesc(operDesc); // 操作描述
            }
            // 获取请求的类名
            String className = joinPoint.getTarget().getClass().getName();
            // 获取请求的方法名
            String methodName = method.getName();
            methodName = className + "." + methodName;

            operlog.setOperMethod(methodName); // 请求方法

            // 请求的参数
            Map<String, String> rtnMap = converMap(request.getParameterMap());
            // 将参数所在的数组转换成json
            String params = JSON.toJSONString(rtnMap);

            operlog.setOperRequParam(params); // 请求参数
            operlog.setOperRespParam(JSON.toJSONString(keys)); // 返回结果
            operlog.setOperUri(request.getRequestURI()); // 请求URI
            operationLogService.insert(operlog);
            log.info("当前的记录是:"+methodName+params);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 异常返回通知,用于拦截异常日志信息 连接点抛出异常后执行
     *
     * @param joinPoint 切入点
     * @param e         异常信息
     */
    @AfterThrowing(pointcut = "operExceptionLogPoinCut()", throwing = "e")
    public void saveExceptionLog(JoinPoint joinPoint, Throwable e) {
        // 获取RequestAttributes
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        // 从获取RequestAttributes中获取HttpServletRequest的信息
        HttpServletRequest request = (HttpServletRequest) requestAttributes
                .resolveReference(RequestAttributes.REFERENCE_REQUEST);

        ExceptionLog excepLog = new ExceptionLog();
        try {
            // 从切面织入点处通过反射机制获取织入点处的方法
            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
            // 获取切入点所在的方法
            Method method = signature.getMethod();
            excepLog.setExcId(UUID.randomUUID().toString());
            // 获取请求的类名
            String className = joinPoint.getTarget().getClass().getName();
            // 获取请求的方法名
            String methodName = method.getName();
            methodName = className + "." + methodName;
            // 请求的参数
            Map<String, String> rtnMap = converMap(request.getParameterMap());
            // 将参数所在的数组转换成json
            String params = JSON.toJSONString(rtnMap);
            excepLog.setExcRequParam(params); // 请求参数
            excepLog.setOperMethod(methodName); // 请求方法名
            excepLog.setExcName(e.getClass().getName()); // 异常名称
            excepLog.setExcMessage(stackTraceToString(e.getClass().getName(), e.getMessage(), e.getStackTrace())); // 异常信息
            excepLog.setOperUri(request.getRequestURI()); // 操作URI


            exceptionLogService.insert(excepLog);
            log.info("当前的异常是:"+e.getClass().getName()+excepLog.getExcMessage());
        } catch (Exception e2) {
            e2.printStackTrace();
        }

    }

结果如下图所示:
操作:

image.png

异常:
image.png

5、 记录返回的信息,采用国际化的返回方式,设置示例,中文及英文

后端接收到一个请求后,需要返回一个提示信息,而此时我们可以使这个返回信息支持国际化,这里就用到了org.springframework.context.MessageSource接口,MessageSource提供了三个方法

@Nullable//参数字段可为空
String getMessage(String var1, @Nullable Object[] var2, @Nullable String var3, Locale var4);

String getMessage(String var1, @Nullable Object[] var2, Locale var3) throws NoSuchMessageException;

String getMessage(MessageSourceResolvable var1, Locale var2) throws NoSuchMessageException;
  • String getMessage(String var1, @Nullable Object[] var2, @Nullable String var3, Locale var4):用来从MessageSource获取消息的基本方法。如果在指定的locale中没有找到消息,则使用默认的消息。var2中的参数将使用标准类库中的MessageFormat来作消息中替换值。

  • String getMessage(String code, Object[] args, Locale loc):本质上和上一个方法相同,其区别在:没有指定默认值,如果没找到消息,会抛出一个NoSuchMessageException异常。

  • String getMessage(MessageSourceResolvable resolvable, Locale locale):上面方法中所使用的属性都封装到一个MessageSourceResolvable实现中,而本方法可以指定MessageSourceResolvable实现。

这里首先我们需要获取到当前请求的Locale,有两种方法:

Locale locale = LocaleContextHolder.getLocale();
Locale locale = RequestContextUtils.getLocale(request);

SpringBoot自动配置好了管理国际化资源文件的组件;

@ConfigurationProperties(prefix = "spring.messages")
public class MessageSourceAutoConfiguration {
  private String basename = "messages";      
    //我们的配置文件可以直接放在类路径下叫messages.properties;
    ...
}
spring:
  messages:
    basename: i18N.message #修改默认路径

运行结果如下图所示:

image.png

6、 错误的分类定义,你有什么建议,在代码中进行示例

7、 记录返回给客户端的时候,应包含哪些信息,方便未来的故障排查。在代码中进行示例。

自己定义合适的异常,再交由异常处理器去处理,返回给前端封装好的处理结果。针对于不同的异常,返回不同的错误码。

public class CustomerException extends RuntimeException {
    private static final long serialVersionUID = 1L;

    public CustomerException(String message) {
        super(message);
    }
}
@RestControllerAdvice
@Slf4j
public class DemoExceptionHandler {
    @ExceptionHandler(CustomerException.class)
    public Result<?> customerException(CustomerException e) {
        log.error(e.getMessage(), e);
        return Result.custom("自定义异常处理");
    }
}
package com.example.demo;

/*import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;*/

import lombok.Data;

import java.io.Serializable;

//import org.jeecg.common.constant.CommonConstant;

/**
 *   接口返回数据格式
 * @author scott
 * @email jeecgos@163.com
 * @date  2019年1月19日
 */
@Data
//@ApiModel(value="接口返回对象", description="接口返回对象")
public class Result<T> implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * 成功标志
     */
    //@ApiModelProperty(value = "成功标志")
    private boolean success = true;

    /**
     * 返回处理消息
     */
    //@ApiModelProperty(value = "返回处理消息")
    private String message = "操作成功!";

    /**
     * 返回代码
     */
    //@ApiModelProperty(value = "返回代码")
    private Integer code = 0;
    private Long frontBackUnicode ;
    
    /**
     * 返回数据对象 data
     */
    //@ApiModelProperty(value = "返回数据对象")
    private T result;
    
    /**
     * 时间戳
     */
    //@ApiModelProperty(value = "时间戳")
    private long timestamp = System.currentTimeMillis();

    public Result() {
        
    }
    
    public static   Result<Object> custom(String message) {
        Result<Object> r = new Result<Object>();
        r.message = message;
        r.code = 9999;
        r.success = false;
        return r;
    }
}

结果展示:

image.png

8、 Download文件中,浏览器在本地能另存为。在本次http的协议字段,需要解释

  • Content-Type 实体头部用于指示资源的MIME类型 media type 。

    在响应中,Content-Type标头告诉客户端实际返回的内容的内容类型。浏览器会在某些情况下进行MIME查找,并不一定遵循此标题的值; 为了防止这种行为,可以将标题 X-Content-Type-Options 设置为 nosniff。

    在请求中 (如POST 或 PUT),客户端告诉服务器实际发送的数据类型。

    语法:

Content-Type: text/html; charset=utf-8Content-Type: multipart/form-data; boundary=something
  • content-disposition响应头控制浏览器以下载的形式打开文件
    Content-Disposition 响应头指示回复的内容该以何种形式展示,是以内联的形式(即网页或者页面的一部分),还是以附件的形式下载并保存到本地。

    在multipart/form-data类型的应答消息体中, Content-Disposition 消息头可以被用在multipart消息体的子部分中,用来给出其对应字段的相关信息。各个子部分由在Content-Type 中定义的分隔符分隔。用在消息体自身则无实际意义。

    Content-Disposition消息头最初是在MIME标准中定义的,HTTP表单及POST 请求只用到了其所有参数的一个子集。只有form-data以及可选的namefilename三个参数可以应用在HTTP场景中。

    在HTTP场景中,第一个参数或者是inline(默认值,表示回复中的消息体会以页面的一部分或者整个页面的形式展示),或者是attachment(意味着消息体应该被下载到本地;大多数浏览器会呈现一个“保存为”的对话框,将filename的值预填为下载后的文件名,假如它存在的话)。

9、 从Http协议字段解释一下,跨域的时候,客户端涉及的字段,服务器涉及的字段的含义,以及它的作业原理。

预检请求
首先,预检请求只会存在于浏览器端的请求中。根据浏览器的同源策略,就是出于安全考虑,浏览器会限制从脚本发起的跨域HTTP请求。
像XMLHttpRequest和Fetch都遵循同源策略。
浏览器限制跨域请求一般有两种方式:
0.浏览器限制发起跨域请求

1.跨域请求可以正常发起,但是返回的结果被浏览器拦截了

在浏览器请求中,HTTP请求包括: 简单请求 和 需预检的请求
简单请求
简单请求不会触发CORS预检请求,简单请求术语并不属于Fetch(其中定义了CORS 关于Fetch:Fetch和Ajax)规范。
若满足所有下述条件,则该请求可视为“简单请求”:
请求Method满足GET,POST,HEAD中的一种,并且Content-Type满足text/plain,
multipart/form-data,
application/x-www-form-urlencoded中的一种,就可以称为简单请求,还要求这个请求没有设置自定义请求头!

WebKit Nightly 和 Safari Technology Preview 为Accept
, Accept-Language
, 和 Content-Language
首部字段的值添加了额外的限制。如果这些首部字段的值是“非标准”的,WebKit/Safari 就不会将这些请求视为“简单请求”。WebKit/Safari 并没有在文档中列出哪些值是“非标准”的,不过我们可以在这里找到相关讨论:Require preflight for non-standard CORS-safelisted request headers Accept, Accept-Language, and Content-Language, Allow commas in Accept, Accept-Language, and Content-Language request headers for simple CORS, and Switch to a blacklist model for restricted Accept headers in simple CORS requests。其它浏览器并不支持这些额外的限制,因为它们不属于规范的一部分。

需预检的请求

“需预检的请求”要求必须首先使用OPTIONS方法发起一个预检请求到服务区,以获知服务器是否允许该实际请求。“预检请求”的使用,可以避免跨域请求对服务器的用户数据产生未预期的影响。
非简单请求都是需预检的请求,包括:

0.使用下列请求Method:
PUT,DELETE,CONNET,OPTIONS,TARCE,PATCH

1.使用人为设置了对CORS 安全的首部字段集合之外的其他首部字段。安全首部字段包括:
Accept,Accpet-Language,Content-Language,Content-Type,DPR,DownLink,Save-Data,Viewport-Width,Width

2.Content-Type不在text/plain,
multipart/form-data,
application/x-www-form-urlencoded中的。

 Origin: http://foo.example
Access-Control-Request-Method: POST
Access-Control-Request-Headers: stoken, Content-Type

首部字段Origin后知服务器该请求来源为http:foo/example,
首部字段 Access-Control-Request-Method 告知服务器,实际请求将使用 POST 方法。首部字段 Access-Control-Request-Headers 告知服务器,实际请求将携带两个自定义请求首部字段:stoken 与 Content-Type。服务器据此决定,该实际请求是否被允许。
接下来看预检请求的响应。

Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: stoken, Content-Type
Access-Control-Max-Age: 86400

看到前面对请求字段的解释,响应字段的含义也就很明了了。而这些响应头的设置都是需要我们后端程序员在服务器端设置的。
Access-Control-Allow-Origin:* 表明服务器端运行来源于任何站点的请求,这个是一定要设置的,如果不进行设置的话,表明服务器不允许来自任何站点的请求,导致浏览器无法进行正式请求。我们这里把这个响应头设置为*,代表运行任何站点,但从安全角度来看,最好是设置为特定的站点。Access-Control-Max-Age:86400的含义:表明该响应的有效时间为 86400 秒,也就是24小时。在有效时间内,浏览器无须为同一请求再次发起预检请求。请注意,浏览器自身维护了一个最大有效时间,如果该首部字段的值超过了最大有效时间,将不会生效。

服务端设置自定义Header响应头
当遇到自定义Header请求的时候,我们在服务器端可以通过设置Access-Control-Allow-Headers来解决跨域问题。那么如果我们是想设置自定义响应头呢,该怎么办呢?
首先,我们看服务端没有做任何处理的情况。当服务端没有做处理的时候,前端想要获取服务端设置的自定义Header的时候,会报出如下错误:
Refused to get unsafe header "stoken"
原因1:W3C的 xhr 标准中做了限制,规定客户端无法获取 response 中的 Set-Cookie、Set-Cookie2这2个字段,无论是同域还是跨域请求;
原因2:W3C 的 cors 标准对于跨域请求也做了限制,规定对于跨域请求,客户端允许获取的response header字段只限于“simple response header”和“Access-Control-Expose-Headers”

“simple response header”包括的 header 字段有:Cache-Control,Content-Language,Content-Type,Expires,Last-Modified,Pragma;

很明显,自定义header不在simple response header中。

“Access-Control-Expose-Headers”:首先得注意是”Access-Control-Expose-Headers”进行跨域请求时响应头部中的一个字段,对于同域请求,响应头部是没有这个字段的。这个字段中列举的 header 字段就是服务器允许暴露给客户端访问的字段。

Spring MVC在4.2及之后的版本中提供一个注解@CorsOrigin,很方便的对细粒度的跨域问题进行处理。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,451评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,172评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,782评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,709评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,733评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,578评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,320评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,241评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,686评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,878评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,992评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,715评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,336评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,912评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,040评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,173评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,947评论 2 355

推荐阅读更多精彩内容

  • 前端开发面试题 面试题目: 根据你的等级和职位的变化,入门级到专家级,广度和深度都会有所增加。 题目类型: 理论知...
    怡宝丶阅读 2,584评论 0 7
  • 面试题一:https://github.com/jimuyouyou/node-interview-questio...
    R_X阅读 1,623评论 0 5
  • 包含的重点内容:JAVA基础JVM 知识开源框架知识操作系统多线程TCP 与 HTTP架构设计与分布式算法数据库知...
    消失er阅读 4,327评论 1 10
  • 单精度浮点数的精度为1/2的24次方,或1/16777216,或百万分之六。这意味着在单精度浮点格式下,16777...
    皮卡丘_83e1阅读 336评论 0 0
  • 穷游风刮不停,有钱没钱去旅行。 但现在,纯粹的穷游时代已经过去了,只有边旅行边挣钱才是王道。 不过,想边旅行边挣钱...
    九天玄女_e992阅读 265评论 0 0