RuPengWang项目

 

 

项目


Day1-------------------------


说明:建外键约束、ashx+Razor

RupengWang
创建三个类库Model DAL BLL
后台:RupengWang.Admin
前台:RupengWang.Front
建一个后台管理员的表T_AdminUsers(Id UserName Password)
打开动软,生成三层代码 (报错,是因为UAC是不用随意的写入数据,方法是以管理员运行,没有就去属性兼容性中找),(工具-选项-配置-命令规则),(新建net项目-工厂模式-生成项目-需要DbHelperSql.cs),(去掉Oarcle,自己添加一个连接字符串),DataCache.cs,(key="ModelCache" value="30"),
//作业:用Razor做一个比动软更好的,允许用户自定义模板的代码生成器
RupengWang.Razor类库:其中有RPHelper.cs,HttpContext需要添加System.Web程序集
参数默认值,有点重载的感觉,有默认值的参数可参数可不传值,(不为null才能拼接可扩展属性)
RupengWang.Common类库:放常用的一些类
jQuery EasyUI用html画出的一个后台模板界面
编码问题,需要都修改为UTF-8或者其它
AdminUser的CRUD,CRUD都在AdminUserController.ashx,Model是弱类型转为User会有自动提示,(Ctrl+J可以快速定位行号)
需要客服端提交数据的,用ajax来做
AjaxHelper.cs用来专门给浏览器返回一些消息,(引用最好用根目录,不容易乱掉)
public string void WriteJson(HttpContext context,string status,string msg,object data) //需要引用System.Web.Script.Serialization
{
}
该代码生成器生成的代码没有检查用户名是否存在这个方法(有但是sql拼接有注入攻击问题),需要自己去写,不要去改代码生成器生成的代码,但是他们都是部分类,可以再写一个同名部分类,这样他们就可以合并为一个类
MD5处理放到BLL中,UI层代码越少越好,最好只有用户输入,检查合法性等
//作业:后台用户的删除
用户的禁用 IsEnabled,可以再BLL中先查询再更新
用户批量删除btnBatchDelete
var inputs = $(":checkbox[name='selectUserId']:checked");
var strs = inputs.map(function(){return $(this).val()}); //对于每一项都获得一个数据,获得的是数组
把这个数组拼接为一个带空格的字符串,发给服务器
Delete from T whele Id in idArray //idArray这个数字数组过滤掉了strs中除了数字的其它危险字符
//作业:批量的禁用 (反选全选也用上)
//作业:后台登陆:(UserName,Password,YanzhengCode),用AJAX、Form各一个版本;服务器检查:用户名是否存在,密码是否正确,用户名是否已经存在


Day1代码:

第一天项目任务:

项目任务:后台用户单个的删除;批量禁用。
项目任务:后台登陆:用户名、密码、验证码。登陆的时候要求使用AJAX、Form表单两种方式各做一个版本。服务器端检查:用户是否存在;密码是否正确;验证码是否正确;用户是否被禁用。
任务提交到这个帖子中。

 

 

 

Day2-----------------------

 

<one>后台登录:

后台登录:用户名、密码、验证码ValidCode(汉字)、自动登录
//0 汉字验证码:验证码可以用Guid.NewGuid().ToString(),去后四位,但不是随机的;可以用一个常用汉字字符串来随机取四个汉字作为验证码,new Font(new FontFamily("宋体"),12) ,g.Clear(Color:red); //用指定颜色清空画布背景颜色
Random是依赖于当前时间的,必须放到for循环外面
for(int i=0;i<500;i++) //画上500个随机点
{
int x=rand.Next(0,100);
int y=rand.Next(0,30);
g.DrawLine(Pens.Red,x,y,x,y); //画一个点
}
g.Clear(Color.red)
string.IsNullOrWhiteSpace(username) //是否是连续的空白字符串
//1 登录所有情况都写入BLL
//2 用枚举判断登陆结果: public enum LoginResult {OK,UserNameNotFond,PasswordError} //用枚举来表示返回结果
//3 自动刷新验证码: 点击登陆后,如果登陆失败,自动刷新一次验证码
//4 重置验证码:一旦点击登陆就重置验证码,这样即使浏览器不刷新验证码,该验证码也将失效;如果正常情况,该验证码是不会被使用的,因为接下来服务器响应后浏览器会再次刷新(客服端不可信)
//5 自动登陆:如果选择记住密码,把用户名、密码存入Cookie中,密码需要MD5加密
LoginHelper.cs //记住用户名Remenber(),尝试自动登陆TryAutoLogin() (与Cookie相关,必须写在UI),(把登陆的Id和用户名放入Session存起来StoreInSession(),后面用,设置用户名为唯一索引),工具类不需要实现Session接口
//6 退出登陆:销毁Session,Cookie,把有效时间设置为过去时间,就是销毁Cookie的作用 ---Session.Abandon() ---Expores=DateTime.Now.AddDays(-1);
//7 权限检查:进入任何一个页面,如果用户名没有登陆,则让她重新登陆(如果Session中登陆Id为null,就跳转到登陆页面) 除了检查用户是否登陆、后面还要检查是否有权限

 

<two>系统日志:(程序级别日志,面向程序员的)

Log4net(Log for net:为.net提供的日志,开源工具):可以帮助开发人员快速定位错误
如果访问量大,用IO写入会造成堵塞
Log4Net的好处:方便;实现各种需求只要改配置文件就可以了
文件日志:把日志记入文件中.(放入App_Code原代码、App_Data文件,不让访问者访问,保证安全)
滚动日志文件:每个日志最多100M,一个日志满了,就往新的日志文件中保存,最多保存10个日志文件,如果再有日志就把最旧的日志删除,依次循环;
日志级别:修改level可以控制哪些级别的信息显示
//1 添加log4net.dll
//2 添加configSections节点(在所有节点之前),添加log4net节点
<configSections>
<section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler,log4net"/>
</configSections>
<log4net>
<!-- OFF, FATAL, ERROR, WARN, INFO, DEBUG, ALL -->
<!-- Set root logger level to ERROR and its appenders -->
<root>
<level value="DEBUG" />
<appender-ref ref="RollingFileTracer" />
</root>
<!-- Print only messages of level DEBUG or above in the packages -->
<appender name="RollingFileTracer" type="log4net.Appender.RollingFileAppender,log4net">
<param name="File" value="App_Data/Log/" />
<param name="AppendToFile" value="true" />
<param name="RollingStyle" value="Date" />
<param name="MaxSizeRollBackups" value="10" />
<param name="MaximumFileSize" value="1MB" />
<param name="DatePattern" value="&quot;Logs_&quot;yyyyMMdd&quot;.txt&quot;" />
<param name="StaticLogFileName" value="false" />
<layout type="log4net.Layout.PatternLayout,log4net">
<param name="ConversionPattern" value="%d [%t] %-5p %c - %m%n" />
</layout>
</appender>
</log4net>
//3 例子:Console程序
在Global.asax(全局应用程序类),Application_Start时,执行log4net.Config.XmlConfigurator.Configure();
ILog log=LogManager.GetLogger(typeof(Test1)); //记录指定文件的日志
log.Debug("调试信息");
log.Warn("警告信息");
log.Error("错误信息");
log.Fatal("严重错误");
级别:Fatal>Error>Warn>DbBug(属于程序的一些调试信息,系统开发过成功中用Debug把尽可能多的信息打印出来,使用时就用Error)
WebClient wc=new WebClient(); //下载网页html
string s=wc.DrowLoadString(url);
log.Debug("下载结束"); 如果异常,记录异常log.Error("下载失败",ex);
//4 添加Global.asax(全局应用程序类),当系统发生未处理异常时,在Application_Error()中记录日志
ILog log = LogManager.GetLogger(typeof(Global));
log.Error("系统发生未处理异常:",Context.Error);


系统操作日志:面向业务人员
T_AdminOperationLogs(Id,UserId,CreateDateTime,Description),UserId在关系中建外键约束
RecordOperationLog(string description)//记录当前用户操作日志

问题1: iframe嵌套问题? (通过js判断是否是在iframe中,如果是在iframe中,就跳转到父类iframe)
问题2:检测是否登陆,是否有权限
作业1:查询操作日志(User,Date,Discription) AdminOperationLogSearch.ashx
作业2:后台密码重置(888888),用户自己修改密码(essayUI,取当前用户登陆的Id)

 

<three>权限管理

//有外键约束的CRUD+RPHelpler.cs
用户User(yzk,admin),角色Role(网站编辑,班主任,系统管理员),权限Power(删除用户,重置密码,查看订单):一个人可以有多个角色,一个角色可以有多个权限
T_Powers T_Roles T_RolePowers (某角色可以有哪些权限)
T_Roles T_AdminUsers T_AdminUserRoles (某用户可以有哪些角色)
Constatins(selctedValue,itemValue) //如果这些项在选中的值集合中就selected,(选中的为空就传一个空的long[]{})
用name+i作为id
//获得选中的checkbox的value数组
var selectedPowerIds=new Array();
$(":checkbox[name='selectUserId']:checked").each(function(){
selectedPowerIds.push($(this).val()); //遍历元素,往数组中加这些元素的值
});
alert(selectedPowerIds.join(","));
//查询刚刚插入的id,Add()本身就有返回id
insert into output
selected @IDENTITY;
//新增角色权限对应关系


项目任务:
1 角色的删除(先删除引用的表,在删除被引用的表)
2 T_Power权限CRUD
3 后台用户的修改、新增,
4 后台系统日志说明

//超链接不跳转
<a href="#"></a>
<a href="javascript:;"></a>
<a href="javascript:void(0);"></a>
以上方法都可以,推荐二,三方法,第一种方法页面可能会跳到其它位置.

 

 


Day3----------30150325----------------

 


<one>配置文件的继承和覆盖

问题1:未能加载文件或程序集(有可能是MySql.Web程序集不存在,程序集存在没找到,程序集的依赖项有问题)
连接mysql时,报错,因为它的程序集自动加了一个MySqlMembershipProvider,在machine.config中把这个配置信息注释掉
.net中的config有几个级别:机器级别,应用程序级别,子目录级别
Web.config是从machine.config继承的
直接去读取machine.config中的配置信息connectionString (可以证明是继承关系)
MySqlMembershipProvider、MySqlRoleProvider启用后进行傻瓜化:把用户管理,权限管理,登陆都被做好了.
如果machine.config中已经默认一个配置项name,那么web.config中就不能再用这个name,
如果一定要再用这个配置项,那么在当前config中此配置项前面加<remove name="之前的配置项"/>
或者用<clear/> (把之前父类的所有的信息都清除掉),才可以再次使用这个name
string age=ConfigurationManager.AppSettings["age"]; //读取AppSettings中的age
子文件的web.config继承父文件的web.config,所以子文件的可以读取父文件的配置信息,父文件不可以读取子文件中的配置信息


<two>权限检查:判断用户是否有某个权限

(教学总监,班主任,网站管理员,网站编辑)
@两个作用:后面字符串不转义,后面字符串是个多行文本
public bool HasPower(long adminUserId,string powerName)
{
//获得当前登录用户的Id
//获得powerName对应的权限Id
//T_RolePower中查询有哪些角色RoleId有这个权限
//判断当前用户是否拥有这些角色RoleId中至少一个
}

//例子:
select COUNT(*) from (
select AdminUserId from T_AdminUserRole where RoleId in (
select RoleId from T_RolePower where PowerId=(
select Id from T_Power where Name='编辑管理用户'
)
)
) au
where au.AdminUserId='34'

context.Response.End(); //输出当前缓存,停止该页执行,发出终止请求命令
没有权限也可以新增用户:直接向ashx发送请求AdminController.ashx?action=addnewSave&username=aa&password=123来新增用户,所以必须在保存时也进行权限验证
无论新增还是保存都需要判断是否有“新增用户”权限,(避免用户直接向addnewSave发请求实现新增)
安全不是建立在不透明的前提下,而是在别人知道你在怎么做,也没法黑你
任务1:给程序中所有的需要权限控制的地方加上权限限制

 

<three>课程管理


课程 章节 段落(多个课程,多个章节,多个段落(视频\笔记))
T_Course (Id Name)
T_Chapter (Id Name ChapterNo CourseId ) //序号,用于排序
T_Segment (Id Name SegmentNo VideoCode Note ChapterId)

关联字段\关联表
一对多只需要关联字段,多对多才需要关系表
一对多(0…N):一个父亲有N个孩子,一个孩子只属于一个父亲;一个课程有N个章节,一个章节只属于一个课程。只要在“N这一端”增加一个指向“1这一端”的外键即可。Father:Id,Name.Child:id,Name,FatherId。
多对多:一个用户有N个角色,一个角色可允许被多个用户使用;一个老师有N个学生,一个学生有N个老师。需要额外的关系表。Teacher:Id,Name;Student:Id,Name;TeacherStudent:Id,TeacherId,StudentId

