目录

  1. 概述
  2. 功能介绍
  3. 程序结构
  4. 服务器端介绍
  5. 客户端介绍
  6. “契约”
  7. Web API设计规则
  8. 并行写入冲突与时间戳
  9. 身份验证详解
  10. Web API验证规则
  11. 客户端MVVM简介
  12. Web.Config
  13. 本DEMO的一些问题

概述

我之前写的一些关于ASP.net Web API的博客中,得到了一些朋友的反响,我一直也想整理下代码贴出来供大家参考,但后来发觉从整个项目工程中单独把一部分代码剥离出来还真是不容易,一转眼就把这个事情忘记了,最近终于下定决心弄一弄,于是才有了此文,本DEMO虽然不完美,但已经包括了我目前所掌握的全部的关于WEB API的相关技术,至于有哪些地方还需要改进的,我会在文章末尾一一指出,由于Web API的服务器端是没有界面的,这样不容易演示,所以我还提供了一个用WPF写的客户端,先一睹为快:

功能介绍

下面是这套程序所要演示的相关的技术或功能的介绍:

服务器端:

  • 完整的,代码结构精良的(至少我这么认为)ASP.net Web API的服务器端
  • 使用DataAnnotations进行Model验证
  • 自定义Model验证
  • 分层(分开UI层和业务逻辑层)
  • 优异的日志记录方式
  • 安全系数很高的身份验证(好吧,我这么写是为了用激将法来引出高手给我挑挑毛病)
  • 敏感信息加密
  • 纯Web API代码(去除了css/js及不需要的视图引擎)

关于身份验证的思路,可以参考《如何实现RESTful Web API的身份验证

客户端:

  • 自行设计的MVVM简易框架
  • 大量的WPF实用技巧
  • 使用DataAnnotations进行客户端Model验证
  • HttpClient的完整示例

其它:

  • 自动对象映射(使用AutoMapper)
  • 独立运行,零配置,用Visual Studio 2010打开即用
  • 我已经尽量减少重复代码……(其实做得还是不太够)
  • 二进制文件的上传和下载

程序结构

本程序主要目的是做一个文档齐全,功能比较全面和零配置的demo,所以不涉及到DBMS的使用,尽管真正使用的时候DBMS几乎是必须的,但这次我就用一个XML来代替DBMS了。

程序分为两个部分,一是服务器端,另一是客户端,而且是分开成两个不同的solution,这样做完全是为了方便调试。但这样带来的问题是会产生一些重复的东西,比如这三个库:CommLib,WebApiKit,WebApiContract,它们是公共库,但又分别存在于不同的solution中,我在实际工作中是用了SVN这种工具来避免它们“改了这个忘了那个”的,而这次我用了一个自己很久以前写的工具来让它们“同步”:

这工具也会在本文后面提供下载。

服务器端介绍

服务器端的文件结构如图:

BLL - 业务逻辑层 UserInfo_BLL.cs - 就是用户信息类,后缀“BLL”表示它属于业务逻辑层,我习惯这样区分各个层面不同的Model UserManager.cs - 业务逻辑层的主类,提供各种“增删查改”的方法 CommLib - 公共库,包括DES加密类,MD5类,日志类,一些正则表达式,全局常量等等…… Server - ASP.net Web API的主工程 AutoMapperConfig.cs - 自动对象映射的配置类,比如将UserInfo_BLL直接转为UserInfo_API_Get,而不需要一个一个属性地赋值 WebApiConfig.cs - ASP.net Web API的路由配置类 AvatarsController.cs - 头像的获取、修改和删除 EntranceController.cs - 登录并获取自己的信息 PasswordController.cs - 修改自己的密码 UsersInfoController.cs - 获取单个用户信息、获取用户列表、修改用户信息、增加用户(未实现)和删除用户(未实现) ModelValidationFilter.cs - 针对所有请求的全局Model验证过滤器 WebApiAuthFilter.cs - 针对绝大部分(不排除有些地方不需要身份验证)的Controller的身份验证器 WebApiExceptionFilter.cs - 全局异常处理器 WebApiRoleFilter.cs - 针对某些Action的角色权限过滤器,比如某些动作只能管理员来做 GuidSet.cs - 用于防止重发攻击的Guid集合帮助类 WebApiPrincipal.cs - 登录用户的身份类 GlobalServerData.cs - 里面包括一个静态的GuidSet Managers.cs - 里面包括一个静态的UserManager WebApiContract - 就是用这个库来跟客户端“磋商”的 WebApiKit - 客户端/服务器端都能用到的一些工具

