欢迎您来到 数字平台。 您尚未登录。[登录] [注册新用户]
当前位置: 论坛首页 / 栏目 产品与服务 / 文章 679

点击:64723[回复顶层] [树状] [详细]
[回复留言] [回复(需要先登录)] [引用(需要先登录)]精品第 1 楼
文章id: 679
批量修改读者密码

作者: 精灵


    最近我们馆升级的DP2管理系统,升级后OPAC具有了书评、荐书等功能。这些功能没有问题一直很稳定,不过我总是感觉有些隐忧,因为很多读者证没有设定密码,如果别有用心的人利用这一点登陆系统在书评中乱加评论,就算我们可以追究读者密码保管不利的问题。但是,学校恐怕也会追究我们馆,监管不严的责任。为了杜绝这一点点的隐忧,我想对所有没有密码的读者证添加一个密码。这样,出了问题也好有个交代。

    为了解决这个问题,我想到了“添加或删除读者证”这个方案,这个方案原本是修改读者证状态或者是删除读者数据用的。我开始考虑的比较简单,我想这个方案既然可以删除读者证数据,或者修改读者证状态那么也应该可以修改读者证的密码。打开方案后,仔细看了一下,修改了里面的“string strState = DomUtil.GetElementText(this.ReaderDom.DocumentElement, "state");”语句,另外注掉了里面的一个判断,结果一测试没有效果,跟踪了里面的返回值返现返回值正常。后来,我又仔细看了一下系统的参考手册,发现里面还有一个“ChangeReaderPassword”函数专门用来修改密码的,我自己试着使用了一下,感觉还是不行。最后,实在没有办法,只好求助江老师,下面是江老师给我发回来的他与谢老师的聊天记录片段。

API SetReaderInfo()是不能修改读者的密码的。道理很简单,工作人员不应随意修改读者的密码,而API SetReaderInfo()正好是拿给读者窗使用的。如果要修改密码,可以使用API ChangeReaderPassword()。

long lRet = this.ReaderStatisForm.Channel.ChangeReaderPassword(

                    null, // stop,

                    this.textBox_reader_barcode.Text,      // 读者证条码号

                    this.textBox_reader_oldPassword.Text,        // 旧密码

                    this.textBox_reader_newPassword.Text,       // 新密码

                    out strError);

函数返回1为成功。0和-1都是失败。

如果是工作人员身份调用这个API,则旧密码参数无意义,也就是说,工作人员可以强制修改读者密码(需要changereaderpassword权限)。如果是读者身份调用这个API,则需要提供旧密码。

    下面是谢老师编写的关于修改读者密码的一个统计窗的统计方案main.cs中的内容:

// 测试修改读者密码

// 创建日期:2011/11/1

using System;

using System.Windows.Forms;

using System.IO;

using System.Text;

using System.Xml;

using System.Drawing; // Size

using DigitalPlatform;

using DigitalPlatform.Xml;

using DigitalPlatform.Text;

using DigitalPlatform.dp2.Statis;

using DigitalPlatform.CirculationClient;

using DigitalPlatform.CirculationClient.localhost;

using dp2Circulation;

public class MyStatis : ReaderStatis

{

    Table table = new Table(4);

    string strOutputErrorFilename = ""; // 输出的错误信息文件名

    StreamWriter sw_error = null;

    int nErrorCount = 0; // 发生错误的读者记录数

    public override void OnBegin(object sender, StatisEventArgs e)

    {

        this.ClearConsoleForPureTextOutputing();

        strOutputErrorFilename = this.ProjectDir + "\\error.txt";

        sw_error = new StreamWriter(strOutputErrorFilename,

                    false, // append

                    Encoding.GetEncoding(936)); // gb2312

        sw_error.Write("<pre>\r\n");

    }

    public override void OnRecord(object sender, StatisEventArgs e)

    {

        string strError = "";

        int nRet = 0;

        // 读者条码

        string strReaderBarcode = DomUtil.GetElementText(this.ReaderDom.DocumentElement, "barcode");

        this.WriteTextToConsole(this.CurrentRecordIndex.ToString() + " : " + strReaderBarcode + "\r\n");

        long lRet = this.ReaderStatisForm.Channel.ChangeReaderPassword(

                null, // stop,

                strReaderBarcode,      // 读者证条码号

                null,        // 旧密码

                "test1",       // 新密码

                out strError);

        if (lRet != 1)

        {

            nErrorCount++;

            sw_error.Write("读者记录 " + this.CurrentRecPath + " 修改密码时发生错误: " + strError + "\r\n");

        }

        return;

    ERROR1:

        MessageBox.Show(this.ReaderStatisForm, strError);

        e.Continue = ContinueType.SkipAll;

    }

    public override void FreeResources()

    {

        if (sw_error != null)

        {

            sw_error.Close();

            sw_error = null;

        }

    }

    public override void OnEnd(object sender, StatisEventArgs e)

    {

        if (sw_error != null)

        {

            sw_error.Close();

            sw_error = null;

        }

        string strText = "";

        if (nErrorCount > 0)

        {

            strText += "\r\n\r\n错误信息 " + nErrorCount.ToString() + " 条,请看文件 " + strOutputErrorFilename + " (在读者统计窗的打印结果里面可以看到)";

            // 写入打印输出文件

            string strPrintFile = this.NewOutputFileName();

            File.Copy(this.strOutputErrorFilename, strPrintFile, true);

        }

        MessageBox.Show(this.ReaderStatisForm, strText);

    }

}

    下面是我参考谢老师编写的方案后,自己修改出来的

// 测试修改读者密码

// 创建日期:2011/11/3

using System;

using System.Windows.Forms;

using System.IO;

using System.Text;

using System.Xml;

using System.Drawing;    // Size

using DigitalPlatform;

using DigitalPlatform.Xml;

using DigitalPlatform.Text;

using DigitalPlatform.dp2.Statis;

using DigitalPlatform.CirculationClient;

using DigitalPlatform.CirculationClient.localhost;

using dp2Circulation;

public class MyStatis : ReaderStatis

{

    Table table = new Table(4);

    string strOutputErrorFilename = "";    // 输出的错误信息文件名

    StreamWriter sw_error = null;

    int nErrorCount = 0;    // 发生错误的读者记录数

    public override void OnBegin(object sender, StatisEventArgs e)

