public static bool PatronDataExists() { try { using (BiblioCacheContext context = new BiblioCacheContext()) { context.Database.EnsureCreated(); return(context.Patrons.Any()); } } catch (Exception ex) { WpfClientInfo.WriteErrorLog($"PatronDataExists() 出现异常: {ExceptionUtil.GetDebugText(ex)}"); return(false); } }
// 第二阶段:根据操作日志进行同步 // 注:中途遇到异常(例如 Loader 抛出异常),可能会丢失 INSERT_BATCH 条以内的日志记录写入 operlog 表 // parameters: // strLastDate 处理中断或者结束时返回最后处理过的日期 // last_index 处理或中断返回时最后处理过的位置。以后继续处理的时候可以从这个偏移开始 // return: // -1 出错 // 0 中断 // 1 完成 public static async Task <ReplicationResult> DoReplication( string strStartDate, string strEndDate, LogType logType, // string serverVersion, CancellationToken token) { string strLastDate = ""; long last_index = -1; // -1 表示尚未处理 // bool bUserChanged = false; ProcessInfo info = new ProcessInfo(); // strStartDate 里面可能会包含 ":1-100" 这样的附加成分 StringUtil.ParseTwoPart(strStartDate, ":", out string strLeft, out string strRight); strStartDate = strLeft; if (string.IsNullOrEmpty(strStartDate) == true) { return(new ReplicationResult { Value = -1, ErrorInfo = "DoReplication() 出错: strStartDate 参数值不应为空" }); } LibraryChannel channel = App.CurrentApp.GetChannel(); var old_timeout = channel.Timeout; channel.Timeout = TimeSpan.FromSeconds(10); try { int nRet = OperLogLoader.MakeLogFileNames(strStartDate, strEndDate, true, // 是否包含扩展名 ".log" out List <string> dates, out string strWarning, out string strError); if (nRet == -1) { return(new ReplicationResult { Value = -1, ErrorInfo = strError }); } if (dates.Count > 0 && string.IsNullOrEmpty(strRight) == false) { dates[0] = dates[0] + ":" + strRight; } using (BiblioCacheContext context = new BiblioCacheContext()) { context.Database.EnsureCreated(); ProgressEstimate estimate = new ProgressEstimate(); OperLogLoader loader = new OperLogLoader { Channel = channel, Stop = null, // this.Progress; // loader.owner = this; Estimate = estimate, Dates = dates, Level = 0, AutoCache = false, CacheDir = "", LogType = logType, Filter = "setReaderInfo,borrow,return,setSystemParameter,writeRes", // 借书还书时候都会修改读者记录 // ServerVersion = serverVersion }; //TimeSpan old_timeout = channel.Timeout; //channel.Timeout = new TimeSpan(0, 2, 0); // 二分钟 // loader.Prompt += Loader_Prompt; try { // int nRecCount = 0; string strLastItemDate = ""; long lLastItemIndex = -1; // TODO: 计算遍历耗费的时间。如果太短了,要想办法让调主知道这一点,放缓重新调用的节奏,以避免 CPU 和网络资源太高 foreach (OperLogItem item in loader) { token.ThrowIfCancellationRequested(); //if (stop != null) // stop.SetMessage("正在同步 " + item.Date + " " + item.Index.ToString() + " " + estimate.Text + "..."); if (string.IsNullOrEmpty(item.Xml) == true) { goto CONTINUE; } XmlDocument dom = new XmlDocument(); try { dom.LoadXml(item.Xml); } catch (Exception ex) { /* * if (this.HasLoaderPrompt()) * { * strError = logType.ToString() + "日志记录 " + item.Date + " " + item.Index.ToString() + " XML 装入 DOM 的时候发生错误: " + ex.Message; * MessagePromptEventArgs e = new MessagePromptEventArgs * { * MessageText = strError + "\r\n\r\n是否跳过此条继续处理?\r\n\r\n(确定: 跳过; 取消: 停止全部操作)", * IncludeOperText = true, * // + "\r\n\r\n是否跳过此条继续处理?", * Actions = "yes,cancel" * }; * Loader_Prompt(channel, e); * if (e.ResultAction == "cancel") * throw new ChannelException(channel.ErrorCode, strError); * else if (e.ResultAction == "yes") * continue; * else * { * // no 也是抛出异常。因为继续下一批代价太大 * throw new ChannelException(channel.ErrorCode, strError); * } * } * else */ throw new ChannelException(channel.ErrorCode, strError); } string strOperation = DomUtil.GetElementText(dom.DocumentElement, "operation"); if (strOperation == "setReaderInfo") { var trace_result = TraceSetReaderInfo( dom, info); if (trace_result.Value == -1) { WpfClientInfo.WriteErrorLog("同步 " + item.Date + " " + item.Index.ToString() + " 时出错: " + trace_result.ErrorInfo); } } else if (strOperation == "borrow" || strOperation == "return") { var trace_result = await TraceBorrowOrReturn( dom, info); if (trace_result.Value == -1) { WpfClientInfo.WriteErrorLog("同步 " + item.Date + " " + item.Index.ToString() + " 时出错: " + trace_result.ErrorInfo); } } else if (strOperation == "setSystemParameter") { var trace_result = TraceSetSystemParameter( dom, info); if (trace_result.Value == -1) { WpfClientInfo.WriteErrorLog("同步 " + item.Date + " " + item.Index.ToString() + " 时出错: " + trace_result.ErrorInfo); } } else if (strOperation == "writeRes") { var trace_result = TraceWriteRes( dom, info); if (trace_result.Value == -1) { WpfClientInfo.WriteErrorLog("同步 " + item.Date + " " + item.Index.ToString() + " 时出错: " + trace_result.ErrorInfo); } } else { continue; } #if NO if (nRet == -1) { strError = "同步 " + item.Date + " " + item.Index.ToString() + " 时出错: " + strError; /* * if (this.HasLoaderPrompt()) * { * MessagePromptEventArgs e = new MessagePromptEventArgs * { * MessageText = strError + "\r\n\r\n是否跳过此条继续处理?\r\n\r\n(确定: 跳过; 取消: 停止全部操作)", * IncludeOperText = true, * // + "\r\n\r\n是否跳过此条继续处理?", * Actions = "yes,cancel" * }; * Loader_Prompt(channel, e); * if (e.ResultAction == "cancel") * throw new Exception(strError); * else if (e.ResultAction == "yes") * continue; * else * { * // no 也是抛出异常。因为继续下一批代价太大 * throw new Exception(strError); * } * } * else */ throw new ChannelException(channel.ErrorCode, strError); } #endif // lProcessCount++; CONTINUE: // 便于循环外获得这些值 strLastItemDate = item.Date; lLastItemIndex = item.Index + 1; // index = 0; // 第一个日志文件后面的,都从头开始了 } // 记忆 strLastDate = strLastItemDate; last_index = lLastItemIndex; } finally { // loader.Prompt -= Loader_Prompt; channel.Timeout = old_timeout; } } return(new ReplicationResult { Value = last_index == -1 ? 0 : 1, LastDate = strLastDate, LastIndex = last_index, ProcessInfo = info }); } catch (ChannelException ex) { return(new ReplicationResult { Value = -1, ErrorInfo = ex.Message, ProcessInfo = info }); } catch (InterruptException ex) { // 2019/7/4 return(new ReplicationResult { Value = -1, ErrorInfo = ex.Message, ProcessInfo = info }); } catch (Exception ex) { string strError = "DoReplication() exception: " + ExceptionUtil.GetDebugText(ex); return(new ReplicationResult { Value = -1, ErrorInfo = strError, ProcessInfo = info }); } finally { channel.Timeout = old_timeout; App.CurrentApp.ReturnChannel(channel); } }
// 第一阶段:获得全部读者库记录,进入本地数据库 // result.Value // -1 出错 // >=0 实际获得的读者记录条数 public static async Task <ReplicationPlan> DownloadAllPatronRecordAsync(CancellationToken token) { LibraryChannel channel = App.CurrentApp.GetChannel(); var old_timeout = channel.Timeout; channel.Timeout = TimeSpan.FromMinutes(5); // 设置 5 分钟。因为读者记录检索需要一定时间 try { ReplicationPlan plan = GetReplicationPlan(channel); if (plan.Value == -1) { return(plan); } int nRedoCount = 0; REDO: if (token.IsCancellationRequested) { return new ReplicationPlan { Value = -1, ErrorInfo = "用户中断" } } ; // 检索全部读者库记录 long lRet = channel.SearchReader(null, // stop, "<all>", "", -1, "__id", "left", "zh", null, // strResultSetName "", // strOutputStyle out string strError); if (lRet == -1) { // 一次重试机会 if (lRet == -1 && (channel.ErrorCode == ErrorCode.RequestCanceled || channel.ErrorCode == ErrorCode.RequestError) && nRedoCount < 2) { nRedoCount++; goto REDO; } return(new ReplicationPlan { Value = -1, ErrorInfo = strError, ErrorCode = channel.ErrorCode.ToString() }); } long hitcount = lRet; // 把超时时间改短一点 channel.Timeout = TimeSpan.FromSeconds(20); DateTime search_time = DateTime.Now; // 获取和存储记录 ResultSetLoader loader = new ResultSetLoader(channel, null, null, $"id,xml,timestamp", "zh"); using (BiblioCacheContext context = new BiblioCacheContext()) { context.Database.EnsureCreated(); // 删除 Patrons 里面的已有记录 context.Patrons.RemoveRange(context.Patrons.ToList()); await context.SaveChangesAsync(token); // loader.Prompt += this.Loader_Prompt; if (hitcount > 0) { int i = 0; foreach (DigitalPlatform.LibraryClient.localhost.Record record in loader) { if (token.IsCancellationRequested) { return new ReplicationPlan { Value = -1, ErrorInfo = "用户中断" } } ; PatronItem item = new PatronItem(); var result = Set(item, record, search_time); if (result.Value == -1) { // TODO: 是否汇总报错信息? continue; } context.Patrons.Add(item); if ((i % 10) == 0) { await context.SaveChangesAsync(token); } i++; } await context.SaveChangesAsync(token); } } return(new ReplicationPlan { Value = (int)hitcount, StartDate = plan.StartDate }); } catch (Exception ex) { return(new ReplicationPlan { Value = -1, ErrorInfo = $"DownloadAllPatronRecord() 出现异常:{ex.Message}" }); } finally { channel.Timeout = old_timeout; App.CurrentApp.ReturnChannel(channel); } }
// 第一阶段:获得全部读者库记录,进入本地数据库 // result.Value // -1 出错 // >=0 实际获得的读者记录条数 public static async Task <ReplicationPlan> DownloadAllPatronRecordAsync( Delegate_writeLog writeLog, CancellationToken token) { _inDownloadingPatron++; // 2020/9/26 if (_inDownloadingPatron > 1) { _inDownloadingPatron--; return(new ReplicationPlan { Value = -1, ErrorCode = "running", ErrorInfo = "前一次的“下载全部读者记录到本地缓存”过程还在进行中,本次触发被放弃" }); } writeLog?.Invoke($"开始下载全部读者记录到本地缓存"); LibraryChannel channel = App.CurrentApp.GetChannel(); var old_timeout = channel.Timeout; channel.Timeout = TimeSpan.FromMinutes(5); // 设置 5 分钟。因为读者记录检索需要一定时间 try { ReplicationPlan plan = GetReplicationPlan(channel); writeLog?.Invoke($"GetReplicationPlan() return {plan.ToString()}"); if (plan.Value == -1) { return(plan); } int nRedoCount = 0; REDO: if (token.IsCancellationRequested) { return new ReplicationPlan { Value = -1, ErrorInfo = "用户中断" } } ; // 检索全部读者库记录 long lRet = channel.SearchReader(null, // stop, "<all>", "", -1, "__id", "left", "zh", null, // strResultSetName "", // strOutputStyle out string strError); if (lRet == -1) { writeLog?.Invoke($"SearchReader() 出错, strError={strError}, channel.ErrorCode={channel.ErrorCode}"); // 一次重试机会 if (lRet == -1 && (channel.ErrorCode == ErrorCode.RequestCanceled || channel.ErrorCode == ErrorCode.RequestError) && nRedoCount < 2) { nRedoCount++; goto REDO; } return(new ReplicationPlan { Value = -1, ErrorInfo = strError, ErrorCode = channel.ErrorCode.ToString() }); } long hitcount = lRet; writeLog?.Invoke($"共检索命中读者记录 {hitcount} 条"); // 把超时时间改短一点 channel.Timeout = TimeSpan.FromSeconds(20); DateTime search_time = DateTime.Now; Hashtable pii_table = new Hashtable(); int skip_count = 0; int error_count = 0; // 获取和存储记录 ResultSetLoader loader = new ResultSetLoader(channel, null, null, $"id,xml,timestamp", "zh"); using (BiblioCacheContext context = new BiblioCacheContext()) { context.Database.EnsureCreated(); // 删除 Patrons 里面的已有记录 context.Patrons.RemoveRange(context.Patrons.ToList()); await context.SaveChangesAsync(token); // loader.Prompt += this.Loader_Prompt; if (hitcount > 0) { int i = 0; foreach (DigitalPlatform.LibraryClient.localhost.Record record in loader) { if (token.IsCancellationRequested) { return new ReplicationPlan { Value = -1, ErrorInfo = "用户中断" } } ; PatronItem item = new PatronItem(); // result.Value: // -1 出错 // 0 需要跳过这条读者记录 // 1 成功 var result = Set(item, record, search_time); if (result.Value == -1 || result.Value == 0) { // TODO: 是否汇总报错信息? if (result.Value == -1) { writeLog?.Invoke($"Set() ({item.RecPath}) 出错: {result.ErrorInfo}"); error_count++; } if (result.Value == 0) { skip_count++; } continue; } // if (pii_table.ContainsKey(result.PII)) { string recpath = (string)pii_table[result.PII]; writeLog?.Invoke($"发现读者记录 {item.RecPath} 的 PII '{result.PII}' 和 {recpath} 的 PII 重复了。跳过它"); continue; } pii_table[result.PII] = item.RecPath; // TODO: PII 应该是包含 OI 的严格形态 context.Patrons.Add(item); if ((i % 10) == 0) { await context.SaveChangesAsync(token); } i++; } await context.SaveChangesAsync(token); } } writeLog?.Invoke($"plan.StartDate='{plan.StartDate}'。skip_count={skip_count}, error_count={error_count}。返回"); return(new ReplicationPlan { Value = (int)hitcount, StartDate = plan.StartDate }); } catch (Exception ex) { // 2020/9/26 writeLog?.Invoke($"DownloadAllPatronRecord() 出现异常:{ExceptionUtil.GetDebugText(ex)}"); return(new ReplicationPlan { Value = -1, ErrorInfo = $"DownloadAllPatronRecord() 出现异常:{ex.Message}" }); } finally { channel.Timeout = old_timeout; App.CurrentApp.ReturnChannel(channel); writeLog?.Invoke($"结束下载全部读者记录到本地缓存"); _inDownloadingPatron--; } }