客户端介绍

客户端的项目结构图:

Client - 客户端的主工程 PasswordHelper.cs - 密码控件的帮助类,用于将密码控件的密码文本绑定到View Model,WPF出自于安全的需要默认不提供这种绑定支持 UIVisibleConverter.cs - 一些WPF界面用的转换器,用于根据View Model的一些属性来控制界面元素的显示与隐藏 ChangePassword_VM.cs - 修改密码界面用的View Model,后缀“VM”就是View Model的意思。 Login_VM.cs - 登录界面用的View Model。 UserInfo_VM.cs - 主界面上显示/修改用户信息用的View Model。 ViewModelBase.cs - 所有的View Model的基类,实现了INotifyPropertyChanged接口、IDataErrorInfo接口和一些帮助方法

“契约”

契约(Contract)这个词其实来自于Web Service,但Web Service是一套很重量级的技术,我个人并不不喜欢它。其实契约简单地说,就是:Web API如何用?契约中应该包括:调用地址是什么,方法是什么,有那些内容,有什么验证。以UserInfo_API_Put为例:

    public class UserInfo_API_Base
{
[Required(ErrorMessage = Verifier.ERRMSG_CANNOT_BE_NULL)]
[RegularExpression(Verifier.REG_EXP_CHINESE_NAME, ErrorMessage = Verifier.ERRMSG_REG_EXP_CHINESE_NAME)]
public string RealName { get; set; } //真实姓名 public float Height { get; set; } //身高 public DateTime Birthday { get; set; } //生日
} //修改用户信息(普通用户只能修改自己的信息)
//PUT api/usersinfo/{username}
public class UserInfo_API_Put : UserInfo_API_Base
{
[EnuValueValidator(RoleType.ADMINISTARTOR, RoleType.NORMAL)]
public string Role { get; set; } //角色Administrator, Normal, 普通用户无法修改此字段
}

如要修改“guogangj”这个用户的信息,那就往“api/usersinfo/guogangj”这个uri地址put这么一个对象,其中RealName这个属性不得为空,还必须是2-10个中文字符,当然了,Height和Birthday也都不可为空,因为float型和DateTime型都是不可空的类型,Role属性则要执行一个自定义的验证,确保其值必须为“Administrator”或“Normal”。

这样的契约必须同时被服务器端和客户端所理解,所以做成了一个类库的形式,服务器端和客户端都引用这个类库,这样做的最大的问题就在于这个类库发生了变动的情况下,更新了一边却忘了另一边,我目前是用一些工具来尽量避免这种情况的发生的,比如SVN的Externals参数设置。对此,各位高人有什么更好的方法?希望能分享一下。

Web API设计规则

尽管在《对RESTful Web API的理解与设计思路》中,我已经提了一下Web API的“法则”,这里再老调重弹外加几句补充吧。

RESTFul的核心内容是“R”,也就是资源,我们把对资源的增删查改具体化为HTTP的四个动作:POST、DELETE、GET和PUT。现在有这么个问题:假如我的用户名是guogangj,我要获取我的信息,是“GET /api/myinfo”呢,还是“GET /api/usersinfo/guogangj”呢?从技术上来说都没问题,现在关键是要从“资源”的角度考虑,如果你认为“/api/myinfo”是一个资源,那就意味着每个用户对这个资源的GET会得到不同的结果,而对于“/api/usersinfo/guogangj”这样的资源,不管是谁,获取到的内容应该是一致的(如果有权限获取的话),从这个角度看,“/api/usersinfo/guogangj”这种方式更加RESTFul,这是我的理解,不一定正确,还有请高手的分析。

并行写入冲突与时间戳

在对资源进行PUT和DELETE动作的时候,需要对其进行并行写入冲突检查,因为写入的时候,资源可能已经被别人动过,这个检查通常是用一个“时间戳”来实现的,我使用的是DateTime类型的Ticks,这是一个long类型,足够反映出资源发生变动的时间了。例如我现在要对用户guogangj的信息进行修改:

