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

推荐订阅源

H
Help Net Security
Scott Helme
Scott Helme
爱范儿
爱范儿
WordPress大学
WordPress大学
博客园 - 三生石上(FineUI控件)
阮一峰的网络日志
阮一峰的网络日志
博客园 - Franky
V
V2EX
腾讯CDC
博客园_首页
博客园 - 司徒正美
酷 壳 – CoolShell
酷 壳 – CoolShell
T
Tailwind CSS Blog
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
小众软件
小众软件
J
Java Code Geeks
大猫的无限游戏
大猫的无限游戏
月光博客
月光博客
Microsoft Azure Blog
Microsoft Azure Blog
B
Blog
雷峰网
雷峰网
Stack Overflow Blog
Stack Overflow Blog
IT之家
IT之家
罗磊的独立博客
Recorded Future
Recorded Future
博客园 - 聂微东
O
OpenAI News
S
Secure Thoughts
Hacker News: Ask HN
Hacker News: Ask HN
S
Schneier on Security
Hacker News - Newest:
Hacker News - Newest: "LLM"
Y
Y Combinator Blog
C
Cyber Attacks, Cyber Crime and Cyber Security
Project Zero
Project Zero
宝玉的分享
宝玉的分享
K
Kaspersky official blog
N
Netflix TechBlog - Medium
T
The Exploit Database - CXSecurity.com
Google Online Security Blog
Google Online Security Blog
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Webroot Blog
Webroot Blog
云风的 BLOG
云风的 BLOG
Simon Willison's Weblog
Simon Willison's Weblog
C
Check Point Blog
D
Darknet – Hacking Tools, Hacker News & Cyber Security
L
LINUX DO - 热门话题
美团技术团队
L
Lohrmann on Cybersecurity

元视角

.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。这样看起来是没有问题的,可当你结合今天这篇博客的背景来看是,就会发现一个问题,所有的数值在展示的时候都必须要知道,数据库里存储的数值的原始单位是什么,而使用者希望在界面上看到的数值的单位又是什么。

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

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