上篇文章我们介绍了

VUE+.NET应用系统的国际化-多语言词条服务

系统国际化改造整体设计思路如下:

  1. 提供一个工具,识别前后端代码中的中文,形成多语言词条,按语言、界面、模块统一管理多有的多语言词条
  2. 提供一个翻译服务,批量翻译多语言词条
  3. 提供一个词条服务,支持后端代码在运行时根据用户登录的语言,动态获取对应的多语言文本
  4. 提供前端多语言JS生成服务,按界面动态生成对应的多语言JS文件,方便前端VUE文件使用。
  5. 提供代码替换工具,将VUE前端代码中的中文替换为$t("词条ID"),后端代码中的中文替换为TermService.Current.GetText("词条ID")

今天,我们在上篇文章的基础上,继续介绍基于Roslyn抽取词条、更新代码。

一、业务背景

先说一下业务背景,后端.NET代码中存在大量的中文提示和异常消息,甚至一些中文返回值文本。

这些中文文字都需要识别出来,抽取为多语言词条,同时将代码替换为调用多语言词条服务获取翻译后的文本。

例如:

private static void CheckMd5(string fileName, string md5Data)
{
string md5Str = MD5Service.GetMD5(fileName);
if (!string.Equals(md5Str, md5Data, StringComparison.OrdinalIgnoreCase))
{
throw new CustomException(PackageExceptionConst.FileMd5CheckFailed, "服务包文件MD5校验失败:" + fileName);
}
}

代码中需要将“服务包文件MD5校验失败”这个文本做多语言改造。

这里通过调用多语言词条服务I18NTermService,根据线程上下文中设置的语言,获取对应的翻译文本。例如以下代码:

var text=T.Core.I18N.Service.TermService.Current.GetTextFormatted("词条ID","默认文本"); 

throw new CustomException(PackageExceptionConst.FileMd5CheckFailed, text + fileName);

以上背景下,我们准备使用Roslyn技术对代码进行中文扫描,对扫描出来的文本,做词条抽取、代码替换。

二、使用Roslyn技术对代码进行中文扫描

首先,我们先定义好代码中多语言词条的扫描结果类TermScanResult

 1  [Serializable]
2 public class TermScanResult
3 {
4 public Guid Id { get; set; }
5 public string OriginalText { get; set; }
6
7 public string ChineseText { get; set; }
8
9 public string SlnName { get; set; }
10
11 public string ProjectName { get; set; }
12
13 public string ClassFile { get; set; }
14
15 public string MethodName { get; set; }
16
17 public string Code { get; set; }
18
19 public I18NTerm I18NTerm { get; set; }
20
21 public string SlnPath { get; set; }
22
23 public string ClassPath { get; set; }
24 28 public string SubSystemCode { get; set; }
29
30 public override string ToString()
31 {
32 return Code;
33 }
34 }

上述代码中SubSystemCode是一个业务管理维度。大家忽略即可。

我们会以sln解决方案为单位,扫描代码中的中文文字。

以下是具体的实现代码