    {

        this.ClearConsoleForPureTextOutputing();

        strOutputErrorFilename = this.ProjectDir + "\\error.txt";

        sw_error = new StreamWriter(strOutputErrorFilename,

                    false,    // append

                    Encoding.GetEncoding(936));    // gb2312

        sw_error.Write("<pre>\r\n");

    }

    public override void OnRecord(object sender, StatisEventArgs e)

    {

        if (DomUtil.GetElementText(this.ReaderDom.DocumentElement, "password") == "")

        {

            string strError = "";

            int nRet = 0;

            // 读者条码

            string strReaderBarcode = DomUtil.GetElementText(this.ReaderDom.DocumentElement, "barcode");

            // 读者证号

            string strcardNumber = DomUtil.GetElementText(this.ReaderDom.DocumentElement, "cardNumber");

            // 读者类型

            string strreaderType = DomUtil.GetElementText(this.ReaderDom.DocumentElement, "readerType");

            if (strreaderType == "中文专业" || strreaderType == "本科" || strreaderType == "专科")

            {

                if (strcardNumber == "")

                {

                    strcardNumber = Guid.NewGuid().ToString();

                }

            }

            else if (strreaderType == "教参借阅" || strreaderType == "教师" || strreaderType == "工人" || strreaderType == "特殊借阅")

            {

                strcardNumber = strReaderBarcode;

            }

            this.WriteTextToConsole(this.CurrentRecordIndex.ToString() + " : " + strReaderBarcode + "\r\n");

            //if (strcardNumber == "") { strcardNumber = "4fd56s4fs65f46s"; }

            long lRet = this.ReaderStatisForm.Channel.ChangeReaderPassword(

                    null,    // stop,

                    strReaderBarcode,      // 读者证条码号

                    null,        // 旧密码

                    strcardNumber,       // 新密码

                    out strError);

            if (lRet != 1)

            {

                nErrorCount++;

                sw_error.Write("读者记录 " + this.CurrentRecPath + " 修改密码时发生错误: " + strError + "\r\n");

            }

            return;

        ERROR1:

            MessageBox.Show(this.ReaderStatisForm, strError);

            e.Continue = ContinueType.SkipAll;

        }

    }

    public override void FreeResources()

    {

        if (sw_error != null)

        {

            sw_error.Close();

            sw_error = null;

        }

    }

    public override void OnEnd(object sender, StatisEventArgs e)

    {

        if (sw_error != null)

        {

            sw_error.Close();

            sw_error = null;

        }

        string strText = "";

        if (nErrorCount > 0)

        {

            strText += "\r\n\r\n错误信息 " + nErrorCount.ToString() + " 条,请看文件 " + strOutputErrorFilename + " (在读者统计窗的打印结果里面可以看到)";

            // 写入打印输出文件

            string strPrintFile = this.NewOutputFileName();

            File.Copy(this.strOutputErrorFilename, strPrintFile, true);

        }

        MessageBox.Show(this.ReaderStatisForm, strText);

    }

}

    我上面的方案是在谢老师写的方案的基础上,首先判断此读者是否有密码,如果,这个读者已经设置密码则跳过,如果没有设置密码,则想根据读者类型区分出老师与学生这两类读者,对于学生这类没有设置密码的读者首先判断这个读者是否有证号(证号这个字段中存放的是学生的学号),如果有证号这个字段,就用证号作为这个读者的密码。因为我们馆不是所有的学生数据在入库时都填写了证号,所以,对于没有证号的这类读者,我就采取了一个比较极端的处理方式,用GUID函数直接生成一个不可重复的字符串作为密码写入库中,这样,可以强制这类读者来馆修改密码。对于老师这类读者,因为人数少,所以就用他自己的借阅证号作为密码写入库中。

    以上就是这件事情的经过,以及我自己参考谢老师的方案写出的处理方法。在这里首先对谢老师以及江老师的指导表示感谢。呵呵。。谢谢二位的指导,另外,也想请二位老师,以及其他馆的老师,一起讨论一下,看看我这么做是否得当。。。是不是我把这个问题想的太严重了。同时,如果有跟我需求相同的同行们,也可以参考一下谢老师写的方案。方便大家处理这类问题。



发表时间: 2011-11-03 15:59:53
最后修改时间: 2011-11-03 16:08:24
[回复留言] [回复(需要先登录)] [引用(需要先登录)]普通文章第 2 楼
文章id: 680
关于修改密码的一些补充意见

作者: xietao


谢谢精灵把自己的心得体会写出来,与大家分享。

关于读者帐户没有密码而可能导致隐私泄露问题,绝不是空穴来风。我们对隐私和安全问题应当抱有严肃的态度。江汇泉曾经提出过这个问题,并且给出了一定的解决办法建议就是读者记录创建的时候其初始密码可以采用姓名拼音或者生日或者几个字段的内容的组合。

当时我觉得解决办法还不太成熟,也想听听用户单位的意见;另外我们的系统以灵活和可以配置为傲,我一直在想是不是可以用一个二次开发脚本函数来定制这个缺省密码的功能,但又觉得动用脚本函数是不是有些牛刀了,也要考虑大部分用户单位对于管理的简便性的要求 ---- 于是在纠结和试图平衡的过程中时间就流逝了。

我想,是不是还是尽早把这个缺省密码的功能,也可以做出来再改进,不过那样就有可能带来用户的一些颠簸了 --- 可能要跟着把配置文件改来改去。

即便是解决了缺省密码的问题,但是那些以前创建的没有密码的读者记录怎么办呢?怎么给它们加上密码?那么精灵编写的这个统计方案还是有现实意义的。可以认为是缺省密码机制的一个补充。

但,目前这个统计方案还需要通用化以后,才能作为公司推荐的统计方案放在网站上,提供给所有用户单位使用。我建议可以设定一些预定义的动作类型,比如采用生日字符串,采用姓名拼音,采用其他什么字段来填入密码。

(顺便悄悄问一句:有拼音字段么,读者记录中?)

这样使用这个统计方案的操作者就可以选用预定义的动作来执行了。

~~~

我前两天还在琢磨,精灵同志将怎样在程序中判断一个读者记录有没有密码呢?

看了今天的代码,我不得不解释一下了。精灵所采用的判断<password>元素的值是否为空的办法,是可以用的,但是理论上不严密。