PUT http://localhost.:57955/api/usersinfo/guogangj?UpdateTicks=635054404507843749 {"Role":"Administrator","RealName":"蒋国纲","Height":1.67,"Birthday":"1981-11-12T00:00:00"}

也许仔细的你注意到了,localhost后面貌似多了个“.”,这是为了让Fiddler能够捕捉到这个http包而加的。

我会在URI中带上UpdateTicks参数,服务器端的业务逻辑层在执行Update的时候,会判断这个时间戳和现在数据库里的时间戳是否一致,如果不一致,则抛出并行写入冲突的异常。

我把UpdateTicks放在URI中的理由是:这个UpdateTicks也可以算是资源的一部分。例如对于上面这个PUT动作,我的意图是:我要更新时间戳为“635054404507843749”的“/api/usersinfo/guogangj”这个资源,如果它的时间戳不是“635054404507843749”,那就不是我要更新的资源。

这是我的方法,另一种我能想出的办法是把时间戳放在HTTP头中,如:

PUT http://localhost.:57955/api/usersinfo/guogangj UpdateTicks:635054404507843749 {"Role":"Administrator","RealName":"蒋国纲","Height":1.67,"Birthday":"1981-11-12T00:00:00"}

这样服务器端在处理的时候一样可以取出时间戳,只不过方法稍有些不同,那种更好呢?就我个人而言,是偏向于前者,这里也请高手指教一下。

身份验证详解

好吧,终于到重头戏了,那就是Web API的身份验证,为了使大家马上有个直接的了解,我用Fiddler截取一个包,看看我每次请求到底发了些什么?

PUT http://localhost.:57955/api/usersinfo/guogangj?UpdateTicks=635054404507843749 HTTP/1.1 Custom-Auth-Name: guogangj Custom-Auth-Key: 58E595EC40A74FF4EEF0856D7E59018F6141E12EA3DB965F74B416A4DFDB5746E6DCFDEDBDF5DA0C524254763FEE207B1FA8EF6D948132DF45C9C89AA7BF3A7373C509687C03BDE5 Accept: application/json Content-Type: application/json; charset=utf-8 Host: localhost.:57955 Content-Length: 94 Expect: 100-continue
{"Role":"Administrator","RealName":"蒋国纲","Height":1.67,"Birthday":"1981-11-12T00:00:00"}

这是一个完整的HTTP请求,在HTTP头中多了这么两个东西:“Custom-Auth-Name”和“Custom-Auth-Key”,Custom-Auth-Name不用说,一看就知道是User ID,表示发起人是谁,但如果他说自己是谁服务器就认为他是谁的话,那就没有任何安全可言了,所以还要Custom-Auth-Key(下面简称Key)这个东西来验证一番,这个Key是长长的一串东西,这是经过加密和转码后的文本,下面说说这个Key是怎么来的。

在WebApiKit这个库中有这么一个方法:WebApiClientHelper.MakePrincipleHeader,代码全在里面,不多,我一一解释:

private static void MakePrincipleHeader(HttpRequestMessage reqMsg, string strUri)
{
//即便是一模一样的请求内容,我也希望生成不同的key,所以每次都需要生成一个新的GUID,防止“重发”用的也是这个GUID,用这个GUID使得每次请求(不管URI和内容是否一样)都是唯一的,不可复制和重复的
Guid guid = Guid.NewGuid(); //获取有效的URI,如这个请求的这一长串的URI获取到的内容是“/api/usersinfo/guogangj”
strUri = InternalHelper.GetEffectiveUri(strUri); //有效URI连上GUID,进行一次MD5加密,(用这种方法来获得长度一致但每次都截然不同的内容)再连上GUID,这个结果作为对称加密的明文
string strToEncrypt = Md5.MD5Encode(strUri + guid) + " " + guid; //明文密码执行两次MD5之后作为对称加密的密钥,加密前面产生的那一串“明文”,好吧,Key就这样生成了
string strTheAuthKey = Des.Encode(strToEncrypt, Md5.MD5TwiceEncode(Password)); //将结果加入到HTTP请求的Header中去
reqMsg.Headers.Add(Consts.HTTP_HEADER_AUTH_USER, UserName);
reqMsg.Headers.Add(Consts.HTTP_HEADER_AUTH_KEY, strTheAuthKey);
}