public async Task<List<TermScanResult>> CheckSln(string slnPath, System.ComponentModel.BackgroundWorker backgroundWorker, SubSystemFile subSystemFiles, string subSystem)
{
var slnFile = new FileInfo(slnPath);
var results = new List<TermScanResult>(); MSBuildHelper.RegisterMSBuilder();
var solution = await MSBuildWorkspace.Create().OpenSolutionAsync(slnPath); var subSystemInfo = subSystemFiles?.SubSystemSlnMappings.FirstOrDefault(w => w.SlnName.Select(s => s += ".sln").Contains(slnFile.Name.ToLower())); if (solution.Projects != null && solution.Projects.Count() > 0)
{
foreach (var project in solution.Projects.ToList())
{
backgroundWorker.ReportProgress(10, $"扫描Project: {project.Name}");
var documents = project.Documents.Where(x => x.Name.Contains(".cs")); if (project.Name.ToLower().Contains("test"))
{
continue;
}
var codeReplace = new CodeReplace();
foreach (var document in documents)
{
var tree = await document.GetSyntaxTreeAsync();
var root = tree.GetCompilationUnitRoot();
if (root.Members == null || root.Members.Count == 0) continue;
//member
var classDeclartions = root.DescendantNodes().Where(i => i is ClassDeclarationSyntax); foreach (var classDeclare in classDeclartions)
{
var programDeclaration = classDeclare as ClassDeclarationSyntax;
if (programDeclaration == null) continue; foreach (var memberDeclarationSyntax in programDeclaration.Members)
{
foreach (var item in GetLiteralStringExpression(memberDeclarationSyntax))
{
var statementCode = item.Item1;
foreach (var syntaxNode in item.Item3)
{
ExpressionSyntaxParser expressionSyntaxParser = new ExpressionSyntaxParser();
var text = "";
var expressionSyntax = expressionSyntaxParser
.GetExpressionSyntaxVerifyRule(syntaxNode as ExpressionSyntax, statementCode);
if (expressionSyntax != null)
{
// 排除
if (expressionSyntaxParser.IsExcludeCaller(expressionSyntax, statementCode))
{
continue;
} text = expressionSyntaxParser.GetExpressionSyntaxOriginalText(expressionSyntax, statementCode);
if (expressionSyntax is Microsoft.CodeAnalysis.CSharp.Syntax.InterpolatedStringExpressionSyntax)
{
text = expressionSyntaxParser.GetExpressionSyntaxOriginalText(expressionSyntax, statementCode); if (expressionSyntax is Microsoft.CodeAnalysis.CSharp.Syntax.LiteralExpressionSyntax)
{
if (!expressionSyntax.IsKind(SyntaxKind.StringLiteralExpression))
{
continue;
}
text = expressionSyntax.NormalizeWhitespace().ToString();
}
}
}
if (CheckChinese(text) == false) continue;
if (string.IsNullOrWhiteSpace(text)) continue;
if (string.IsNullOrWhiteSpace(text.Replace("\"", "").Trim())) continue; results.Add(new TermScanResult()
{
Id = Guid.NewGuid(),
ClassPath = programDeclaration.SyntaxTree.FilePath,
SlnPath = slnPath,
OriginalText = text.Replace("\"", "").Trim(),
ChineseText = text,
SlnName = slnFile.Name,
ProjectName = project.Name,
ClassFile = programDeclaration.Identifier.Text,
MethodName = item.Item2,
Code = statementCode,
SubSystemCode = subSystem
});
}
}
}
}
}
}
} return results;
}

上述代码中,我们先使用MSBuilder编译,构建 sln解决方案

MSBuildHelper.RegisterMSBuilder();
var solution = await MSBuildWorkspace.Create().OpenSolutionAsync(slnPath);

然后遍历solution下的各个Project中的class类

foreach (var project in solution.Projects.ToList())
var documents = project.Documents.Where(x => x.Name.Contains(".cs"));

然后遍历类中声明、成员、方法中的每行代码,通过正则表达式识别是否有中文字符

public static bool CheckChinese(string strZh)
{
Regex re = new Regex(@"[\u4e00-\u9fa5]+");
if (re.IsMatch(strZh))
{
return true;
}
return false;
}

如果存在中文字符,作为扫描后的结果,识别为多语言词条

results.Add(new TermScanResult()
{
Id = Guid.NewGuid(),
ClassPath = programDeclaration.SyntaxTree.FilePath,
SlnPath = slnPath,
OriginalText = text.Replace("\"", "").Trim(),
ChineseText = text,
SlnName = slnFile.Name,
ProjectName = project.Name,
ClassFile = programDeclaration.Identifier.Text,
MethodName = item.Item2,
Code = statementCode, //管理维度
SubSystemCode = subSystem //管理维度
});

TermScanResult中没有对词条属性赋值。

public I18NTerm I18NTerm { get; set; }

下一篇文章的代码中,我们会通过多语言翻译服务,将翻译后的文本放到I18NTerm 属性中,作为多语言词条。

三、代码替换

代码替换这块逻辑中,我们设计了一个类SourceWeaver,对上一步的代码扫描结果,进行代码替换

CodeScanReplace这个方法中完成了代码的二次扫描和替换
 /// <summary>