为什么呢?因为读者记录中<password>这个元素中存储的是读者密码的hash码,而“空”密码hash以后可不是空哟!当然恰好现在的dp2library服务器程序在创建读者记录的时候让<password>元素内容是空,这是一个巧合。理论上说,<password>元素内容是空,或者是“空”的hash码,两种情况都算空。

那么这个“空”的hash码是什么呢?这,就是天机不可泄漏了。hash码的用意就是不要你知道,故意扰乱的。我们就不要沿着这个方向去走了。

可以改换方向思考。在dp2circulation的出纳窗中,有一种校验读者密码的方式,那么肯定dp2circulation前端采用了什么API调用来验证读者密码的。我们可以使用这个API来,验证一下读者的密码是不是空。这个方法最好,因为这个API是系统提供的,有足够的可靠性和稳定性,我们作为二次开发者就不必操心那些hash码之类的事情了。

 

这个校验读者密码的API的类型是:

long VerifyReaderPassword(Stop stop, string strReaderBarcode, string strPassword, out strError)

返回-1是出错,返回0是校验不匹配,返回1是匹配。

调用这个API,需要当前用户具有verifyreaderpassword的权限。如果是读者类型的帐户,则只能校验自己的密码,不能校验其他人的密码。

希望精灵可以试一试用这个函数判断读者当前是否有了密码。

当然,这个API和其他API一样,是在Channel里面的一个函数,调用时形态如下:

long lRet = this.ReaderStatisForm.Channel.VerifyReaderPassword(

null, // stop,

strReaderBarcode, // 读者证条码号

"", // 要检测的密码

out strError);



发表时间: 2011-11-03 16:22:36
最后修改时间: 2011-11-03 16:33:30



[回复留言] [回复(需要先登录)] [引用(需要先登录)]普通文章第 3 楼
文章id: 681

作者: xietao


精灵你说:

> 后来,我又仔细看了一下系统的参考手册,发现里面还有一个“ChangeReaderPassword”函数专门用来修改密码的,我自己试着使用了一下,感觉还是不行...

现在我想知道,同样是用了这个API,你第一次为什么“不行”呢?具体出现了什么问题?我想了解这些细节的目的是,想看看我们的函数本身或者文档或者其他环境因素有没有妨碍二次开发的地方,如果有,我们好想办法克服。



发表时间: 2011-11-03 16:37:52



[回复留言] [回复(需要先登录)] [引用(需要先登录)]普通文章第 4 楼
文章id: 682
是否可以用更强硬点的手段?



当前系统默认创建出的读者数据密码为空。

假如想逼迫读者添加上密码,是否可以考虑系统对于密码为空的帐户就不允许登录,提示他输入姓名、证号(或其它的鉴别内容),折腾两三次后才能登录进入。

读者嫌这样麻烦后,或许就会主动设置自己的密码了。

提示输入姓名、证号等鉴别信息,也可以防止其它读者恶意猜测到空密码并进入了。



发表时间: 2011-11-03 16:38:58
[回复留言] [回复(需要先登录)] [引用(需要先登录)]普通文章第 5 楼
文章id: 683
感谢,谢老师的支持与提醒。

作者: 精灵


以下是引用 xietao 于 2011-11-3 16:22:36 发表的文字:

谢谢精灵把自己的心得体会写出来,与大家分享。

关于读者帐户没有密码而可能导致隐私泄露问题,绝不是空穴来风。我们对隐私和安全问题应当抱有严肃的态度。江汇泉曾经提出过这个问题,并且给出了一定的解决办法建议就是读者记录创建的时候其初始密码可以采用姓名拼音或者生日或者几个字段的内容的组合。

当时我觉得解决办法还不太成熟,也想听听用户单位的意见;另外我们的系统以灵活和可以配置为傲,我一直在想是不是可以用一个二次开发脚本函数来定制这个缺省密码的功能,但又觉得动用脚本函数是不是有些牛刀了,也要考虑大部分用户单位对于管理的简便性的要求 ---- 于是在纠结和试图平衡的过程中时间就流逝了。

我想,是不是还是尽早把这个缺省密码的功能,也可以做出来再改进,不过那样就有可能带来用户的一些颠簸了 --- 可能要跟着把配置文件改来改去。

即便是解决了缺省密码的问题,但是那些以前创建的没有密码的读者记录怎么办呢?怎么给它们加上密码?那么精灵编写的这个统计方案还是有现实意义的。可以认为是缺省密码机制的一个补充。

但,目前这个统计方案还需要通用化以后,才能作为公司推荐的统计方案放在网站上,提供给所有用户单位使用。我建议可以设定一些预定义的动作类型,比如采用生日字符串,采用姓名拼音,采用其他什么字段来填入密码。

(顺便悄悄问一句:有拼音字段么,读者记录中?)

这样使用这个统计方案的操作者就可以选用预定义的动作来执行了。

~~~

我前两天还在琢磨,精灵同志将怎样在程序中判断一个读者记录有没有密码呢?

看了今天的代码,我不得不解释一下了。精灵所采用的判断元素的值是否为空的办法,是可以用的,但是理论上不严密。

为什么呢?因为读者记录中这个元素中存储的是读者密码的hash码,而“空”密码hash以后可不是空哟!当然恰好现在的dp2library服务器程序在创建读者记录的时候让元素内容是空,这是一个巧合。理论上说,元素内容是空,或者是“空”的hash码,两种情况都算空。

那么这个“空”的hash码是什么呢?这,就是天机不可泄漏了。hash码的用意就是不要你知道,故意扰乱的。我们就不要沿着这个方向去走了。

可以改换方向思考。在dp2circulation的出纳窗中,有一种校验读者密码的方式,那么肯定dp2circulation前端采用了什么API调用来验证读者密码的。我们可以使用这个API来,验证一下读者的密码是不是空。这个方法最好,因为这个API是系统提供的,有足够的可靠性和稳定性,我们作为二次开发者就不必操心那些hash码之类的事情了。

 

这个校验读者密码的API的类型是:

long VerifyReaderPassword(Stop stop, string strReaderBarcode, string strPassword, out strError)

返回-1是出错,返回0是校验不匹配,返回1是匹配。

调用这个API,需要当前用户具有verifyreaderpassword的权限。如果是读者类型的帐户,则只能校验自己的密码,不能校验其他人的密码。

