DigitalPlatform / dp2

Integrated Library System / 图书馆集成系统
http://digitalplatform.github.io/dp2
Apache License 2.0
105 stars 54 forks source link

借阅双链相关代码重构 #1183

Open DigitalPlatform opened 5 months ago

DigitalPlatform commented 5 months ago

一直以来 dp2library 中的借阅双链都是使用的读者证条码号和册条码号。由于允许存在没有册条码号的册记录,为了允许这部分册进行借还,双链中也允许用 @refID:xxx 形态使用册记录的参考 ID。但不允许没有证条码号的读者记录。

这样的缺点是,当读者记录修改了证条码号字段内容以后,或者册记录修改了册条码号字段内容以后,双链的关系就被破坏。若修改的同时要自动去修改双链中的连接字段内容,则寻找双链的连接字段的过程会比较复杂,难以实现。

现计划对双链进行改造,全面允许使用读者记录和册记录的参考 ID。并兼容以前的证条码号和册条码号用法。这样等系统运行一段时间以后,读者以前借阅的图书都被还回了,新产生的借阅信息链可以确保都是新的参考 ID 形态,这样等读者记录和册记录修改条码号字段以后,双链依然可以保持正确。

本 issue 将记载代码重构中涉及到的数据格式变动,给出测试建议。

DigitalPlatform commented 5 months ago

读者记录

改进点: 1) borrows/borrow 元素中,增加了一个 refID 属性,值为册记录的参考 ID。原有的 barcode 属性依然有效,是册条码号 2) reservations/request 元素的 arrivedItemBarcode 属性,内容可能是册条码号,现在也可能是 @refID:xxx 形态 3) reservations/request 元素的 items 属性,内容可能是册条码号列表,现在也可能是 @refID:xxx 形态的列表 4) outofReservations/request 元素的 itemBarcode 属性,内容原来是册条码号,现在还可能是 @refID:xxx 形态 5) overdues/overdue 元素中,增加了一个 refID 属性,值为册记录的参考 ID。原有的 barcode 属性依然有效,是册条码号

册记录

改进点: 1) borrower 元素的内容,目前改为写入 @refID:xxxx 形态。以前的 证条码号形态 依然有效,注意使用的时候对两种可能性加以判断 2) Reservation() API 中,预约时对册记录的 reservations/request/@reader 属性中写入的可能是证条码号或 @refID:xxx 形态

预约队列记录

改进点: 1) 增加了一个 patronRefID 元素,内容为读者记录的参考 ID。原有 readerBarcode 元素依然有效 (注: 其实 2016 年开始预约队列库的 keys 配置文件里面就为 patronRefID 元素添加了一个检索点,只是当时的程序代码里面还没有给预约队列记录主动写入这个元素)

违约金记录

改进点: 1) 增加 itemRefID 元素,内容为超期册的参考 ID。 2) 增加 readerRefID 元素,内容为读者参考 ID。 3) 违约金库 keys 模板里面为上述两个元素增加了检索点,名为“册参考ID”和“读者参考ID”。

出纳日志记录(mongodb)

改进点: 1) ItemBarcode 字段原来是存储册条码号。现在改为 @refID:xxx 形态。升级版本的时候,有个模块会自动把以前记录的全部此字段内容替换为新的形态。 2) PatronBarcode 字段原来是存储读者证条码号。现在改为 @refid:xxx 形态。升级版本的时候,有个模块会自动把以前记录的全部此字段内容替换为新的形态。

2024/5/22 发现写入mongodb的borrow的BorrowDate是一个默认值,说明如下: 这个 BorrowDate 字段的语义是“还书的时候,回顾当时借书的时间”,也就是说这个字段对借书动作不管用(属于多余的字段),对还书动作才管用。借书动作要看借书时间,要看 OperTime 字段。(类似道理,还书动作要看还书时间,看 OperTime 字段。也就是说,并没有一个 ReturnDate 字段) 出现多余的字段,这是由数据行要整齐地放入同一个数据记录类引起的。假设如果不同动作的行放入不同的数据记录类,就不会有这个问题,比如负责定义借书动作的类就根本不提供这个 BorrowDate 字段就可以了。不过,这面这种假设的做法,成本太高一般不会考虑使用。