任务2:Course、Chapter、Segment的CRUD //IEnumerable<Course> 遍历

 

<four>用反射封装Handler

检查是否登陆、检查是否有权限、都实现IRequiresSessionState (每个页面都要做,所以可以放到一起)
public BaseHandler:IHttpHandler,IRequiresSessionState
public void ProcessRequest()
//检查是否登陆
//我约定,参数中都要有一个action参数,表示执行什么方法
//方法名与action值一样
由父类处理(登陆检查,权限检查),然后调用子类的CRUD方法

//BaseController.cs
public class BaseController:IHttpHandler,IRequiresSessionState
{
public bool IsReusable //bool属性,是否可重复使用
{
get { return true; }
}
public void ProcessRequest(HttpContext context)
{
context.Response.ContentType = "text/html";
//判断是否登陆
AdminHelper.CheckAdminUserIdAccess(context);
string action = context.Request["action"];
if (action == null)
{
throw new Exception("action错误");
}
//刚进是new子类,执行父类时的this当前对象未子类对象
Type type = this.GetType();
MethodInfo method = type.GetMethod(action); //约定:方法名与action 值相同
method.Invoke(this, new object[] { context }); //执行当前对象的method方法
}
}


//CourseController.ashx
public class CourseController : BaseHandler
{
//面向对象中继承的优点:把通用的工作交给父类完成
public void list(HttpContext context)
{
}
public void addnew(HttpContext context)
{
}
public void addnewSave(HttpContext context)
{
}
...
}


扩展任务:T_Power增加ConrollerName,
配置进程外的Session:以管理员权限运行命令提示符...
以管理员身份运行 C:\Windows\Microsoft.NET\Framework\v4.0.30319>aspnet_regsql.
exe -ssadd -sstype p -S 127.0.0.1 -U sa -P abcd5226584
windows的Dos复制:标记+左键选中+右键(到粘贴板)+ctrlV

Front前端展示
ViewSegment.cshtml
error.cshtml
视频提交代码中未检测的代码---从客服端检测到存在危险的request值---4.0或更高版本--方法3--在web.config加上<system.web><httpRuntime requestValidationMode="2.0" /></system.web> //如果4.0已经有这个节点加个属性就行了--------------???------------
在.cshmtl中直接输出字符串的特殊字符会被转义字符,所以必须通过RPHelper.Raw()表示不转义进行原样输出 ,Note:显示超链接和ul
把手写编辑html代码进行可视化的编辑(所键即所得编辑器)

 

<five>UEditor
1 下载UEDitor
百度UEditor.baidu.com,下载NET的UTF8版本(用IE可以看到默认版本)
UE.getEditor('containerid'):获得UEditor的内容
ueditor本质就是动态生成的一个textarea

var ue = UE.getContent();
//对编辑器的操作最好在编辑器ready之后再做
$(function () {
ue.ready(function () {
ue.setContent('@RupengWangRazor.RPHelper.Raw(Model.note)');
});
$("#btnSave").click(function () {
var html = ue.getContent();
...});
});

<script id="container" name="content" type="text/plain">

</script>

<!-- 配置文件 -->
<script src="/ueditor/ueditor.config.js"></script>
<!-- 编辑器源码文件 -->
<script src="/ueditor/ueditor.all.js"></script>
<!-- 实例化编辑器 -->
<script type="text/javascript">
var ue = UE.getEditor('container');
</script>


用IE可以看到UEditor所发送请求报文
任务:配置文件的上传,云存储
服务器请求统一路径
案例是php/config --->当前为net/config
访问ueditor/net/controller.ashx?action=config 看是否正常--->抛出:未能找到类型或命名空间Newtonsoft
引用Newtonsoft.Json.dll 这是一个第三方序列化或反序列化
为什么图片没显示出来 --->因为相对路径在另一个网站发生了变化,前台根本没有这个路径
imageUrlPrefix: 图片访问路径前缀,可是使上传的文件保存在前台去;但是后台的imagePathFormat必须是虚拟路径,因为在前台MapPath了,所以无法用全路径直接上传保存到前台去

 

2 云存储的上传
上传文件需要查看上传配置说明
ueditor.config中配置,上传路径配置,访问查看是否返回json,如果不能返回json就引用newtonjson.dll
把文件放到单独域名
相对路径在前台看不到,
大型网站架构,为什么大型网站都把图片放到一个单独的一个域名(单独的服务器):降低服务器压力,降低Cookie的流量占用,发送Cookie会浪费流量;还可以使用CDN分发流量;安全性的好处,图片都放到了一个单独的静态域名中,就算是个程序伪装的也没有权限执行代码
CDN:内容分发流量(有很多服务器,会智能的挑选一台距离最近的让你访问)
云存储:把图片放到单独的服务器,价格低
FTP上传文件
云存储:七牛,亚马逊的AWS,又拍云
云计算:租,按需使用,按需付费。虚拟主机(不推荐)
云主机:盛大云,阿里云,百度云
云视频:保利威视
//注册又拍云---创建空间---空间名yangguodemo/操作员名yangguodemo/密码***********/已绑定域名yangguodemo.b0.upaiyun.com---
1--ftp上传:
//notepad库---用ftp的方式来访问这个upyun---右键登陆(用户名:操作员名/空间名,如:operator/mybucket)---复制一个图片---然后可以通过这个默认域名访问这个图片
2--http上传:怎样通过http上传一个文件
通过什么方式上传文件:FTP(File transfer protocal 文件传输协议),HTTP,API
3--api:HTTP REST API
使用 REST API,您可以使用任何方式发送 HTTP 请求与 UPYUN 服务器通信。因此,你可以使用任何编程语言来使用 REST API。
已经提供了现成的C# SDK,下载下来把Program.cs拷贝到程序中
键一个UpYun,拷贝所有UpYun.cs
流读取某个文件到byte[]中,设置内容的md5中,是否成功
下载SDK开发包
问题:又拍云对接
完善文件上传
大系统怎么进行架构
上传到又拍云
不能用用户上传的文件名作为文件名,可能文件重名,用MD5值做文件名可以避免重复

//Program.cs
UpYun upyun = new UpYun("yangguodemo", "yangguodemo", "abcd5226584");
using (FileStream fs = new FileStream(@"G:\RuPeng_YZK_150107\Rupeng_20150320_ASPNET_RupengWang\TestApi.csproj\yuyan.png", FileMode.Open, FileAccess.Read))
{
BinaryReader r = new BinaryReader(fs);
byte[] postArray = r.ReadBytes((int)fs.Length);
/// 设置待上传文件的 Content-MD5 值(如又拍云服务端收到的文件MD5值与用户设置的不一致,将回报 406 Not Acceptable 错误)
upyun.setContentMD5(UpYun.md5_file(@"G:\RuPeng_YZK_150107\Rupeng_20150320_ASPNET_RupengWang\TestApi.csproj\yuyan.png"));
Console.WriteLine("上传文件");
bool b = upyun.writeFile("/a/yuyan.png", postArray, true);
// 上传文件时可使用 upyun.writeFile("/a/test.jpg",postArray, true); //进行父级目录的自动创建(最深10级目录)
Console.WriteLine(b);
Console.ReadKey();
}


上传到upyun,文件按年、月为文件夹来存储,便于管理和定位,文件名用文件的md5表示,避免文件名的重复
//首先获得上传文件后的的路径
//如果上传失败,需要进行日志记录


//上传到upyun
//上传文件目录为年月日、文件为文件的md5值
DateTime today = DateTime.Today;
string uploadYunFileName = CommonHelper.MD5Encrypt(uploadFileBytes) + Path.GetExtension(uploadFileName); //上传云的文件
string uploadYunFilePath = "/upload/" + today.Year + "/" + today.Month + "/" + today.Day + "/" + uploadYunFileName; //上传到云得路径
try
{
UpYun upyun = new UpYun("yangguodemo", "yangguodemo", "abcd5226584"); //上传指定云服务域名
/// 设置待上传文件的 Content-MD5 值(如又拍云服务端收到的文件MD5值与用户设置的不一致,将回报 406 Not Acceptable 错误)
upyun.setContentMD5(CommonHelper.MD5Encrypt(uploadFileBytes)); //对上传字节进行md5加密
bool uploadResult = upyun.writeFile(uploadYunFilePath, uploadFileBytes, true); //把上传字节写入指定云路径中 //----------------???-----------------------
if (uploadResult) //如果上传成功
{
Result.Url = "http://yangguodemo.b0.upaiyun.com" + uploadYunFilePath; //返回插入编辑器的图片url
Result.State = UploadState.Success;
}
else
{
Result.State = UploadState.FileAccessError;
Result.ErrorMessage = "上传upyun服务器失败";
log.Error("上传upyun服务器失败");
}
}
catch (Exception e)
{
Result.State = UploadState.FileAccessError;
Result.ErrorMessage = e.Message;
log.Error("上传文件失败:发生异常:" + e);
}
finally
{
WriteResult();
}


XSS 跨站脚本攻击
过滤script标签
如果允许网友上传任何信息而又原样显示,就会可能出现跨站脚本攻击
js显示不出来,在config.js中取消script
//在config.js中的,allHtmlEnabled:false 将false-->true依然没效果 //提交到后台的数据是否包含整个html字符串
获得编辑器的var ue=UE.getUEditor('container',{"allowDivTransToP":true}); //依然没作用
测试按钮,ue.getContent(); //可以获得script标签
asp.net帮我们自动阻拦了这些特殊字符,如果在编辑时没有限制,那么显示时应该进行转义显示
这就是如果不在根的web.config中修改requestValidationMode="2.0"就会报错

 

 


Day4--------------20150326-------------


查看课程


<one> url重写:

Front ViewCourse.ashx?id=1
FrontHelper.cs //包含章节错误信息
对请求的地址进行重写
用正则匹配,重写路径
string url = Context.Request.Url.ToString();
Match match = Regex.Match(url, @"Segment(\d)+.ashx");
if(match.Success)
{
string value = match.Groups[1].Value;
Context.RewritePath("ViewSegment.ashx?segmentId=" + value);
}

Segement(@segment.Id) 如果不能识别就加个括号

 


<two> 缓存优化:

降低数据库压力
<appSettings><add key="ModelCache" value="1"/></appSettings> //设置实体缓存时间

public RupengWang.Model.Course GetModelByCache(long Id)
{
string CacheKey = "CourseModel-" + Id;
object objModel = Maticsoft.Common.DataCache.GetCache(CacheKey);
if (objModel == null)
{
objModel = dal.GetModel(Id);
if (objModel != null)
{
int ModelCache = Maticsoft.Common.ConfigHelper.GetConfigInt("ModelCache");
Maticsoft.Common.DataCache.SetCache(CacheKey, objModel, DateTime.Now.AddMinutes(ModelCache), TimeSpan.Zero);
}
}
return (RupengWang.Model.Course)objModel;
}

 

<three> shtml技术:

http://pan.baidu.com/s/1mgwvV4G //切图工具
后台程序员 前端程序员 切图工程师 页面设计
BootStrap:前端框架随着设备大小进行变化
aboutus.html,joinus.html
article:HTML5的语义话标签
避免静态页面的头和尾重复,使用SSI,除了Cassini的主流浏览器都是支持的
ServerSideInclude:SSI 服务器端包含,默认为.shtml
joinus.shtml+head.html+foot.html //服务端用SSI完成的的拼接,客服端是不知道有这么回事的
如果每个页面需要的中间的标题<title>是不一样的,可以通过进一步拆分进行实现:headstart.html,linkscript.html,headend.html,navbar.html(导航条) (错位:增加聊天室的原因)
高级保存选项里保存:UTF-8 无签名(去掉BOM),这样页面头部就没有空白
Razor不认识<!--#include file="/head.html"-->
对于纯静态页面用<!--#include file="/head.html"-->,对于动态页面要自己写Include的Raw方法原样返回html @RupengWangRazor.RPHelper.Include("/headstart.html")
shtml是静态的,不需要asp.net引擎处理;而cshtml是动态的,需要引擎

如鹏网公开课里去考视频代码


项目任务:把课程页面中的章节、段落按照SeqNo排序;在课程、章节、段落的后台List页面中也是按照SeqNo排序显示。

今天的代码:http://pan.baidu.com/s/1hqiWTgw

 


Day5----------------------20150330-------------------------

 

 

新闻管理