希望精灵可以试一试用这个函数判断读者当前是否有了密码。

当然,这个API和其他API一样,是在Channel里面的一个函数,调用时形态如下:

long lRet = this.ReaderStatisForm.Channel.VerifyReaderPassword(

null, // stop,

strReaderBarcode, // 读者证条码号

"", // 要检测的密码

out strError);

    感谢谢老师的支持与提醒。呵呵。。我一直以为空密码仅仅是<password></password>中的内容为空。其实以前我也观察过在使用“内务”-》“文件菜单”中的“修改密码窗”,修改读者的密码为空后在“读者窗”中的“XML”标签下的确观察到了<password></password>中的确还有字符串存在,但是当时由于,在查询机上测试正常(修改后读者没有填写密码就可以登陆查询机)我就一直以为是我没有刷新读者显示数据造成这种显现的,所以,我就没再深究这个细节。呵呵。。。。。今天看到谢老师的帖子才明白过来。呵呵。。。感谢谢老师的提醒,回头我就修改一下这个脚本。。。。呵呵。。。。

    对于设置读者的初始密码我在这方面经验不多,呵呵。。。。我在这里仅仅是谈谈我个人的看法,不一定对,呵呵。。如果有不当的地方,还望各位老师切勿见笑,我个人感觉无论是什么密码都得满足这么一点条件。就是密码中的字符毫无逻辑性、关联性。如果说密码有逻辑性或者是关联性,那么一旦被有心人看出来,就很容易破解。如果按照这个想法去想的话,那么符合以上条件的只有随机数。我们可以随机生成一组数字或者是随机生成一组数字与字母的组合,这样,就可以就完全满足以上的条件了(其实采用随机数的方式,是在跟江老师聊读者初始密码的时候,江老师想到的。呵呵。。)。但是,接着还会面临一个小问题,就是如何通知读者,而且还得是一对一的通知读者,这时候,我想到了两种方式,第一种是手机短信,把密码通过短信的形式发送给读者。但是,需要系统有短信接口,并且,图书馆还得具备全网段短信发送的能力。这个要求对于系统或者对于图书馆而言,有点高。呵呵。。接着我又想到了第二种方式,邮件。。。。。系统可以通过邮件的方式发送给读者密码,但是,这也要求系统有邮件发送功能(这个目前系统已经达到了),另外还得要求图书馆有自己的邮件服务器,虽然相比上面那种具有全网段短信发送能力的要求而言,已经降低很多了,但是,这个要求感觉还是有点高,那么我们能不能这样做,图书馆导入读者信息,读者在第一次登录的时候,如果读者没有初始密码,那么读者需要再填写一遍图书馆所导入的读者本身的个人信息,让系统自动核对,如果核对无误,系统就显示出一组随机字母与数字的组合作为读者的初始密码(盗用一下江老师在上面谈到的想法。呵呵。。)。但是,如果这么做的话也有一个小问题,如果图书馆导入的读者信息是错误的,那么这个读者可能将无法获取自己的初始密码,他还得跑到图书馆工作台前,让图书馆的老师手动修改一下自己的密码。这样我感觉似乎是在无形中增加了读者登陆“OPAC”的成本。。呵呵。。。。。以上是个人感觉,不一定准确。。。。。



发表时间: 2011-11-04 09:04:18
最后修改时间: 2011-11-04 09:22:40
[回复留言] [回复(需要先登录)] [引用(需要先登录)]普通文章第 6 楼
文章id: 684
回复:

作者: 精灵


以下是引用 xietao 于 2011-11-3 16:37:52 发表的文字:

精灵你说:

> 后来,我又仔细看了一下系统的参考手册,发现里面还有一个“ChangeReaderPassword”函数专门用来修改密码的,我自己试着使用了一下,感觉还是不行...

现在我想知道,同样是用了这个API,你第一次为什么“不行”呢?具体出现了什么问题?我想了解这些细节的目的是,想看看我们的函数本身或者文档或者其他环境因素有没有妨碍二次开发的地方,如果有,我们好想办法克服。


    谢老师您好,其实参考手册写的很详细。我在开始查找参考手册的时候在调用“ChangeReaderPassword”函数的时候我没有采用“long lRet = this.ReaderStatisForm.Channel.ChangeReaderPassword”这种调用方式,我当时调用错了,我是先把这个函数的类NEW出来后,调用的。呵呵。。。后来监控“strError”值一直在提示我说“System.UriFormatException:Invalid URI: The URI is empty.”。呵呵。。

    其实就我个人而言,我感觉能否在参考手册中增补一下对于参考手册内容的索引,这样用户查询起来也比较方便。呵呵。。。另外,如果不牵扯您那里过多精力的情况下,能否再推出一个“速查手册”,在手册中除了有详细的函数调用方式以外,能否再增补一下这个函数的调用示例。。这样,会像我这样的初学者就能很快掌握这个函数的使用方法了。呵呵。。。。。个人意见。。仅供参考。呵呵。。。



发表时间: 2011-11-04 09:26:32
最后修改时间: 2011-11-04 09:28:52
[回复留言] [回复(需要先登录)] [引用(需要先登录)]普通文章第 7 楼
文章id: 685
回复: 回复:

作者: xietao


以下是引用 精灵 于 2011-11-4 9:26:32 发表的文字:

以下是引用 xietao 于 2011-11-3 16:37:52 发表的文字:

精灵你说:

> 后来,我又仔细看了一下系统的参考手册,发现里面还有一个“ChangeReaderPassword”函数专门用来修改密码的,我自己试着使用了一下,感觉还是不行...

现在我想知道,同样是用了这个API,你第一次为什么“不行”呢?具体出现了什么问题?我想了解这些细节的目的是,想看看我们的函数本身或者文档或者其他环境因素有没有妨碍二次开发的地方,如果有,我们好想办法克服。


    谢老师您好,其实参考手册写的很详细。我在开始查找参考手册的时候在调用“ChangeReaderPassword”函数的时候我没有采用“long lRet = this.ReaderStatisForm.Channel.ChangeReaderPassword”这种调用方式,我当时调用错了,我是先把这个函数的类NEW出来后,调用的。呵呵。。。后来监控“strError”值一直在提示我说“System.UriFormatException:Invalid URI: The URI is empty.”。呵呵。。

    其实就我个人而言,我感觉能否在参考手册中增补一下对于参考手册内容的索引,这样用户查询起来也比较方便。呵呵。。。另外,如果不牵扯您那里过多精力的情况下,能否再推出一个“速查手册”,在手册中除了有详细的函数调用方式以外,能否再增补一下这个函数的调用示例。。这样,会像我这样的初学者就能很快掌握这个函数的使用方法了。呵呵。。。。。个人意见。。仅供参考。呵呵。。。

 