/// 源代码替换服务
/// </summary>
public class SourceWeaver
{
List<CommonTermDto> commonTerms = new List<CommonTermDto>();
List<CommonTermDto> commSubTerms = new List<CommonTermDto>(); public SourceWeaver()
{
commonTerms = JsonConvert.DeserializeObject<List<CommonTermDto>>(File.ReadAllText("comm_data.json"));
commSubTerms = JsonConvert.DeserializeObject<List<CommonTermDto>>(File.ReadAllText("comm_sub_data.json"));
}
public async Task CodeScanReplace(Tuple<List<I18NTerm>, List<TermScanResult>> result, System.ComponentModel.BackgroundWorker backgroundWorker)
{
try
{
backgroundWorker.ReportProgress(0, "正在对代码进行替换.");
var termScanResultGroupBy = result.Item2.GroupBy(g => g.SlnName);
foreach (var termScanResult in termScanResultGroupBy)
{
var termScan = termScanResult.FirstOrDefault();
MSBuildHelper.RegisterMSBuilder();
var solution = await MSBuildWorkspace.Create().OpenSolutionAsync(termScan.SlnPath).ConfigureAwait(false);
if (solution.Projects.Any())
{
foreach (var project in solution.Projects.ToList())
{
if (project.Name.ToLower().Contains("test"))
{
continue;
}
var projectTermScanResults = result.Item2.Where(f => f.ProjectName == project.Name); var documents = project.Documents.Where(x =>
{
return x.Name.Contains(".cs") && projectTermScanResults.Any(f => $"{f.ClassPath}" == x.FilePath);
}); foreach (var document in documents)
{
var tree = await document.GetSyntaxTreeAsync().ConfigureAwait(false);
var root = tree.GetCompilationUnitRoot();
if (root.Members.Count == 0) continue; var classDeclartions = root.DescendantNodes()
.Where(i => i is ClassDeclarationSyntax);
List<MemberDeclarationSyntax> syntaxNodes = new List<MemberDeclarationSyntax>();
foreach (var classDeclare in classDeclartions)
{
if (!(classDeclare is ClassDeclarationSyntax programDeclaration)) continue;
var className = programDeclaration.Identifier.Text; // SyntaxNode classSyntaxNode = await classDeclare.SyntaxTree.GetRootAsync().ConfigureAwait(false); foreach (var method in programDeclaration.Members)
{
if (method is ConstructorDeclarationSyntax)
{
syntaxNodes.Add((ConstructorDeclarationSyntax)method);
}
else if (method is MethodDeclarationSyntax)
{
syntaxNodes.Add((MethodDeclarationSyntax)method);
}
else if (method is PropertyDeclarationSyntax)
{
syntaxNodes.Add(method);
}
else if (method is FieldDeclarationSyntax)
{
// 注:常量不支持
syntaxNodes.Add(method);
}
}
} var terms = termScanResult.Where(
f => f.ProjectName == document.Project.Name && f.ClassPath == document.FilePath).ToList();
backgroundWorker.ReportProgress(10, $"正在检查{document.FilePath}文件.");
ReplaceNodesAndSave(root, syntaxNodes, terms, result, backgroundWorker, document.Name);
}
}
}
}
}
catch (Exception ex)
{
LogUtils.LogError(string.Format("异常类型:{0}\r\n异常消息:{1}\r\n异常信息:{2}\r\n",
ex.GetType().Name, ex.Message, ex.StackTrace));
backgroundWorker.ReportProgress(0, ex.Message);
}
} public async void ReplaceNodesAndSave(SyntaxNode classSyntaxNode, List<MemberDeclarationSyntax> syntaxNodes, IEnumerable<TermScanResult> terms, Tuple<List<I18NTerm>, List<TermScanResult>> result,
System.ComponentModel.BackgroundWorker backgroundWorker, string className)
{ {//check pro是否存在词条
if (AppConfig.Instance.IsCheckTermPro)
{
backgroundWorker.ReportProgress(15, $"词条验证中.");
var termsCodes = terms.Select(f => f.I18NTerm.Code).ToList();
var size = 100;
var p = (result.Item2.Count() + size - 1) / size; using DBHelper dBHelper = new DBHelper();
List<I18NTerm> items = new List<I18NTerm>();
for (int i = 0; i < p; i++)
{
var list = termsCodes
.Skip(i * size).Take(size);
Thread.Sleep(10);
var segmentItems = await dBHelper.GetTermsAsync(termsCodes).ConfigureAwait(false);
items.AddRange(segmentItems);
} List<TermScanResult> termScans = new List<TermScanResult>();
foreach (var term in terms)
{
if (items.Any(f => f.Code == term.I18NTerm.Code))
{
termScans.Add(term);
}
else
{
backgroundWorker.ReportProgress(20, $"词条{term.OriginalText}未导入到词条库,该词条将忽略替换.");
}
}
terms = termScans;
}
} var newclassDeclare = classSyntaxNode;
newclassDeclare = classSyntaxNode.ReplaceNodes(syntaxNodes,
(methodDeclaration, _) =>
{
// var methodName = methodDeclaration.Identifier.Text;
//var methodTerms = terms.Where(term => term.MethodName == methodName)
// .OrderByDescending(term => term.OriginalText.Length); MemberDeclarationSyntax newMemberDeclarationSyntax = methodDeclaration;
var className = ((ClassDeclarationSyntax)newMemberDeclarationSyntax.Parent).Identifier.Text;
List<StatementSyntax> statementSyntaxes = new List<StatementSyntax>(); switch (newMemberDeclarationSyntax)
{
case ConstructorDeclarationSyntax:
{
var blockSyntax = (newMemberDeclarationSyntax as ConstructorDeclarationSyntax).NormalizeWhitespace().Body;
if (blockSyntax == null)
{
break;
}
foreach (var statement in blockSyntax.Statements)
{
var nodeStatement = statement.DescendantNodes(); statementSyntaxes.Add(new CodeReplace().ReplaceStatementNodes(statement,
new ExpressionSyntaxParser().LiteralStringExpression(nodeStatement), terms, commonTerms, commSubTerms));
} break;
} case MethodDeclarationSyntax:
{
var blockSyntax = (methodDeclaration as MethodDeclarationSyntax).NormalizeWhitespace().Body;
if (blockSyntax == null)
{
break;
}
foreach (var statement in blockSyntax.Statements)
{
var nodeStatement = statement.DescendantNodes();
statementSyntaxes.Add(new CodeReplace().ReplaceStatementNodes(statement,
new ExpressionSyntaxParser().LiteralStringExpression(nodeStatement), terms, commonTerms, commSubTerms));
} break;
} case PropertyDeclarationSyntax:
{
var propertyDeclarationSyntax = newMemberDeclarationSyntax as PropertyDeclarationSyntax; var nodeStatement = propertyDeclarationSyntax.DescendantNodes(); return new CodeReplace().ReplacePropertyNodes(newMemberDeclarationSyntax as PropertyDeclarationSyntax,
new ExpressionSyntaxParser().LiteralStringExpression(nodeStatement), terms, commonTerms, commSubTerms);
} case FieldDeclarationSyntax:
{
var fieldDeclarationSyntax = newMemberDeclarationSyntax as FieldDeclarationSyntax;
var nodeStatement = fieldDeclarationSyntax.DescendantNodes();
return new CodeReplace().ReplaceFiledNodes(fieldDeclarationSyntax,
new ExpressionSyntaxParser().LiteralStringExpression(nodeStatement), terms, commonTerms, commSubTerms);
}
}
backgroundWorker.ReportProgress(50, $"解析并对类文件{className}中的方法做语句替换.");
// 替换方法内部
if (newMemberDeclarationSyntax is MethodDeclarationSyntax)
{
return new CodeReplace().ReplaceMethodDeclaration(newMemberDeclarationSyntax as MethodDeclarationSyntax, statementSyntaxes);
}
else if (newMemberDeclarationSyntax is ConstructorDeclarationSyntax)
{
return new CodeReplace().ReplaceConstructorDeclaration(newMemberDeclarationSyntax as ConstructorDeclarationSyntax, statementSyntaxes);
}
return newMemberDeclarationSyntax;
}); var sourceStr = newclassDeclare.NormalizeWhitespace().GetText().ToString();
File.WriteAllText(newclassDeclare.SyntaxTree.FilePath, sourceStr);
backgroundWorker.ReportProgress(100, $"完成{className}的替换.");
}
}