新闻分类管理:类别是无限数次、树状结构:
T_NewsCategory(Id Name ParentId)
T_News(Id Title NewsContent PostDateTIme CategoryId)
军事新闻、体育新闻
categoryList()
如果没有parentId就赋值为-1,显示根目录
项目任务1:类别的CRUD(如果有子类别,则不能删除)
newsList()
根据类别Id查询所有新闻
项目任务1:新闻的CRUD

<one> 静态化:

新闻静态化:
新闻基本是不变的,缓存虽然降低了数据库压力,但是依然会访问web服务器,可以页面静态化,;查看课程不能静态化的,因为有业务逻辑,没有购买不能查看
生成静态化页面:解析获得html,保存到指定文件中去,如果指定文件的文件夹不存在就创建文件夹

//生成静态化页面
//获得ViewNews.cshtml页面,保存到Front的.shtml静态页面中
string cshtml = RPHelper.RPGetHtml(context, "~/NewsFile/ViewNews.cshtml", new {
title = title ,
newsContent = newsContent,
postDateTime=news.PostDateTime
});
string filePath = @"G:\RuPeng_YZK_150107\Rupeng_20150320_ASPNET_RupengWang\RupengWang.Front\NewsFile\" + categoryId + "\\" + newsId + ".shtml";
string shtmlDirect = Path.GetDirectoryName(filePath); //获得这个全路径静态文件的目录
if(!Directory.Exists(shtmlDirect))
{
Directory.CreateDirectory(shtmlDirect);
}
File.WriteAllText(filePath, cshtml); //把动态的cshtml页面写入静态的shtml文件页面中


一键静态化:

把所有的文章都进行静态化得生成 rebuildStatic
<app key="ViewStaticDirect" value=""/> //静态页面目录

public static void CreateStaticPage(News news)
{
//生成静态化页面
//获得ViewNews.cshtml页面,保存到Front的.shtml静态页面中
string cshtml = RPHelper.RPGetHtml(HttpContext.Current, "~/NewsFile/ViewNews.cshtml", new
{
title = news.Title,
newsContent = news.NewsContent,
postDateTime = news.PostDateTime
});
string ViewStaticDirecPre = ConfigurationManager.AppSettings["ViewStaticDirecPre"]; //配置静态化目录前缀
string filePath = ViewStaticDirecPre + news.CategoryId + "\\" + news.Id + ".shtml";
string shtmlDirect = Path.GetDirectoryName(filePath); //获得这个全路径静态文件的目录
if (!Directory.Exists(shtmlDirect))
{
Directory.CreateDirectory(shtmlDirect);
}
File.WriteAllText(filePath, cshtml); //把动态的cshtml页面写入静态的shtml文件页面中
}

 

<two> 分页:


文章分页:

因为分页通用,所以写一个通用的分页组件
list.ashx?pagenum={pagenum},{pagenum}作为当前页码的占位符
string urlFormat,long totalSize,long pageSize,long currentPage :超链接格式,总条数,每页的条数,当前页码
string totalPageCount = Math.Ceiling((totalSize*1.0f)/(pageSize*1.0f)) //取天花板数:如果整除就是本身,不能整除就+1
string url=urlFormat.Replace("{pagenum}",i.ToString()); //超链接格式

//RPHelper.cs
/// <summary>
/// 分页组件
/// </summary>
/// <param name="urlFormat">超链接格式</param>
/// <param name="totalSize">总数据条数</param>
/// <param name="pageSize">每页的条数</param>
/// <param name="currentPage">当前页</param>
/// <returns>原样返回分页的html(不转义)</returns>
public static RawString Pager(string urlFormat,long totalSize,long pageSize,long currentPage)
{
//<ul>
//<li>1</li><li>2</li>...
//</ul>
//总页数
long totalPageCount = (long)Math.Ceiling((totalSize * 1.0f) / (pageSize * 1.0f)); //取天花板数:如果刚好整除,就是她本身;不能整除就+1 ---> 大于等于这个浮点数的最小整数
//第一页
long firstPage = Math.Max(currentPage - 5, 1);
//最后页
long lastPage=Math.Min(currentPage+5,totalPageCount);
//currentPage+5
StringBuilder sb = new StringBuilder();
sb.Append("<ul>");
for (long i = firstPage; i <= lastPage; i++)
{
string url=urlFormat.Replace("{pagenum}",i.ToString());
if (i == currentPage)
{
sb.Append("<li>" + i + "</li>");
}
else
{
sb.Append("<li><a href=\"" + url + "\">" + i + "</a></li>");
}
}
return new RawString(sb.ToString());
}

//Test5/TestPager.ashx
public void ProcessRequest(HttpContext context)
{
context.Response.ContentType = "text/html";
long pagenum = Convert.ToInt64(context.Request["pagenum"]); //当前页
long totalSize = 105, pageSize = 10;
string[] strs;
if (pagenum <= totalSize / pageSize ) //当页数<=整除页
{
strs = new string[pageSize]; //展示的数据,每页10条数据
for (long i = 0; i < strs.Length; i++)
{
long index = (pagenum - 1) * pageSize + (i + 1);
strs[i] = "第" + index + "条数据";
}
}
else //当页数>整除页
{
long duo = totalSize % pageSize; //最后页有几条数据
strs = new string[duo]; //展示的数据,每页10条数据
for (long i = 0; i < strs.Length; i++)
{
long index = (pagenum - 1) * pageSize + (i + 1);
strs[i] = "第" + index + "条数据";
}
}
RPHelper.RPOutputHtml(context, "~/Test5/TestPager.cshtml", new {
strs=strs,
currentPage=pagenum,
totalSize = totalSize,
pageSize = pageSize
});
}

//Test5/TestPager.cshtml
<ul>
@{
foreach(var str in Model.strs)
{
<li>@str</li>
}
}
</ul>
@RupengWangRazor.RPHelper.Pager("TestPager.ashx?pagenum={pagenum}",Model.totalSize,Model.pageSize,Model.currentPage)

 

列表分页:

insert into T(Name,Age)
select Name,Age from T //把查询结果批量插入T
获得行号:(Id不能做行号,因为不连续)
select * from
(
select ROW_NUMBER() over(order by Id) rownum,* from T_News //根据title排序所获得的行号
) t
where t.rownum>=3 and t.rownum<=5 //获取根据title排序的3-5行的数据

//获得某个类别下的总条数(已有) bll.GetRecordCount()
//获得介于startrownum,endrownum之间的数据
public GetPageNews(long categoryId,long startrownum,long endrownum){...}
//总页数:天花板数
//每一个页下生成一个静态页面
//获得当前页得数据:
var newses=bll.GetPageNews(categoryId,(i-1)*10+1,i*10);
//静态化:解析cshtml返回一个html,写入到指定文件夹
//每次新增一篇文章都把该类别下的所有静态列表页面生成
mysql获得部分数据是limit
预期的查询结果非常多就需要分页

步骤: ViewNewsList.cshtml(新闻列表)--->RPHelper(分页组件)--->NewsController.ashx(GetNewsesPageByRowNum()获得指定类别指定某页得新闻列表集合--->解析cshtml并获得cshtml生成静态shtml)

//RPHelper.cs
/// <summary>
/// 分页组件
/// </summary>
/// <param name="urlFormat">超链接格式</param>
/// <param name="totalSize">总数据条数</param>
/// <param name="pageSize">每页的条数</param>
/// <param name="currentPage">当前页</param>
/// <returns>原样返回分页的html(不转义)</returns>
public static RawString Pager(string urlFormat,long totalSize,long pageSize,long currentPage)
{
//<ul>
//<li>1</li><li>2</li>...
//</ul>
//总页数
long totalPageCount = (long)Math.Ceiling((totalSize * 1.0f) / (pageSize * 1.0f)); //取天花板数:如果刚好整除,就是她本身;不能整除就+1 ---> 大于等于这个浮点数的最小整数
//第一页
long firstPage = Math.Max(currentPage - 5, 1);
//最后页
long lastPage=Math.Min(currentPage+5,totalPageCount);
//currentPage+5
StringBuilder sb = new StringBuilder();
sb.Append("<li><a href=\"index_1.shtml\">首页</a></li>");
for (long i = firstPage; i <= lastPage; i++)
{
string url=urlFormat.Replace("{pagenum}",i.ToString());
if (i == currentPage)
{
sb.Append("<li class='active'><a>第" + i + "页</a></li>");
}
else
{
sb.Append("<li><a href=\"" + url + "\">第" + i + "页</a></li>");
}
}
sb.Append("<li><a href=\"index_"+totalPageCount+".shtml\">末页</a></li>");
return new RawString(sb.ToString());
}