{
  "_id": {
    "$oid": "664d5385562b1ade073690cd"
  },
  "LibraryCode": "",
  "Operation": "borrow",
  "Action": "borrow",
  "ItemBarcode": "@refID:94623e97-65e2-470e-b55e-160f0ce178a4",
  "PatronBarcode": "@refID:9131f365-772d-4ce1-b6ae-334380d41a21",
  "BiblioRecPath": null,
  "Period": "31day",
  "No": "0",
  "Volume": null,
  "ClientAddress": "localhost",
  "Operator": "supervisor",
  "OperTime": {
    "$date": "2024-05-22T02:08:05.000Z"
  },
  "BorrowDate": {
    "$date": {
      "$numberLong": "-62135596800000"
    }
  }
}

Borrow() API 操作日志记录

Return() API 操作日志记录

改进点: 1) 增加一个 readerRefID 元素,内容为读者记录的参考 ID。 2) 增加了一个 itemRefID 元素,内容为册记录的参考 ID。

Reservation() API 操作日志记录

改进点: 1) 增加了一个 itemRefIdList 元素。原有 itemBarcodeList 元素依然有效 2) 增加了一个 readerRefID 元素。原有 readerBarcode 元素依然有效

Amerce() API 操作日志记录

改进点: 1) 增加了一个 readerRefID 元素。原有 readerBarcode 元素依然有效

WriteRes() API 操作日志记录

改进点: 1) 增加了一个 record 元素和一个 oldRecord 元素(和其它类型的日志记录中的 record 和 oldRecord 元素用法相同)。分别表示新旧文本 XML 记录。适用于文本记录。如果使用了这两个元素,日志记录就不会有附件了。

DigitalPlatform commented 5 months ago

Borrow() API

改进点: 1) 返回的 BorrowInfo 结构中,strItemBarcode 字段的内容可能是册条码号,现在也可能是 @refID:xxx 形态 注意验证内务等前端的适应性

Return() API

改进点: 1) 返回的 ReturnInfo 结构中,ItemBarcode 字段的内容可能是册条码号,现在也可能是 @refID:xxx 形态 注意验证内务等前端的适应性

VerifyBarcode() API

改进点: 1) strBarcode 参数现在允许 @refID:xxx 形态的内容请求校验。会对所有读者库的参考 ID 检索途径进行试探检索,判断出参考 ID 到底是读者参考 ID 还是册参考 ID

2024/5/22 用接口测试符合预期。

SearchCharging() API

改进点: 1) patronBarcode 参数允许的内容形态,增加了 @readerRefID:xxx(或者别名 @refID:xxx) 形态,表示读者参考 ID。 注:总共可以使用以下几种类型的值: a) 如果没有前缀,表示读者证条码号; b) 如果 以 "@itemBarcode:" 前缀引导,表示这是册条码号; c) 如果 以 "@itemRefID:" 前缀引导,表示这是册参考 ID; d) 如果 以 "@readerRefID:" 或 "@refID:" 前缀引导,表示这是读者参考 ID。 2) patronBarcode 参数内容如果是读者证条码号或者册条码号,实际上在执行检索之前,程序会自动替换为读者参考 ID 或者册参考 ID,以便适应 mongodb 借阅历史库的升级变化。

2024/5/22 在接口用partronBarcode参数用4种形状测试均符合预期。但如果action参数为空时,会报异常(todo)

DigitalPlatform commented 5 months ago

根据操作日志记录重建 mongodb 出纳动作库

这个批处理任务的算法有改进。

原先是处理操作日志记录 borrow 和 return 动作。现在还要处理 setReaderInfo 和 setEntity 动作。

处理 borrow 和 return 动作

新算法要从日志记录中的读者和册记录中尽量提取参考 ID(以@refID:xxx形态),用于创建 mongodb 出纳动作记录。找不到参考 ID 的情况下再用条码号创建。

处理 setReaderInfo 和 setEntity 动作

除了原来算法应该具备的功能,修改和删除 mongodb 动作记录中匹配的两类条码号以外,新算法还要

1) 关注参考 ID 字段发生的改变。对 mongodb 动作记录相关字段进行查找替换。 2) 关注 mongodb 动作记录中可能存在的条码号形态的相关字段内容,用从日志记录中抽取出的“条码号 -- 参考 ID”对照关系去进行查找替换,以便 mongodb 动作记录符合新版本要求。

查找替换批处理