关键的代码语义替换的实现代码:

 public StatementSyntax ReplaceStatementNodes(StatementSyntax statement, List<ExpressionSyntax> expressionSyntaxes, IEnumerable<TermScanResult> terms
, List<CommonTermDto> commonTerms, List<CommonTermDto> commSubTerms)
{
var statementSyntax = statement.ReplaceNodes(expressionSyntaxes, (syntaxNode, _) =>
{
var statementStr = statement.NormalizeWhitespace().ToString(); var argumentLists = statement.DescendantNodes().
OfType<InvocationExpressionSyntax>();
ExpressionSyntaxParser expressionSyntaxParser = new ExpressionSyntaxParser();
return expressionSyntaxParser.ExpressionSyntaxTermReplace(syntaxNode, statementStr, terms, commonTerms, commSubTerms); }); return statementSyntax;
}

这里,我们抽象了一个ExpressionSyntaxParser 类,负责替换代码:

T.Core.I18N.Service.TermService.Current.GetTextFormatted
 public ExpressionSyntax ExpressionSyntaxTermReplace(ExpressionSyntax syntaxNode, string statementStr, IEnumerable<TermScanResult> terms
, List<CommonTermDto> commonTerms, List<CommonTermDto> commSubTerms)
{
var expressionSyntax = GetExpressionSyntaxVerifyRule(syntaxNode, statementStr);
var originalText = GetExpressionSyntaxOriginalText(expressionSyntax, statementStr); var I18Expr = "";
var interpolationSyntaxes = syntaxNode.DescendantNodes().OfType<InterpolationSyntax>();
var term = terms.FirstOrDefault(i => i.ChineseText == originalText); if (term == null)
return syntaxNode;
string termcode = term.I18NTerm.Code;
if (syntaxNode is InterpolatedStringExpressionSyntax)
{
if (interpolationSyntaxes.Count() > 0)
{
var parms = "";
foreach (var item in interpolationSyntaxes)
{
parms += $",{item.ToString().TrimStart('{').TrimEnd('}')}";
}
I18Expr = "$\"{T.Core.I18N.Service.TermService.Current.GetTextFormatted(\"" + termcode + "\", " + originalText + parms + ")}\"";
var token1 = SyntaxFactory.Token(default, SyntaxKind.StringLiteralToken, I18Expr, "", default);
return SyntaxFactory.LiteralExpression(SyntaxKind.StringLiteralExpression, token1);
}
else
{ var startToken = SyntaxFactory.Token(SyntaxKind.InterpolatedStringStartToken);
if ((syntaxNode as InterpolatedStringExpressionSyntax).StringStartToken.Value == startToken.Value)
{
// 如果本身有"$"
I18Expr = "$\"{T.Core.I18N.Service.TermService.Current.GetText(\"" + termcode + "\"," + originalText + ")}";
}
else
{
// 如果没有"$"
I18Expr = "$\"{T.Core.I18N.Service.TermService.Current.GetText(\"" + termcode + "\",\\teld\"" + originalText + "\")}";
I18Expr = I18Expr.Replace("\\teld", "$");
}
}
}
else
{
I18Expr = "$\"{T.Core.I18N.Service.TermService.Current.GetText(\"" + termcode + "\"," + originalText + ")}";
} var token = SyntaxFactory.Token(default(SyntaxTriviaList), SyntaxKind.InterpolatedVerbatimStringStartToken, I18Expr, "$\"", default(SyntaxTriviaList));
var literalExpressionSyntax = SyntaxFactory.InterpolatedStringExpression(token);
return literalExpressionSyntax;
}
T.Core.I18N.Service.TermService这个就是多语言词条服务类,这个类中提供了一个GetText的方法,通过词条编号,获取多语言文本。

