一個關於單據審核的流程演變

来源:http://www.cnblogs.com/xie-zhonglai/archive/2017/05/19/verifyFlow.html
-Advertisement-
Play Games

本文簡要介紹一個關於單據的常規審核流從雛形到形成標準系統結構的思維轉變, 沒有什麼高深的技術, 有的只是循序漸進的思維轉變.希望能給有類似需求或在軟體設計過程中有困惑的朋友一個簡明參考. ...


本文簡要介紹一個關於單據的常規審核流從雛形到形成標準系統結構的思維轉變, 沒有什麼高深的技術, 有的只是循序漸進的思維轉變.希望能給有類似需求或在軟體設計過程中有困惑的朋友一個簡明參考.

1. 某天, 甲方的採購經理說: 我們的採購申請單需要審核,審核後才能參與下一步流程中.經過簡單考慮於是有了下麵的偽代碼

/// <summary>
/// 採購申請單審核
/// </summary>
/// <param name="id">單據Id</param>
public void PurchaseRequestVerify(string id)
{
    using (var repository = GetRepository<ProjJXCPurchaseRequest>())
    {
        var mainData = repository.Get(id);      //獲取採購申請單

        //驗證單據是否不存在或狀態不為待審核
        mainData.ThrowIfNullOrDelete()
            .ThrowIf(t => t.Status != BillStatus.VerifyWaite,
                    "審核失敗,只有狀態為[{0}]的單據才可以審核操作",
                    Remarks.GetRemark(BillStatus.VerifyWaite));
        
        //更新狀態
        mainData.Status = BillStatus.VerifyOK;
        mainData.ModUser = AppRuntimes.Instance.CurrentUser.Name;
        mainData.ModDate = DateTime.Now;

        repository.Update(mainData);
        repository.SaveDbChange();
    }
}

/// <summary>
/// 保存採購訂單
/// </summary>
/// <param name="orderInfo">採購訂單實例</param>
public void PurchaseOrderSave(ProjJXCPurchaseOrder orderInfo)
{
    //驗證參數
    orderInfo.ThrowIfIsNull()
        .ThrowIfPropertyIsNullOrEmpty(t => t.FromBillId, "採購申請單");

    using (var repository = GetRepository<ProjJXCPurchaseOrder>())
    {
        //獲取採購申請單
        var purchaseRequest = repository.Context
                .ProjJXCPurchaseRequests.Find(orderInfo.FromBillId);
        purchaseRequest.ThrowIfNullOrDelete()
            .ThrowIf(t => t.Status != BillStatus.VerifyOK, "所選採購申請單還沒有成功審核");

        //.....

        repository.SaveDbChange();
    }
}

2. 兩天後, 甲方工程監理同事部說: 工程立項單需要審核, 並且同時需要多個人審核後才能生效.剛要準備開工呢, 路過銷售部時候, 銷售部總經理叫住我說: 我們的銷售計劃需要審核, 並且需要逐級多次審核, 中午吃飯遇上商務部同事:我們的投標保證金申請單支付成功後, 得自動弄個通知告訴我們, 以便我們開展下一步業務......

經過多方位溝通,  看來我們的審核流需要重新進行系統化的設計了, 並希望將其獨立於我們的業務之外, 目前提煉出來的需求包含如下部分;

a). 單據審核人群可以預設,也能支持在提交前選擇需要審核的人群;

b).部分單據需要逐級審核, 部分單據不需要逐級審核,只需要審核人中全部審核通過即可;

c). 單據審核人可以轉發給他人代替自己審核;

d).其中一人審核不通過後,單據直接恢復到新建狀態,並通知提交人需要修改單據並重新提交;

e). 單據在全部審核通過後可以觸發消息通知到指定人員,在審核前,審核後,審核不通過這些節點中可以觸發系類似通知.

f). 用戶需要有自己的待審核列表,並可以直觀的看到單據信息.

針對此功能, 簡單分析如下:

a). 需要一個設定, 主要包括: 標示某個具體的單據是按預設審核流程還是由創建人自行指定審核人; 審核是否需要逐級;審核完成後需要通知的用戶.

b).獲取單據的審核信息的審核, 根據單據類型不同,可能是預設的審核流程,也可能是創建人自行添加的審核人.

c).用戶查看單據的時候, 若單據處於待審核狀態, 判斷當前用戶是否是待審核人, 若是給出審核功能,否則屏蔽審核功能.

d).當單據的全部審核人審核後,更改單據狀態為審核通過.並觸發通知到指定用戶群.

e).用戶在自己的待審核列表中,可以通過在審核信息中冗餘單據詳情地址來直觀的看到單據信息.

f).如何標示單據類型, 有人說可以用枚舉,可是當系統中上百張單據的時候,我想不光定義麻煩,寫switch也能把人寫瘋.在此我們可以借鑒單據編號中的單據定義: 直接使用單據類名來表示BillType.

g). 定義委托集合(或稱之為審核回調方法庫),用於指定當單據審核過程中針對各個節點的對應後續處理.

h).  在BS MVC中,我們直接將審核相關的邏輯集成到 BaseController 和 BaseBusinessService 中, 方便各個單據直接調用.

我們大致需要以下幾個對象來輔助完成這個大業:image

簡單介紹一下上圖中各對象所表示的含義:

BillCodeDefine : 用於描述單據定義及編號創建規則, 其中 BillType 為單據的類名,參考寫法如 typeOf(ProjJXCPurchaseOrder).Name;

SysBillDefine : 主要用來描述單據審核流, IsVerifySetting 表示是否為預設審核,IsVerifySort 表示為是否按順序審核(亦即前文提到的逐級審核), VerifyDetails表示設定的審核用戶列表,MsgDetails 表示審核成功後需要推送的消息列表.

        註意此對象大可和上面的BillCodeDefine 合併, 只不過這個屬於歷史的產物, BillCodeDefine 這個出生在幾年前,已經集成在Framework中,無奈之舉,衍生出了這個SysBillDefine;

VerifyBillUser: 主要用來描述單據的審核用戶列表,其中的DetailUrl 表示具體的單據詳情地址,UserId表示審核用戶,FromId 和NextId分別為審核用戶轉入轉出標識;

SysUserMsg: 用於推送給用戶的消息提醒;

VerifyActionFilter: 用於處理單據審核過程中的各個節點的的回調操作;

VerifyActionFactory: 用於註冊各個單據在審核過程中各個節點的回調方法;

有了以上的基本認識, 我們簡單的介紹下實現過程.

1. 獲取單據的審核定義,

/// <summary>
 /// 獲取單據審核明細數據
 /// </summary>
 /// <param name="billId">單據Id</param>
 /// <param name="billType">單據業務</param>
 /// <returns></returns>
 public BillVerifyDetail GetVerifyDetails(string billId, string billType)
 {
     var rs = new BillVerifyDetail();
     if (string.IsNullOrWhiteSpace(billType))
     {
         return rs;
     }

     var billDefine = GetRepository<SysBillDefine>().Get(t => t.BillType == billType).FirstOrDefault();
     rs.IsVerifySetting = billDefine != null && billDefine.IsVerifySetting;
     rs.IsVerifySort = billDefine != null && billDefine.IsVerifySort;

     var verifyUsers = GetVerifyUserListByBillId(billId); 
     if (verifyUsers.IsNullEmpty())
     {
         #region 從單據審核設置中獲取審核配置
         List<SysBillVerifyDefine> defines = null;
         if (rs.IsVerifySetting)
         {
             defines = GetVerifyDefineByBillType(billType);
         }
         if (!defines.IsNullEmpty())
         {
             verifyUsers = defines.Select(t => new VerifyBillUser()
             {
                 BillId = billId,
                 BillNum = "",
                 BillType = billType,
                 DepartmentId = t.DepartmentId,
                 DepartmentName = t.DepartmentName,
                 UserId = t.UserId,
                 UserCode = t.UserCode,
                 UserName = t.UserName,
                 VerifyTime = new DateTime(1900, 1, 1),
                 Status = BillStatus.New,
                 VerifySortType = t.VerifySortType,
                 SortIndex = t.SortIndex
             }).ToList();
         }
         #endregion
     }

     rs.VerifyUsers = verifyUsers;
     return rs;
 }