对称加密,没有密钥就无法还原,而密钥并没有在网络上传输,不可能被第三者通过截包等方式跟踪到,所以这个密文应该来说是无法破解的。服务器端拿到这个请求包之后,执行一个逆向操作:

public static bool VerifyAuthKey(string strAuthUser, string strAuthKey, string strRequestUri, string strPwdMd5TwiceSvr, ref Guid guidRequest)
{
try
{
//对称加密的解密,密钥为用户密码的二次MD5,服务器端知道的
string strUrlAndGuid = Des.Decode(strAuthKey, strPwdMd5TwiceSvr); //如果解密成功,用空格劈开成两段,一段是“有效URI连上GUID,进行一次MD5加密”,另一段就是GUID了
string[] arrUrlAndGuid = strUrlAndGuid.Split(new[] { ' ' });
if (arrUrlAndGuid.Count() != 2)
return false; string strUrl = arrUrlAndGuid[0];
string strGuid = arrUrlAndGuid[1]; //将解密出来的这个GUID作为返回参数,以便将其加入一个全局的集合中来防止“重发”(“重发”会在另一处地方检查)
guidRequest = Guid.Parse(strGuid); //再按照与客户端一致的办法生成“有效URI连上GUID,进行一次MD5加密”的结果,把这个结果与刚解密出来的结果比对,如果一致,身份验证通过
strRequestUri = InternalHelper.GetEffectiveUri(strRequestUri);
if (string.Compare(Md5.MD5Encode(strRequestUri + guidRequest), strUrl, true) == 0)
{
return true;
}
}
catch (Exception) //忽略这其中产生的任何异常,将它认为是验证不通过
{
//Ignore any exception
}
return false;
}

这种验证方法可以杜绝了“身份冒充””和“重发”,而且完全不依赖于第三方的库,方法十分简单,开发者能很轻易地对它进行进一步的强化,我认为对于大多数场合,够了。好吧,等待高人来指正。

Web API验证规则

验证始终是应用程序的一个关键的功能,如前面提到的身份验证其实也是一种验证,验证的目的是:确保正确的人做正确的事。

有些验证仅仅是一个简单的规则,比如中文名验证:不可为空,必须是2-10中文字符;有些验证则需要访问数据库才知道,比如:添加一个用户,不能和已有用户的ID重复;还有些综合型的验证,在本例子中也有体现:用户可以修改自己的信息,但只有管理员才能修改别人的信息。

验证究竟是放在UI层还是放在业务逻辑层呢?其实这不只是Web API才有的问题,所有的系统,在设计的时候都要考虑这样的问题。以前我在做系统的时候,认为层与层之间是互相不信任的,因此业务逻辑层要进行一套完整的验证,而UI层当然也要进行一套完整的验证,这样带来的后果是重复代码增加,看起来有些凌乱,后来我这么考虑:如果网站的UI层对用户提供的信息执行过了验证,为什么业务逻辑层还需要再执行一次?应该不需要了,因为UI层和业务逻辑层都放在服务器端,这是我们自己能够控制的,我们只需要针对客户端过来的数据做验证即可,于是我大刀阔斧地把业务逻辑层的验证代码削除掉了,程序果然看起来整洁了许多。

*注:在这个DEMO中,Server这个站点属于UI层,而BLL这个类库属于业务逻辑层

但有些跟数据相关的验证就不是那么容易放在UI层做,比如前面说的“添加一个用户,不能和已有的用户的ID重复”,这个就需要到数据库里面查查到底有没有这个用户ID先。

所以,一般来说,我的规则是这样:身份验证、输入验证和权限判断能放在UI层就放在UI层,UI层做不到(比如涉及到具体数据的验证),才放在业务逻辑层,UI层验证和业务逻辑层的验证最好不要重复。

客户端MVVM简介

本文的重点是Web API,但也顺便简单说说客户端的MVVM模型,MVVM即“Model - View - ViewModel”,ViewModel与View绑定,绑定在这里的意思就是:当View发生变化时,ViewModel要体现出来,反之,当ViewModel发生变化时,View也要体现出来。大概就是这样,具体开来还要分什么双向绑定和单向绑定。

View发生变化,ViewModel也要跟着变,这个看起来并不难,比如你在UserName的文本框里输入“zhangsan”,当你的输入焦点离开这个文本框时,程序会产生一个事件,它会去处理这个事件并把文本框的值赋到ViewModel去,这个“事件”不一定是失去焦点,还有可能是键入,也可能是手动触发。