曾设想过在新旧版本日志动作切换的时刻,执行一次批处理查找替换 mongodb 中的两类条码号。现在看来没有必要了,因为 处理 setReaderInfo 和 setEntity 动作的时候已经分散解决了这个问题。

DigitalPlatform commented 3 months ago

SetReaderInfo() API 的一些改进

change 动作

权限检查: 当前账户是否具备修改 barcode 元素的权限。比如,账户权限中包含 setreaderinfo,或者包含 setreaderinfo:barcode,就表示可以修改证条码号字段。

如果读者记录中具有在借信息(borrow 元素),change 动作依然可以自动处理好相关册记录的 borrower 元素联动修改。(旧版本在这种情况下会直接报错拒绝,并返回出错信息建议前端改用 changereaderbarcode 动作)

字段限制: 如果数据库中已经存在的读者记录中 refID 元素为空,则允许修改 refID 元素内容为任意值。但如果已经存在的读者记录中 refID 元素不为空,则不允许修改 refID 元素内容。如果试图修改,SetReaderInfo() API 会返回 result.Value 值 1,表示部分元素的修改被拒绝。(而改用 changereaderrefid 动作可以修改 refID 元素值,包括已经存在的记录中 refID 元素值不为空的情形。changereaderrefid 动作是最新版新增的动作)

此动作不允许修改 foregift 字段。

此动作不允许修改 hire 元素的 expireDate 属性值。也就是说,hire 元素的文本可以修改,但无法改变已经存在的 expreDate 属性值。

changereaderbarcode 动作

权限检查: 当前账户具备修改 barcode 元素的权限,或者具备 changereaderbarcode 权限。(可以理解为 changereaderbarcode 权限提升了当前账户的修改读者记录字段的能力)

如果读者记录中具有在借信息(borrow 元素),change 动作依然可以自动处理好相关册记录的 borrower 元素联动修改。这一点旧版本已经做到了。

字段限制: 此动作的旧版本似乎可以修改 barcode 以外的一些字段。最新版改变了这一行为,改为,只允许修改 barcode 和 comment 字段。

changestate 动作

权限检查: 当前账户具备修改 state 元素的权限,或者具备 changereaderstate 权限。(可以理解为 changereaderstate 权限提升了当前账户的修改读者记录字段的能力)

字段限制: 只允许修改 state 和 comment 字段。

changeforegift 动作

权限检查: 当前账户具备修改 foregift 元素的权限,或者具备 changereaderforegift 权限。(可以理解为 changereaderforegift 权限提升了当前账户的修改读者记录字段的能力)

字段限制: 只允许修改 foregift 和 comment 字段。

changereaderrefid 动作

权限检查: 当前账户具备修改 refID 元素的权限,或者具备 changereaderrefid 权限。(可以理解为 changereaderrefid 权限提升了当前账户的修改读者记录字段的能力)

如果读者记录中具有在借信息(borrow 元素),changereaderrefid 动作依然可以自动处理好相关册记录的 borrower 元素联动修改。(注: 最新版的册记录中 borrower 元素值为 @refID:xxx 形态。但旧版本的册记录中 borrower 元素值为证条码号,此时如果相关读者记录的证条码号或参考 ID 字段发生修改,就有必要自动修改册记录中的 borrower 元素值)

字段限制: 只允许修改 refID 和 comment 字段。

自动修改册记录中 borrower 元素过程出错处理

在自动修改册记录中 borrower 元素的过程中如果出错,最新版会返回 result.Value 值 1,表示成功但部分修改没有兑现。同时会把错误情况写入错误日志中年。(旧版本这种情况 API 会返回报错,并且读者记录已经被修改了,但没有写入操作日志,处在一种尴尬的状态)

DigitalPlatform commented 3 months ago

RepairBorrowInfo() API

从读者一侧修复

API 参数说明: strReaderKey 参数指明了要修复的读者记录。 strItemKey 参数指明了读者记录中,哪一个 borrow 元素需要被修复。因为读者记录中的 borrow 元素可能不止一个,如果不用 strItemKey 参数指明,函数就无法完成功能。(这里有个值得未来改进的点,就是可以尝试允许 strItemKey 参数为空,此时默认指明读者记录中唯一的 borrow 元素参与修复)

建议的测试场景如下:

1) 读者记录中 borrow 元素指向册记录,册记录中的 borrower 元素指向读者记录,双链完整。期待的返回值是 result.Value 为 -1,result.ErrorCode 为 ErrorCode.NoError,result.ErrorInfo 中提示这是完整的链条、没有必要修复。

