惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

U
Unit 42
S
Securelist
小众软件
小众软件
WordPress大学
WordPress大学
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
B
Blog
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
The GitHub Blog
The GitHub Blog
Apple Machine Learning Research
Apple Machine Learning Research
博客园 - 司徒正美
博客园 - Franky
Hugging Face - Blog
Hugging Face - Blog
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
酷 壳 – CoolShell
酷 壳 – CoolShell
O
OpenAI News
Cloudbric
Cloudbric
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
TaoSecurity Blog
TaoSecurity Blog
MongoDB | Blog
MongoDB | Blog
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
V
V2EX
PCI Perspectives
PCI Perspectives
T
Troy Hunt's Blog
Schneier on Security
Schneier on Security
P
Palo Alto Networks Blog
M
MIT News - Artificial intelligence
V2EX - 技术
V2EX - 技术
阮一峰的网络日志
阮一峰的网络日志
Hacker News - Newest:
Hacker News - Newest: "LLM"
G
Google Developers Blog
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
The Last Watchdog
The Last Watchdog
The Register - Security
The Register - Security
腾讯CDC
N
News and Events Feed by Topic
C
Check Point Blog
爱范儿
爱范儿
T
Tailwind CSS Blog
Webroot Blog
Webroot Blog
P
Proofpoint News Feed
S
Schneier on Security
MyScale Blog
MyScale Blog
N
News | PayPal Newsroom
Recorded Future
Recorded Future
T
Tenable Blog
I
InfoQ
www.infosecurity-magazine.com
www.infosecurity-magazine.com
Microsoft Security Blog
Microsoft Security Blog
Simon Willison's Weblog
Simon Willison's Weblog
Engineering at Meta
Engineering at Meta

元视角