/// <summary>
/// 根據單據id獲取審核明細數據
/// </summary>
/// <param name="billId">單據Id</param>
/// <param name="verifyStatus">狀態</param>
/// <returns></returns>
public List<VerifyBillUser> GetVerifyUserListByBillId(string billId,
    params BillStatus[] verifyStatus)
{
    List<VerifyBillUser> lst = null;
    if (string.IsNullOrWhiteSpace(billId))
    {
        lst = new List<VerifyBillUser>();
        return lst;
    }
    var query = GetRepository<VerifyBillUser>().Get(t => t.BillId == billId);
    if (verifyStatus != null && verifyStatus.Length > 0)
    {
        query = query.Where(t => verifyStatus.Contains(t.Status));
    }
    lst = query.OrderBy(t => t.SortIndex).ToList();
    return lst;
}

2. 審核通過

/// <summary>
/// 審核單據
/// </summary>
/// <param name="billId">單據Id</param>
public void VerifyBillOK(string billId)
{
    billId.ThrowIfIsNull();
    AppRuntimes.Instance.CurrentUser.ThrowIfIsNull();
    using (var repository = GetRepository<VerifyBillUser>())
    {
        //找尋當前用戶第一條待審核數據
        var verifyDetail = repository.Get(t => t.BillId == billId
                                            && t.UserId == AppRuntimes.Instance.CurrentUser.Id
                                            && t.Status == BillStatus.VerifyWaite).OrderBy(t => t.SortIndex).FirstOrDefault();
        if (verifyDetail == null)
        {
            throw new BusinessException("親~ 好像沒有你的待審核數據哦,請確認是否已審核或你的上一步審核人已經審核.");
        }

        //執行審核前動作
        VerifyActionFilter.BeforeVerify(verifyDetail);

        //通過單據剩餘待審核人數量來判斷當前單據審核後狀態
        var waiteVerifyCount = repository.Get(t => t.BillId == billId
                    && (t.Status == BillStatus.VerifyWaite || t.Status == BillStatus.VerifyWaiteLastStep)
                    && t.Id != verifyDetail.Id).Count();

        BillStatus mainBillStatus = waiteVerifyCount == 0 ? BillStatus.VerifyOK : BillStatus.VerifyPart;

        verifyDetail.Status = BillStatus.VerifyOK;
        verifyDetail.VerifyTime = DateTime.Now;
        verifyDetail.Remark = string.Empty;
        repository.Context.Entry(verifyDetail).State = EntityState.Modified;

        //如果為全部審核通過
        if (mainBillStatus == BillStatus.VerifyOK)
        {
            //執行全部審核通過後動作
            VerifyActionFilter.VerifyFullComplete(verifyDetail); 
        }

        //如果單據是順序審核,將下一條審核記錄狀態改為待審核
        var nextVerifyData = repository.Get(t => t.BillId == billId 
                                && t.Status == BillStatus.VerifyWaiteLastStep 
                                && t.SortIndex > verifyDetail.SortIndex)
                                .OrderBy(t => t.SortIndex).FirstOrDefault();
        if (nextVerifyData != null)
        {
            nextVerifyData.Status = BillStatus.VerifyWaite;
            nextVerifyData.ModDate = DateTime.Now;
            nextVerifyData.ModUser = AppRuntimes.Instance.CurrentUser.Name;
            repository.Context.Entry(nextVerifyData).State = EntityState.Modified;
        }

        //執行審核後動作
        VerifyActionFilter.AfterVerify(verifyDetail);

        //更新單據狀態
        UpdateBillStatus(repository.Context, verifyDetail.BillType, verifyDetail.BillId, mainBillStatus);

        repository.SaveDbChange();
    }
}