2) 读者记录中的 borrow 元素指向册记录,但册记录中的 borrower 元素指向另一读者记录。期待的返回值是 result.Value 为 0。读者记录中的 borrow 元素被清除,册记录中的 borrower 元素和相关元素被清除。

3) 读者记录中的 borrow 元素指向册记录,但册记录中的 borrower 元素为空。期待的返回值是 result.Value 为 0。读者记录中的 borrow 元素被清除。

4) 读者记录中的 borrow 元素指向一条不存在的册记录。但 strItemKey 参数代表的册记录存在,册记录中的 borrower 元素指向读者记录,或者指向另外一条读者记录。

期待的返回值是 result.Value 为 -1,result.ErrorInfo 中提示请求参数不正确“读者记录中并不存在有关册记录的 xxx 的借阅信息”。

从册一侧修复

API 参数说明: strReaderKey 参数指明了册记录相关的读者记录。 (注1:目前 API 要求 strReaderKey 参数值不为空,好像是“考验”一下前端,看看前端是否能正确给出相关读者 Key。实际上,在前端请求之前,往往只知道册记录,并且从册记录中的 borrower 元素中获得了读者 key,用于请求的 strReaderKey 参数。这种情况下,确实除了考验一下前端以外,并无其它意义) (注2: 和读者一侧相比,读者记录中有多个 borrow 元素的情况,而册一侧的册记录中,只有一个 borrower 元素,所以也不存在需要用 strReaderKey 来从多个 borrower 元素中定位一个元素的理由) strItemKey 参数指明了册记录。 (这里有个值得未来改进的点,就是可以尝试允许 strReaderKey 参数为空,此时可以通过册记录中的 borrower 元素得知相关的读者记录的 key)

建议的测试场景如下:

1) 册记录中 borrower 元素指向读者记录,读者记录中 borrow 元素指向册记录,双链完整。期待的返回值是 result.Value 为 -1,result.ErrorCode 为 ErrorCode.NoError,result.ErrorInfo 中提示这是完整的链条、没有必要修复。

2) 册记录中 borrower 元素指向读者记录,但读者记录中的 borrow 元素指向另一册记录。期待的返回值是 result.Value 为 0。读者记录中的 borrow 元素因为不相干不应被清除,册记录中的 borrower 元素和相关元素被清除。

3) 册记录中的 borrower 元素指向读者记录,但读者记录中并没有和此册相关的 borrow 元素。期待的返回值是 result.Value 为 0。读者记录中的 borrow 元素因为不相干不应被清除,册记录中的 borrower 元素和相关元素被清除。

4) 册记录中的 borrower 元素指向一条不存在的读者记录。但 strReaderKey 参数代表的读者记录存在,读者记录中的 borrow 元素指向册记录,或者指向另外一条册记录。

期待的返回值是 result.Value 为 -1,result.ErrorInfo 中提示请求参数不正确“册记录中的 borrower 并未指向指定的读者 xxx”。

读者记录中 borrow 元素中 barcode 和 refID 属性值不一致的情况

当出现 borrow 元素中 barcode 和 refID 属性值指向的不是同一条册记录的情况,优先依据 refID 属性。

如果 borrow 元素缺乏 refID 属性,只有 barcode 属性,那就不会出现不一致的情况,这时候依据 barcode 属性。

对于 RepairBorrowInfo() API 和 Borrow() Return() API 里面的判断都是采用这种策略。

操作日志

readerRecord 和 itemRecord 元素特点

从读者一侧修复的时候:

recordRecord 元素必然会有文本内容(代表读者记录 XML),也会有 recPath 属性值。也就是说读者记录一定是存在的。

itemRecord 元素一定会存在,也会有 recPath 属性值。元素文本值可能会表示没有修改以前的册记录 XML,也可能会表示修改后的册记录 XML,这要看 changed 属性,如果 changed 属性值为 'false',则表示册记录在操作中没有被修改过(缺省为修改过)。也就是说册记录一定是存在的。如果册记录不存在,API 会报错,那样就不会创建操作日志记录了。

从册一侧修复的时候:

recordRecord 元素一定会存在,但文本内容(代表读者记录 XML)可能为空。recPath 属性值也可能为空。changed 属性值恒为 'false'。existing 属性值可能为 'false',这时候元素文本内容也会为空。也就是说读者记录可能存在,也可能不存在。