.NET 生态下的 Agent 框架选型:从 ReAct 到原生推理 - 元视角 从「能用」到「好用」:LLM 流式响应实现方式的探索之路 - 元视角 当我用 2000 条聊天记录,让 AI 为我画一幅自画像 - 元视角 基于 Supabase 的 AI 应用开发探索 - 元视角 微博 × MCP:社交媒体新玩法解锁 - 元视角 四点钟海棠花未眠 - 元视角 Semantic Kernel × MCP:智能体的上下文增强探索 - 元视角 基于 K-Means 聚类分析实现人脸照片的快速分类 - 元视角 容器技术驱动下的代码沙箱实践与思考 - 元视角 温故而知新:后端通用查询方案的再思考 - 元视角 浅议 CancellationToken 在前后端协同取消场景中的应用 - 元视角 Semantic Kernel 视角下的 Text2SQL 实践与思考 - 元视角 关于 ChatGPT 的流式传输,你需要知道的一切 - 元视角 RAG 的是与非、Rewrite 和 Rerank - 元视角 使用 EFCore 和 PostgreSQL 实现向量存储及检索 - 元视角 基于 LLaMA 和 LangChain 实践本地 AI 知识库 - 元视角 使用 llama.cpp 在本地部署 AI 大模型的一次尝试 - 元视角 如何为 Git 配置多个 SSH Key - 元视角 C# 使用 LibUsbDotNet 实现 USB 设备检测 - 元视角 基于 C# 实现样式与数据分离的打印方案 - 元视角 基于 SVG 的图形交互方案实践 - 元视角 前端视频播放技术概览 - 元视角 温故而知新,再话 Python 动态导入 - 元视角 后 GPT 时代,NLP 不存在了? - 元视角 视频是不能 P 的系列:使用 Milvus 实现海量人脸快速检索 - 元视角 GDI+下字体大小自适应方案初探 - 元视角 小爱音箱集成 ChatGPT 的不完全教程 - 元视角 程序员视角下的三体世界随想 - 元视角 关于 Docker 容器配置信息的渐进式思考 - 元视角 在 Docker 容器内集成 Crontab 定时任务 - 元视角 为你的服务器集成 LDAP 认证 - 元视角 似花还似非花 - 元视角 视频是不能 P 的系列:使用 Dlib 实现人脸识别 - 元视角 浅议分布式链路追踪与日志的整合 - 元视角 关于 Git 大文件上传这件小事 - 元视角 .NET 进程内队列 Channel 的入门与应用 - 元视角 使用 Fody 实现 .NET 的静态编织 - 元视角 .NET Core + ELK 搭建可视化日志分析平台(下) - 元视角 聊一聊前端图片懒加载背后的故事 - 元视角 支持外部链接跳转的 Vue Router 扩展实现 - 元视角 视频是不能 P 的系列:OpenCV 和 Dlib 实现表情包 - 元视角 不得不说的 ASP.NET Core 集成测试 - 元视角 再议 DDD 视角下的 EFCore 与 领域事件 - 元视角 Vue.js 前端项目容器化部署实践极简教程 - 元视角 再见,人间四月天 - 元视角 Python 图像风格化迁移助力画家梦想 - 元视角 利用 ASP.NET Core 中的标头传播实现分布式链路追踪 - 元视角 利用 gRPC 实现文件的上传与下载 - 元视角 七种武器:延迟队列的原理和实现总结 - 元视角 gRPC 流式传输极简入门指南 - 元视角 Envoy 集成 Jaeger 实现分布式链路追踪 - 元视角 浅议非典型 Web 应用场景下的身份认证 - 元视角 gRPC 借助 Any 类型实现接口的泛化调用 - 元视角 分布式丛林探险系列之 Redis 集群模式 - 元视角 分布式丛林探险系列之 Redis 主从复制模式 - 元视角 通过 Python 预测 2021 年双十一交易额 - 元视角 gRPC 搭配 Swagger 实现微服务文档化 - 元视角 SSL/TLS 加密传输与数字证书的前世今生 - 元视角 使用 Python 自动识别防疫健康码 - 元视角 你不可不知的容器编排进阶技巧 - 元视角 ASP.NET Core 搭载 Envoy 实现 gRPC 服务代理 - 元视角 再话 AOP,从简化缓存操作说起 - 元视角 ASP.NET Core 搭载 Envoy 实现微服务身份认证(JWT) - 元视角 ASP.NET Core 搭载 Envoy 实现微服务的监控预警 - 元视角 ASP.NET Core 搭载 Envoy 实现微服务的反向代理 - 元视角 ASP.NET Core gRPC 打通前端世界的尝试 - 元视角 EFCore 实体命名约定库:EFCore.NamingConventions - 元视角 ASP.NET Core gRPC 集成 Polly 实现优雅重试 - 元视角 ASP.NET Core gRPC 健康检查的探索与实现 - 元视角 ASP.NET Core gRPC 拦截器的使用技巧分享 - 元视角 SnowNLP 使用自定义语料进行模型训练 - 元视角 使用 HttpMessageHandler 实现 HttpClient 请求管道自定义 - 元视角 ABP vNext 的实体与服务扩展技巧分享 - 元视角 ABP vNext 对接 Ant Design Vue 实现分页查询 - 元视角 源代码探案系列之 .NET Core 跨域中间件 CORS - 元视角 源代码探案系列之 .NET Core 限流中间件 AspNetCoreRateLimit - 元视角 源代码探案系列之 .NET Core 并发限制中间件 ConcurrencyLimiter - 元视角 通过 EmbededFileProvider 实现 Blazor 的静态文件访问 - 元视角 低代码,想说爱你不容易 - 元视角 记一次失败的 ThoughtWorks 面试经历 - 元视角 从 C# 1.0 到 C# 9.0,历代 C# 语言特性一览 - 元视角 通过 Python 分析 2020 年全年微博热搜数据 - 元视角 基于 Python 和 Selenium 实现 CSDN 一键三连自动化 - 元视角 使用多线程为你的 Python 爬虫提速的 N 种姿势,你会几种? - 元视角 实现网页长截图的常见思路总结 - 元视角 温故而知新,由 ADO.NET 与 Dapper 所联想到的 - 元视角 视频是不能 P 的系列:OpenCV 人脸检测 - 元视角 作为技术宅的我,是这样追鬼滅の刃的 - 元视角 使用 Python 抽取《半泽直树》原著小说人物关系 - 元视角 厉害了!打工人用 Python 分析西安市职位信息 - 元视角 使用 dotTrace 对 .NET 应用进行性能分析与优化 - 元视角 一道 HashSet 面试题引发的蝴蝶效应 - 元视角 基于选项模式实现.NET Core 的配置热更新 - 元视角 Dapper.Contrib 在 Oracle 环境下引发 ORA-00928 异常问题的解决 - 元视角 .NET Core 中对象池(Object Pool)的使用 - 元视角 利用 MySQL 的 Binlog 实现数据同步与订阅(下):EventBus 篇 - 元视角 利用 MySQL 的 Binlog 实现数据同步与订阅(中):RabbitMQ 篇 - 元视角 利用 MySQL 的 Binlog 实现数据同步与订阅(上):基础篇 - 元视角 记一次从已损坏的 Git 仓库中找回代码的经历 - 元视角 .NET Core 原生 DI 扩展之属性注入实现 - 元视角
关于单位转换相关问题的常见思路 - 元视角
飞鸿踏雪 · 2019-11-15 · via 元视角