//NewsController.ashx
/// <summary>
/// 重新生成所有新闻静态列表
/// </summary>
/// <param name="context"></param>
public void reBuildAllStaticNewsList(long categoryId)
{
NewsCategory newCategory = new NewsCategoryBLL().GetModel(categoryId);
//获得指定类别的新闻总条数
long totalNewsCount = new NewsBLL().GetRecordCount("CategoryId=" + categoryId);
long pageSize = 10;
//总页数
long totalPageCount = (long)Math.Ceiling((totalNewsCount * 1.0f) / (pageSize * 1.0f));
//遍历每一页
for (long i = 1; i <= totalPageCount;i++ )
{
//每一页,获得该页得新闻列表集合 //根据rownum获得新闻
List<News> newses = new NewsBLL().GetNewsesPagerByRowNum(categoryId, (i - 1) * pageSize + 1, i * pageSize); //就算最后行没有满,依然满足<=虚拟最大行
//每一页,其页面解析并获得该页html
string cshtml = RPHelper.RPGetHtml(HttpContext.Current, "~/NewsFile/ViewNewsList.cshtml", new
{
newCategoeyName = newCategory.Name,
newses=newses,
CategoryId=categoryId,
totalSize=totalNewsCount,
pageSize=pageSize,
currentPage=i
});
//静态化该列表页
string ViewStaticDirecPre=ConfigurationManager.AppSettings["ViewStaticDirecPre"];
string fullPath = ViewStaticDirecPre + "/" + categoryId + "/index_" + i + ".shtml"; //每一页的静态文件全路劲
string directName = Path.GetDirectoryName(fullPath);
if(!Directory.Exists(directName))
{
Directory.CreateDirectory(directName);
}
File.WriteAllText(fullPath, cshtml);
}

//NewsDAL.cs
/// <summary>
/// 获得指定类别、指定起始行数的新闻集合
/// </summary>
/// <param name="categoryId"></param>
/// <param name="startRowNum"></param>
/// <param name="endRowNum"></param>
/// <returns></returns>
public List<News> GetNewsesPagerByRowNum(long categoryId, long startRowNum, long endRowNum)
{
StringBuilder sb = new StringBuilder();
sb.Append("select * from (select ROW_NUMBER() over(order by Id asc ) rownum,* from T_News ) t ");
sb.Append("where t.rownum>=@startRowNum and t.rownum<=@endRowNum ");
DataSet ds = DbHelperSQL.Query(sb.ToString(), new SqlParameter() { ParameterName = "@startRowNum", Value = startRowNum },
new SqlParameter() { ParameterName = "@endRowNum", Value = endRowNum });
List<News> list = new List<News>();
foreach(DataRow row in ds.Tables[0].Rows)
{
list.Add(DataRowToModel(row));
}
return list;
}

 

 

今日项目任务:
1、类别的新增、类别的删除(如果有子类别,则不能删除),类别的修改。新闻的编辑、删除。
2、完成后台“文章管理”的分页、后台用户管理以及分页等等
3、前台用户登陆,完成验证码、DAL、BLL完善,符合三层的规则。注册;密码要用MD5散列处理。
4、注册的时候账户默认处于“未激活状态”,系统给用户的邮箱发送一封激活邮件(程序如何发送邮件,自己研究),用户点击邮件后才能进行进入“激活状态”,才能进行后续的操作。
5、所有页面都是静态页面,所以通过ajax检查用户是否登陆,如果登陆则显示用户名/【退出登录】链接,否则显示【登陆/注册】链接。可以参考目前如鹏网的功能来实现。
6、把前台的登陆改成进程外Session:SQLServer。


/// <summary>
/// 发送邮箱,指定发送方和接收方
/// </summary>
public static void SendMail()
{
//简单邮件传输协议类
System.Net.Mail.SmtpClient client = new System.Net.Mail.SmtpClient();
client.Host = "smtp.163.com";//邮件服务器
client.Port = 25;//smtp主机上的端口号,默认是25.
client.DeliveryMethod = System.Net.Mail.SmtpDeliveryMethod.Network;//邮件发送方式:通过网络发送到SMTP服务器
client.Credentials = new System.Net.NetworkCredential("yingxinggedou", "abcd5226584");//凭证,发件人登录邮箱的用户名和密码

//电子邮件信息类
System.Net.Mail.MailAddress fromAddress = new System.Net.Mail.MailAddress("yingxinggedou@163.com", "杨国");//发件人Email,在邮箱是这样显示的,[发件人:小明<panthervic@163.com>;]
System.Net.Mail.MailAddress toAddress = new System.Net.Mail.MailAddress("adolphyangguo@163.com");//收件人Email,在邮箱是这样显示的, [收件人:小红<43327681@163.com>;]
System.Net.Mail.MailMessage mailMessage = new System.Net.Mail.MailMessage(fromAddress, toAddress);//创建一个电子邮件类
mailMessage.Subject = "邮件的主题:如鹏网账户激活";
string filePath = HttpContext.Current.Server.MapPath("/index.shtml");//邮件的内容可以是一个html文本.
System.IO.StreamReader read = new System.IO.StreamReader(filePath, System.Text.Encoding.UTF8); //System.Text.Encoding.GetEncoding("GB2312")
string mailBody = read.ReadToEnd();
read.Close();
mailMessage.Body = mailBody;//可为html格式文本
//mailMessage.Body = "邮件的内容";//可为html格式文本
mailMessage.SubjectEncoding = System.Text.Encoding.UTF8;//邮件主题编码
mailMessage.BodyEncoding = System.Text.Encoding.UTF8;//邮件内容编码
mailMessage.IsBodyHtml = true;//邮件内容是否为html格式
mailMessage.Priority = System.Net.Mail.MailPriority.High;//邮件的优先级,有三个值:高(在邮件主题前有一个红色感叹号,表示紧急),低(在邮件主题前有一个蓝色向下箭头,表示缓慢),正常(无显示).
try
{
client.Send(mailMessage);//发送邮件
//client.SendAsync(mailMessage, "ojb");异步方法发送邮件,不会阻塞线程.
}
catch (Exception e)
{
throw new Exception(e.Message);
}
}

 

 

 


//Day6------------------20150404--------------


邮件激活:

 


任何页面都可以是动态页面,经过ashx进行动态页面的请求处理: ashx---> cshtml---> ashx
任何页面也都可以是静态页面,经过shtml的ajax进行静态页面的请求处理: shtml---> (ajax ---> ashx ---> shtml)
静态页面用替换、用ajax从服务器拿到数据
把登陆注册请求都放入一个ashx
密码需要输入2次
焦点离开时,正则表达式验证邮箱的格式,及用户名是否存在(可用)
新增和修改应该放在BLL中
项目任务: 加一个注册日期

邮箱激活:目的是验证这个邮件地址是存在的,
激活码:目的是必须进入这个邮箱才可以激活controller.ashx?action=active?username=yang&activeCode=007008
激活码如果放入session,在其他浏览器就不是一个激活码了,应存入数据库中,因为只使用一次所以建单独表
username=" +context.Server.UrlEncode(username)+"&activeCode="+activeCode 如果不能激活就黏贴下面的链接
邮件客服端:很久以前是用邮件客服端发送邮件,很少使用Web ---Foxmail:原理是Smtp邮件发送协议;POP3邮件接收协议 ---发送邮件方一定要开启SMTP协议
yzk PHONE:13401087865
项目任务:一个邮箱只能注册一个账号
项目任务:用qq服务器发送邮件
不可能用163、qq等免费邮箱发大量的邮件,smtp:163.com等服务器会限制邮件数量,只有Edm专用服务器掏钱才可以发送,但要保证不发垃圾邮件(大公司的白名单联盟; SendCloud、 等适用于小网站大量发送邮件)

找回密码也需要发送邮件,所以把发送邮件封装起来,再把发件人等配置一下
项目任务:激活码有效期是30分钟,如果已经激活则显示“已经激活不需要重复激活”
个人任务:自己写个收发邮件的客服端软件

应该有个报错页面,显示一些信息(激活成功、激活失败)
加载每个页面都显示已登录的用户名,用ajax -- $("#uid").show(); //show()就是把display去掉
把这个ajax放入一个单独的文件中,这样就只需要引用这个文件就可以了
项目中一般吧js文件放入一个单独的文件中,如果用户再次访问不会再次下载,只会从缓存只会获得
项目任务:找回密码(注册邮箱)

 

Day7------------20150405----------------

 


<one>学习卡:

免费的课程不用学习卡也可以看,收费课程必须有学习卡才可以看
T_LearnCard(Id CourseId CardNum ExpireDays有效天数 Password UserId卡贝谁激活 ActiveDateTime什么时候激活) //激活过程用,激活后就可以扔掉
T_UserCourse(Id CourseId ExpireDate UserId) //激活之后用
1 检查学习卡密是否正确,如果正确则激活成功
bool CheckLearnCardIsRight(string cardNum,string password,long userId) //CardNum加上唯一约束
2 生成学习卡
bool GenerateCards(cardNumPrefix,int expireDays,int startNo,int endNo,List<LearnCard> LearnCards) //返回是否生成成功,集合用户接收生成的学习卡
cmd.Transaction=tx; //使用sqlserver需sqlserver需要注意 //借用事务来避免卡号冲突时,已经生成的卡号回滚 tx.Commit() tx.RllBack() //把一场记录到日志]
cmd,Parameters.Clear();//每次添加新的参数都需要把旧的参数去掉
output inserted.Id values() //获得新增数据的id
把异常信息记入日志
3 获得某用户的所有可用(在有效期中的)课程
List<UserCourse> GetUsableCourses(long userId) //GetDate() //sql语句中获得当前时间
4 用户userId是否激活了某个课程
bool IsActiveCourse(long userId,long courseId)
字符串拼接有注入漏洞,int没有
CardNum加上唯一约束,该列就不能重复了
多加防护,虽然麻烦,但是更安全
把要加入的所有激活卡信息加入一个字符串,再附加
当发现function嵌套太多,可以抽出来var fun=function(){...}; 然后$("#").click(fun);
οnkeyup="this.value=this.value.replace(/[^\d]*/g,'')" //替换非数字
下拉列表:可以通过ajax直接加载(easyui);可以用Razor引擎解析Model


新人:加小功能,改bug
项目任务:
1 加课程名称;
2 学习卡激活,然后进入我的课程页面;
3 在线报名的功能,然后发送邮件(接收者邮箱配置到Web.config中);
4 完善后台操作日志Log4net程序日志;
5 学习记录的功能,谁在某某时候学了某课程,用户每次ViewSegment.ashx查看课程时往表中插入一条记录,
ServerPush(性能比较差)/计时器,缓存,如果没有该数据就加入最前面prependTo,
界面中定时获得学习记录,动态添加到界面中(display:none slideDown() $("li[rid='"+record.Id+"']").length<=0学习记录的id不存在才添加);slidedown()
6 用户表再增加几个字段(Mobile,QQ,Shcool)
7 虚拟班级T_Class(Id Name) T_StudentClass 班级成员管理-->
like查询的占位符:
{where Mobile like @Moblie
new sqlparameter("@","%data%");
不用写 ' '}

适合于批量查询:where charindex(ltrim(Id),@idStrList)>0 //@idStrList就是 1,2,3
相当于:where UserId in (1,2,3)

 

 

<two>Redis:


NOSQL:Not Only SQL,存储键值对,数据过期处理,适合存储零时的数据
Redis服务器Linux(正式服务器),Windows版;
1 解压Redis安装包redisbin_x32.zip到一个文件下--
以普通的ext程序运行:redis-server.exe运行redis服务器(不推荐,需要长期手动开启Redis Watcher服务)---
以windows服务运行:安装应该以window服务装起来(这样系统启动起来之后就运行,不需要登录)
2 解压RedisWatcher1.zip:把Redis注册成系统服务--
--到安装目录RedisWatcher下修改watcher.conf配置文件--
---配置文件中exepath和workingdir 这个路径指向radis解压的文件夹 (解压路劲不能有中文)
到windows服务器启动这个服务并为自动, 这样确保windows服务装RedisWatcher处于启动状态
3 Redis驱动添加到项目中

添加一个类RedisManager.cs ,配置了最大写入大小,最大读取大小
获得一个RadisClient连接,存数据(Set设置值),Get读取,设值时可以设置超时

public static PooleRedisClientManager ClientManager{get;private set;} //内部可写,外部只能读
static RedisManager()
{
redisClientManagerConfig redisConfig=new RedisClientManagerConfig();
redisConfig.MaxWritePoolSize=128;
redisConfig.MaxReadPoolSize=128;
ClientManager=new PoolRedisClientManager(new string[]{"127.0.0.1"},
new string[]{"127.0.0.1'"},redisConfig); //radis的读写分离,通过集群在多台服务器进行读写
}

using(IRedisClient client = RedisManager.ClientManager.GetClient()) //调用ClientManager的GetClient()得到一个连接
{
client.Set<int>("age",18,DateTime.Now.AddSeconds(30)); //30s后过期
Dictionary<string,string> dict=new Dictionary<string,string>();
dict.Add("aaa","bbb");
client.Set<Dictionary<string,string>>("dict",dict);
}

using (iredisclient client = redismanager.clientmanager.getclient())
{
int age = client.get<int>("age");
dictionary<string, string> dict = client.get<dictionary<string, string>>("dict");
string iilove = dict["tuhan"];
console.writeline(age + "=====" + iilove);
}


读写主机的地址,读写分离,多台Redis组成集群
内部赋值外部取值
Redis可以吧数据持久化到磁盘中,Memcached则是存到内存(分布式缓存,不适合保存很重要的东西)
Redis的key加个前缀,避免冲突
可以接受自己的请求存数据,也可以接受别人的,大家都可以从中读取,是大家共同的存储空间,A写入,B可以读出,也可以去覆盖,需要加个前后缀


//Redis应用1:
Redis服务器代替数据库存储用户激活码

//Redis应用1:
一个用户名只能登陆一次(同一时间一个用户只能在一个session中登录)
获得当前用户的SessionID: context.Session.SessionID

//LoginController.ashx
//把用户名存入Session,判断每个页面是否登陆
LoginHelper.StoreInSession(context, username, password);
//每次登录都需要把当前用户名所在session的sessionId存入Redis
using(IRedisClient client = RedisManager.ClientManager.GetClient())
{
client.Set<string>(USERNAME_SESSIONID + username, context.Session.SessionID);
}


//Glibal.asax
public override void Init()
{
base.Init();
//必须到Init来监听
//每个需要Session页面启动都会执行AcquireRequestState事件
//执行该事件的时候session已经准备好
//客服端每次访问实现了IRequiresSessionState的接口都会触发
this.AcquireRequestState+=Global_AcquireRequestState; //执行该事件,session已准备好
}

//访问任何一个页面都要检测当前请求的sessionId是否是Redsis中的sssionId,如果不是说明在别处(不同session中登录),需要退出(自杀)
private void Global_AcquireRequestState(object sender, EventArgs e)
{
if(HttpContext.Current.Session==null) //-----!!!---
{
return;
}
string username = LoginHelper.GetUserNameInSession();
if(username==null)
{
return;
}
using(ServiceStack.Redis.IRedisClient client = RedisManager.ClientManager.GetClient())
{
string sessionIdInRedis = client.Get<string>(LoginController.USERNAME_SESSIONID+username);
if(sessionIdInRedis!=null && sessionIdInRedis!= HttpContext.Current.Session.SessionID)
{
//redis中当前用户的sessionId存在 并与当前用户的sessionId不一致,说明redis中sessionId被覆盖,有人又登录,以前登录的当前用户会退出
HttpContext.Current.Session.Clear();
HttpContext.Current.Session.Abandon();
}
}
}

 


//Day8----------------20150414-----------------

 


<one>查询操作日志

封装AJAX
function CommonAjax(url,data,success){
...
}

left join冗余字段问题:
$("#").empty(); //清空旧数据
没有反应就看是否发出请求,如果没有发出请求就是js的问题,切换到Console可以看到错误信息
left join查询的Model可以重新写
item.UserName=(string)row["UserName"]; //row["UserName"].ToString(); //前者只能转string类型的(效率高些),后者可以转任何类型。
CreateDateTime=log.CreateDateTime.ToString(); //日期格式转换

日期问题:
jquery easyui 日期选择DateBox,如果没有找到,有可能是easyui的版本太低没有datebox(function(){})这个方法,可以下载一个新的easyui
查找替换---文件替换--在文件中查找--替换所有低版本的easyui-1.4
DateTime opEndTIme=DateTime.Parse(strOpEndTime);

My97DatePicker:
My97DatePicker是一款非常灵活好用的日期控件。使用非常简单。
1、下载My97DatePicker组件包
2、在页面中引入该组件js文件:
<script type="text/javascript" src="My97DatePicker/WdatePicker.js"></script>
3、页面使用两个方式:
常规调用: <input id="d11" type="text" onClick="WdatePicker()"/>
图标触发:
<input id="d12" type="text"/>
<img οnclick="WdatePicker({el:$dp.$('d12')})" src="My97DatePicker/skin/datePicker.gif" _fcksavedurl="My97DatePicker/skin/datePicker.gif" width="16" height="22" align="absmiddle">
注:$dp.$ 相当于 document.getElementById

搜索条件相当于一个对象吧 如果不赋值就不拼接select(obj) {//便利属性以及属性的值 如果值为空就不拼接 }
if(Session==null){return;}报错(此时Session还没有准备好),用HttpContext.Current.Session 处理

 

 

<two>Attribute:

Attribute注解,是附加上方法、属性、类等上面的标签,
可以通过方法的GetCustomAttribute获得粘贴的这个Attribute对象
通过反射调用到粘贴到属性、方法、类等等的对象
任务:改造ORM
ORM约定:类得名字与表的名字一样,主键必须是Id,
改造一下ORM(类名还可以和表名不一样,Id可以与主键不一样)

//namespace TestAttribute.csProj

//只能只能粘贴方法上
[AttributeUsage(AttributeTargets.Method)]
class RupengAttribute : Attribute
{
public RupengAttribute() { }
public RupengAttribute(string name)
{
this.Name = name;
}
public string Name { get; set; }
}
//如果不加Attribute,则使用这个Attribute粘贴标签时必须写全名
class TeXing : Attribute
{
public string Name { get; set; }
}


class Program
{
static void Main(string[] args)
{
//Attribute注解:Attribute是附加到方法、属性、类等上面的特殊标签,在类Type信息初始化加载,无法在运行时修改
Type type = typeof(Person);
/*
object[] objs = type.GetMethod("SayHello").GetCustomAttributes(typeof(RupengAttribute), true);
foreach(object obj in objs)
{
RupengAttribute rp = (RupengAttribute)obj;
Console.WriteLine(rp.Name);
}
F1();
*/
MethodInfo[] methods = type.GetMethods();
foreach (MethodInfo method in methods)
{
object[] obAttrs = method.GetCustomAttributes(typeof(ObsoleteAttribute),true);
if(obAttrs.Length>0)
{
ObsoleteAttribute ob = (ObsoleteAttribute)obAttrs[0];
Console.WriteLine(method.Name + ":该方法不可用,因为" + ob.Message);
}
}
Console.ReadKey();
}
}
class Person
{
//在sayHello方法的描述信息MethodInfo上粘了一个RupengAttribute对象
//注解的值必须是常量,不能是动态算出来的 [RupengAttribute(Name=DateTime.Now.ToString())]
//[RupengAttribute(Name="rupengwnag")]
//一般特性Attribute的类名都以Attribute结尾,这样用的时候就不用谢"Attribute"了
[RupengAttribute("ruepngiloveyou")]
public void SayHello()
{
}
[Obsolete("这个方法已过时,拒绝访问")]
public void F1()
{
}
[TeXing(Name="texing")]
public void Love()
{
}
}


//Attribute在权限控制方面的一个应用:
PermissionActionAttribute.cs
PermissionAction("保存新增新闻");
真正气作用的不是Atrribute,而是读取并解释Attribute的代码 //"拆"-->拆迁队


[AttributeUsage(AttributeTargets.Method)]
public class PermissionActionAttribute:Attribute
{
public string Name { get; set; }
public PermissionActionAttribute(string name)
{
this.Name = name;
}
}


public class BaseController:IHttpHandler,IRequiresSessionState
{
public bool IsReusable //bool属性,是否可重复使用
{
get { return true; }
}
public void ProcessRequest(HttpContext context)
{
context.Response.ContentType = "text/html";
//判断是否登陆
AdminHelper.CheckAdminUserIdAccess(context);
string action = context.Request["action"];
if (action == null)
{
throw new Exception("action错误");
}
//刚进是new子类,执行父类时的this当前对象未子类对象
Type type = this.GetType();
MethodInfo method = type.GetMethod(action); //约定:方法名与action 值相同
object[] obAttrs = method.GetCustomAttributes(typeof(PermissionActionAttribute), t
if (obAttrs.Length > 0)
{
PermissionActionAttribute attr = (PermissionActionAttribute)obAttrs[0];
AdminHelper.CheckAdminUserHasPower(context, attr.Name);
}
method.Invoke(this, new object[] { context }); //执行当前对象的method方法
}
}

 


<three>二维码:


添加并引用二维码的组件
.net生成二维码
遇到一个新的组件,可以先用一个测试项目把它调通
如果数据经常被用户访问,而且大量的访问,而且内容一般不变就可以用缓存(静态页也可以看作一种缓存)

http://www.cnblogs.com/Soar1991/archive/2012/03/30/2426115.html

//TestQrCode.csProj
QrEncoder qrEncoder = new QrEncoder(ErrorCorrectionLevel.H);
QrCode qrCode = new QrCode();
qrEncoder.TryEncode("菡,现在想起你,真的是太突然了,你跳个舞给我看嘛,能不能不要这么诱人啊,我已经逻辑混乱了", out qrCode);
int ModuleSize = 12; //大小
QuietZoneModules QuietZones = QuietZoneModules.Two; //空白区域
var render = new GraphicsRenderer(new FixedModuleSize(ModuleSize, QuietZones));
using (System.IO.Stream stream = File.OpenWrite("d:/1.png"))
{
render.WriteToStream(qrCode.Matrix, System.Drawing.Imaging.ImageFormat.Png, stream);
}

/// 生成文章的二维码
public static void CreateQrCode(string ViewStaticDirecPre,long categoryId,long id)
{
string newsUrl = "http://localhost:9172/NewsFile/"+categoryId + "/" + id + ".shtml"; //文章地址路径
string qrCodePath = Path.Combine(ViewStaticDirecPre, categoryId + "\\" + id + ".png"); //生成的二维码路径
QrEncoder qrEncoder = new QrEncoder(ErrorCorrectionLevel.H);
QrCode qrCode = new QrCode();
qrEncoder.TryEncode(newsUrl, out qrCode);
int ModuleSize = 6; //大小
QuietZoneModules QuietZones = QuietZoneModules.Two; //空白区域
var render = new GraphicsRenderer(new FixedModuleSize(ModuleSize, QuietZones));
using (System.IO.Stream stream = File.OpenWrite(qrCodePath))
{
render.WriteToStream(qrCode.Matrix, System.Drawing.Imaging.ImageFormat.Png, stream);
}
}


分享一下:jq生成二维码,不用再生成一堆png了。
<script src="/js/jquery.qrcode-0.11.0.min.js"></script>
<script type="text/javascript">
$(function () {
$("#qrcode").qrcode({
//render: "table", //table方式
width: 200, //宽度
height: 200, //高度
text: "http://localhost:6158/News/@(Model.CategoryId)/@(Model.Id).shtml" //任意内容
});
})
</script>

 


<four>网上支付:


支付宝模拟器 http://paytest.rupeng.cn/

怎么防止顾客在票上盖一个章,Nike专柜与收银员没有实时通信?
暗号: Nike和收银员约定密码:abc888
记录进票据上
"md5(编号+金额+Nike名称+abc888暗号)"
对约定的暗号进行加密,通过暗号是否改变来确定有没有被篡改

项目任务:调通网银在线的接口
在线购买课程的功能


网关地址,向网关地址发请求


//1 支付宝:

流程参考《实物商品交易服务集成技术文档2.0.pdf》
网关地址http://paytest.rupeng.cn/AliPay/PayGate.ashx

网关参数说明:
partner:商户编号
return_url:回调商户地址(通过商户网站的哪个页面来通知支付成功!)
subject:商品名称
body:商品描述
out_trade_no:订单号!!!(由商户网站生成,支付宝不确保正确性,只负责转发。)
total_fee:总金额
seller_email:卖家邮箱
sign:数字签名。为按顺序连接 总金额、 商户编号、订单号、商品名称、商户密钥的MD5值。

回调商户接口地址参数说明:
out_trade_no:订单号。给PayGate.ashx传过去的out_trade_no再传回来
returncode:返回码,字符串。ok为支付成功,error为支付失败。
total_fee:支付金额
sign:数字签名。为按顺序连接 订单号、返回码、支付金额、商户密钥为新字符串的MD5值。


//Zhihubao.ashx
public void ProcessRequest(HttpContext context)
{
context.Response.ContentType = "text/html";
string action = context.Request["action"];
if(action=="zhihubao") //支付宝支付
{
ZhihuParam zpara = new ZhihuParam();
zpara.body = "学编程那家强,请到如鹏找老杨";
zpara.out_trade_no = "qw0011wd";
zpara.partner = "5";
zpara.return_url = "http://localhost:19262/Zhihubao.ashx?action=zhihubaoResult";
zpara.seller_email = "adolphyangguo@163.com";
zpara.subject = "中科电子";
zpara.total_fee = "1100";
zpara.key="abc123";
string signStr = zpara.total_fee + zpara.partner + zpara.out_trade_no + zpara.subject + zpara.key;
zpara.sign = CommonHelper.MD5EncryptByUTF8(signStr).ToLower();
context.Response.Redirect(@"http://paytest.rupeng.cn/AliPay/PayGate.ashx?body=" + context.Server.UrlEncode(zpara.body) +
"&out_trade_no=" + zpara.out_trade_no +
"&partner=" + zpara.partner +
"&return_url=" + zpara.return_url +
"&seller_email=" + zpara.seller_email +
"&subject=" + context.Server.UrlEncode(zpara.subject) +
"&total_fee=" + zpara.total_fee +
"&sign=" + zpara.sign);
}
else if (action == "zhihubaoResult") //支付宝支付成功后返回
{
ZhihuParam zParam = new ZhihuParam();
zParam.key = "abc123";
zParam.out_trade_no = context.Request["out_trade_no"];
zParam.total_fee = context.Request["total_fee"];
string returncode = context.Request["returncode"];
zParam.sign = context.Request["sign"]; //数字签名。为按顺序连接 订单号、返回码、支付金额、商户密钥为新字符串的MD5值。
string newSignStr = zParam.out_trade_no + returncode + zParam.total_fee + zParam.key;
string newSign = CommonHelper.MD5EncryptByUTF8(newSignStr).ToLower();
if (newSign != zParam.sign)
{
context.Response.Write("<h1 style='color:green;'>支付失败</h1>");
return;
}
context.Response.Write("<h1 style='color:red;'>支付成功</h1>");
}
}

 

//2 网银:

流程参考《网银在线支付B2C系统商户接口文档.zip》
网关地址http://paytest.rupeng.cn/ChinaBank/PayGate.ashx

网关参数说明:
v_mid:商户编号
v_oid:订单号
v_amount:总金额
v_moneytype:币种。0为人民币,1为外币。
v_url:回调商户地址
v_md5info:数字签名。为按顺序连接 总金额、币种、订单号、商户编号、商户密钥为新字符串的MD5值。
style:网关模式:0(普通列表),2(银行列表中带外卡)
remark1:备注1。可空。
remark2:备注2。可空。

回调商户接口地址参数说明:
v_oid:订单号
v_pmode:支付银行。目前值衡为0.
v_pstatus:支付结果。20为成功,30为支付失败
v_amount:总金额
v_moneytype:币种。0为人民币,1为外币。
remark1:传递的备注1。
remark2:传递的备注1。
v_md5str:数字签名。为按顺序连接 订单号、支付结果、总金额、币种、商户密钥为新字符串的MD5值。


//EBankZhifu.ashx
public void ProcessRequest(HttpContext context)
{
context.Response.ContentType = "text/html";
string action = context.Request["action"];
if (action == "ebankzhifu") //网银支付 //目的只是获得md5
{
EBank ebank = new EBank();
ebank.v_mid = "2";
ebank.v_oid = "19990720-20000400-000001234";
ebank.v_amount = "10000.01";
ebank.v_moneytype = "0";
ebank.v_url = "http://localhost:9940/EBankZhifu.ashx?action=ebankzhifuResult"; //支付动作完成后返回到该url,支付结果以POST方式发送
ebank.v_key="abc123";
ebank.v_style = "0";
ebank.v_md5info = CommonHelper.MD5EncryptByUTF8(ebank.v_amount + ebank.v_moneytype + ebank.v_oid + ebank.v_mid + ebank.v_key).ToLower(); //"1630DC083D70A1E8AF60F49C143A7B95";
context.Response.Redirect("http://paytest.rupeng.cn/ChinaBank/PayGate.ashx?v_mid=" + ebank.v_mid +
"&v_oid=" + context.Server.UrlEncode(ebank.v_oid) +
"&v_amount="+ebank.v_amount+
"&v_moneytype="+ebank.v_moneytype+
"&v_url="+ebank.v_url+
"&style="+ebank.v_style+
"&v_md5info=" + ebank.v_md5info);
}
else if (action == "ebankzhifuResult") //网银支付返回
{
EBank ebank = new EBank();
ebank.v_oid = context.Request["v_oid"];
ebank.v_pmode = context.Request["v_pmode"];
ebank.v_pstatus = context.Request["v_pstatus"];
ebank.v_amount = context.Request["v_amount"];
ebank.v_moneytype = context.Request["v_moneytype"];
ebank.v_key="abc123";
ebank.v_md5str = context.Request["v_md5str"];
string newMd5Str = CommonHelper.MD5EncryptByUTF8(context.Server.UrlDecode(ebank.v_oid) + ebank.v_pstatus + ebank.v_amount + ebank.v_moneytype + ebank.v_key).ToLower();
if(newMd5Str!=ebank.v_md5str)
{
context.Response.Write("<h1 style='color:green;'>支付失败</h1>");
return;
}
context.Response.Write("<h1 style='color:red;'>支付成功</h1>");
}
}

 


//项目任务:未进行激活码激活课程的用户,需要每次购买课程(应该增加课程价格)

 

 

//Day9----------20150421--------------


<one>网上支付(改进):

T_OrderCourse(Id CourseId UserId CreateDateTime PayDateTime IsPay) //支付时间允许为空
给商户编号加上唯一约束

项目任务:ewS课程页面可以预览,即ViewCourse谁都可以进入,但是Viegment只有购买了(在)其所在的Course之后才可以看(免费课程除外)。怎么判断用户购买了?在T_UserCourses中能查到这个这个用户的这个课程,并且没有过期。
项目任务: ViewCourse中,如果课程为免费,则不显示“购买课程”,显示为“免费课程”,否则还显示“课程价格”;如果是收费课程并且已经购买,则显示“本课程已购买”。
项目任务:我已经购买的课程的列表页面。 http://www.rupeng.com/BuyCourses/MyCourse
项目任务:生成订单后,让用户选择不同的支付方式。

 

 


<two>Redis消息队列:

MQ:Message Queue
消息队列服务器:MSMQ、ActiveMQ、Redis等
项目任务:确定邮件的发送,重置密码的发送(发送可能会很慢,而且有可能还需要重试),用消息队列把注册过程和邮件发送过程分开

TestMessageQueue_Enqueue.csProj
//生产者,入队列
static void Main(string[] args)
{
while (true) //while all the time,wait input email --生产邮箱
{
string input = Console.ReadLine();
using(ServiceStack.Redis.IRedisClient client = RedisManager.ClientManager.GetClient())
{
client.EnqueueItemOnList("emails", input); //向集合emails中入队用户输入的邮箱
}
}
}

TestMessageQueue_Dequeue.csProj
//消费者,出队列
static void Main(string[] args)
{
using(ServiceStack.Redis.IRedisClient client = RedisManager.ClientManager.GetClient())
{
while (true) //while all the time,wait output email --消费邮箱
{
string email = client.DequeueItemFromList("emails"); //从指定集合emails出队邮箱
if (email == null)
{
Console.WriteLine("没找到");
Thread.Sleep(500);
continue;
}
Console.WriteLine("fount email :" + email + ",active this email");
}
}
}

 

 

<three>Quartz.Net:定时任务框架

每隔一段时间执行一次任务
给计划者IScheduler一个工作IJob,让她在Trigger这个条件下执行这个工作d
添加Quartz.Net的两个dll的引用---用一段固定代码进行配置(不需要记)---
建一个任务,实现了IJob接口
Timer十分不精准,且只适用于控制台程序,Quartz.Net比WebForm的Timer更精准
创建一个计划,
得到这个计划者,
创建一个任务

//TestQuartz.NET(定时任务)
private void button1_Click(object sender, EventArgs e)
{
//每隔一段时间执行一个任务
ISchedulerFactory sf = new StdSchedulerFactory();
IScheduler sched = sf.GetScheduler(); //获得一个计划任务
JobDetail job = new JobDetail("job1", "group1", typeof(MyJog)); MyJog为实现了IJob接口的类
DateTime dt = TriggerUtils.GetNextGivenSecondDate(null, 5); //5秒后开始第一次运行
TimeSpan interval = TimeSpan.FromSeconds(5); //时间段--每隔5s执行一次
//每若干小时运行一次,小时间隔由appsettings中的IndexIntervalHour参数指定
Trigger trigger = new SimpleTrigger("trigger1", "group1", "job1", "group1", dt, null, SimpleTrigger.RepeatIndefinitely, interval);
sched.AddJob(job,true);
sched.ScheduleJob(trigger);
sched.Start();
}

class MyJog : IJob
{
public void Execute(JobExecutionContext context)
{
MessageBox.Show("我执行了l");
}
}

Quartz.Net实现:每10分钟定时发送系统数据:新增用户数据
每次用户注册的时候,都把用户的注册信息:用户名、邮箱、手机号等.除了正常的注册之外,额外再把这些数据放入消息队列。
每隔5分钟,定时从消息队列中取出新注册用户信息,然后发送邮件给业务人员.
SendNewRegUserEmailJob.cs
1 直接Global不行: RupengTimer类库 //如鹏定时任务类库(因为在Global中进行会有兼容性问题)
2 Console控制台程序也不可以: Application.Run(new FormMain()); //加载WinForm时直接启动WinForm程序,因为上面是在WinForm中测试成功的
3 WinForm中执行定时任务还是不行: 因为时间(与世界标准时间是差8h) ---革零为致时间与北京(东八区)时间差8h
DateTime.Now 应该是: DateTime dt = TriggerUtils.GetNextGivenSecondDate(null, 1);

项目任务:确定邮件的发送;重置密码邮件的发送都放入单独的定时任务类库中.

//namespace RupengWangTimer
public class SendNewRegisterUM //用于发送新注册用户信息
{
/// <summary>
/// 定时执行计划--把队列中的用户信息定时发送给指定人员
/// </summary>
public static void TimeExecuteSchedule()
{
//每隔一段时间执行一个任务
ISchedulerFactory sf = new StdSchedulerFactory();
IScheduler sched = sf.GetScheduler(); //获得一个计划任务
JobDetail job = new JobDetail("jobTSUM", "groupTSUM", typeof(SendFromQueueUserMessage)); MyJog为实现了IJob接口的类
//DateTime dt = TriggerUtils.GetNextGivenSecondDate(null, 5); //5秒后开始第一次运行
//DateTime dt = DateTime.Now; //立即执行
DateTime dt = TriggerUtils.GetNextGivenSecondDate(null, 1); //1s后开始执行
TimeSpan interval = TimeSpan.FromHours(1); //时间段--每隔50s执行一次
//每若干小时运行一次,小时间隔由appsettings中的IndexIntervalHour参数指定
Trigger trigger = new SimpleTrigger("triggerTSUM", "groupTSUM", "jobTSUM", "groupTSUM", dt, null, SimpleTrigger.RepeatIndefinitely, interval);
sched.AddJob(job, true);
sched.ScheduleJob(trigger);
sched.Start();
}

/// <summary>
/// 把队列中的用户信息定时发送给指定人员
/// </summary>
class SendFromQueueUserMessage : IJob
{
public void Execute(JobExecutionContext context)
{
using(IRedisClient client = RedisManager.ClientManager.GetClient())
{
string newUserMessages = "";
while(true)
{
string newUserMessage = client.DequeueItemFromList("NewUserMessageQueue.");
if (newUserMessage == null)
{
if (newUserMessages.Length<=0)
{
return;
}
//把所有新注册用户信息作为指定邮件发送
MailSendHelper.MailSend("新注册用户信息", "245573276@qq.com", newUserMessages);
//Thread.Sleep(500);
//continue;
}
else
{
newUserMessages += newUserMessage + "\r\n";
}
}
}
}
}

 

//Day10---------------20150422---------------------------

 


关于搜索:站内搜索技术

 

1 全文检索
like查询是全表扫描(为性能杀手)
Lucene.Net搜索引擎,开源,比sql搜索引擎是收费的
Lucene.Net只是一个全文检索开发包(只是帮我们存数据取数据,并没有界面,可以看作一个数据库,只能对文本信息进行检索)
Lucene.Net原理:把文本切词保存,然后根据词汇表的页来找到文章
分词算法:
引用Lucene.Net


2 一元分词算法 //StandardAnalyzer默认的分词算法
Analyzer analyzer=new StandardAnalyzer();
TokenStream tokenStream=analyzer.TokenStream("",new StringReader("北京,HI欢饮你hello word"));
Lucene.Net.Analysis.Token token=null;
while((token=tokenStream.Next())!=null)
{
Console.WriteLine(token.TernText());
}
Console.ReadKey();


3 二元分词算法 //CJKAnalyzer.cs和CJKTokenizer.cs //CJK:China Japan Korean
Analyzer analyzer=new CJKAnalyzer(); // new StandardAnalyzer();
TokenStream tokenStream=analyzer.TokenStream("",new StringReader("北京,HI欢饮你"));
Lucene.Net.Analysis.Token token=null;
while((token=tokenStream.Next())!=null)
{
Console.WriteLine(token.TernText());
}
Console.ReadKey();
基于词库的分词算法(庖丁解牛\盘古分词算法)


4 盘古分词算法
打开PanGu4Luene\WebDemo\Bin,将Dictionaries添加到项目根路径(改名Dict),
添加PanGu.dll的引用(如果直接引用PanGu.dll则必须不带PanGu.xml)、
添加PanGu4Luene\Release中PanGu.Luene.Analyzer.dll的引用
Analyzer analyzer=new PanGuAnalyzer();
TokenStream tokenStream=analyzer.TokenStream("",new StringReader("北京,HI欢饮你hello word"));
Lucene.Net.Analysis.Token token=null;
while((token=tokenStream.Next())!=null)
{
Console.WriteLine(token.TernText());
}
//报错:文件\属性\复制到输出目录\如果较新则复制(可以把文件文档拷贝到Debug中)
其中PanGu_Release_V2.3.1.0\Release\DictManage.exe可以查看Dict.dct二进制词库,既可以查看词汇也可以加入词汇


5 Luene.Net写入类介绍
步骤:
打开文件夹,指定要写入的文件夹
文件加锁,避免两个人同时写入文件(并发)
判断是否文件中有数据,有的话就更新数据,没有就创建
逐一读取待读文件中文本并写入文档
写之后进行close,则表示解锁,可以由其他人写入(加锁写入过程中程序出现bug需要强制解锁时可能出问题)
各种类的作用:
Directory保存数据:FSDirectory(文件中),RAMDirectory(内存中)
IndexReader对索引库进行读取的类,IndexWriter对索引库进行写的类
IndexReader的bool IndexExists(Directory directory)判断目录是否是一个索引目录
IndexWriter的bool IsLocked(Directory directory)判断目录是否是锁定的
IndexWriter在进行写操作时会自动加锁,close的时候会自动解锁.IndexWriter.Unlock方法手动解锁(比如还没来得及close IndexWriter程序就崩溃了,可能造成一直被锁定)
IndexWriter(Directory dir,Analyzer a,bool create,MaxFieldLength mfl)写入哪个文件夹,采用什么分词算法,是否是创建,最大大小
void AddDocument(Document doc),向索引中添加文档
Add(Field field)向文档中添加字段
DeleteAll()删除所有文档,DeleteDocuments按照条件删除文档
File类得构造函数 Field(string name,string value,Field.Store store,Field.Index index,Field.TermVector termVector)
上面依次表示:(字段名,字段值,是否把原文保存到索引中,index表示如何创建索引(Field.Index需要进行全文检索,NOT_ANALYZED不需要的),termVector表示索引词之间的距离,超出则关联度低)
处理并发(写的时候只能逐一写入):用消息队列保证只有一个程序(线程)对索引操作,其他程序不直接进行索引库的写入,而是把要写入的数据放入消息队列,由单独的程序从消息队列中取数据进行索引库的写入


6 文章写入索引集成
文章新增编辑时把新闻放入消息队列
NewsIndexer.cs对新闻消息队列进行处理,然后写入索引库的类

/// <summary>
/// 对新闻队列进行处理,然后加入新闻索引
/// </summary>
class NewsIndexer
{
public void Start()
{
while(true)
{
using(var client = RedisManager.ClientManager.GetClient())
{
string json = client.DequeueItemFromList("NewsQueue");
if(json==null)
{
Thread.Sleep(100);
}
else
{
//把新闻队列中的json反序列化,加入索引
Dictionary<string, object> dict = (Dictionary<string, object>)new JavaScriptSerializer().DeserializeObject(json);
long id = Convert.ToInt64(dict["Id"]);
long categoryId = Convert.ToInt64(dict["CategoryId"]);
string title = dict["Title"].ToString();
string newsContent = dict["NewsContent"].ToString();
//写入索引库
WriteToIndex(id, categoryId, title, newsContent);
}
}
}
}

/// <summary>
/// 写入索引库
/// </summary>
static void WriteToIndex(long id, long categoryId, string title, string newsContent)
{
FSDirectory directory = null;
IndexWriter writer = null;
try
{
string indexPath = "d:rupeng_news_index"; //索引库路径
directory = FSDirectory.Open(new DirectoryInfo(indexPath), new NativeFSLockFactory()); //索引保存到指定文件目录,并加锁
bool isExist = IndexReader.IndexExists(directory); //读索引时,指定目录中索引是否存在
if (isExist)
{
if(IndexWriter.IsLocked(directory)) //写索引时,如果索引目录是被锁定(比如索引过程中程序异常退出),需要先解锁
{
IndexWriter.Unlock(directory);
}
}
//一条一条写入目录索性
writer = new IndexWriter(directory, new PanGuAnalyzer(), !isExist, Lucene.Net.Index.IndexWriter.MaxFieldLength.UNLIMITED);
Document document = new Document();
document.Add(new Field("id", id.ToString(), Field.Store.YES, Field.Index.NOT_ANALYZED)); //不需要索引
document.Add(new Field("categoryId", id.ToString(), Field.Store.YES, Field.Index.NOT_ANALYZED));
document.Add(new Field("title", id.ToString(), Field.Store.YES, Field.Index.ANALYZED,Lucene.Net.Documents.Field.TermVector.WITH_OFFSETS));//需要索引
document.Add(new Field("newsContent", id.ToString(), Field.Store.YES, Field.Index.ANALYZED, Lucene.Net.Documents.Field.TermVector.WITH_OFFSETS));
writer.AddDocument(document);
}
finally
{
if(writer!=null) //不要忘了回收资源,否则搜索不到
{
writer.Close();
}
if (directory!=null)
{
directory.Close();
}
}
}
}

//用多线程避免页面卡死,避免每篇都写入---???---


7 文章的搜索
query.Add(new Term("字段名","关键词"))
query.Add(new Term("字段名2","关键词2"))
类似于:where 字段名contains关键词 and 字段名2contains关键词2
PhraseQuery用于进行多个关键词的检索
PhraseQuery.SetSlop(int slop)用来设置单词之间的最大距离
BooleanQuery可以实现字段名contains关键词or字段名2contains关键词2

NewsSearchController.ashx
/// 文章搜索
public void Search(HttpContext context)
{
string keyword = context.Request["keyword"];
string[] words = keyword.Split(' ');
PhraseQuery query = new PhraseQuery();
foreach(string word in words)
{
query.Add(new Term("newsContent", word)); //新闻索引中字段名newsContent包含关键字word
}
query.SetSlop(1000);

List<SearchResult> results = new List<SearchResult>();

FSDirectory directory = FSDirectory.Open(new DirectoryInfo("D:/rupeng_news_index"), new NoLockFactory());
IndexReader reader = IndexReader.Open(directory, true);//采用IndexReader来打开索引目录
IndexSearcher searcher = new IndexSearcher(reader);//通过IndexSearcher来进行搜索
TopScoreDocCollector collector = TopScoreDocCollector.create(1000, true);//通过TopScoreDocCollector来获得查询结果,最多100条结果
searcher.Search(query, null, collector);//开始搜索,使用query这个条件进行搜索
ScoreDoc[] docs = collector.TopDocs(0, collector.GetTotalHits()).scoreDocs;//取得其中的多少条,collector.TopDocs(m,n)获得结果中第m到n条结果
for (int i = 0; i < docs.Length;i++ )
{
int docId = docs[i].doc;//ScoreDoc是查询的文档的结果的数据,ScoreDoc.doc是获得lucene为每个document分配的主键
Document doc = searcher.Doc(docId);//根绝docId再次查询文档的信息
long id = Convert.ToInt64(doc.Get("id"));
long categoryId = Convert.ToInt64(doc.Get("categoryId"));
string title = doc.Get("title");
//string newsContent = doc.Get("newsContent");
SearchResult result = new SearchResult();
result.Url = "/NewsFile/" + categoryId + "/" + id + ".shtml";
result.Title = title;
results.Add(result);
}
RPHelper.RPOutputHtml(context, "~/SearchFile/NewsSearch.cshtml", new { results = results, keyword = keyword });

}
}
public class SearchResult
{
public string Url { get; set; }
public string Title { get; set; }
}


8 帮用分词和分页
//分词
加入索引说采用的分词算法需要与搜索的分词算法一致
用盘古分词算法的Segment类进行切词
PanGu.Segment segment = new PanGu.Segment();
var wordInfos = segment.DoSegment(keyword);//切分关键词得到关键词集合
foreach(var wordInfo in wordInfos)
{
query.Add(new Term("newsContent", wordInfo.Word));
//where contains(newsContent,"北京") and contains(newsContent,"工程师")
}
//分页
NewsSearchController.ashx
//点击页码时跳转到所点击的页面
string pagenumStr = context.Request["pagenum"];
int pagenum = 1;//默认为初始页
if(!string.IsNullOrWhiteSpace(pagenumStr))
{
pagenum = Convert.ToInt32(pagenumStr);
}

//获得当前页所有文档
int pageSize=5; //如果pageSize=5; 0--4 5--9
//查询结果集合应该是从(pagenum-1)*5,pagenum*5-1,但是collector.TopDocs(m,n)的n是条数
ScoreDoc[] docs = collector.TopDocs((pagenum - 1) * 5, pageSize).scoreDocs;

RPHelper.RPOutputHtml(context, "~/SearchFile/NewsSearchResult.cshtml", new {
results = results,
keyword = keyword,
totalSize = collector.GetTotalHits(),
pageSize=pageSize,
currentPage=pagenum
});

NewsSearchResult.cshtml
//在结果列表时,可以直接调用RPHelper中的Pager方法确定页码格式
<ul class='pagination col-md-12' style='width:auto'>
@RupengWangRazor.RPHelper.Pager("/SearchFile/NewsSearchController.ashx?action=Search&keyword="+RupengWangRazor.RPHelper.UrlEncode(Model.keyword)+"&pagenum={pagenum}",Model.totalSize,Model.pageSize,Model.currentPage)
</ul>

//搜索不出来:索引库路径是否正确,写入索引与搜索的分词算法是否一致


9 通过多线程避免界面卡死
耗时操作阻塞了主线程
Thread thread=new Thread(F1); //委托
thread.IsBackground=true;//主线程(界面线程)指向结束后子线程自动结束,因为把子线程设置为了后台线程
thread.Start(); //在子线程中指向F1方法
//子线程不能直接操作界面控件
//textBox1.Text="正在读取第"+i+"次"
//在子线程中操作界面控件的时候必须通过BeginInvoke
textBox1.BeginInvoke(new Action(()=>
{
textBox1.Text="正在读取第"+i+"次"
}));

TestThread.Form1.cs
private void button1_Click(object sender, EventArgs e)
{
Thread thread = new Thread(F1);//将F1的耗时操作委托给子线程
//主线程结束后子线程自动结束(关闭窗口后,子线程依然在执行,因为子线程也在前台,需要将子线程设置到后台)
thread.IsBackground = true;
thread.Start();//在子线程中执行F1
}
public void F1()
{
//点击时不能进行任何操作,因为耗时操作阻塞了主线程,需要将耗时操作委托给子线程
for (int i = 1; i < 100; i++)
{
File.ReadAllBytes(@"D:\NET Framework 4.5\Net Framework 4.5\dotnetfx45_full_x86_x64_XiaZaiBa.zip");
//子线程不能直接操作界面控件
//textBox1.Text = "正在读取第" + i + "次";
//在子线程中操作界面控件必须通过BeginInvoke
textBox1.BeginInvoke(new Action(() => {
textBox1.Text = "正在读取第" + i + "次";
}));
}
}

RuPengWangTimerDingShi.csProj---Form1.cs---SendNewRegisterUM.TimeExecuteWriteNewsIndex();
//定时任务,子进程中出队列,然后写入文章索引,关闭窗口时终止子进程(出队列)和quartz.net进程
首先,启动窗体,执行定时任务,而定时的任务是进行新闻的出队列
然后,新闻的出队列是耗时操作,需要委托子进程,并设为后台进程,然后开始执行进程,其中出队列进程的控制由while(IsRunning)控制,先预先设置IsRunning=true
IsRunning = true;
Thread thread = new Thread(RunScan);//委托给子线程去RunScan
thread.IsBackground = true;//该子线程为后台线程
thread.Start();//执行该后台子线程,去执行RunScan方法
然后,执行出队列这个后台子进程
public static bool IsRunning { get; set; }//是否继续线程
public void RunScan()
{
while (IsRunning)//一旦窗体关闭,IsRunning=false,该进程终止
{...
然后,一直执行这个子进程,直到窗体被关闭,这时设置IsRunning=false使还在执行的这个后台子进程Thread的RunScan()终止,同时还需终止后台Quartz.net进程,避免窗体关闭而进程还在
private void Form1_FormClosed(object sender, FormClosedEventArgs e)
{
NewsIndexer.IsRunning = false;//终止后台子进程RunScan方法
SendNewRegisterUM.schedWNI.Shutdown();//还需要终止后台Quartz.net进程,避免窗体已关闭,但是进程依然在
}

 

10 获取html的InnerText

搜索出来的不仅只是Title,还需要预览一部分内容body
用Lucene.net放入索引的时候需要过滤html标签
解决索引中body中全是html标签的问题,不利于搜索,很多垃圾信息,显示不方便。
使用HtmlAgilityPack进行innerText处理.
考虑文章编辑\重新索引等问题,需要先把旧的文档删除,再增加新的(等价于update)HTML解析器:输入一个html文档,提供对html文档操作的接口
开发包HtmlAgilityPack.1.4.0.zip,用于把html标签进行innerText后再放入索引库

TestHtmlAgilityPack.csProg---Program.cs
HtmlDocument htmlDoc = new HtmlDocument();
htmlDoc.Load(@"D:\temp\htmlAgilityPack.txt");
//HtmlNode node = htmlDoc.GetElementbyId("p11");//获得hmtl文档中id为p11的标签节点
//Console.WriteLine(node.InnerText);
Console.WriteLine(htmlDoc.DocumentNode.InnerText);//获得html文档中的文档节点的innerText显示
//htmlDoc.DocumentNode.DescendantNodes()
Console.ReadKey();

 

11 一键重建全文检索
HtmlAgilityPack.dll提供操作Html文档的标签方法
获得网页title:doc.DocumentNode.SelectSingleNode("//title").InnerText;//XPath中"//title"表示所有title节点;SelectSingleNode用于获取满足条件的唯一节点
获得所有超链接:doc.DocumentNode.Descendants("a");
获得name为kw的input,相当于getElementByName();
var kwBox=doc.DocumentNode.SelectSingleNode("//input[@name='kw']");//"//input[@name='kw']"也是XPath语法,表示name=kw的input标签

首先 把news中body的html标签去掉后加入队列,
NewsController.ashx
/// 一键重新加入全文检索索引
public void ReJoinAllNewIndex(HttpContext context)
{
//1
//List<News> list = new NewsBLL().GetModelList("");
获得所有新闻,但是如果大量新闻,需要分页获取新闻,如每次100条,暂时将就----???-----
//foreach(News news in list)
//{
// //把新闻的body中html标签去掉hmtl标签,加入队列,然后建立索引
// AdminHelper.JoinAllNewQueue(news.Id, news.CategoryId, news.Title, news.NewsContent);
//}
//AjaxHelper.WriteJson(context, "ok", "", null);

//2
//获得所有新闻,但是如果大量新闻,需要分页获取新闻,如每次100条,暂时将就----!!!-----
//select * from (select ROW_NUMBER() over(order by Id asc) rownum,* from T_News) t where rownum>-1 and rownum<=20 ------***-----
//1-100,101-200,201-300,(i-1)*100+1,i*100
//int total=220;
int total = new NewsBLL().GetRecordCount("");//总的新闻条数
int n = (int)Math.Ceiling(total/100.0);//新闻条数/100的水仙花数
for (int i = 1; i <= n;i++ )//如果新闻数量太多,每次获取100条数据,总获取n次
{
List<News> list = new NewsBLL().GetNewsesPagerByRowNum(((i - 1) * 100 + 1), i * 100);
foreach (News news in list)
{
//把新闻的body中html标签去掉hmtl标签,加入队列,然后建立索引
AdminHelper.JoinAllNewQueue(news.Id, news.CategoryId, news.Title, news.NewsContent);
}
}
AjaxHelper.WriteJson(context, "ok", "", null);
}

AdminHelper.cs
/// 将每个新闻的body中html标签去掉后加入队列,然后建立索引
public static void JoinAllNewQueue(long id,long categoeyId,string title,string newsContent)
{
News news = new News();//该新闻对象的newsContent会被innerText
news.Id = id;
news.CategoryId = categoeyId;
news.Title = title;
HtmlDocument htmlDoc = new HtmlDocument();
htmlDoc.LoadHtml(newsContent);
news.NewsContent = htmlDoc.DocumentNode.InnerText;//用HtmlAgilityPack解析器将去掉文档中的html,变为innerText
NewsQueue(news);//加入新闻队列
}

然后 从出队列后写入检索索引之前,删除重复id的索引
NewsIndexer.cs
//每次出队列加入检索索引之前,都需要删除文档索引中的相同id的文档索引,因为"编辑新闻"和"一键重建全文索引"都会再次加入同id的索引
writer.DeleteDocuments(new Term("id", id.ToString()));

然后 优化:每次出队列都进行一次索引路径的打开读取和关闭,效率低
全部出队列之前先打开索引目录,之后才关闭索引目录,最后才等待下一次client的队列中新数据
public static bool IsRunning { get; set; }//是否继续线程
public void RunScan()
{
while (IsRunning)//一旦窗体关闭,IsRunning=false,该进程终止
{
using (var client = RedisManager.ClientManager.GetClient())
{
ProessQueue(client);//一直进行队列,结束后才回收队列
}
}
}

//全部出队列之前先打开索引目录,之后才关闭索引目录,最后等待队列中新数据
private static void ProessQueue(ServiceStack.Redis.IRedisClient client)
{
FSDirectory directory = null;
IndexWriter writer = null;
try
{
string indexPath = "d:rupeng_news_index"; //索引库路径
directory = FSDirectory.Open(new DirectoryInfo(indexPath), new NativeFSLockFactory()); //索引保存到指定文件目录,并加锁
bool isExist = IndexReader.IndexExists(directory); //读索引时,指定目录中索引是否存在
if (isExist)
{
if (IndexWriter.IsLocked(directory)) //写索引时,如果索引目录是被锁定(比如索引过程中程序异常退出),需要先解锁
{
IndexWriter.Unlock(directory);
}
}
writer = new IndexWriter(directory, new PanGuAnalyzer(), !isExist, Lucene.Net.Index.IndexWriter.MaxFieldLength.UNLIMITED);

//一直进行队列,结束后才关闭索引目录
while (true)
{
string json = client.DequeueItemFromList("NewsQueue");
if (json == null)
{
//如果json 这个队列李没有数据,线程就一直sleep、
//这里应该把代码放到另一个线程中,让另一个线程去执行
//主线程仍然继续执行程序
Thread.Sleep(100);
return;//全部出队列后,在finally中关闭索引目录,最后回收队列,然后等待队列,直到IsRunning=false
}
else
{
//把新闻队列中的json反序列化,加入索引
Dictionary<string, object> dict = (Dictionary<string, object>)new JavaScriptSerializer().DeserializeObject(json);
long id = Convert.ToInt64(dict["Id"]);
long categoryId = Convert.ToInt64(dict["CategoryId"]);
string title = dict["Title"].ToString();
string newsContent = dict["NewsContent"].ToString();
//写入索引库
WriteToIndex(writer,id, categoryId, title, newsContent);
//File.AppendAllText(@"D:\temp\1.txt","id="+id);//用于判断写入索引速度,很快
}
}

//全部出队列后关闭索引目录
}
finally
{
if (writer != null) //不要忘了回收资源,否则搜索不到
{
writer.Close();
}
if (directory != null)
{
directory.Close();
}
}
}

/// 写入索引库
static void WriteToIndex(IndexWriter writer,long id, long categoryId, string title, string newsContent)
{
//每次出队列加入检索索引之前,都需要删除文档索引中的通id索引,因为"编辑新闻"和"一键重建全文索引"都会再次加入同id的索引
writer.DeleteDocuments(new Term("id", id.ToString()));

Document document = new Document();
document.Add(new Field("id", id.ToString(), Field.Store.YES, Field.Index.NOT_ANALYZED)); //不需要索引
document.Add(new Field("categoryId", categoryId.ToString(), Field.Store.YES, Field.Index.NOT_ANALYZED));
document.Add(new Field("title", title.ToString(), Field.Store.YES, Field.Index.ANALYZED, Lucene.Net.Documents.Field.TermVector.WITH_OFFSETS));//需要索引
document.Add(new Field("newsContent", newsContent.ToString(), Field.Store.YES, Field.Index.ANALYZED, Lucene.Net.Documents.Field.TermVector.WITH_OFFSETS));
writer.AddDocument(document);
}

NewsSearchController.ashx
/// 文章搜索
public void Search(HttpContext context)
{
string keyword = context.Request["keyword"];
string pagenumStr = context.Request["pagenum"];
int pagenum = 1;//默认为初始页
if(!string.IsNullOrWhiteSpace(pagenumStr))
{
pagenum = Convert.ToInt32(pagenumStr);
}

PhraseQuery query = new PhraseQuery();
//因为加入索引时是用盘古分词算法,所以切词时也应该是盘古分词算法
//盘古分词算法中的Segment类可以更好的进行切词
PanGu.Segment segment = new PanGu.Segment();
var wordInfos = segment.DoSegment(keyword);//切分关键词得到关键词集合
foreach(var wordInfo in wordInfos)
{
query.Add(new Term("newsContent", wordInfo.Word));
//where contains(newsContent,"北京") and contains(newsContent,"工程师")
}
//string[] words = keyword.Split(' ');
//foreach(string word in words)
//{
// query.Add(new Term("newsContent", word)); //新闻索引中字段名newsContent包含关键字word
//}
query.SetSlop(1000);

List<SearchResult> results = new List<SearchResult>();

FSDirectory directory = FSDirectory.Open(new DirectoryInfo("D:temp/rupeng_news_index"), new NoLockFactory());
IndexReader reader = IndexReader.Open(directory, true);//采用IndexReader来打开索引目录
IndexSearcher searcher = new IndexSearcher(reader);//通过IndexSearcher来进行搜索
TopScoreDocCollector collector = TopScoreDocCollector.create(1000, true);//通过TopScoreDocCollector来获得查询结果,最多100条结果
searcher.Search(query, null, collector);//开始搜索,使用query这个条件进行搜索
//ScoreDoc[] docs = collector.TopDocs(0, collector.GetTotalHits()).scoreDocs;//取得其中的多少条,collector.TopDocs(m,n)获得结果中第m到n条结果
int pageSize=5; //如果pageSize=5; 0--4 5--9
//查询结果集合应该是从(pagenum-1)*5,pagenum*5-1,但是collector.TopDocs(m,n)的n是条数
ScoreDoc[] docs = collector.TopDocs((pagenum - 1) * 5, pageSize).scoreDocs;

for (int i = 0; i < docs.Length;i++ )
{
int docId = docs[i].doc;//ScoreDoc是查询的文档的结果的数据,ScoreDoc.doc是获得lucene为每个document分配的主键
Document doc = searcher.Doc(docId);//根绝docId再次查询文档的信息
long id = Convert.ToInt64(doc.Get("id"));
long categoryId = Convert.ToInt64(doc.Get("categoryId"));
string title = doc.Get("title");
string newsContent = doc.Get("newsContent");
SearchResult result = new SearchResult();
result.Url = "/NewsFile/" + categoryId + "/" + id + ".shtml";
result.Title = title;
//result.NewContent = newsContent;
result.NewContent = HighLight(keyword, newsContent);
results.Add(result);
}
RPHelper.RPOutputHtml(context, "~/SearchFile/NewsSearchResult.cshtml", new {
results = results,
keyword = keyword,
totalSize = collector.GetTotalHits(),
pageSize=pageSize,
currentPage=pagenum
});
}

 

12 搜索结果高亮显示
添加PanGu.HighLight.dll引用

/// 显示的内容:关键字高亮显示,以及获得最匹配摘要段
/// <param name="keyword">关键字</param>
/// <param name="content">新闻内容body</param>
public string HighLight(string keyword, string content)
{
//创建HTMLFormatter,参数为高亮单词的前后缀
//PanGu.HighLight.SimpleHTMLFormatter simpleHTMLFormatter =
// new PanGu.HighLight.SimpleHTMLFormatter("<font color=\"red\">", "</font>");
PanGu.HighLight.SimpleHTMLFormatter simpleHTMLFormatter =
new PanGu.HighLight.SimpleHTMLFormatter("<span class=\"keywordHightlight\">", "</span>");//正规网站的颜色应该用css表示
//创建Highlighter,输入HTMLFormatter和盘古分词对象Segment
PanGu.HighLight.Highlighter highlighter = new PanGu.HighLight.Highlighter(
simpleHTMLFormatter, new Segment());
//设置每个摘要段得字符数
highlighter.FragmentSize = 100;
//获得最匹配的摘要段
return highlighter.GetBestFragment(keyword, content);
}

项目任务:完成新闻搜索、视频笔记搜索功能,而且是综合搜索
//搜索(分页\高亮显示)-->建立索引-->出队列-->入队列T_Segment(Id,Name,note,ChapterId)\T_News(Id,Title,NewsContent,CategoryId)


13 短信验证码调用接口
yuntongxun.com有8元得免费短信
调用短信接口大量发送短信,对于短信模板进行审核,重要短信模板没有非法信息就可以通过
模板短信--Rest API
短信运营商提供的接口,其实就是http接口,把要发送的手机号\模板短信id通过http协议发送这个接口(发给运营商,运营商去发请求)
API--体验及SDK下载--短信验证码--Demo下载--Rest Server Demo--下载NET--CCPRestSDK.dll

TestRestServer.csProj--Program.cs
protected void Page_Load(object sender, EventArgs e)
{
string ret = null;
CCPRestSDK.CCPRestSDK api = new CCPRestSDK.CCPRestSDK();
bool isInit = api.init("sandboxapp.cloopen.com", "8883");
api.setAccount(主帐号, 主帐号令牌);
api.setAppId(应用ID);
try
{
if (isInit)
{
Dictionary<string, object> retData = api.SendTemplateSMS(短信接收号码, 短信模板id, 内容数据);
ret = getDictionaryData(retData);
}
else
{
ret = "初始化失败";
}
}
catch (Exception exc)
{
ret = exc.Message;
}
finally
{
Response.Write(ret);
}
}

private string getDictionaryData(Dictionary<string, object> data)
{
string ret = null;
foreach (KeyValuePair<string, object> item in data)
{
if (item.Value != null && item.Value.GetType() == typeof(Dictionary<string, object>))
{
ret += item.Key.ToString() + "={";
ret += getDictionaryData((Dictionary<string, object>)item.Value);
ret += "};";
}
else
{
ret += item.Key.ToString() + "=" + (item.Value == null ? "null" : item.Value.ToString()) + ";";
}
}
return ret;
}
}

//ret=statusCode=000000;statusMsg=成功;data={TemplateSMS={dateCreated=20150531193711;smsMessageSid=201505311937105998598;};};

项目任务:
注册时短信获取验证码;
虚拟班级内群发短信活动通知(各位同学,今天{0}点班级举办线上活动,请准备参加)

 

 

 

 

 

 

 


Lucene.Net:开源免费

找出关键词,记录位置(页码),根绝关键词所在的位置去查找


分词算法:把一句话分成一个个单词(盘古分词算法)

把一个文件复制到输出目录,如果较新则复制

 


HtmlDocument htmlDoc = new HtmlDocument();
htmlDoc.Load("d:/temp/1.html");
// HtmlNode node = htmlDoc.GetElementbyId("p1");
// Console.WriteLine(node.InnerText);
Console.WriteLine(htmlDoc.DocumentNode.InnerText);*/

 

posted on 2015-11-20 08:36 AdolphYang 阅读( ...) 评论( ...) 编辑 收藏

转载于:https://www.cnblogs.com/adolphyang/p/4979686.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值