而ViewModel变化,View也要跟着变化,这个如何实现呢?WPF提供了一个接口INotifyPropertyChanged,这个接口里只有一个叫“PropertyChanged”的event,ViewModel发生变化的时候,就通过触发这个event来通知View改变。我在客户端代码中提供了一个叫“ViewModelBase”的基类,就实现了这个接口,我的其它的ViewModel都从这个基类派生下来,在给它们的属性SetValue的时候,就会触发这个接口中的那个event,实现对View的通知。

网上关于MVVM的文章还是很多的,还有些相当重量级的框架,如Prism,要掌握这些东西就绝非一朝一夕之力了,但我相信万变不离其宗,原理就如我所说的那样。

另外关于WPF的一些技术,我就不在这里提了,毕竟这不是本文重点,大家可以参考一些别的资料。

Web.Config

这也许是你见过的最简单的Web.Config,因为我把不用的都去除了。

<?xml version="1.0" encoding="utf-8"?>
<!--
For more information on how to configure your ASP.NET application, please visit
http://go.microsoft.com/fwlink/?LinkId=169433
-->
<configuration>
<system.web>
<compilation debug="true" targetFramework="4.0" />
<authentication mode="None" />
</system.web>
</configuration>

没错,上面的内容就是全部的,唯一值得一提的是authentication的mode属性,我们由于使用的是自定义的身份验证方式,所以得把这个设为None,否则服务器端很可能会使用Windows身份验证机制。并且此程序已经在IIS下验证过,正常使用没什么问题。

本DEMO的一些问题

DEMO毕竟是DEMO,我在写的过程中也发现了一些问题,有些是因为条件的限制,有些则真是问题,所以必须列一下,以便大家在正式开发的时候注意避免:

  • 时间戳使用UTC时间而不是本地时间是否更佳?(考虑到如果使用UTC时间的话得多一点转换,所以我在此DEMO中就不用了)
  • 没有使用事务。事务功能通常是DBMS的功能,本DEMO没有使用DBMS,另外,一个好的系统还能做到文件的回滚,不只是DBMS,但这远超本DEMO的范畴了。
  • usersinfo的POST和DELETE功能没做(偷懒)
  • 客户端的网络通信均会阻塞UI线程,用户体验不佳。改进参考
  • 客户端ViewModel的验证与API Contract的验证存在重复,请问高手这个重复如何消除?
  • 身份验证需要调用业务逻辑层,没有在UI层做缓存,在正式的大型应用场合,没有缓存的话效率会很低的,但缓存的更新也是个很大的问题,我相信大型的网络应用在这方面都有一套严谨而复杂的规则。
  • 密码在服务器端的保存格式固定为二次MD5,这样不利于将来对加密算法的改进。
  • 客户端的输入验证做得不够好,例如在年龄里输入“abc”,虽然有出错提示(转换成数字失败),但居然也可以提交(提交的内容是之前的数字)
  • 由于服务器端的一些全局数据是static的,因而可能存在线程安全的问题