请原谅我使用了这样一个“直白”的标题,因为我实在想不到更好的描述方法。或许,是因为临近年底的“996”式冲刺,让许久没有读完一本书的我,第一次感受到输出时的闭塞。是时候为自己的知识体系补充新鲜血液啦,而不是输给那些“无聊”的流程和关系。说这句话的缘由,是想到《Unnatural》中的法医三澄美琴,一个视非正常死亡为敌人的女法医。而对程序员来说,真正的敌人则是难以解决 Bug 和问题。可更多的时间,我们其实是在为流程和关系方面的事情消耗精力。

我越来越发现,人类所面对的绝大多数问题,都并非是寻求一个最优解,而是在于平衡和牵制。人类总是不可避免地堕入熵增的圈套,伴随着流程产生的除了规范还有复杂度。每当人们试图为这种复杂度找一种友好的说辞的时候,我终于意识到,有的人不愿意去寻找问题的本质,它们需要的就只是一种友好的说辞,仿佛只要有了这种说辞,问题就能自动解决一样。我想,我大概知道这段时间感到焦灼的原因了,因为这样的事情在工作中基本是常态。人类每天面对的事情,无外乎两种:“明知不可为而为之"和"什么都想兼顾的美好理想”。

我今天想说的是,一个业务中遇到的单位转换的问题,我们平时在存储货物的重量时,默认都是以千克作为单位来存储的,直到我们对接了一家以大宗商品交易作为主要业务的客户,对方要求我们在界面上统一用吨来展示数据,因为这样更符合客户方的使用习惯。按理说,这是一个非常简单的需求,是不需要用一篇博客来说这件事情的,可我觉得这是个有意思的话题,还是想和大家一起来聊聊相关方案的思路。带着问题,我首先拜访了Cather Wong大佬,大佬微微一笑,表示在视图层上加个字段就可以了嘛。的确,这是最简单的做法,大概是下面这个样子:

class OrderInfoQueryDTO
{
   /// <summary>
   /// 以千克为单位的净重
   /// </summary>
   public decimal? NET_WEIGHT { get; set; }

   /// <summary>
   /// 以吨为单位的净重
   /// </summary>
   public decimal? NET_WEIGHT_WITH_TON
   {
       get { return NET_WEIGHT / 1000; }
   }
}