3. 上圖中有一個方法調用,UpdateBillStatus(repository.Context, verifyDetail.BillType, verifyDetail.BillId, mainBillStatus); 簡明意思就是根據審核情況將對應單據類型指定單據單據狀態更新.這裡有一個小問題是如何通過單據類型找到這個單據的存儲表,或者說如何通過單據類型名稱轉化為對應的DbSet<T>. 沒錯,各位已經想到了通過反射去動態創建DbSet<T>.但因為感覺過於繁瑣,在此我們並沒有如此嘗試,而是通過找到定義在實體對象上的 [Table] 特性去完成的這個動作,參考如下代碼

/// <summary>
 /// 更新單據狀態
 /// </summary>
 /// <param name="billType">單據類型</param>
 /// <param name="billId">單據Id</param>
 /// <param name="billStatus">單據狀態</param>
 public void UpdateBillStatus(PowerPlantDbContext dbContext, string billType, 
     string billId, BillStatus billStatus)
 {
     var tableName = GetBillTypeTableName(billType);
     var updateSql = string.Format("update {0} set Status={1},ModUser='{2}',ModDate=getdate() where Id='{3}'",
         tableName, (int)billStatus, AppRuntimes.Instance.CurrentUser.Name, billId);

     dbContext.Database.ExecuteSqlCommand(updateSql);
 }

 /// <summary>
 /// 緩存實體與資料庫中TableName關係
 /// </summary>
 private static ConcurrentDictionary<string, string> 
     _EntityTableNameDic = new ConcurrentDictionary<string, string>();


/// <summary>
/// 根據實體類型名稱獲取資料庫中TableName
/// </summary>
/// <param name="billType">實體類型名稱</param>
/// <returns></returns>
private string GetBillTypeTableName(string billType)
{
    return _EntityTableNameDic.GetOrAdd(billType, t =>
    {
        var tableName = string.Empty;
        var villEntityAsmStr = string.Format("XZL.Web.Domain.Enties.{0},XZL.Web.Domain.Enties", t);
        var billEntityType = Type.GetType(villEntityAsmStr);
        if (billEntityType != null)
        {
            var tableAttrs = billEntityType.GetCustomAttributes(typeof(TableAttribute), false);
            if (tableAttrs != null || tableAttrs.Length == 1)
            {
                tableAttrs = billEntityType.GetCustomAttributes(typeof(TableAttribute), true);
            }
            if (tableAttrs != null && tableAttrs.Length == 1)
            {
                tableName = ((TableAttribute)tableAttrs[0]).Name;
            }
        }
        return tableName;
    });
}

4. 下麵我們簡單的看下 VerifyActionFilter 及 VerifyActionFactory, 簡單的說就是針對性的定義單據審核執行後續回調方法.參考如下

public class VerifyActionFilter
 { 
     public static void VerifyFullComplete(VerifyBillUser verifyDetail)
     {
         VerifyActionFactory.GetVerifyExecTimeAction(verifyDetail, 
    VerifyMethodExecTime.FullComplete)?.Invoke(verifyDetail);
     }
 }

public class VerifyActionFactory
{ 
    private static Dictionary<string, Action<VerifyBillUser>> fullCompleteDictionary;
    private static object syncLock = new object();

    public static Action<VerifyBillUser> GetVerifyExecTimeAction(VerifyBillUser verifyDetail,
        VerifyMethodExecTime execTime)
        {
            Action<VerifyBillUser> action = null;
            switch (execTime)
            {
             
                case VerifyMethodExecTime.FullComplete:
                    {
                        if (fullCompleteDictionary.ContainsKey(verifyDetail.BillType))
                        {
                            action = fullCompleteDictionary[verifyDetail.BillType];
                        }
                        break;
                    }
                default:
                    break;
            }
            return action;
        }  