itemRecord 元素一定会存在,也会有 recPath 属性值。元素文本值表示修改后的册记录 XML。changed 属性不存在,代表册记录在操作中一定会被修改。也就是说册记录一定是存在的。如果册记录不存在,API 会报错,那样就不会创建操作日志记录了。

renyh commented 2 months ago

记录中的operation算法更新

用途

读者/书目/册(及订购/期/评注)记录中的operations/operation元素,用于记录这条记录是谁创建的,该修改过,还可用于统计工作量,参于编辑的每个人做的都要留下痕迹。

以前版本效果

1)之前版本读者没有operation元素。书目 和 册(订购/期刊/评注)有。 2)首次创建会有一个create类型的operation,位于第一个且只有一个 3)书目以后每次修改会产生一个新的change,不区分人,但不是无限多,当超过10个会把前面的顶出去,导致分布不均匀,一个人可能修改多次。 4)册以后每次修改是只有一个lastModified元素,也不区分人。

新版本算法

1)为读者增加了operation,包括create和change两种。 2)create原理不变,还是针对初始创建产生,位于第一个且只有一个。 3)针对change改为:对于某一个人无论多少次修改,对这个人只产生一个change/lastModified(册/订购/期/评注对这个名称因为已经建了检索点,还继续用lastModified。书目和读者都叫change),这个人多次修改,只会更新他自己这一个operation=change的时间time。 4)这些operation元素是按时间排序的,如果之前修改过的人,又进行了修改,会将该用户的operation移动最后。因为检索点是按xpath配的,xpath有局限,取最后一个元素方便。 5)operation数量超过10个,会顶出排前面的change,但不会顶出第一个create。 (注:由这一点想到在实际应用中,要看看参与编辑的会有多少人,如果超过10个,要能特殊配置。)

测试要点

1)创建人与时间 2)一个人多次修改,应只有一条change(或lastModified) 3)不同的人修改,更新对应人的change(或lastModified),且这个operation会移到最后。 4)用超过10个帐户测试,第11个operation,会顶出排在最前面的change(或lastModified),注不会顶出create。 5)针对上面的4点,分别针对 读者、书、册、订购/期/评注 分别进行测试。

测试结果 2024/5/20-1102 针对 读者 测试1-4要点,符合预期。 2024/5/20-1114 针对 书目 测试1-4要点,符合预期。 2024/5/20-1119 针对 册 测试1-4要点,符合预期。 2024/5/20-1130 针对 订购/期 测试1-4要点,符合预期。

2024/5/20-1143 针对 评注 测试1-4要点,符合预期。注:创建者可以修改自己创建的记录,其它人需要具有managecomment权限才能修改他人的记录。

SetItemInfo()
Value:-1
ErrorCode:AccessDenied
ErrorInfo:不允许当前用户 'a1' 修改由其他用户 'supervisor' 创建的评注记录
DigitalPlatform commented 1 month ago

修改数据记录相关 API 改进

这次也顺便对几个负责修改数据记录的 API 进行了改进。SetBiblioInfo() SetReaderInfo() SetEntities() SetOrders() SetIssues() SetComments()。

new 和 change 两种动作之间的异同

早期为修改记录类的 API 都提供了一个 strAction 参数和一个 strRecPath 参数,分别指定要进行的操作,和要修改的记录的路径。

最早的设想场景如下:当 strAction 值为 "new" 时,strRecPath 值为类似“中文图书/?” 这样的表示追加的记录路径;当 strAction 值为 "change" 时,strRecPath 值为类似 "中文图书/100" 这样的表示确定位置的记录路径。

后来随着不断应用实践,逐渐扩展了应用场景,当 strAction 值为 "new" 时,也可能在 strRecPath 值中表达确定位置的记录路径,表示“这是向一个确定位置创建新记录的操作,前端能确保这个位置现在并不存在记录”;和当 strAction 值为 "change" 时,也可能在 strRecPath 值中表达追加的记录路径(不确定位置),表示“这是向一个不确定位置追加新记录的操作,前端能确保这个位置现在并不存在记录,实际上这个位置是服务器临时来决定”。