> ...我是先把这个函数的类NEW出来后,调用的...
 
哦,原来是这样。
 
这引出了一个很有意思的话题。
 
所谓二次开发,基本上是使用一个宿主程序现有的设置,用新的需求和顺序把那些对象、功能重新组合。
 
我们常见的一个缩略语是DOM,这是“Document Object Model”的意思,就是“文档对象模型”。其实能叫做“Object Model”的东西很多,内务前端的每一个统计窗,都有一个“Object Model”概念,你可以把它理解为你旅行要去住的酒店,或者稍微长时间一点你租住的公寓,房间里面有一些现成的设施,它们已经在那里,你只需要使用就可以,并不需要你住进去的时候新装修房间、购买家具,当然,你走的时候也不能带走。
 
统计窗多半要和应用服务器打交道,为了便于理解,我们构造了一个“通讯通道”的概念,好比打电话时候看到的电话线。这就是几乎每个统计窗里面你二次开发的时候看到的XXXXForm的Channel对象,这就是一根“通讯通道”。
 
通讯通道自然是有某些代码执行的时候创建的,但当你在二次开发中关注它的时候,你需要认为它已经创建好了,只等你来使用它,而不必亲自去创建它。并且,因为你编写二次开发的派生类的时候,类似OnRecord()这样的函数被宿主程序在循环中一次次触发,那么从概念你应该假定每次在OnRecord()函数中使用完这个通道后,不要去摧毁或者释放它,因为后面还要被用到呢。那么最后统计结束的时候谁来收拾它,也就是摧毁或者释放它呢?那也是宿主程序的责任。好比你在酒店退房后,你不必操心酒店服务员去整理房间更换床单之类的事情。
 
不过,宿主程序里面提供的固定设施,不一定能满足我们的全部需求,有时候我们在处理过程中需要new新的类的对象,可能是宿主程序提供的现成类我们new出来新的对象,也有可能是我们在二次开发代码里面自己编写的类。这类对象的特点是我们自己创建,使用,然后我们自己释放。生命周期很短,通常是在一个被调用的函数期间就使用完毕了。如果这种对象的生存周期很长,例如跨越整个批处理循环的期间,那么我们可以把对象设计为派生类的成员(而不是函数内部的临时对象)。如果对象需要生命周期更长,例如跨越多次完整的批处理调用之间,那有可能需要动用宿主程序在MainForm也就是框架窗口内的存储机制了,不过,概念也是非常简单的。
 
只需要明确搞清楚这些概念:对象是谁创建的?生存周期有多长?打算把对象放在哪里?其实这些问题恰好不是软件开发的特殊问题,而有点类似于生活常识了,并不复杂。有时候显得复杂,可能是因为没有人去讲清楚,或者为了显得专业故意给弄复杂了。
 
> ...我感觉能否在参考手册中增补一下对于参考手册内容的索引,这样用户查询起来也比较方便。呵呵。。。另外,如果不牵扯您那里过多精力的情况下,能否再推出一个“速查手册”,在手册中除了有详细的函数调用方式以外,能否再增补一下这个函数的调用示例...
 
这些都是很好的建议。正巧我们这一段集中研究如何升级两个手册,相信这些要求都会体现在新版的手册中。
 


发表时间: 2011-11-04 12:11:28



[回复留言] [回复(需要先登录)] [引用(需要先登录)]普通文章第 8 楼
文章id: 686
感谢,谢老师的指点。

作者: 精灵


    感谢,谢老师的指点,刚才结合您的讲解我又仔细看了一下参考手册,我是否可以这样认为,例如我要在“书目统计窗”的脚本中调用系统的内部函数,那么我前面的句柄是否就是“BiblioStatisForm.Channel”?如果我使用“册统计窗”的脚本中调用系统的内部函数,那么我前面的句柄是否就是“ItemStatisForm.Channel”以此类推?



发表时间: 2011-11-04 14:06:11
[回复留言] [回复(需要先登录)] [引用(需要先登录)]普通文章第 9 楼
文章id: 688
回复: 感谢,谢老师的指点。

作者: xietao


==========

以下是引用 精灵 于 2011-11-4 14:06:11 发表的文字:

    感谢,谢老师的指点,刚才结合您的讲解我又仔细看了一下参考手册,我是否可以这样认为,例如我要在“书目统计窗”的脚本中调用系统的内部函数,那么我前面的句柄是否就是“BiblioStatisForm.Channel”?如果我使用“册统计窗”的脚本中调用系统的内部函数,那么我前面的句柄是否就是“ItemStatisForm.Channel”以此类推?

==========

对啊,非常好,你的理解很到位。

内务前端的窗口种类繁多,光我在编写手册的时候关于MDI子窗口的介绍就有40多个小节,也就是说有至少40多种重要的窗口存在。从开发程序的角度,这么多的窗口意味着有不少重复模式的编码,所以,如果能节省脑力思考和记忆的量,把窗口和各种设施的命名规范化,便于开发者和使用者类推类比,就是一个很重要的注意点。

首先,这些窗口一般都是“....Form”结尾的类名。这当然也是.NET的一种命名习惯了。所以书目统计窗的类名就是BiblioStatisForm,册统计窗的类名就是ItemStatisForm,读者统计窗的类名就是ReaderStatisForm,...。

但是,请注意,这是类名。类名是创建这个类的对象时候的模板,它是对象的定义,而不是对象。好比,一个人是“人”,人就是他的生物类属。但是我们平时不能把他喊作“人!”,比方说“人!过来吃饭了!”,那么势必100个人都会觉得在叫自己,也不像在叫自己。那样也不礼貌。我们可能会这样叫“小马!小张!”,每个具体的人都有自己的名字,这样就知道在叫谁了。

差不多,人的名字好比是一个对象的对象名。也可以称为“实例”。好比说我们身边外国人不多,我可能会把孩子老师John称为“英国人的一个实例”。

