金蝶雲星空解鎖時同時解鎖序列號

lanrenka發表於2024-08-19

業務背景

公司業務要求,如果檢查發現序列號有問題,先鎖庫不允許出庫。

如果已經鎖庫,此時序列號允許出庫,則可以解鎖。

前置任務:金蝶雲星空鎖庫時同時鎖定序列號 - lanrenka - 部落格園 (cnblogs.com)

系統現狀

即時庫存鎖庫,鎖定的是數量,庫存-鎖庫數=可用數,當可用量小於等於0就不可以再出庫了。

如果想要控制鎖到序列號,系統就不支援了。

方案設計

鎖庫時同時鎖定庫存和根據輸入的序列號鎖定序列號,解鎖的時候,同時解鎖庫存和根據輸入的序列號進行解鎖。

詳細設計

解鎖操作-擴充套件
新增頁籤控制元件,將原來的解鎖資訊放到第一個頁籤,第二個頁籤新增一個單據體,單據體增加一個基礎資料,繫結序列號主檔。

選單項,新增按鈕,按序列號解鎖

找到表單外掛,新建外掛,繼承它。

實現

解鎖數量不能大於鎖庫

解鎖=序列號

序列號必須在當前即時庫存序列號主檔標記鎖定

釋放庫存清空鎖定標記必須一起成功

金蝶雲星空解鎖時同時解鎖序列號
#region << 版 本 注 釋 >>
/*----------------------------------------------------------------
 * 版權所有 (c) 2024  NJRN 保留所有權利。
 * CLR版本:4.0.30319.42000
 * 機器名稱:INC1507245
 * 公司名稱:Increase
 * 名稱空間:Krystal.K3.SCM.Business.PlugIn.STK.DynamicForm
 * 唯一標識:38bf5845-45d5-4367-ad27-ceac9c162040
 * 檔名:UnLockOperateBySerialExtend
 * 當前使用者域:INC1507245
 * 
 * 建立者:Krystal
 * 電子郵箱:543375940@qq.com
 * 建立時間:2024/8/14 15:46:46
 * 版本:V1.0.0
 * 描述:
 *
 * ----------------------------------------------------------------
 * 修改人:
 * 時間:
 * 修改說明:
 *
 * 版本:V1.0.1
 *----------------------------------------------------------------*/
#endregion << 版 本 注 釋 >>

using Kingdee.BOS;
using Kingdee.BOS.App.Data;
using Kingdee.BOS.Core;
using Kingdee.BOS.Core.DynamicForm;
using Kingdee.BOS.Core.DynamicForm.PlugIn.Args;
using Kingdee.BOS.Core.Metadata.EntityElement;
using Kingdee.BOS.Core.Metadata.FieldElement;
using Kingdee.BOS.Orm.DataEntity;
using Kingdee.BOS.Resource;
using Kingdee.BOS.Util;
using Kingdee.K3.Core.SCM.STK;
using Kingdee.K3.SCM.ServiceHelper;
using Kingdee.K3.SCM.Stock.Business.PlugIn;
using Krystal.K3Cloud.Core.Const;
using Krystal.K3Cloud.Core.Model.STK;
using Krystal.K3Cloud.Core.Util;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Transactions;

namespace Krystal.K3.SCM.Business.PlugIn.STK.DynamicForm
{
    /// <summary>
    /// 功能描述    :UnLockOperateBySerialExtend  
    /// 創 建 者    :Administrator
    /// 建立日期    :2024/8/14 15:46:46 
    /// 最後修改者  :Krystal
    /// 最後修改日期:2024/8/14 15:46:46 
    /// </summary>
    [Description("即時庫存解鎖操作外掛--二開繼承"), HotUpdate]
    public class UnLockOperateBySerialExtend: UnLockStockOperate
    {
        #region <常量>
        /// <summary>
        /// 操作型別:StockLock-庫存鎖庫,Inv-即時庫存,SaleOrder-銷售訂單 StockLockLog-庫存日誌解鎖
        /// </summary>
        private string opType = "Inv";
        #endregion <常量>

        #region <變數>
        /// <summary>
        /// 標示是否關閉介面
        /// </summary>
        private bool IsClose;
        #endregion <變數>