代码完成替换后,打开VS,对工程引用多语言词条服务的Nuget包/dll,重新编译代码,手工校对替换后的代码即可。
以上是.NET应用系统的国际化-基于Roslyn抽取词条、更新代码的分享。

周国庆
2023/3/19



.NET应用系统的国际化-基于Roslyn抽取词条、更新代码的更多相关文章

  1. springmvc国际化 基于请求的国际化配置

    springmvc国际化 基于请求的国际化配置 基于请求的国际化配置是指,在当前请求内,国际化配置生效,否则自动以浏览器为主. 项目结构图: 说明:properties文件中为国际化资源文件.格式相关 ...

  2. springmvc国际化 基于浏览器语言的国际化配置

    当前标签: springmvc   springmvc国际化 基于浏览器语言的国际化配置 苏若年 2013-10-09 13:03 阅读:305 评论:0   SpringMVC中应用Ajax异步通讯 ...

  3. 如何写好、管好单元测试?基于Roslyn+CI分析单元测试,严控产品提测质量

    上一篇文章中,我们谈到了通过Roslyn进行代码分析,通过自定义代码扫描规则,将有问题的代码.不符合编码规则的代码扫描出来,禁止签入,提升团队的代码质量. .NET Core技术研究-通过Roslyn ...

  4. 基于 Roslyn 实现动态编译

    基于 Roslyn 实现动态编译 Intro 之前做的一个数据库小工具可以支持根据 Model 代码文件生成创建表的 sql 语句,原来是基于 CodeDom 实现的,最近改成使用基于 Roslyn ...

  5. 基于roslyn的动态编译库Natasha

    人老了,玩不转博客园的编辑器,详细信息转到:https://mp.weixin.qq.com/s/1r6YKBkyovQSMUgfm_VxBg 关键字:Github, NCC, Natasha,Ros ...

  6. 基于 Roslyn 实现一个简单的条件解析引擎

    基于 Roslyn 实现一个简单的条件解析引擎 Intro 最近在做一个勋章的服务,我们想定义一些勋章的获取条件,满足条件之后就给用户颁发一个勋章,定义条件的时候会定义需要哪些参数,参数的类型,获取勋 ...

  7. 搭建基于SornaQube的自动化安全代码检测平台

    一.背景和目的 近年来,随着新业务.新技术的快速发展,应用软件安全缺陷层出不穷.虽然一般情况下,开发者基本都会有单元测试.每日构建.功能测试等环节来保证应用的可用性.但在安全缺陷方面,缺乏安全意识.技 ...

  8. 基于Quick-cocos2d-x的资源更新方案 二

    写在前面 又是12点半了,对于一个程序员来说,这是一个黄金时间,精力旺盛,我想,是最适合整理和分享一些思路的时候了. 自从上次写了 基于Quick-cocos2d-x的资源更新方案 同样可见quick ...

  9. jQuery基于ajax实现星星评论代码

    本文实例讲述了jQuery基于ajax实现星星评论代码.分享给大家供大家参考.具体如下: 这里使用jquery模仿点评网的星星评论功能,Ajax评论模块,鼠标点击星星即可评价,下边是分数,可以点击后给 ...

  10. 基于eclipse的mybatis映射代码自动生成的插件

    基于eclipse的mybatis映射代码自动生成的插件 分类: JAVA 数据库 工具相关2012-04-29 00:15 2157人阅读 评论(9) 收藏 举报 eclipsegeneratori ...