你提到的“BiblioStatisForm.Channel”,这一个称呼学问可不少。刚才我明明说了人是一种类属,而每个人都有具体的名字,按照这个比喻,难道一个BiblioStatisForm不该有自己的名字?这里的BiblioStatisForm到底指一个类的名字还是一个对象的名字?

哈哈,理论归理论,但是我们不能消化不良哦。这里稍微有点说来话长。

“BiblioStatisForm.Channel”,其实全称为“this.BiblioStatisForm.Channel”。那么这个this是个什么?this,如果是你在针对书目统计窗二次开发的时候编写的代码,那你一定是按照规定编写在一个从BiblioStatis类派生出来的叫做MyStatis的类,在这个MyStatis类里面你把MyStatis叫做this。

这个MyStatis类名不重要。可以修改为MyNewClass等等也无妨。为什么这个名字不重要是因为二次开发的当时关注的环境中只有一个这样的需要从BiblioStatis派生的类,没有别的干扰,所以叫什么都无所谓(只要不和系统缺省的某些类名或者保留字重复就行了)。

我们知道,这个MyStatis类中,宿主程序已经通过基类BiblioStatis准备好了一些设施,也就是成员变量。这里有个小问题:为什么当初设计这个体系结构的时候,不直接把书目统计窗的BiblioStatisForm作为基类,让二次开发者来派生出新的类,实现其功能呢?这个想法其实是可行的。但没有这么做,主要是习惯上的原因。因为如果这样那么BiblioStatisForm不但要照顾一次开发的需要,也要照顾二次开发的需要,这负担对它有些重。所谓二次开发的需要,就是要结构稳定,已经公布出来的成员名字不能随便改,函数名不能随便改,要不然二次开发者就头疼了,每升级一次版本就要修改代码才能运行了。所谓一次开发的需要,恰恰是要不断重构,优化,成员和函数名经常会大幅度改动。这两种需求是有些矛盾的。

所以,我们采取了一种习惯性折衷措施,就是为BiblioStatisForm相关的二次开发需求设置一个“客厅”,专门用来会客,对于客人来说,这个客厅的陈设什么的都要保持稳定,让他们一旦熟悉了之后就感觉稳定安详,不要动来动去了。所谓BiblioStatis类就是一个“客厅”类,行话就是“接口类”,专门用来做接口的。

这个BiblioStatis类的用途就是接口,代码很简洁,为了便于观赏我将它的全部代码放到本帖的最后。

所以,在二次开发的时候,要使用书目统计窗这个窗口对象,那么就要通过this.BiblioStatisForm来访问。从代码中看到,

        public BiblioStatisForm BiblioStatisForm = null;

这个窗口类的类名叫做BiblioStatisForm(左边),而成员对象名也叫做BiblioStatisForm(右边)。哈哈,这真应验了上面的那个笑话,一个人的名字叫做“人”!

这里的用意是为了减轻二次开发者的记忆量。试想,如果我们不直接用类名作为变量名,而故作雅致地采用类似OneBiblioStatisForm之类的成员变量名,那还不是要沾上这个“BiblbioStatisForm”字符串,也好不到哪里去,反而平添了额外的规则,增加了记忆量。这里的关键是,BiblioStatis这个接口类中只有这唯一的一个属于BiblioStatisForm的成员,那么不把它直接用类名命名,更待何时?好比上面那个笑话,如果世界上总共只有一个人,那么把他的名字叫“人”又有何妨呢?

所以我归结为“习惯”的力量。即便在.NET的基础源代码中,你也会发现很多这样的直接用类名作为成员变量名的例子。

通过上面的解剖,我们透彻了解了系统二次开发里面的一种命名惯例的来龙去脉。不过话又说回来,即便是有二次开发者稀里糊涂连类名和对象(变量)名都分不清楚,那这种命名的方式正好也能稀里糊涂带领他走向“成功”,歪打正着了。所以这个习惯也是一种良性的习惯 --- 虽然从学习知识的角度来说未必是一种凸显概念差异的好习惯。

以上这一通文字可以称为“类名的故事”。

虽然我上面说了,从一次开发的角度,BiblioStatisForm可能会内部变来变去,但是既然它通过接口类暴露给了二次开发者,那么其中特别是Channel成员的名字就不要随意乱动了。

Channel这个成员的名字也很有一些讲究。其实它的类型是LibraryChannel。因为系统中的通讯通道种类很多,比如针对数据库内核的通道,针对图书馆应用服务器的通道,所以名字上有所区别,不再是最通用的那个称呼了。但是,因为在BiblioStatisForm这里,通道对象只有一个,所以就简化为用Channel而不是LibraryChannel作为对象名字了。

但BiblioStatis类里面的BiblioStatisForm为什么不能叫做StatisForm,或者甚至称为Form呢?因为这里也只是一个。这里我不得不谈到,当时心里有一种念头就是各种类型的统计窗是不同的,平时也尤其担心用户把其他类型的统计窗的统计方案导入到了不恰当的统计窗内,所以就故意把对象名没有采用最通用的称呼。也就是说,如果你从这个统计窗的二次开发代码片段复制到那个类型的统计窗中,this.XXXXStatisForm这个名字之间的差异会迫使二次开发者修改名字才能编译通过,借此也促使二次开发者思考一下“我现在在哪里?this代表什么?”

因为不同的统计窗之间,虽然有一些相同功能和名字的成员,但是也有一些不同功能不同名字的成员。当然,要看试图把共性突出,还是把差异突出,也就是设计时候的一念之差了。从逻辑完满,自圆其说的角度,我自己也觉得变量命名的几个因素之间是有矛盾的,一会儿你要这么说,一会儿你要那么说,好像怎么你都有理,但其实经过推敲会发现,换一种做法未必不行,未必就差。

这里我想起来一个故事,就是说许多美国人烤火鸡的时候,都是把火鸡腿砍掉再放入烤箱的。如果你要问他们为什么?他们会说:我妈妈当年就是这么做的,我也这么做。有人研究了一下,发现早先的烤箱很小,放不下整只巨大的火鸡,于是只好砍成几个部分多次烤。但是现在的烤箱太大了,放下整只火鸡没有问题。不过,习惯的力量很还大 --- 大家并不是任何时候都要问为什么的,“传统”自然就是成了自然,...。