        #region <方法>
        /// <summary>
        /// 獲得動態物件的值,優先返回MasterId
        /// </summary>
        /// <param name="obj"></param>
        /// <returns></returns>
        private long GetDynamicValue(DynamicObject obj)
        {
            if (obj == null)
            {
                return 0L;
            }
            if (obj.DynamicObjectType.Properties.ContainsKey(FormConst.MASTER_ID))
            {
                return Convert.ToInt64(obj[FormConst.MASTER_ID]);
            }
            if (obj.DynamicObjectType.Properties.ContainsKey("Id"))
            {
                return Convert.ToInt64(obj["Id"]);
            }
            return 0L;
        }
        /// <summary>
        /// 校驗序列號是否在庫,且未鎖定
        /// </summary>
        /// <param name="objList"></param>
        /// <param name="invId"></param>
        /// <returns></returns>
        private bool CheckSerial(List<DynamicObject> objList, string invId, out string serialMsg)
        {
            bool isSucceed = true;
            serialMsg = string.Empty;
            //序列號物料編碼,在庫狀態
            List<SqlParam> para = new List<SqlParam>()
            {
                //new SqlParam("@STOCKID", KDDbType.Int32,493513),
                new SqlParam("@InvId", KDDbType.String,invId),
                new SqlParam("@IsLock", KDDbType.Int32,1),
                //new SqlParam("@ENDTIME",KDDbType.String,_filterArgs.EndTime.ToString("yyyy-MM-dd")),
                //new SqlParam("@t",KDDbType.String,temp_detail)
            };
            //執行sql語句並將結果集填充至DataSet  XXXX_PR_STK_SerialInfoByInvId未使用
            DataSet getSerial = DBUtils.ExecuteDataSet(this.Context, System.Data.CommandType.StoredProcedure
                , string.Format(@"{0}XXXX_PR_STK_SerialInfoByInvId", OtherConst.DIALECT), para);



            //呼叫上面的將DataSet轉換為List<Menu>實體集合
            List<SerialMainFile> serialListInStock = (List<SerialMainFile>)KrystalCommonUtil
                .DataSetToList<SerialMainFile>(getSerial, 0);
            if (serialListInStock == null || serialListInStock.Count <= 0)
            {
                isSucceed = false;
            }
            //LINQ兩個集合關聯,左集合在右邊找不到,則為空,找出為空的資料提示出來
            //使用查詢語句
            var list = from left in objList
                       join right in serialListInStock on left["F_XXXX_SerialId_Id"] + "" equals right.FSERIALID + "" into temp
                       select new
                       {
                           serialId = left["F_XXXX_SerialId_Id"] + "",
                           serialNumber = (left["F_XXXX_SerialId"] as DynamicObject)["Number"] + "",
                           serialNo = temp.Select(t => t.FSERIALNO).FirstOrDefault()
                       };

            var isEmpty = list.Where(s => s.serialNo.IsNullOrEmptyOrWhiteSpace()).ToList();
            if (isEmpty != null && isEmpty.Count > 0)
            {
                isSucceed = false;
                List<string> empNumber = isEmpty.Select(s => s.serialNumber).ToList();
                serialMsg = string.Format(@"序列號不在當前庫存裡,可以查詢序列號報表。在庫則可能未鎖定【{0}】", string.Join(",", empNumber));
                //this.View.ShowErrMessage(string.Format(@"序列號不在當前庫存裡,可以查詢序列號報表。在庫則可能未鎖定【{0}】", string.Join(",", empNumber)));
                return isSucceed;
            }
            return isSucceed;
        }


