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

来源: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
  • 前言 本文介紹一款使用 C# 與 WPF 開發的音頻播放器,其界面簡潔大方,操作體驗流暢。該播放器支持多種音頻格式(如 MP4、WMA、OGG、FLAC 等),並具備標記、實時歌詞顯示等功能。 另外,還支持換膚及多語言(中英文)切換。核心音頻處理採用 FFmpeg 組件,獲得了廣泛認可,目前 Git ...
  • OAuth2.0授權驗證-gitee授權碼模式 本文主要介紹如何筆者自己是如何使用gitee提供的OAuth2.0協議完成授權驗證並登錄到自己的系統,完整模式如圖 1、創建應用 打開gitee個人中心->第三方應用->創建應用 創建應用後在我的應用界面,查看已創建應用的Client ID和Clien ...
  • 解決了這個問題:《winForm下,fastReport.net 從.net framework 升級到.net5遇到的錯誤“Operation is not supported on this platform.”》 本文內容轉載自:https://www.fcnsoft.com/Home/Sho ...
  • 國內文章 WPF 從裸 Win 32 的 WM_Pointer 消息獲取觸摸點繪製筆跡 https://www.cnblogs.com/lindexi/p/18390983 本文將告訴大家如何在 WPF 裡面,接收裸 Win 32 的 WM_Pointer 消息,從消息裡面獲取觸摸點信息,使用觸摸點 ...
  • 前言 給大家推薦一個專為新零售快消行業打造了一套高效的進銷存管理系統。 系統不僅具備強大的庫存管理功能,還集成了高性能的輕量級 POS 解決方案,確保頁面載入速度極快,提供良好的用戶體驗。 項目介紹 Dorisoy.POS 是一款基於 .NET 7 和 Angular 4 開發的新零售快消進銷存管理 ...
  • ABP CLI常用的代碼分享 一、確保環境配置正確 安裝.NET CLI: ABP CLI是基於.NET Core或.NET 5/6/7等更高版本構建的,因此首先需要在你的開發環境中安裝.NET CLI。這可以通過訪問Microsoft官網下載並安裝相應版本的.NET SDK來實現。 安裝ABP ...
  • 問題 問題是這樣的:第三方的webapi,需要先調用登陸介面獲取Cookie,訪問其它介面時攜帶Cookie信息。 但使用HttpClient類調用登陸介面,返回的Headers中沒有找到Cookie信息。 分析 首先,使用Postman測試該登陸介面,正常返回Cookie信息,說明是HttpCli ...
  • 國內文章 關於.NET在中國為什麼工資低的分析 https://www.cnblogs.com/thinkingmore/p/18406244 .NET在中國開發者的薪資偏低,主要因市場需求、技術棧選擇和企業文化等因素所致。歷史上,.NET曾因微軟的閉源策略發展受限,儘管後來推出了跨平臺的.NET ...
  • 在WPF開發應用中,動畫不僅可以引起用戶的註意與興趣,而且還使軟體更加便於使用。前面幾篇文章講解了畫筆(Brush),形狀(Shape),幾何圖形(Geometry),變換(Transform)等相關內容,今天繼續講解動畫相關內容和知識點,僅供學習分享使用,如有不足之處,還請指正。 ...
  • 什麼是委托? 委托可以說是把一個方法代入另一個方法執行,相當於指向函數的指針;事件就相當於保存委托的數組; 1.實例化委托的方式: 方式1:通過new創建實例: public delegate void ShowDelegate(); 或者 public delegate string ShowDe ...