所以,大抵上我说了自己的想法和习惯的由来,我没有底气说这些代表着多么正确。如果将来升级改版,软件还有采纳其他新想法和习惯的机会。

~~~

附:BiblioStatis类的源代码。供参考观赏用。

using System;

using System.Collections.Generic;

using System.Text;

using System.Windows.Forms;

using System.IO;

using System.Xml;

using System.Web;

using DigitalPlatform.Xml;

using DigitalPlatform.dp2.Statis;

using DigitalPlatform.CirculationClient;

using DigitalPlatform.CirculationClient.localhost;

namespace dp2Circulation

{

    public class BiblioStatis

    {

        private bool disposed = false;

        public WebBrowser Console = null;

        public BiblioStatisForm BiblioStatisForm = null;    // 引用

        public string CurrentDbSyntax = "";

        public string CurrentRecPath = "";    // 当前书目记录路径

        public long CurrentRecordIndex = -1; // 当前书目记录在整批中的偏移量

        public string ProjectDir = "";

        public List<string> OutputFileNames = new List<string>(); // 存放输出的html文件

        int m_nFileNameSeed = 1;

        public XmlDocument BiblioDom = null;    // Xml装入XmlDocument

        string m_strXml = "";    // XML记录体

        public string Xml

        {

            get

            {

                return this.m_strXml;

            }

            set

            {

                this.m_strXml = value;

            }

        }

        public string MarcRecord = "";

        public BiblioStatis()

        {

            //

            // TODO: Add constructor logic here

            //

        }

        // Use C# destructor syntax for finalization code.

        // This destructor will run only if the Dispose method 

        // does not get called.

        // It gives your base class the opportunity to finalize.

        // Do not provide destructors in types derived from this class.

        ~BiblioStatis()

        {

            // Do not re-create Dispose clean-up code here.

            // Calling Dispose(false) is optimal in terms of

            // readability and maintainability.

            Dispose(false);

        }

        // Implement IDisposable.

        // Do not make this method virtual.

        // A derived class should not be able to override this method.

        public void Dispose()

        {

            Dispose(true);

            // This object will be cleaned up by the Dispose method.

            // Therefore, you should call GC.SupressFinalize to

            // take this object off the finalization queue 

            // and prevent finalization code for this object

            // from executing a second time.

            GC.SuppressFinalize(this);

        }

        // Dispose(bool disposing) executes in two distinct scenarios.

        // If disposing equals true, the method has been called directly

        // or indirectly by a user's code. Managed and unmanaged resources

        // can be disposed.

        // If disposing equals false, the method has been called by the 

        // runtime from inside the finalizer and you should not reference 

        // other objects. Only unmanaged resources can be disposed.

        private void Dispose(bool disposing)

        {

            // Check to see if Dispose has already been called.

            if (!this.disposed)

            {

                // If disposing equals true, dispose all managed 

                // and unmanaged resources.

                if (disposing)

                {

                    // Dispose managed resources.

                }

                // 删除所有输出文件

                if (this.OutputFileNames != null)

                {

                    Global.DeleteFiles(this.OutputFileNames);

                    this.OutputFileNames = null;

                }

                /*

                // Call the appropriate methods to clean up 

                // unmanaged resources here.

                // If disposing is false, 

                // only the following code is executed.

                CloseHandle(handle);

                handle = IntPtr.Zero;

                 * */

                try // 2008/6/26 new add

                {

                    this.FreeResources();

                }

                catch

                {

                }

            }

            disposed = true;

        }

        public virtual void FreeResources()

        {

        }

        // 初始化

        public virtual void OnInitial(object sender, StatisEventArgs e)

        {

        }

        // 开始

        public virtual void OnBegin(object sender, StatisEventArgs e)

        {

        }

        // 每一记录,在触发MARCFilter之前

        public virtual void PreFilter(object sender, StatisEventArgs e)

        {

        }

        // 每一记录处理

        public virtual void OnRecord(object sender, StatisEventArgs e)

        {

        }

        // 结束

        public virtual void OnEnd(object sender, StatisEventArgs e)

        {

        }

        // 打印输出

        public virtual void OnPrint(object sender, StatisEventArgs e)

        {

        }

        public void ClearConsoleForPureTextOutputing()

        {

            Global.ClearForPureTextOutputing(this.Console);

        }

        public void WriteToConsole(string strText)

        {

            Global.WriteHtml(this.Console, strText);

        }

        public void WriteTextToConsole(string strText)

        {

            Global.WriteHtml(this.Console, HttpUtility.HtmlEncode(strText));

        }

        // 获得一个新的输出文件名

        public string NewOutputFileName()

        {

            string strFileNamePrefix = this.BiblioStatisForm.MainForm.DataDir + "\\~biblio_statis";

            string strFileName = strFileNamePrefix + "_" + this.m_nFileNameSeed.ToString() + ".html";

            this.m_nFileNameSeed++;

            this.OutputFileNames.Add(strFileName);

            return strFileName;

        }

        // 将字符串内容写入文本文件

        public void WriteToOutputFile(string strFileName,

            string strText,

            Encoding encoding)

        {

            StreamWriter sw = new StreamWriter(strFileName,

                false,    // append

                encoding);

            sw.Write(strText);

            sw.Close();

        }

        // 删除一个输出文件

        public void DeleteOutputFile(string strFileName)

        {

            int nIndex = this.OutputFileNames.IndexOf(strFileName);

            if (nIndex != -1)

                this.OutputFileNames.RemoveAt(nIndex);

            try

            {

                File.Delete(strFileName);

            }

            catch

            {

            }

        }

        List<ItemInfo> m_itemInfos = null;

        public void ClearItemDoms()

        {

            this.m_itemInfos = null;

        }

        public List<ItemInfo> ItemInfos