随机推荐

  1. Flask的介绍、安装和使用

    Flask的介绍 Flask其实就是Python-web中的一个框架,也就是说Flask是一个工具,提供了库和技术来让你建立一个web的应用程序.这个程序可以使一些web页面.博客.基于web的日历应 ...

  2. 「NOTE」常系数齐次线性递推

    要不是考到了,我还没发现这玩意我不是很会-- # 前置 多项式取模: 矩阵快速幂. # 常系数齐次线性递推 描述的是这么一个问题,给定数列 \(c_1,c_2,\dots,c_k\) 以及数列 \(f ...

  3. kafka消费者3种分配策略

    0.10.2.1 版本的Kafka 有两种分配策略,由消费者测的参数partition.assignment.strategy来控制. RoundRobinAssignor分配策略 RangeAssi ...

  4. JS实现异步的方法:回调函数callback、事件监听、setTimeout、Promise、生成器Generators/yield、async/awt

    所有异步任务都是在同步任务执行结束之后,从任务队列中依次取出执行. 回调函数是异步操作最基本的方法,比如AJAX回调,回调函数的优点是简单.容易理解和实现,缺点是不利于代码的阅读和维护,各个部分之间高 ...

  5. 解决LayUI中的 laydate 点击一闪而过问题

    加一个:trigger: 'click' <input name="apbegin" id="apbegin" class="layui-inp ...

  6. Flutter showModalBottomSheet 自适应高度

    showModalBottomSheet( context: context, shape: const RoundedRectangleBorder( borderRadius: BorderRad ...

  7. js 导出excle文件(多页)

      --前提:页面生成相应的表格数据    例如: <table id="a"> <tr> <th></th> </tr> ...

  8. Think in UML 其二

    UML基本元素 参与者 1.参与者位于系统边界之外. 思考参与者究竟是谁时,以下两个问题有助于了解 ·谁对系统有着明确的目标和要求并且主动发出动作? ·系统是为谁服务的? 2.参与者可以非人 功能性需 ...

  9. uniapp+uView单选框多选框使用与模糊搜索

    <template> <!-- 类别筛选组件 --> <view class="timeInput">{{filterArea}} <u- ...

  10. Filbeat采集nginx-ingress日志

    一.创建configmap配置文件 注:filebeat6以上版本需要将prospectors改为inputs,paths下指定的nginx-ingress日志路径匹配模式以及hosts指定的kafk ...