     /// <summary>
     /// 註冊審核回調方法
     /// </summary>
     private static void InitAction()
     {
         if (fullCompleteDictionary == null)
         {
             lock (syncLock)
             {
                 if (fullCompleteDictionary == null)
                 { 
                     var saleService = new SaleService(); 

                     fullCompleteDictionary = new Dictionary<string, Action<VerifyBillUser>>();
                     fullCompleteDictionary.Add(typeof(SaleProject).Name, saleService.SaleProjectVerifyComplete);

                     //....
                 }
             }
         } 
     }
    
}

5.  經過以上改造後, 我們的整個審核流程貌似就很順暢了, 但是在最後一步, 註冊審核回調方法, 這個常常被很多小伙伴忘記了, 前文也提到了, 我們系統中有上百種單據, 當我們的代碼變成如下截圖的時候,實在是很難排查到底哪個需要設定審核動作的單據沒有設定, 幸好密集恐怖症還算不嚴重, 故此我們需要進一步的優化這個註冊方法.

image

6. 相比各位或許已經有答案了, 沒錯,我們需要借鑒我們之前經驗, 引入AOP的概念.在需要設定為審核回調的方法上通過"特性"來標註或類似MVC中通過特定的方法名稱來自動實現這個步驟. 然後程式自動識別系統中的這些方法添加到回調方法庫中. 本文簡單介紹通過特性的方式切入.

/// <summary>
 /// 用於描述審核過程中調用的回調方法
 /// </summary>
 [AttributeUsage(AttributeTargets.Method)]
 public class VerifyMethodAttribute : Attribute
 {
     /// <summary>
     /// 方法執行時機
     /// </summary>
     public VerifyMethodExecTime ExecTime { get; private set; }

     /// <summary>
     /// 執行審核主體對象名稱
     /// </summary>
     public string EntityTypeName { get; private set; }

     public Type EntityType { get; set; }

     public VerifyMethodAttribute(VerifyMethodExecTime execTime, Type entityType)
     {
         this.EntityType = entityType;
         this.ExecTime = execTime;
         this.EntityTypeName = this.EntityType.Name;
     }
 }
  
 public enum VerifyMethodExecTime
 {
     /// <summary>
     /// 審核前
     /// </summary>
     Before = 1,

     /// <summary>
     /// 審核後(只審核成功)
     /// </summary>
     After = 2,

     /// <summary>
     /// 審核失敗
     /// </summary>
     Fail = 3,

     /// <summary>
     /// 全部審核完成
     /// </summary>
     FullComplete = 4
 }

 

7. 改良後的回調函數庫即VerifyActionFactory 中初始化方法,看著順眼多了.