        {

            get

            {

                // 优化速度

                if (this.m_itemInfos != null)

                    return this.m_itemInfos;

                // 如果当前书目库下没有包含实体库,调用会抛出异常。特殊处理

                // TODO: 是否需要用hashtable优化速度?

                string strBiblioDBName = Global.GetDbName(this.CurrentRecPath);

                string strItemDbName = this.BiblioStatisForm.MainForm.GetItemDbName(strBiblioDBName);

                if (String.IsNullOrEmpty(strItemDbName) == true)

                    return new List<ItemInfo>();    // 返回一个空的数组

                this.m_itemInfos = new List<ItemInfo>();

                long lPerCount = 100; // 每批获得多少个

                long lStart = 0;

                long lResultCount = 0;

                long lCount = -1;

                for (; ; )

                {

                    EntityInfo[] infos = null;

                    string strError = "";

                    long lRet = this.BiblioStatisForm.Channel.GetEntities(

                        null,

                        this.CurrentRecPath,

                        lStart,

                        lCount,

                        "",

                        "zh",

                        out infos,

                        out strError);

                    if (lRet == -1)

                        throw new Exception(strError);

                    lResultCount = lRet;    // 2009/11/23 new add

                    if (infos == null)

                        return this.m_itemInfos;

                    for (int i = 0; i < infos.Length; i++)

                    {

                        EntityInfo info = infos[i];

                        string strXml = info.OldRecord;

                        if (String.IsNullOrEmpty(strXml) == true)

                            continue;

                        ItemInfo item_info = new ItemInfo();

                        item_info.RecPath = info.OldRecPath;

                        item_info.Timestamp = info.OldTimestamp;

                        item_info.OldRecord = strXml;

                        item_info.Dom = new XmlDocument();

                        try

                        {

                            item_info.Dom.LoadXml(strXml);

                        }

                        catch

                        {

                            continue;

                        }

                        this.m_itemInfos.Add(item_info);

                    }

                    lStart += infos.Length;

                    if (lStart >= lResultCount)

                        break;

                    if (lCount == -1)

                        lCount = lPerCount;

                    if (lStart + lCount > lResultCount)

                        lCount = lResultCount - lStart;

                } // end of for

                return this.m_itemInfos;

            }

        }

        // 保存修改过的册信息。

        // 调用本函数前,要修改Dom成员

        // return:

        //      -1  error

        //      0   succeed

        public int SaveItemInfo(List<ItemInfo> iteminfos,

            out string strError)

        {

            List<EntityInfo> entityArray = new List<EntityInfo>();

            for (int i = 0; i < iteminfos.Count; i++)

            {

                ItemInfo item = iteminfos[i];

                EntityInfo info = new EntityInfo();

                if (String.IsNullOrEmpty(item.RefID) == true)

                {

                    item.RefID = Guid.NewGuid().ToString();

                }

                info.RefID = item.RefID;

                DomUtil.SetElementText(item.Dom.DocumentElement,

                    "parent", Global.GetID(CurrentRecPath));

                string strXml = item.Dom.DocumentElement.OuterXml;

                info.OldRecPath = item.RecPath;

                info.Action = "change";

                info.NewRecPath = item.RecPath;

                info.NewRecord = strXml;

                info.NewTimestamp = null;

                info.OldRecord = item.OldRecord;

                info.OldTimestamp = item.Timestamp;

                entityArray.Add(info);

            }

            // 复制到目标

            EntityInfo[] entities = null;

            entities = new EntityInfo[entityArray.Count];

            for (int i = 0; i < entityArray.Count; i++)

            {

                entities[i] = entityArray[i];

            }

            EntityInfo[] errorinfos = null;

            long lRet = this.BiblioStatisForm.Channel.SetEntities(

                null,   // this.BiblioStatisForm.stop,

                this.CurrentRecPath,

                entities,

                out errorinfos,

                out strError);

            if (lRet == -1)

                return -1;

            // string strWarning = ""; // 警告信息

            if (errorinfos == null)

                return 0;

            strError = "";

            for (int i = 0; i < errorinfos.Length; i++)

            {

                if (String.IsNullOrEmpty(errorinfos[i].RefID) == true)

                {

                    strError = "服务器返回的EntityInfo结构中RefID为空";

                    return -1;

                }

                // 正常信息处理

                if (errorinfos[i].ErrorCode == ErrorCodeValue.NoError)

                    continue;

                strError += errorinfos[i].RefID + "在提交保存过程中发生错误 -- " + errorinfos[i].ErrorInfo + "\r\n";

            }

            if (String.IsNullOrEmpty(strError) == false)

                return -1;

            return 0;

        }

    }

    // 册信息

    public class ItemInfo

    {

        public string RecPath = "";

        public byte[] Timestamp = null;

        public XmlDocument Dom = null;

        public string OldRecord = "";

        public string RefID = "";

    }

}



发表时间: 2011-11-04 15:08:30
最后修改时间: 2011-11-04 15:21:04



[回复留言] [回复(需要先登录)] [引用(需要先登录)]普通文章第 10 楼
文章id: 689
感谢,谢老师的指点。

作者: 精灵


    非常感谢谢老师的指点,我是否可以这么理解“this.BiblioStatisForm.Channel”,“this”是代指“MyStatis”类,而这个类本身叫什么其实并不重要,它可以叫任意的名字,只要不与系统内置类名冲突就行(这里使用“this”其实是便于移植脚本,因为无论“MyStatis”这个类叫什么“this”始终成立)。这个类是“BiblioStatis”的派生类,而“BiblioStatis”本身只是一个接口类,“this.BiblioStatisForm”其实,是在调用“BiblioStatis”这个接口类中的“BiblioStatisForm”这个成员,而这个成员来自“BiblioStatisForm”类的一个实例,而“Channel”是“BiblioStatisForm”类中的某个类的实例,所以,“this.BiblioStatisForm.Channel”其实是“MyStatis”调用“BiblioStatis”类中的“BiblioStatisForm”这个类的“BiblioStatisForm”实例中的“Channel”实例,而后面的一些函数都是“Channel”这个类中的方法。。。。。



发表时间: 2011-11-07 09:34:02
最后修改时间: 2011-11-07 09:36:07
页 1 / 2 |< 1 2 > >|
 

在线用户
访客访客   访客访客   访客访客   访客访客   访客访客   访客访客   访客访客   访客访客   访客访客   访客访客   访客访客   访客访客   访客访客   访客访客   访客访客   访客访客   访客访客   访客访客   访客访客   访客访客   访客访客   访客访客   访客访客   访客访客 (我自己)   访客访客   访客访客   访客访客   访客访客   访客访客   访客访客   访客访客   访客访客   访客访客   访客访客   访客访客
当前栏目在线用户数 35, 总在线用户数 37