我不甘心地追问,客户要在原来的字段上显示这个数值啊,这样能行吗?大佬稍作沉思,随即问道:“你们公司的项目就算做不到 DDD,AutoMapper 这种实体间映射转换的东西总有吧!”。我连忙接话道:“这个自然是有的”。其实我心里想的是,总算有点符合我的心理预期啦,这样的方案还像个大佬的样子。按照大佬的提示,使用 AutoMapper 来做单位的转换,应该是下面这样:

var config = new MapperConfiguration(cfg => {
    cfg.CreateMap<order_info, OrderInfoQueryDTO>()
        .ForMember(d => d.NET_WEIGHT, opt => opt.MapFrom(x => x.NET_WEIGHT/1000));
});

这样看起来是比加字段要好一点,可实际项目中,我们往往会把单位作为一种配置持久化到数据库中,以我们公司为例,我们实际上是支持千克和吨两种单位混合使用的,不过在表头汇总的时候,为了统一到一起,所以使用了千克作为单位。这样就引申出一个新问题,假如我在数据库里存了多行明细的重量,当需要在表头展示汇总以后的总重量,那么,这个总重量到底是汇总好存在数据库里,还是展示的时候交由调用方 Sum()呢?

我个人倾向于第二种,因为它能有效避免表头和明细行数据不一致的问题,当然缺点是给了调用方一定的计算压力。我们项目中采用的第一种方案,我印象非常深刻,在计算件数、重量和体积的时候,必须要等所有明细行都计算完以后,再通过调用 Sum()方法给表头赋值,实际上这个表头字段,完全可以通过只读属性的方式取值啊,更何况我们还使用了外键,表头实体本身就引用了明细表实体,因为有外键的存在,序列化表头实体的时候会出现循环引用,对此,我想说,干得漂亮!

通过 AutoMapper 中的 ForMember 扩展方法,可以实现我们这里这个功能。可考虑到要在 AutoMapper 里引入权限啊、角色啊这些东西,AutoMapper 作为实体映射的纯粹性就被彻底破坏了。为此,我们考虑使用 AutoMapper 中提供的Value ConvertersType Converters。关于这两者的区别,大家可以参考官方文档中的描述。此时,我们可以通过下面的方式使用这些“转换器”:

var config = new MapperConfiguration(cfg => {
    cfg.CreateMap<order_info,OrderInfoQueryDTO>()
      .ForMember(d => d.NET_WEIGHT, opt => opt.ConvertUsing<WeightValueConverter,decimal?>());
});
  
var mapper = config.CreateMapper();
var orderInfo = new order_info() {
    ORDER_ID = Guid.NewGuid().ToString("N"),
    NET_WEIGHT = 1245.78M,
    CREATED_DATE = DateTime.Now,
    CREATED_BY = "灵犀一指陆小凤"
};

var orderInfoQueryDTO = mapper.Map<order_info,OrderInfoQueryDTO>(orderInfo);

而对于 WeightValueConverter 这个类而言,它实现了 IValueConverter 接口:

 public class WeightValueConverter : IValueConverter<decimal?, decimal?> 
 {
    public decimal? Convert (decimal? sourceMember, ResolutionContext context) 
    {
        //TODO:可以查数据库或者是由规则决定,是否转换以及如何转换
        if (!sourceMember.HasValue)
            return null;
        return sourceMember.Value / 1000;
    }
}

现在,虽然代码还是这个代码,可至少我们不用在 MapFrom 里写太重的业务逻辑了,而且这个转换器是可以复用的。显然,我们的系统中不会只有订单模块会涉及到重量、体积的转换。此时,我们可以考虑使用 ITypeConverter 接口,遗憾地是,这个接口在实现的时候就必须指定源类型和目标类型,这样离我们设想地全局转换器实际上是有一点差距的。例如,我们有时候希望源类型中 Null 值不会覆盖到目标类型,最常见的情况是,从一个 EditDTO 转化为数据库实体对象并更新数据库。为了解决这个问题,AutoMapper 下面的做法就非常棒:

cfg.ForAllMaps((a, b) => b.ForAllMembers(opt => opt.Condition((src, dest, sourceMember) => sourceMember != null)));

可对于我们这里这个场景,显然,我们必须要提供一部分类型信息,我们几乎很难给所有的 Map 增加一个通用的类型转换器。我最终还是通过反射解决了这个问题,即在使用 AutoMapper 前,从数据库查出数据后,首先要做的第一件事情就是对数值进行转换:

var userSetting = UserContext.GetLoginUser().UserSettng;
var formatSetting = userSetting.FormatSetting;

//当默认重量单位为KG时不做任何处理
if (formatSetting.DefaultWeightUom == WeightUnit.KG)
    return;


var properties = typeof(TDestination).GetProperties()
    .Where(p => p.Name.EndsWith("WEIGHT") || p.Name.EndsWith("Weight"));
if (properties == null || !properties.Any())
     return;

foreach(var item in destList)
{
    //转化结果为吨
    foreach(var property in properties)
    {
         var weightValue = property.GetValue(item, null);
         if(property.PropertyType == typeof(decimal))
         {
             property.SetValue(item, (decimal)weightValue / 1000);
         }
         else if(property.PropertyType == typeof(Nullable<decimal>))
         {
             if (weightValue != null)
                  property.SetValue(item, (decimal)weightValue / 1000);
         }
         else if(property.PropertyType == typeof(string))
         {
             if (!string.IsNullOrEmpty(weightValue.ToString()))
                 property.SetValue(item, decimal.Parse(weightValue.ToString()) / 1000);
             }
         }
    }
}

不得不说,这段代码相当无聊,可无论多么无聊的功能,只要客户觉得好就给积极地去做,对吧!其实,说到底,这是我们在设计数据库表结构时遗留的一个问题。假如我们在存储的时候就存储为吨,问题还不会有什么不一样呢?实际上,它还是会有问题,因为你不得不去设计一个单位转换表,类似下面这样的:

原始单位目标单位进率
KgT1/1000
TKg1000
gKg1/1000
Kgg1000

我们目前设计的表结构中实际上是有重量单位的,不同的是,我们以千克为单位存储的量,数据库中对应的 WEIGHT_UOM 存储的是 1,而以吨为单位存储的量,数据库中对应的 WEIGHT_UOM 存储的是 1000。所以,理论上真实的重量都应该是数据库中存储的量 X WEIGHT_UOM。这样看起来是没有问题的,可当你结合今天这篇博客的背景来看是,就会发现一个问题,所有的数值在展示的时候都必须要知道,数据库里存储的数值的原始单位是什么,而使用者希望在界面上看到的数值的单位又是什么。

不单单如此,当用户通过界面查询的时候,一个简单的数字便不等再用简单地使用像大于、小于、等于、不等于这样的查询条件,因为现在每个量都带着单位,你必须明确得知道,用户认为的单位是什么,而数据库里对应的单位又是什么?这样听起来貌似还是统一使用一种单位比较好,正因为如此,博主可以在查询前把吨转化为千克,而在查询后则可以把千克转换为吨。

人类世界总是存在着这些奇奇怪怪的规则,不同的小数位精度要求,不同的货币金额展示方式,不同的日期格式显示要求,就在我写下这篇博客的时候,产品同事反馈我千克转成吨展示以后,应该至少保留三位小数,否则会让人觉得数字会丢失了精度。我还能说什么呢?联想到最近软通因为加班而猝死的同行,我大概只能说一句:**恭喜你,还请节哀顺变,欢迎来到无法随心所欲的爱与欲望的世界!**作为拖延症中晚期的博主,努力写完每月一篇的博客,抽空读读书、看看电影,这已然是种简单的幸福了呢!好了,这篇博客就先写到这里!