后面这种用法主要是因为前端利用 API 编程的时候,不想专门去请求检测一下某个位置是否已经存在记录、然后决定用 strAction 的 "new" 还是 "change",而是希望服务器在执行修改 API 的时候灵活处理。这样可以减少一次查询请求。那么从语义上来说,有一种强烈倾向,就是不再区分 "new" 和 "change" 动作,或者说中立一点统一叫"set" 更好。

但这次重构代码,经过深入测试和研讨以后,发现原教旨主义的 "new" 和 "change" 还是有重要区别的,不能简单混为一谈。首先,记录被创建或者修改以后,operations 元素下要添加或者覆盖适当类型的 operation 元素。到底是当作创建还是修改?另外,操作日志中记载的动作,到底是当作创建还是修改?

经过思考,最后决定,operations 元素下添加或者覆盖 operation 元素的操作类型问题,要看修改的记录这个位置,修改以前是否存在原来的记录。如果存在,表明发生了某种覆盖,那就是 change(modify) 动作;如果不存在,表明属于新创建,那就是 create 动作。也就是说,和请求中 strAction 参数值无关。

操作日志中记载的动作,则可以沿用请求中 strAction 参数值。但需要全面检查一下,看看操作日志记录是否到达一种要求:和动作无关,操作日志记录准确记载了被修改位置,修改前的记录内容,和修改后的记录内容,就可以了。

有一个例外情况,就是当请求的 strStyle 参数中含有 "force" 子参数的时候,无论 strAction 参数值为 "new" 还是 "change",都不会使用记录所在位置原来的记录中的 operations/operation 元素,而是要用请求传来的新记录内容。也就是说,可以把这种情况理解为一个超级用户,具备覆盖记录中所有字段的权力,为了从备份数据中恢复记录到数据库中,是把数据记录完全覆盖到指定的位置,等于把原来存在的记录内容彻底冲走了,或者理解为先彻底删除了原记录,然后写入新记录内容。

那 "force" 情况下写入的记录中是否要在 operations 元素下记载这次写入操作的时间呢?一种做法是不做任何记载,理由是,这是“恢复”操作,要还原到备份时候的样子;另外一种做法是给 operations 下追加或者覆盖一个动作为 "restore" 的 operation 元素。

记录中除了 operations 元素,一些当前账户不允许修改的字段如何处理?如果是非 "force" 情况,则要保留原来记录中的这些字段,如果原记录不存在,则这些字段就不存在,或者如果此时 strAction 为 "new" 要按照新创建记录时候的原则给这些字段赋必要的初始值。

一个例子是读者记录中的 password 元素。如果原记录中有 password 元素,则修改发生后,原来的 password 元素依然不变;如果原记录不存在,并且 strAction 为 "new",那么需要按照新创建读者记录时候的规则,利用记录中的 birthDate 构造出初始密码建立一个 password 元素。

如果是 "force" 情况,此时可以理解为具有 supervisor 权限,可以改写任何字段,那么不必考虑原记录中的任何字段,用新记录内容全部覆盖上去即可。

这里涉及到一个关联的话题,就是这些用于恢复的数据记录是如何得到的?原则上应该用相当于 supervisor 的读权限,准确来说就是 backup 权限来从数据库中读取获得。然后用这样的记录去进行恢复操作,把数据恢复到数据库中,这样才能保证比如读者记录中的 password 这样的元素不会丢失。

试想一下,如果当初备份的时候用的是一个普通账户,那么读取读者记录的时候势必得不到 password 元素(被服务器返回前自动过滤了),而用这样的记录再去进行恢复操作,按照上面介绍的算法,最后保存进去的记录就是缺乏 password 元素的,等于恢复以后,password 元素丢了。

注意上面介绍的是普通导出+恢复操作。而如果是普通导出+普通导入,因为普通导入的时候,覆盖的时候本来就没法覆盖数据库中原记录的 password 元素,那么最终导入完成后的结果就是皆大欢喜,数据库记录中的 password 元素最终没有丢失。不过,这是说覆盖操作,数据库中原记录还在。如果换成,导入以前把数据库内的原有记录都删除了(注意删除本身需要超级用户权限,因为一般账户可能会有一些字段不具备写权限导致无法删除记录),再进行导入,那么原记录已经没有了,导入进去最终数据库记录里面就没有 password 元素。

测试要点

1) 注意观察 action 为 "new" 时操作日志记录中也可能会存在 oldRecord 元素,当原记录存在时。