/// <summary>
/// 自動初始化
/// </summary>
public static void AutoInit()
{
    lock (syncLock)
    {
        beforeDictionary = new Dictionary<string, Action<VerifyBillUser>>();
        afterDictionary = new Dictionary<string, Action<VerifyBillUser>>();
        failDictionary = new Dictionary<string, Action<VerifyBillUser>>();
        fullCompleteDictionary = new Dictionary<string, Action<VerifyBillUser>>();

        //獲取系統中的服務類型
        var svcTypes = Assembly.GetExecutingAssembly().GetTypes()
                .Where(t => t.BaseType.Equals(typeof(PowerPlantBaseService))).ToList();

        if (svcTypes.IsNullEmpty())
        {
            return;
        }
        //迭代添加每個服務中的期望方法
        svcTypes.ForEach(svc =>
        {
            var methods = svc.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly);
            if (methods.IsNullEmpty())
            {
                return;
            }

            //實例化一個服務
            var svcInstance = Activator.CreateInstance(svc);

            foreach (MethodInfo m in methods)
            {
                #region 處理方法,判斷是否有VerifyMethod特性
                var verifyMethodFlags = m.GetCustomAttributes(typeof(VerifyMethodAttribute), false);
                if (verifyMethodFlags.IsNullEmpty())
                {
                    continue;
                }
                Action<VerifyBillUser> methodAction = null;

                try
                {
                    //通過將目標方法轉化為期望的委托
                    methodAction = Delegate.CreateDelegate(typeof(Action<VerifyBillUser>),
                                                m.IsStatic ? null : svcInstance,
                                                m) as Action<VerifyBillUser>;
                }
                catch (Exception ex)
                {
                    Log4NetUtil.WriteLog(ex);
                    continue;
                }
                if (methodAction == null)
                {
                    return;
                }
                verifyMethodFlags.ForEach(f =>
                {
                    #region 根據特性,對應添加到指定集合中
                    var temp = (VerifyMethodAttribute)f;
                    if (temp != null)
                    {
                        switch (temp.ExecTime)
                        {
                            case VerifyMethodExecTime.Before:
                                beforeDictionary[temp.EntityTypeName] = methodAction;
                                break;
                            case VerifyMethodExecTime.After:
                                afterDictionary[temp.EntityTypeName] = methodAction;
                                break;
                            case VerifyMethodExecTime.Fail:
                                failDictionary[temp.EntityTypeName] = methodAction;
                                break;
                            case VerifyMethodExecTime.FullComplete:
                                fullCompleteDictionary[temp.EntityTypeName] = methodAction;
                                break;
                            default:
                                break;
                        }
                    }
                    #endregion
                });
                #endregion
            }
        });
    }
}

最後, 附上兩個程式中有關審核的界面截圖作為本文的收尾.

imageimage

後記, 通過以上對系統中審核流的重新系統設計, 有效的滿足了甲方的需求, 併在可預見的未來,預留了擴能方案.同時也有效簡化了系統中作為開發人員的負擔.使之看是獨立於業務之外運行,又能很好的找到切入點與業務有效的連接.

同時, 理解需求需要多方位多角度,從而能更高角度的去思考.程式的極簡原則並不意味著程式不需要設計,當然本人也十分排斥程式的過度設計.


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • a) setter(重要) b) 構造方法(可以忘記),簡單例子: 用的不多,具體的構造函數重構應用可以參考源文檔 c) 介面註入(可以忘記)。 代碼鏈接: http://pan.baidu.com/s/1pKAe5Vt 密碼: qvyy jar 包: 鏈接: http://pan.baidu.co ...
  • R語言數據可視化之ggplot2包,從柱狀圖開始。從簡單的業務量統計開始。 ...
  • ggplot2介紹:內容包含什麼是ggplot2、與lattice包的比較、基本概念、一個例子。 ...
  • 一、基本概念 1.AOP簡介 DI能夠讓相互協作的軟體組件保持鬆散耦合;而面向切麵編程(aspect-oriented programming,AOP)允許你把遍佈應用各處的功能分離出來形成可重用的組件。把這些橫切關註點與業務邏輯相分離正是面向切麵編程(AOP)所要解決的問題 常見場景:日誌、安全、 ...
  • Discrete Logging Time Limit: 5000MS Memory Limit: 65536K Total Submissions: 5865 Accepted: 2618 Description Given a prime P, 2 <= P < 231, an integer ...
  • R語言簡介,其中說到了R的概況、特點、圖標、界面、一些必要的和裝逼的設置、缺點。 ...
  • 解決quartz定時任務被觸發兩次的問題: 其中<Host/>告訴tomcat,在啟動的時候載入webapps下的所有項目工程文件,<Context/>又讓tomcat再載入了一遍(一般情況下配置<Context/>,主要是由於想功能變數名稱訪問時將工程名去掉的原因配置),這種情況下會導致工程中的quart ...
  • 單一職責原則是面向對象原則五大原則中最簡單,也是最重要的一個原則, 他的字面定義如下: 單一職責原則(Single Responsibility Principle, SRP): 一個類只負責一個功能領域中的相應職責,或者可以定義為:就一個類而言,應該只有一個引起它變化的原因。 從定義中可以看出在定 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...