        /// <summary>
        /// 儲存解鎖資訊
        /// </summary>
        /// <returns></returns>
        private bool SaveUnLockStockBySerial()
        {
            Entity unLockEntity = View.BusinessInfo.GetEntity("FEntity");
            DynamicObjectCollection objList = View.Model.GetEntityDataObject(unLockEntity);
            if (objList == null || objList.Count <= 0 || objList.Count > 1)
            {
                this.View.ShowErrMessage("只能處理一行物料。");
                return true;
            }
            Entity serialEntity = View.BillBusinessInfo.GetEntity("F_XXXX_Serial");
            List<DynamicObject> subEntityList = (from p in View.Model.GetEntityDataObject(serialEntity)
                                                 where p["F_XXXX_SerialId"] != null
                                                 select p).ToList();
            if (subEntityList == null || subEntityList.Count <= 0)
            {
                this.View.ShowErrMessage("序列號必須錄入。");
                return true;
            }
            List<LockStockArgs> stockArgList = new List<LockStockArgs>();
            List<LockStockArgs> warnStockArgList = new List<LockStockArgs>();
            bool isCancel = false;
            bool isWarn = false;
            List<string> messages = new List<string>();
            List<string> warnMessages = new List<string>();
            int rowindex = 0;
            bool haveSelRow = false;
            string preMsg = ResManager.LoadKDString("第{0}行記錄", "004023000017456", SubSystemType.SCM);
            foreach (DynamicObject obj in objList)
            {
                rowindex++;
                if (!Convert.ToBoolean(obj["FSelect"]))
                {
                    continue;
                }
                haveSelRow = true;
                isCancel = false;
                isWarn = false;
                string invId = obj["InvDetailID"] + "";
                decimal unLockQty = Convert.ToDecimal(obj["UnLockQty"]);
                decimal lockQty = Convert.ToDecimal(obj["LockQty"]);
                long secUnitID = GetDynamicValue(obj["SecUnitID"] as DynamicObject);
                decimal secUnLockQty = Convert.ToDecimal(obj["SecUnLockQty"]);
                decimal secLockQty = Convert.ToDecimal(obj["SecLockQty"]);
                Convert.ToDecimal(obj["LeftQty"]);
                decimal secLeftQty = Convert.ToDecimal(obj["SecLeftQty"]);
                decimal baseUnLockQty = Convert.ToDecimal(obj["BaseUnLockQty"]);
                decimal baselockQty = Convert.ToDecimal(obj["BaseLcokQty"]);
                decimal baseLeftQty = Convert.ToDecimal(obj["BaseLeftQty"]);
                if (baseUnLockQty == 0m && secUnLockQty == 0m)
                {
                    continue;
                }
                if (obj["ReleaseDate"] != null && !string.IsNullOrWhiteSpace(obj["ReleaseDate"].ToString()) && DateTime.Parse(obj["ReleaseDate"].ToString()) != DateTime.Today)
                {
                    isWarn = true;
                    string message3 = ResManager.LoadKDString("第{0}行記錄的預計解鎖日期{1}不等於系統當前日期{2}", "004023030009708", SubSystemType.SCM);
                    warnMessages.Add(string.Format(message3, rowindex, ((DateTime)obj["ReleaseDate"]).ToShortDateString(), DateTime.Today.ToShortDateString()));
                }
                if (baseUnLockQty < 0m)
                {
                    messages.Add(string.Format(preMsg, rowindex) + ResManager.LoadKDString("解鎖數量(基本)不能小於0", "004023000022233", SubSystemType.SCM));
                    isCancel = true;
                }
                else if (baselockQty < baseUnLockQty)
                {
                    messages.Add(string.Format(preMsg, rowindex) + ResManager.LoadKDString("解鎖數量(基本)超過可解鎖數量(基本)", "004023000022234", SubSystemType.SCM));
                    isCancel = true;
                }
                else if (secUnitID > 0)
                {
                    if (secLockQty < secUnLockQty)
                    {
                        messages.Add(string.Format(preMsg, rowindex) + ResManager.LoadKDString("解鎖數量(輔助)超過可解鎖數量(輔助)", "004023030000436", SubSystemType.SCM));
                        isCancel = true;
                    }
                    else if (baseLeftQty == 0m && secLeftQty != 0m)
                    {
                        messages.Add(string.Format(preMsg, rowindex) + ResManager.LoadKDString("剩餘解鎖量(基本)和剩餘解鎖量(輔助)一個為0,另一個不為0", "004023000022246", SubSystemType.SCM));
                        isCancel = true;
                    }
                }
                #region 序列號校驗

                //序列號個數=鎖庫數
                decimal serialCount = subEntityList.Count;

                //C#判斷鎖庫數是否為整數
                if (serialCount != unLockQty)
                {
                    //this.View.ShowErrMessage("序列號個數必須與解鎖量一致。");
                    messages.Add("序列號個數必須與解鎖量一致。");
                    isCancel = true;
                }
                string serialMsg = string.Empty;
                if (!CheckSerial(subEntityList, invId, out serialMsg))
                {
                    messages.Add(serialMsg);
                    isCancel = true;
                }
                #endregion

                if (!isCancel)//不取消
                {
                    LockStockArgs stockArg = new LockStockArgs();
                    stockArg.FEntryID = Convert.ToInt64(obj["Id"]);
                    stockArg.FInvDetailID = obj["InvDetailID"].ToString();
                    stockArg.BillDetailID = obj["BillDetailID"].ToString();
                    stockArg.BillNo = Convert.ToString(obj["BILLNO"]);
                    if (Convert.ToInt32(obj["BILLSEQ"]) > 0)
                    {
                        stockArg.BillSEQ = Convert.ToInt32(obj["BILLSEQ"]);
                    }
                    if (obj["Lot"] is DynamicObject dyLot && Convert.ToInt64(dyLot["Id"]) > 0)
                    {
                        stockArg.Lot = GetDynamicValue(dyLot);
                    }
                    stockArg.LockQty = lockQty;
                    stockArg.UnLockQty = unLockQty;
                    stockArg.LockBaseQty = Convert.ToDecimal(obj["BaseLcokQty"]);
                    stockArg.UnLockBaseQty = baseUnLockQty;
                    stockArg.LockSecQty = Convert.ToDecimal(obj["SecLockQty"]);
                    stockArg.UnLockSecQty = secUnLockQty;
                    if (obj["ReserveDate"] != null && !string.IsNullOrWhiteSpace(obj["ReserveDate"].ToString()))
                    {
                        stockArg.ReserveDate = DateTime.Parse(obj["ReserveDate"].ToString());
                    }
                    stockArg.ReserveDays = Convert.ToInt32(obj["ReserveDays"]);
                    if (obj["ReleaseDate"] != null && !string.IsNullOrWhiteSpace(obj["ReleaseDate"].ToString()))
                    {
                        stockArg.ReLeaseDate = DateTime.Parse(obj["ReleaseDate"].ToString());
                    }
                    stockArg.UnLockNote = Convert.ToString(obj["UnLockNote"]);
                    if (isWarn)
                    {
                        warnStockArgList.Add(stockArg);
                    }
                    else
                    {
                        stockArgList.Add(stockArg);
                    }
                }
            }
            if (!haveSelRow)
            {
                View.ShowErrMessage(ResManager.LoadKDString("沒有選擇任何資料解鎖,請先選擇資料。", "004023000019865", SubSystemType.SCM));
                return true;
            }
            isCancel = false;
            if (messages.Count > 0)
            {
                isCancel = true;
                List<FieldAppearance> listFieldApp2 = new List<FieldAppearance>();
                FieldAppearance fielAp2 = K3DisplayerUtil.CreateDisplayerField<TextFieldAppearance, TextField>(View.Context, "FErrInfo", ResManager.LoadKDString("異常資訊", "004023000017458", SubSystemType.SCM));
                fielAp2.Width = new LocaleValue("500", View.Context.UserLocale.LCID);
                listFieldApp2.Add(fielAp2);
                K3DisplayerModel k3Model2 = K3DisplayerModel.Create(View.Context, listFieldApp2.ToArray());
                foreach (string message2 in messages)
                {
                    k3Model2.AddMessage(message2);
                }
                k3Model2.CancelButton.Visible = false;
                View.ShowK3Displayer(k3Model2);
                return true;
            }
            if (warnMessages.Count > 0)
            {
                List<FieldAppearance> listFieldApp = new List<FieldAppearance>();
                FieldAppearance fielAp = K3DisplayerUtil.CreateDisplayerField<TextFieldAppearance, TextField>(View.Context, "FErrInfo", ResManager.LoadKDString("異常資訊", "004023000017458", SubSystemType.SCM));
                fielAp.Width = new LocaleValue("500", View.Context.UserLocale.LCID);
                listFieldApp.Add(fielAp);
                K3DisplayerModel k3Model = K3DisplayerModel.Create(View.Context, listFieldApp.ToArray());
                foreach (string message in warnMessages)
                {
                    k3Model.AddMessage(message);
                }
                k3Model.CancelButton.Visible = true;
                k3Model.CancelButton.Caption = new LocaleValue(ResManager.LoadKDString("", "004023000013912", SubSystemType.SCM));
                k3Model.OKButton.Caption = new LocaleValue(ResManager.LoadKDString("", "004023030005539", SubSystemType.SCM));
                k3Model.OKButton.Visible = true;
                k3Model.SummaryMessage = ResManager.LoadKDString("以下鎖庫記錄的預計解鎖日期與當前日期不同,是否繼續?", "004023000017459", SubSystemType.SCM);
                View.ShowK3Displayer(k3Model, delegate (FormResult o)
                {
                    if (o != null && o.ReturnData is K3DisplayerModel && (o.ReturnData as K3DisplayerModel).IsOK)
                    {
                        isCancel = true;
                        stockArgList.AddRange(warnStockArgList);
                        if (stockArgList.Count > 0)
                        {
                            using (KDTransactionScope scope = new KDTransactionScope(TransactionScopeOption.RequiresNew))
                            {
                                StockServiceHelper.SaveUnLockInfo(base.Context, stockArgList, (opType.Equals("StockLock") || opType.Equals("StockLockLog")) ? "Inv" : opType);
                                int updateCount = UnLockSerial(subEntityList);

                                scope.Complete();
                            }
                            this.View.Model.SetValue("FLockQty", 0, 0);
                            //this.View.Model.SetValue("BaseLcokQty", 0, 0);
                            this.View.UpdateView("FLockQty");
                            IsClose = true;
                            View.ReturnToParentWindow(true);
                            View.Close();
                        }
                    }
                    else
                    {
                        isCancel = true;
                    }
                });
                return true;
            }
            if (!isCancel && stockArgList.Count > 0)
            {
                using (KDTransactionScope scope = new KDTransactionScope(TransactionScopeOption.RequiresNew))
                {
                    StockServiceHelper.SaveUnLockInfo(base.Context, stockArgList, (opType.Equals("StockLock") || opType.Equals("StockLockLog")) ? "Inv" : opType);
                    int updateCount = UnLockSerial(subEntityList);
                    scope.Complete();
                }
                this.View.Model.SetValue("FLockQty", 0, 0);
                //this.View.Model.SetValue("BaseLcokQty", 0, 0);
                this.View.UpdateView("FLockQty");
                View.ShowNotificationMessage(ResManager.LoadKDString("解鎖操作成功!", "004023030000442", SubSystemType.SCM));
                IsClose = true;
                View.ReturnToParentWindow(true);
            }
            else if (!isCancel)
            {
                IsClose = true;
                View.ReturnToParentWindow(false);
            }
            return isCancel;
        }


