From b3484c3618bca02ce49988ad29a580170b8cf5b4 Mon Sep 17 00:00:00 2001 From: tengyufan <1532636164@qq.com> Date: Thu, 30 Jul 2026 11:10:48 +0800 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20=E5=9B=BD=E8=81=94=E6=B0=91=E7=94=9F?= =?UTF-8?q?=E6=94=B6=E7=9B=8A=E4=BA=92=E6=8D=A2=E6=94=AF=E6=8C=81=E5=AE=A1?= =?UTF-8?q?=E6=89=B9=E5=89=8D=E7=94=9F=E6=88=90=E4=BA=A4=E6=98=93=E7=A1=AE?= =?UTF-8?q?=E8=AE=A4=E4=B9=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 仅国联民生收益互换支持在新增待确认、修改待确认、审批中生成交易确认书,期权、其他公司及其他交易状态保持原限制 - 交易确认书页面同步开放上述交易的“生成确认书”按钮,后端批量生成链路同步放开状态过滤 - 审批前生成仅校验客户、标的、初始持仓等模板必需核心数据;不以银行账户、联系人、保证金等非模板必填资料阻断 - 审批前已生成确认书的交易被修改时,使该交易原确认书关联失效,重新生成仍按原覆盖逻辑处理 - 审批拒绝时清理该交易审批前生成的确认书关联、OA 结果及用印队列;无其他有效引用时删除确认书数据,避免遗留无效确认书 - 保持原交易编号逻辑:自动生成和手工填写均不复用已拒绝交易编号 - 新增审批前适用范围、确认书关联及 OA 清理谓词专项单测 --- .../GuolianSwapConfirmBookScopeTest.cs | 106 ++++++++++ .../Modules/SwapModule/SwapTradeService.cs | 11 +- .../TradeContractGenerateService.cs | 19 +- .../DealModule/TradeOpenService.cs | 7 + .../BatchConfirmationGenerateService.cs | 2 +- ...GuolianSwapConfirmBookValidationService.cs | 192 ++++++++++++++++++ .../Scripts/app/trade/tradeConfirmBook.js | 5 +- 7 files changed, 335 insertions(+), 7 deletions(-) create mode 100644 UnitTestProject/Modules/TradeModule/DocGenerateModule/GuolianSwapConfirmBookScopeTest.cs create mode 100644 YLErpDAL/Modules/TradeModule/DocGenerateModule/GuolianSwapConfirmBookValidationService.cs diff --git a/UnitTestProject/Modules/TradeModule/DocGenerateModule/GuolianSwapConfirmBookScopeTest.cs b/UnitTestProject/Modules/TradeModule/DocGenerateModule/GuolianSwapConfirmBookScopeTest.cs new file mode 100644 index 00000000..5dcd3937 --- /dev/null +++ b/UnitTestProject/Modules/TradeModule/DocGenerateModule/GuolianSwapConfirmBookScopeTest.cs @@ -0,0 +1,106 @@ +using YLErp.Configuration; +using YLErp.DBModels; +using YLErp.DBModels.Consts; +using YLErp.DBModels.Enums; +using YLErp.Modules.TradeModule.DocGenerateModule; + +namespace YLErp.UnitTestProject.Modules.TradeModule.DocGenerateModule +{ + [TestClass] + public class GuolianSwapConfirmBookScopeTest + { + [TestMethod] + public void IsPreApprovalTrade_AllAllowedStatuses_ReturnsTrue() + { + var statuses = new[] + { + ConsTrade.新增待确认, + ConsTrade.修改待确认, + ConsTrade.审批中 + }; + + foreach (var status in statuses) + { + var trade = CreateTrade(status); + + Assert.IsTrue(GuolianSwapConfirmBookScope.IsPreApprovalTrade(trade, CompanyEnum.国联), status); + } + } + + [TestMethod] + public void IsPreApprovalTrade_NonGuolianSwapOrInvalidTrade_ReturnsFalse() + { + var confirmedSwap = CreateTrade(ConsTrade.确认成交); + var optionTrade = CreateTrade(ConsTrade.审批中, "香草期权"); + var invalidSwap = CreateTrade(ConsTrade.审批中); + invalidSwap.ValidState = ConsGlobal.InValid; + + Assert.IsFalse(GuolianSwapConfirmBookScope.IsPreApprovalTrade(confirmedSwap, CompanyEnum.国联)); + Assert.IsFalse(GuolianSwapConfirmBookScope.IsPreApprovalTrade(optionTrade, CompanyEnum.国联)); + Assert.IsFalse(GuolianSwapConfirmBookScope.IsPreApprovalTrade(CreateTrade(ConsTrade.审批中), CompanyEnum.国泰君安)); + Assert.IsFalse(GuolianSwapConfirmBookScope.IsPreApprovalTrade(invalidSwap, CompanyEnum.国联)); + } + + [TestMethod] + public void OtherActiveReferencePredicate_OnlyMatchesOtherActiveTrade() + { + const string contractCode = "GLMS-TEST-20260728-FICC-04-000001"; + var predicate = GuolianSwapConfirmBookCleanupService + .BuildOtherActiveReferencePredicate(contractCode, 4) + .Compile(); + + Assert.IsFalse(predicate(CreateRelation(4, contractCode, ContractTypeEnum.Trade, true))); + Assert.IsTrue(predicate(CreateRelation(7, contractCode, ContractTypeEnum.Trade, true))); + Assert.IsFalse(predicate(CreateRelation(7, contractCode, ContractTypeEnum.Trade, false))); + Assert.IsFalse(predicate(CreateRelation(7, "OTHER", ContractTypeEnum.Trade, true))); + Assert.IsFalse(predicate(CreateRelation(7, contractCode, ContractTypeEnum.Clearing, true))); + } + + [TestMethod] + public void TradeConfirmationRelationPredicate_MatchesActiveAndInactiveRelations() + { + const string contractCode = "GLMS-TEST-20260728-FICC-04-000001"; + var predicate = GuolianSwapConfirmBookCleanupService + .BuildTradeConfirmationRelationPredicate(4) + .Compile(); + + Assert.IsTrue(predicate(CreateRelation(4, contractCode, ContractTypeEnum.Trade, true))); + Assert.IsTrue(predicate(CreateRelation(4, contractCode, ContractTypeEnum.Trade, false))); + Assert.IsFalse(predicate(CreateRelation(7, contractCode, ContractTypeEnum.Trade, true))); + Assert.IsFalse(predicate(CreateRelation(4, contractCode, ContractTypeEnum.Clearing, true))); + } + + [TestMethod] + public void ActiveOaResultPredicate_OnlyMatchesRejectedTradeActiveResult() + { + var predicate = GuolianSwapConfirmBookCleanupService + .BuildActiveOaResultPredicate(4) + .Compile(); + + Assert.IsTrue(predicate(new trade_contract_oa_result { trade_id = 4, is_valid = true })); + Assert.IsFalse(predicate(new trade_contract_oa_result { trade_id = 4, is_valid = false })); + Assert.IsFalse(predicate(new trade_contract_oa_result { trade_id = 7, is_valid = true })); + } + + private static trade CreateTrade(string status, string tradeType = "收益互换") + { + return new trade + { + TradeType = tradeType, + TradeStatus = status, + ValidState = ConsGlobal.Valid + }; + } + + private static trade_contract_r CreateRelation(int tradeId, string contractCode, string type, bool isValid) + { + return new trade_contract_r + { + TradeId = tradeId, + ContractCode = contractCode, + Type = type, + IsValid = isValid + }; + } + } +} diff --git a/YLErpDAL/Modules/SwapModule/SwapTradeService.cs b/YLErpDAL/Modules/SwapModule/SwapTradeService.cs index 3deb098c..3040f746 100644 --- a/YLErpDAL/Modules/SwapModule/SwapTradeService.cs +++ b/YLErpDAL/Modules/SwapModule/SwapTradeService.cs @@ -657,8 +657,15 @@ namespace YLErp.Modules.SwapModule } if (changeConfirmStatus) { - //删除交易确认书 TODO - new TradeDocumentDataService(this).DeleteBooksAfterEditTrade(dbTrade); + if (GuolianSwapConfirmBookScope.IsPreApprovalTrade(dbTrade)) + { + new GuolianSwapConfirmBookCleanupService(this).InvalidateAfterEdit(dbTrade); + } + else + { + //删除交易确认书 TODO + new TradeDocumentDataService(this).DeleteBooksAfterEditTrade(dbTrade); + } } int eventType = isAddNew ? (int)SwapEventTypeEnum.新增交易 : (int)SwapEventTypeEnum.修改交易; new SwapEventService(UserInfo).AddSwapEventDate(dbTrade.TradeDate.Value, dbTrade.id, eventType, "", 0, true, "", true); diff --git a/YLErpDAL/Modules/TradeModule/DealModule/TradeContractGenerateService.cs b/YLErpDAL/Modules/TradeModule/DealModule/TradeContractGenerateService.cs index 877ef0c3..316633e5 100644 --- a/YLErpDAL/Modules/TradeModule/DealModule/TradeContractGenerateService.cs +++ b/YLErpDAL/Modules/TradeModule/DealModule/TradeContractGenerateService.cs @@ -226,9 +226,10 @@ namespace YLErp.Modules.TradeModule.DealModule return true; } - // 过滤掉待确认和审批中的交易,这些状态不允许生成确认书 + // 仅国联收益互换允许在审批前生成确认书,其他交易保持原状态限制。 var invalidStatusList = new List { ConsTrade.新增待确认, ConsTrade.修改待确认, ConsTrade.审批中 }; - var invalidTrades = trades.Where(t => invalidStatusList.Contains(t.TradeStatus)).ToList(); + var invalidTrades = trades.Where(t => invalidStatusList.Contains(t.TradeStatus) + && !GuolianSwapConfirmBookScope.IsPreApprovalTrade(t)).ToList(); if (invalidTrades.Any()) { error.AddRange(invalidTrades.Select(t => $"交易 {t.TradeNumber} 状态为【{t.TradeStatus}】,不能生成确认书")); @@ -237,6 +238,20 @@ namespace YLErp.Modules.TradeModule.DealModule logger.Info($"[GJGenerateConfirmBook] 过滤掉 {invalidTrades.Count} 笔待确认/审批中交易"); } + var preApprovalTrades = trades.Where(GuolianSwapConfirmBookScope.IsPreApprovalTrade).ToList(); + foreach (var trade in preApprovalTrades) + { + var validationErrors = new GuolianSwapConfirmBookValidationService(this).Validate(trade); + if (!validationErrors.Any()) + { + continue; + } + + error.Add($"交易 {trade.TradeNumber} 无法生成确认书:{string.Join(";", validationErrors)}"); + tradeIds = tradeIds.Where(id => id != trade.id).ToList(); + trades = trades.Where(item => item.id != trade.id).ToList(); + } + if (!trades.Any()) { logger.Info("[GJGenerateConfirmBook] 过滤后无有效交易可生成确认书"); diff --git a/YLErpDAL/Modules/TradeModule/DealModule/TradeOpenService.cs b/YLErpDAL/Modules/TradeModule/DealModule/TradeOpenService.cs index 8bfd988e..0ec40dad 100644 --- a/YLErpDAL/Modules/TradeModule/DealModule/TradeOpenService.cs +++ b/YLErpDAL/Modules/TradeModule/DealModule/TradeOpenService.cs @@ -7,6 +7,7 @@ using YLErp.Model.Enum; using YLErp.Modules.ClientModule; using YLErp.Modules.SwapModule; using YLErp.Modules.TradeDalModule; +using YLErp.Modules.TradeModule.DocGenerateModule; namespace YLErp.Modules.TradeModule.DealModule { @@ -295,6 +296,7 @@ namespace YLErp.Modules.TradeModule.DealModule /// private TradeOpenResult TradeReject(TradeOpenReqModel req, trade td) { + var isGuolianPreApprovalTrade = GuolianSwapConfirmBookScope.IsPreApprovalTrade(td); var oldOrderId = td.ProcessOrderId; req.comments = FormatCommentsWithRole(oldOrderId, req.comments); td.ProcessOrderId = Convert.ToInt32(PStatusEnum.reject); @@ -367,6 +369,11 @@ namespace YLErp.Modules.TradeModule.DealModule var tcDetails = DbContext.trade_cash_detail.Where(O => O.TradeCashId == tcId); DbContext.trade_cash_detail.RemoveRange(tcDetails); + if (isGuolianPreApprovalTrade) + { + new GuolianSwapConfirmBookCleanupService(this).CleanRejectedPreApprovalTrade(td); + } + DbContext.SaveChanges(); return new TradeOpenResult(td) { RetCode = TradeOpenRetCode.Success }; diff --git a/YLErpDAL/Modules/TradeModule/DocGenerateModule/BatchConfirmationGenerateService.cs b/YLErpDAL/Modules/TradeModule/DocGenerateModule/BatchConfirmationGenerateService.cs index db8136e4..4ee21d4c 100644 --- a/YLErpDAL/Modules/TradeModule/DocGenerateModule/BatchConfirmationGenerateService.cs +++ b/YLErpDAL/Modules/TradeModule/DocGenerateModule/BatchConfirmationGenerateService.cs @@ -75,7 +75,7 @@ namespace YLErp.Modules.TradeModule.DocGenerateModule throw new ServiceException("没有已确认的记录信息!"); } var tradeDic = allTrades.Where(O => - statusList.Contains(O.TradeStatus) + (statusList.Contains(O.TradeStatus) || GuolianSwapConfirmBookScope.IsPreApprovalTrade(O)) && O.ClientId != 0 && O.ParentTradeId == 0) .ToDictionary(n => n.id, n => new trade_contract_group { trade = n }); diff --git a/YLErpDAL/Modules/TradeModule/DocGenerateModule/GuolianSwapConfirmBookValidationService.cs b/YLErpDAL/Modules/TradeModule/DocGenerateModule/GuolianSwapConfirmBookValidationService.cs new file mode 100644 index 00000000..c23410a2 --- /dev/null +++ b/YLErpDAL/Modules/TradeModule/DocGenerateModule/GuolianSwapConfirmBookValidationService.cs @@ -0,0 +1,192 @@ +using System.Linq.Expressions; +using YLErp.Configuration; +using YLErp.DBModels; +using YLErp.DBModels.Consts; +using YLErp.DBModels.Enums; +using YLErp.Model; + +namespace YLErp.Modules.TradeModule.DocGenerateModule +{ + /// + /// 国联收益互换审批前生成确认书的范围与资料校验。 + /// + public static class GuolianSwapConfirmBookScope + { + private static readonly HashSet PreApprovalStatuses = new() + { + ConsTrade.新增待确认, + ConsTrade.修改待确认, + ConsTrade.审批中 + }; + + public static bool IsPreApprovalTrade(trade trade) + { + return IsPreApprovalTrade(trade, PS.Config.Company); + } + + public static bool IsPreApprovalTrade(trade trade, CompanyEnum company) + { + return IsGuolianSwap(trade, company) + && trade.ValidState != ConsGlobal.InValid + && IsPreApprovalStatus(trade.TradeStatus); + } + + public static bool IsGuolianSwap(trade trade) + { + return IsGuolianSwap(trade, PS.Config.Company); + } + + public static bool IsGuolianSwap(trade trade, CompanyEnum company) + { + return company == CompanyEnum.国联 && trade.TradeType == "收益互换"; + } + + public static bool IsPreApprovalStatus(string tradeStatus) + { + return PreApprovalStatuses.Contains(tradeStatus); + } + + } + + /// + /// 清理国联收益互换审批前生成的确认书,不复用按合同编号批量清理的通用逻辑。 + /// + public class GuolianSwapConfirmBookCleanupService : YLBaseService + { + public GuolianSwapConfirmBookCleanupService(YLBaseService baseService) : base(baseService) + { + } + + public void InvalidateAfterEdit(trade trade) + { + if (!GuolianSwapConfirmBookScope.IsGuolianSwap(trade) + || !GuolianSwapConfirmBookScope.IsPreApprovalStatus(trade.TradeStatus)) + { + return; + } + + InvalidateRelations(trade.id); + } + + public void CleanRejectedPreApprovalTrade(trade trade) + { + if (!GuolianSwapConfirmBookScope.IsGuolianSwap(trade)) + { + return; + } + + GlobalConfig.SealHandler.RemoveQueue(trade.id); + InvalidateOaResults(trade.id); + var contractCodes = InvalidateRelations(trade.id); + foreach (var contractCode in contractCodes) + { + var hasActiveReference = DbContext.trade_contract_r.Any( + BuildOtherActiveReferencePredicate(contractCode, trade.id)); + if (!hasActiveReference) + { + var documents = DbContext.trade_contract_document.Where(x => x.Code == contractCode + && x.Type == ContractTypeEnum.Trade); + DbContext.trade_contract_document.RemoveRange(documents); + } + } + } + + public static Expression> BuildOtherActiveReferencePredicate( + string contractCode, + int rejectedTradeId) + { + return relation => relation.ContractCode == contractCode + && relation.Type == ContractTypeEnum.Trade + && relation.IsValid + && relation.TradeId != rejectedTradeId; + } + + public static Expression> BuildTradeConfirmationRelationPredicate( + int tradeId) + { + return relation => relation.TradeId == tradeId + && relation.Type == ContractTypeEnum.Trade; + } + + public static Expression> BuildActiveOaResultPredicate( + int tradeId) + { + return result => result.trade_id == tradeId && result.is_valid; + } + + private void InvalidateOaResults(int tradeId) + { + var oaResults = DbContext.tradeContractOaResult + .Where(BuildActiveOaResultPredicate(tradeId)) + .ToList(); + oaResults.ForEach(x => x.is_valid = false); + } + + private List InvalidateRelations(int tradeId) + { + var relations = DbContext.trade_contract_r + .Where(BuildTradeConfirmationRelationPredicate(tradeId)) + .ToList(); + relations.Where(x => x.IsValid).ToList().ForEach(x => x.IsValid = false); + return relations.Select(x => x.ContractCode).Distinct().ToList(); + } + } + + /// + /// 仅校验模板无法正常生成或选择时必需的交易数据。 + /// + public class GuolianSwapConfirmBookValidationService : YLBaseService + { + public GuolianSwapConfirmBookValidationService(YLBaseService baseService) : base(baseService) + { + } + + public List Validate(trade trade) + { + var errors = new List(); + var client = DataCacheProvider.GetClientDataSource().GetData(trade.ClientId); + var underlying = DataCacheProvider.GetUnderlyingDataSource().GetData(trade.UnderlyingCode); + + if (client == null) + { + errors.Add("客户信息不存在"); + return errors; + } + + if (underlying == null) + { + errors.Add("标的信息不存在"); + } + + ValidateInitialPosition(trade, errors); + + return errors; + } + + private void ValidateInitialPosition(trade trade, List errors) + { + var position = DbContext.swap_position.FirstOrDefault(x => x.SwapTradeId == trade.id + && x.IsInitial + && !x.Invalid + && (x.PositionType == (int)PositionTypeFlag.Long || x.PositionType == (int)PositionTypeFlag.Short)); + + if (position == null) + { + errors.Add("缺少初始标的持仓"); + return; + } + + if (position.PosiDirection != (int)SwapDirectionEnum.支付 + && position.PosiDirection != (int)SwapDirectionEnum.收取) + { + errors.Add("初始标的持仓收支方向无效"); + } + + if (string.IsNullOrWhiteSpace(position.UnderlyingCode)) + { + errors.Add("初始标的持仓缺少标的代码"); + } + } + + } +} diff --git a/YLErpWeb/wwwroot/Scripts/app/trade/tradeConfirmBook.js b/YLErpWeb/wwwroot/Scripts/app/trade/tradeConfirmBook.js index 46c6e487..0f7652e6 100644 --- a/YLErpWeb/wwwroot/Scripts/app/trade/tradeConfirmBook.js +++ b/YLErpWeb/wwwroot/Scripts/app/trade/tradeConfirmBook.js @@ -926,9 +926,10 @@ function showToolName(cellValue, options, rowObject) { var html = showDownload(cellValue, options, rowObject); var title = page.IsAutoSealAndUploadFiles && page.IsAutoSealAfterGeneratedBook ? "生成并用印" : "生成确认书"; - // 待确认和审批中的交易不允许生成确认书 + // 仅国联收益互换允许在审批前生成确认书,其他交易保持原限制。 var isUnconfirmed = rowObject.TradeStatus === "新增待确认" || rowObject.TradeStatus === "修改待确认" || rowObject.TradeStatus === "审批中"; - var canGenerate = page.canGenerate == true && !isUnconfirmed ? "" : "disabled"; + var isGuolianSwap = page.isGuoLian === true && rowObject.TradeType === "收益互换"; + var canGenerate = page.canGenerate == true && (!isUnconfirmed || isGuolianSwap) ? "" : "disabled"; var isWait = (rowObject.MetaDic.SealResult == "用印等待中"); html += "".template(rowObject.id, rowObject.MetaDic.HasGeneratedConfirmBook == "True", title, isWait, canGenerate); if (rowObject.MetaDic.ContractDocUrl && page.canSendEmail == true) { From 7a9702c707719b6fda9170bf297b2ce2d617a293 Mon Sep 17 00:00:00 2001 From: hjhan Date: Thu, 30 Jul 2026 11:23:58 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix(diag):=20JsVersion=20=E7=BA=B3=E5=85=A5?= =?UTF-8?q?=E5=89=8D=E7=AB=AF=E6=96=87=E4=BB=B6=20mtime=EF=BC=8C=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E7=BA=AF=E5=89=8D=E7=AB=AF=E6=94=B9=E5=8A=A8=E7=BC=93?= =?UTF-8?q?=E5=AD=98=E4=B8=8D=E5=88=B7=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 根因(?otcdebug=1 排查时暴露):JsVersion 只取 bin/YLErp*.dll 的 mtime, 纯前端改 wwwroot/Scripts 而不重编 DLL 时,?v= 缓存戳不变,浏览器仍加载旧版 ——即"发布了但不更新"。 修复: - JsVersion = max(DLL mtime, wwwroot/Scripts/** mtime, wwwroot/Statics/bundles/** mtime) - wwwroot 定位用 Directory.GetCurrentDirectory()(与 StaticUrlMiddleware 一致, 开发=项目根/部署=publish 目录),目录不存在则回退仅 DLL mtime - BinFileVersion 保持原义(仅 DLL,用于诊断显示后端构建时间) 附带修复 git 字段:AssemblyInformationalVersion 无 +sha(如固定版本 1.0.0)时, 回退从 .git/HEAD 读取当前 commit 短 sha,不再误显示 '1.0.0' 验证:dotnet build 0 错误 --- YLErpWeb/Common/HtmlUtil.cs | 82 ++++++++++++++++++++++++++++++++++--- 1 file changed, 77 insertions(+), 5 deletions(-) diff --git a/YLErpWeb/Common/HtmlUtil.cs b/YLErpWeb/Common/HtmlUtil.cs index 1292f24e..12b6ed9c 100644 --- a/YLErpWeb/Common/HtmlUtil.cs +++ b/YLErpWeb/Common/HtmlUtil.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.Html; +using System.IO; using System.Reflection; using YLErp.Events; @@ -27,18 +28,44 @@ namespace YLErp static HtmlUtil() { - //获取bin目录中YLErp开头的文件最后修改日期作为JS和CSS文件版本 + //获取bin目录中YLErp开头的文件最后修改日期作为后端构建版本 var binDir = new DirectoryInfo(AppContext.BaseDirectory); var files = binDir.GetFiles("YLErp*"); BinFileVersion = files.Any() ? files.Max(n => n.LastWriteTime) : DateTime.MinValue; - JsVersion = BinFileVersion.ToString("yyMMddHHmmss"); + + //JsVersion(浏览器 ?v= 缓存戳)必须同时反映【后端 DLL】和【前端脚本】的变更: + //历史上只取 DLL mtime,导致纯前端改动(改 wwwroot/Scripts 而不重编 DLL)时 + //缓存戳不变、浏览器仍加载旧版——即"发布了但不更新"(见 ?otcdebug=1 排查时的症状)。 + //现纳入 wwwroot/Scripts(业务脚本)+ wwwroot/Statics/bundles(打包产物)的 mtime 取 max。 + //wwwroot 定位用 Directory.GetCurrentDirectory()(与 StaticUrlMiddleware 一致, + //开发时=项目根、部署时=publish 目录),目录不存在则回退到仅 DLL mtime。 + var versionClock = BinFileVersion; + var webRoot = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot"); + foreach (var sub in new[] { "Scripts", Path.Combine("Statics", "bundles") }) + { + var dir = Path.Combine(webRoot, sub); + if (Directory.Exists(dir)) + { + foreach (var f in new DirectoryInfo(dir).GetFiles("*", SearchOption.AllDirectories)) + { + if (f.LastWriteTime > versionClock) versionClock = f.LastWriteTime; + } + } + } + JsVersion = versionClock.ToString("yyMMddHHmmss"); //从 AssemblyInformationalVersion 读取 git sha(SDK 编译时已嵌入,零额外依赖) var infoVer = typeof(HtmlUtil).Assembly .GetCustomAttribute()?.InformationalVersion; - //格式 "1.0.0+",取 + 之后部分;无则回退到完整字符串 - GitCommit = string.IsNullOrEmpty(infoVer) ? "unknown" - : (infoVer.Contains('+') ? infoVer.Substring(infoVer.LastIndexOf('+') + 1) : infoVer); + //格式 "1.0.0+",取 + 之后部分;无 +(如固定版本号 1.0.0)则回退读 .git/HEAD + if (!string.IsNullOrEmpty(infoVer) && infoVer.Contains('+')) + { + GitCommit = infoVer.Substring(infoVer.LastIndexOf('+') + 1); + } + else + { + GitCommit = ReadGitCommitFromRepo() ?? (infoVer ?? "unknown"); + } EventBus.Subscribe(t => { @@ -46,6 +73,51 @@ namespace YLErp }); } + /// + /// 从 .git/HEAD 读取当前 commit 短 sha,作为 AssemblyInformationalVersion 无 sha 时的回退。 + /// 找不到 .git 或读取失败返回 null(不影响启动)。 + /// + static string ReadGitCommitFromRepo() + { + try + { + //从 ContentRoot 向上找 .git 目录(开发时在项目根/仓库根) + var dir = new DirectoryInfo(Directory.GetCurrentDirectory()); + while (dir != null) + { + var gitDir = Path.Combine(dir.FullName, ".git"); + if (Directory.Exists(gitDir)) + { + return ParseGitHead(gitDir); + } + dir = dir.Parent; + } + } + catch { /* 读取失败不影响启动,返回 null */ } + return null; + } + + static string ParseGitHead(string gitDir) + { + var headFile = Path.Combine(gitDir, "HEAD"); + if (!File.Exists(headFile)) return null; + var head = File.ReadAllText(headFile).Trim(); + //HEAD 格式:"ref: refs/heads/xxx" 或 detached 时的直接 sha + if (head.StartsWith("ref: ")) + { + var refPath = Path.Combine(gitDir, head.Substring(5).Replace('/', Path.DirectorySeparatorChar)); + if (File.Exists(refPath)) + { + return File.ReadAllText(refPath).Trim().Substring(0, 12); + } + } + else if (head.Length >= 12) + { + return head.Substring(0, 12); + } + return null; + } + /// /// 获取基础数据JS连接 ///