ASP.net Web API综合示例的更多相关文章

  1. ASP.NET Web API 开篇示例介绍

    ASP.NET Web API 开篇示例介绍 ASP.NET Web API 对于我这个初学者来说ASP.NET Web API这个框架很陌生又熟悉着. 陌生的是ASP.NET Web API是一个全 ...

  2. ASP.NET Web API 入门示例详解

    REST服务已经成为最新的服务端开发趋势,ASP.NET Web API即为.NET平台的一种轻量级REST架构. ASP.NET Web API直接借鉴了ASP.NET MVC的设计,两者具有非常类 ...

  3. ASP.NET Web API使用示例

    原文地址:https://blog.csdn.net/chinacsharper/article/details/21333311 上篇博客讲解rest服务开发时,曾经提到过asp.net mvc中的 ...

  4. C#版ASP.NET Web API使用示例

    为更好更快速的上手Webapi设计模式的接口开发,本文详细解释了在Web API接口的开发过程中,我们可能会碰到各种各样的问题总结了这篇,希望对大家有所帮助. 1:在接口定义中确定MVC的get或者P ...

  5. 支持Ajax跨域访问ASP.NET Web Api 2(Cors)的简单示例教程演示

    随着深入使用ASP.NET Web Api,我们可能会在项目中考虑将前端的业务分得更细.比如前端项目使用Angularjs的框架来做UI,而数据则由另一个Web Api 的网站项目来支撑.注意,这里是 ...

  6. Asp.Net Web Api 2 实现多文件打包并下载文件示例源码_转

    一篇关于Asp.Net Web Api下载文件的文章,之前我也写过类似的文章,请见:<ASP.NET(C#) Web Api通过文件流下载文件到本地实例>本文以这篇文章的基础,提供了Byt ...

  7. ASP.NET MVC Web API使用示例

    上篇博客讲解rest服务开发时,曾经提到过asp.net mvc中的rest api,由于篇幅原因,没有在上篇博客中进行讲解,这里专门拿出来进行讨论.还是一样引用上次的案例,用asp.net mvc提 ...

  8. 在ASP.NET Web API 2中使用Owin OAuth 刷新令牌(示例代码)

    在上篇文章介绍了Web Api中使用令牌进行授权的后端实现方法,基于WebApi2和OWIN OAuth实现了获取access token,使用token访问需授权的资源信息.本文将介绍在Web Ap ...

  9. ASP.NET Web API Model-ActionBinding

    ASP.NET Web API Model-ActionBinding 前言 前面的几个篇幅把Model部分的知识点划分成一个个的模块来讲解,而在控制器执行过程中分为好多个过程,对于控制器执行过程(一 ...

随机推荐

  1. [转]Myeclipse四种方式发布项目

    原文链接: myeclipse四种方式发布项目

  2. win7 激活码 秘钥

    019.06最新windows7旗舰版系统激活码: 目前市面上的win7旗舰版激活码大部分都已经过期或失效了,下面来分享一些最新的. win7旗舰版激活密钥: BG2KW-D62DF-P4HY6-6J ...

  3. Java开发手册-编程规约精选

    # Java开发手册-编程规约精选 ## 总约 - 采用驼峰写法 ## 变量 - 首字母小写 ## 方法 - 方法名首字母小写- 参数首字母小写 ## 引用 - <阿里巴巴Java开发手册> ...

  4. [转]EL表达式判断是否为空,判断是否为空字符串

    原文地址:https://blog.csdn.net/zhaofuqiangmycomm/article/details/79442730 El表达式判断是否为空字符串 ${empty 值}  返回t ...

  5. 小D课堂 - 零基础入门SpringBoot2.X到实战_第4节 Springboot2.0单元测试进阶实战和自定义异常处理_17、SpringBootTest单元测试实战

    笔记 1.@SpringBootTest单元测试实战     简介:讲解SpringBoot的单元测试         1.引入相关依赖              <!--springboot程 ...

  6. shell编程系列9--文本处理三剑客之sed概述及常见用法总结

    shell编程系列9--文本处理三剑客之sed概述及常见用法总结 sed的工作模式:对文本的行数据一行行处理,如下图 sed(stream editor),是流编辑器,依据特定的匹配模式,对文本逐行匹 ...

  7. 【转】asp获取【微信公众平台】Access Token的源代码下载

    在做微信开发时候,经常要用到Access Token,但是官网提供的都是基于php写的,我用asp写了,有需要可以直接复制去用,模板消息,jdk上传图片,客服消息等全需要这个:'获取 access_t ...

  8. 报错:The specified datastore driver ("com.mysql.jdbc.Driver") was not found in the CLASSPATH. Please check your CLASSPATH specification, and the name of the driver.

    报错背景: CDH中集成hive插件,启动报错. 报错现象: [main]: Metastore Thrift Server threw an exception... javax.jdo.JDOFa ...

  9. Java 终于在 Java 8 中引入了 Lambda 表达式。也称之为闭包或者匿名函数。

    本文首发于 blog.zhaochunqi.com 转载请注明 blog.zhaochunqi.com 根据JSR 335, Java 终于在 Java 8 中引入了 Lambda 表达式.也称之为闭 ...

  10. LODOP打印table超宽用省略号带'-'的内容换行问题

    前面的博文有div超宽隐藏(LODOP打印超过后隐藏内容样式),还有有table设置超宽隐藏(),此外,还有超宽后用省略号表示的css样式,此文是针对这个样式的.该样式正常情况下没问题,但是遇到-短线 ...