        private int UnLockSerial(List<DynamicObject> subEntityList)
        {
            int updateCount = 0;
            List<string> updateSql = new List<string>();
            foreach (var ss in subEntityList)
            {
                updateSql.Add(string.Format(@"{0}UPDATE T_BD_SERIALMASTER SET F_XXXX_ISLOCK=0 WHERE FSERIALID={1};", OtherConst.DIALECT, ss["F_XXXX_SerialId_Id"]));
            }
            if (updateSql != null && updateSql.Count > 0)
            {
                updateCount = DBUtils.ExecuteBatch(this.Context, updateSql, updateSql.Count);
            }
            return updateCount;
        }
        #endregion <方法>

        #region <事件>
        public override void BeforeSetItemValueByNumber(BeforeSetItemValueByNumberArgs e)
        {
            base.BeforeSetItemValueByNumber(e);
            switch (e.BaseDataField.Key.ToUpperInvariant())
            {
                case "F_XXXX_SERIALID":
                    // 未稽核的基礎資料也顯示出來
                    e.IsShowApproved = false;
                    break;
            }
        }

        public override void BeforeF7Select(BeforeF7SelectEventArgs e)
        {
            base.BeforeF7Select(e);
            switch (e.FieldKey.ToUpperInvariant())
            {
                case "F_XXXX_SERIALID":
                    string DelFilter = "";
                    Entity subEntity = View.BillBusinessInfo.GetEntity("FEntity");
                    var getEntity = View.Model.GetEntityDataObject(subEntity);
                    if (getEntity == null || getEntity.Count <= 0 || getEntity.Count > 1)
                    {
                        this.View.ShowErrMessage("只能處理一行物料");
                        return;
                    }
                    var em = getEntity.FirstOrDefault();
                    //物料不為空
                    //var mater = em["MaterialId"] as DynamicObject;
                    //if (mater == null)
                    //{
                    //    this.View.ShowErrMessage("物料為空");
                    //    return;
                    //}
                    //long materialId = Convert.ToInt64(mater["Id"]);
                    ////倉庫不為空
                    //var stock = em["StockId"] as DynamicObject;
                    //if (stock == null)
                    //{
                    //    this.View.ShowErrMessage("倉庫為空");
                    //    return;
                    //}
                    //long stockId = Convert.ToInt64(stock["Id"]);

                    //DelFilter = string.Format(@" t0.FMATERIALID ={0} and t0.F_XXXX_ISLOCK=1 and t2.FSTOCKID={1}  ", materialId, stockId);
                    ////倉位不為空
                    //var stockLoc = em["StockLocId"] as DynamicObject;
                    //if (stockLoc != null)
                    //{
                    //    long stockLocId = Convert.ToInt64(stockLoc["Id"]);
                    //    DelFilter += string.Format(@" and t2.FSTOCKLOCID={0}", stockLocId);
                    //}
                    string invId = em["InvDetailID"] + "";
                    //序列號物料編碼,在庫狀態
                    List<SqlParam> para = new List<SqlParam>()
                    {
                        //new SqlParam("@STOCKID", KDDbType.Int32,493513),
                        new SqlParam("@InvId", KDDbType.String,invId),
                        new SqlParam("@IsLock", KDDbType.Int32,1),
                        //new SqlParam("@ENDTIME",KDDbType.String,_filterArgs.EndTime.ToString("yyyy-MM-dd")),
                        //new SqlParam("@t",KDDbType.String,temp_detail)
                    };
                    //執行sql語句並將結果集填充至DataSet  XXXX_PR_STK_SerialInfoByInvId未使用
                    DataSet getSerial = DBUtils.ExecuteDataSet(this.Context, System.Data.CommandType.StoredProcedure
                        , string.Format(@"{0}XXXX_PR_STK_SerialInfoByInvId", OtherConst.DIALECT), para);



                    //呼叫上面的將DataSet轉換為List<Menu>實體集合
                    List<SerialMainFile> serialListInStock = (List<SerialMainFile>)KrystalCommonUtil
                        .DataSetToList<SerialMainFile>(getSerial, 0);
                    if (serialListInStock == null || serialListInStock.Count <= 0)
                    {
                        //isSucceed = false;//無可解鎖資料
                    }
                    else
                    {
                        //DelFilter = string.Format(@"  EXISTS (SELECT FID   FROM fn_StrSplit('{0}',',') m where m.fid=t0.FSERIALID )"
                        //  , string.Join(",", serialListInStock.ToList().Select(s => s.FSERIALID).ToList()));
                        DelFilter = string.Format(@"  t0.FSERIALID in  ({0})"
                          , string.Join(",", serialListInStock.ToList().Select(s => s.FSERIALID).ToList()));
                    }

                    if (e.ListFilterParameter.Filter.IsNullOrEmptyOrWhiteSpace())
                    {
                        e.ListFilterParameter.Filter = DelFilter;
                    }
                    else
                    {
                        e.ListFilterParameter.Filter = " and " + DelFilter;
                    }

                    // 未稽核的基礎資料也顯示出來
                    e.IsShowApproved = false;
                    // 已禁用的基礎資料也顯示出來
                    //e.IsShowUsed = false;
                    break;
            }
        }
        public override void BarItemClick(BarItemClickEventArgs e)
        {
            base.BarItemClick(e);
            switch (e.BarItemKey.ToUpperInvariant())
            {
                case "XXXX_TBUNLOCKSERIAL":
                    //解鎖的即時庫存只有一條
                    //序列號不為空,序列號的個數=解鎖數
                    //序列號全部在當前即時庫存,未鎖定,一個不滿足,不可以鎖
                    string text;
                    if ((text = opType) == null || !(text == "Inv"))
                    {
                        return;
                    }
                    e.Cancel = SaveUnLockStockBySerial();
                    if (!e.Cancel)
                    {
                        View.Close();
                    }

                    break;
            }

        }

        public override void BeforeClosed(BeforeClosedEventArgs e)
        {
            
        }
        #endregion <事件>
    }
}
完整程式碼

測試

解鎖成功直接關閉視窗,可以查詢鎖庫日誌

相關文章