wenmo8 发布的文章

前言

设计模式在我们日常的软件开发中无处不在,它们帮助我们编写更易扩展、更具可读性的代码。

今天结合我实际工作场景和源码实例,跟大家一起聊聊工作中最常用的8种设计模式,希望对你会有所帮助。

1. 单例模式

单例模式确保一个类只有一个实例,通常用于管理共享资源,如配置、缓存、线程池等。

代码实现:双重检查锁
这是单例模式的标准写法,既保证线程安全,又避免性能损耗。

public class Singleton {
    private static volatile Singleton instance;

    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

JDK 中的应用:

  • java.lang.Runtime.getRuntime()
  • java.util.logging.Logger

Spring 中的应用:
Spring 的
Bean
默认是单例模式。可以通过
@Scope("prototype")
将其改为多例。

2. 工厂模式

工厂模式用于封装对象的创建逻辑,特别是当类实例化过程复杂时,可以降低耦合度。

代码实现:简单工厂
以支付系统为例,不同支付方式需要不同的对象。

public class PaymentFactory {
    public static Payment createPayment(String type) {
        switch (type) {
            case "AliPay":
                return new AliPay();
            case "WeChatPay":
                return new WeChatPay();
            default:
                throw new IllegalArgumentException("Unknown payment type");
        }
    }
}

JDK 中的应用:

  • java.util.Calendar.getInstance()
  • javax.xml.parsers.DocumentBuilderFactory.newInstance()

Spring 中的应用:

  • BeanFactory

    ApplicationContext
    都是工厂模式的体现。

3. 策略模式

策略模式将不同算法封装为独立类,并允许在运行时选择不同的策略。

代码实现:促销策略
以电商促销为例,支持满减、打折等多种策略。

public interface PromotionStrategy {
    void applyPromotion();
}

public class DiscountStrategy implements PromotionStrategy {
    @Override
    public void applyPromotion() {
        System.out.println("Applying discount...");
    }
}

public class PromotionContext {
    private PromotionStrategy strategy;

    public PromotionContext(PromotionStrategy strategy) {
        this.strategy = strategy;
    }

    public void executePromotion() {
        strategy.applyPromotion();
    }
}

JDK 中的应用:

  • java.util.Comparator
    是典型的策略模式。

Spring 中的应用:

  • 事务管理(
    TransactionManager
    ),支持编程式和声明式事务。

4. 代理模式

代理模式通过代理对象控制对目标对象的访问,常用于权限控制、日志记录等场景。

代码实现:静态代理
模拟对一个服务的权限控制。

public interface Service {
    void execute();
}

public class RealService implements Service {
    @Override
    public void execute() {
        System.out.println("Executing real service...");
    }
}

public class ServiceProxy implements Service {
    private RealService realService;

    @Override
    public void execute() {
        System.out.println("Checking permissions...");
        if (realService == null) {
            realService = new RealService();
        }
        realService.execute();
    }
}

JDK 中的应用:

  • 动态代理
    java.lang.reflect.Proxy
  • RMI(远程方法调用)

Spring 中的应用:

  • AOP(面向切面编程)广泛使用代理模式。

5. 观察者模式

观察者模式定义一对多的依赖,当一个对象状态变化时,所有依赖它的对象都会收到通知。

代码实现:事件通知
模拟微博用户的粉丝通知。

public interface Observer {
    void update(String message);
}

public class User implements Observer {
    private String name;

    public User(String name) {
        this.name = name;
    }

    @Override
    public void update(String message) {
        System.out.println(name + " received message: " + message);
    }
}

public class Weibo {
    private List<Observer> observers = new ArrayList<>();

    public void follow(Observer observer) {
        observers.add(observer);
    }

    public void post(String message) {
        for (Observer observer : observers) {
            observer.update(message);
        }
    }
}

JDK 中的应用:

  • java.util.Observer

    java.util.Observable
  • javax.swing.event.ChangeListener

Spring 中的应用:

  • ApplicationEvent

    ApplicationListener
    是典型实现。

最近建了一些工作内推群,收集了不少工作岗位,加我微信:su_san_java,备注:博客园+所在城市,即可进群。

6. 装饰器模式

装饰器模式在不改变原始类的基础上,动态扩展其功能。

代码实现:咖啡加料
模拟一个咖啡订单系统,可以动态加料。

public interface Coffee {
    String getDescription();
    double getCost();
}

public class SimpleCoffee implements Coffee {
    @Override
    public String getDescription() {
        return "Simple Coffee";
    }

    @Override
    public double getCost() {
        return 5.0;
    }
}

public class MilkDecorator implements Coffee {
    private Coffee coffee;

    public MilkDecorator(Coffee coffee) {
        this.coffee = coffee;
    }

    @Override
    public String getDescription() {
        return coffee.getDescription() + ", Milk";
    }

    @Override
    public double getCost() {
        return coffee.getCost() + 1.5;
    }
}

JDK 中的应用:

  • java.io.BufferedInputStream

    java.io.BufferedOutputStream

Spring 中的应用:

  • BeanPostProcessor
    用于动态修改 Bean 的行为。

7. 模板方法模式

模板方法模式定义一个算法的骨架,把具体的实现留给子类。

代码实现:任务执行模板
模拟定时任务的执行流程。

public abstract class Task {
    public final void execute() {
        init();
        doWork();
        cleanup();
    }

    protected abstract void init();
    protected abstract void doWork();
    protected void cleanup() {
        System.out.println("Default cleanup...");
    }
}

public class DataProcessingTask extends Task {
    @Override
    protected void init() {
        System.out.println("Initializing data...");
    }

    @Override
    protected void doWork() {
        System.out.println("Processing data...");
    }
}

JDK 中的应用:

  • java.util.AbstractList

    java.util.AbstractMap

Spring 中的应用:

  • JdbcTemplate

    RestTemplate

8. 建造者模式

建造者模式用于创建复杂对象,特别是当对象有多个可选参数时。

代码实现:构建 HTTP 请求

public class HttpRequest {
    private String method;
    private String url;
    private String body;

    private HttpRequest(Builder builder) {
        this.method = builder.method;
        this.url = builder.url;
        this.body = builder.body;
    }

    public static class Builder {
        private String method;
        private String url;
        private String body;

        public Builder method(String method) {
            this.method = method;
            return this;
        }

        public Builder url(String url) {
            this.url = url;
            return this;
        }

        public Builder body(String body) {
            this.body = body;
            return this;
        }

        public HttpRequest build() {
            return new HttpRequest(this);
        }
    }
}

JDK 中的应用:

  • StringBuilder
  • Stream.Builder

Spring 中的应用:

  • UriComponentsBuilder
    用于构建 URI。

总结

这些设计模式不仅在日常开发中有着广泛应用,更在 JDK 和 Spring 中深度体现。

了解它们的本质和应用场景,能够让我们写出更优雅、更健壮的代码。

下次再遇到类似问题时,希望你能得心应手地选择合适的模式!

最后说一句(求关注,别白嫖我)

如果这篇文章对您有所帮助,或者有所启发的话,帮忙关注一下我的同名公众号:苏三说技术,您的支持是我坚持写作最大的动力。

求一键三连:点赞、转发、在看。

关注公众号:【苏三说技术】,在公众号中回复:进大厂,可以免费获取我最近整理的10万字的面试宝典,好多小伙伴靠这个宝典拿到了多家大厂的offer。

平板以及二合一平板均是触控屏,Laptop现在也有很多屏幕带触控

触控屏,都会配置触控笔配件,目前市场上一般是电容屏+电容笔的技术方案。

触控笔分为主动笔和被动笔,主动笔占绝大部分。主动笔是通过内部电池或电源供电的,可以主动发送信号给设备,采用电磁感应原理,通过在屏幕上放置感应器,实现对笔尖位置、压感等信息的精确捕捉

主动笔主要有以下几类

1.Wacom 主动笔 - 和冠,专业做触控屏及触控笔的。详细的可见其官网
Wacoom Home

2.MPP主动笔 - 微软触控笔协议,其它触控笔也可以认证、应用此协议,支持Microsoft Pen Protocol协议的触控笔可以无缝对接windows系统。最新协议是MPP2.5,相比2.0有很大改进,通过减少延迟、提高压力灵敏度的精度和改进倾斜功能来提高笔的性能等。
触控笔实现指南 | Microsoft Learn

3.USI 主动笔 - USI(Universal Stylus Initiative)协议,是一个行业联盟创建,旨在推动不同品牌的平板电脑和笔之间的互操作性。基于这协议,系统内应用交互可定制性比较强
universalstylus.org

其它品牌的如华为HPP、京东方BPP、联想LPP,很多大厂都自己搞了一套协议

下面我们基于Surface pro 8及配置的Surface Slim Pen 2(其它触控笔以及区别可访问
Surface 触控笔型号和功能 - Microsoft 支持
),列下这个MPP触控笔相关的系统功能矩阵:

1.点击 - 单击、双击、长按与鼠标操作一样,另外长按会显示圆圈

2.书写

笔尖书写根据压感PresureFactor调整笔迹粗细,笔帽会识别成擦除。在WriteBoard中,可以直接使用笔帽进行擦除

书写这块,微软白板使用了书写预测,能在正常书写速度时降低书写延时。书写预测在极端书写速度时会有残影,书写结束时因取消预测笔迹会有笔迹撤回的体验不佳问题。我也在另一篇实现了书写预测,对比微软白板效果会更好,大家有兴趣的可以了解
.NET 白板书写加速-曲线拟合预测 - 唐宋元明清2188 - 博客园

需要补充的是,Win11支持MPP协议触控笔直接书写输入文本,任何输入框如系统输入框、web端搜索框等,在书写完成后会自动识别成文字。是个体验很不错的功能
Windows 手写功能 | Microsoft Windows

3. 按键

笔帽按键,可以分成单击、双击、长按操作,长按会有震动反馈。三类操作可以在设置中分别定义快捷方式,默认无快捷方式,设置如下图。

侧边按键 - 按住侧边功能键时,将笔的单击识别成右键单击。长按时移动笔,在WriteBoard可圈选内容,圈选完成时有震动反馈(注:震动反馈强度,可在设置-触觉信号中调整)

4. 连接配对

蓝牙配对 - 蓝牙配对完成后,可在设备列表中显示设备型号以及电量
自动连接 - 提起设备时,能自动连接。过一段时间不使用,会自动断开连接,进入触摸笔设备休眠、低功耗状态。
5. 充电槽
放入充电槽自动充电,有指示灯亮几秒再熄灭。拿出充电槽,有提笔检测功能,windows自动弹出笔菜单
参考文章:

主动笔+平板电脑_主动笔协议mpp和usi-CSDN博客

windows触控战五渣?教你玩转戴尔高级主动式触控笔-PN579X - 知乎

Wacom emr(S-pen)、Apple pencil、Surface pen、M-pencil等手写笔技术的一份总结 - 哔哩哔哩

主动式与被动式触控笔:所有标准解释 - HowToHi

(3 封私信 / 82 条消息) 为什么surface没有对手写功能进行更深入的开发? - 知乎

一:背景

1. 讲故事

前些天训练营里的一位学员找到我,说他们的差旅后台系统出现了CPU爆高的情况,爆高之后就下不去了,自己分析了下也没找到原因,事情比较紧急,让我帮忙看下是什么回事,手里也有dump,丢过我之后我们上 windbg 分析吧。

二:WinDbg分析

1. 为什么会CPU爆高

看过这个系列的朋友都知道CPU是否爆高,可以用
!tp
命令来验证,参考输出如下:


0:000> !tp
CPU utilization: 100%
Worker Thread: Total: 66 Running: 66 Idle: 0 MaxLimit: 32767 MinLimit: 4
Work Request in Queue: 93
    Unknown Function: 00007ffc744f1750  Context: 000002c3acdad7d8
    AsyncTimerCallbackCompletion TimerInfo@000002bf57193d20
    Unknown Function: 00007ffc744f1750  Context: 000002c3acb2aef0
    ...
    Unknown Function: 00007ffc744f1750  Context: 000002c3ad336718
--------------------------------------
Number of Timers: 0
--------------------------------------
Completion Port Thread:Total: 1 Free: 1 MaxFree: 8 CurrentLimit: 1 MaxLimit: 1000 MinLimit: 4

从卦中的
CPU utilization: 100%
可以确认此时CPU被打满了,同时也有一个现象就是这个
线程池队列
有堆积的情况,一般来说堆积就表示下游处理不力,常常表现为程序卡死,Http超时,和
CPU爆高问题
没有直接关系,这是一个经验问题,大家一定要甄别,以免陷入误区。

接下来我们看下这台机器的CPU能力如何,能力越弱越容易被打爆,可以用
!cpuid
观察。


0:000> !cpuid
CP  F/M/S  Manufacturer     MHz
 0  6,85,7  <unavailable>   2095
 1  6,85,7  <unavailable>   2095
 2  6,85,7  <unavailable>   2095
 3  6,85,7  <unavailable>   2095

从卦中可以看到当前的
CPU=4core
,只要4个满负荷的Thread就可以轻松打爆,接下来我们的研究方向在哪里呢?对,就是从 Thread 入手。

2. 线程都在干什么

要想看线程都在干什么?可以使用
~*e !clrstack
观察即可,参考输出如下:


OS Thread Id: 0x968 (87)
        Child SP               IP Call Site
000000e63babb9a8 00007ffc879a6974 [GCFrame: 000000e63babb9a8] 
000000e63babba78 00007ffc879a6974 [HelperMethodFrame_1OBJ: 000000e63babba78] System.Threading.Monitor.ObjWait(Boolean, Int32, System.Object)
000000e63babbb90 00007ffc5e735bbf System.Threading.ManualResetEventSlim.Wait(Int32, System.Threading.CancellationToken) [f:\dd\ndp\clr\src\BCL\system\threading\ManualResetEventSlim.cs @ 669]
000000e63babbc20 00007ffc5e72e9c5 System.Threading.Tasks.Task.SpinThenBlockingWait(Int32, System.Threading.CancellationToken) [f:\dd\ndp\clr\src\BCL\system\threading\Tasks\Task.cs @ 3320]
000000e63babbc90 00007ffc5f0cc188 System.Threading.Tasks.Task.InternalWait(Int32, System.Threading.CancellationToken) [f:\dd\ndp\clr\src\BCL\system\threading\Tasks\Task.cs @ 3259]
000000e63babbd60 00007ffc5f0c9176 System.Threading.Tasks.Task`1[[System.__Canon, mscorlib]].GetResultCore(Boolean) [f:\dd\ndp\clr\src\BCL\system\threading\Tasks\Future.cs @ 559]
000000e63babbda0 00007ffc1b98f2cf xxxCheckFilterAttribute.GetRequestParameter(System.Web.Http.Controllers.HttpActionContext)
...

从卦中可以看到大量的 Wait 等待,其实就是代码中调用
Task.Result
所致,即异步中混合同步,虽然这是一个可能导致线程饥饿的问题,但和我们的目标:
CPU爆高
无关,所以我们需要在茫茫调用栈中寻找其他可能导致的 CPU 爆高线程,经过仔细而耐心的查找,终于找到了疑似调用栈,刚好有5个都停留在这个位置。参考如下:


OS Thread Id: 0x26a8 (35)
        Child SP               IP Call Site
000000e62537b048 00007ffc879a64a4 [HelperMethodFrame: 000000e62537b048] 
000000e62537b190 00007ffc5e68e04a System.String.Concat(System.String, System.String) [f:\dd\ndp\clr\src\BCL\system\string.cs @ 3207]
000000e62537b1e0 00007ffc1dbe85eb xxx.GetParentDeptName_All(System.Collections.Generic.List`1<xxx.xxxDepts>, Int64)
...
000000e62537c870 00007ffc1e0af75b DynamicClass.lambda_method(System.Runtime.CompilerServices.Closure, System.Object, System.Object[])
000000e62537c8b0 00007ffc1b29b3b8 System.Web.Http.Controllers.ReflectedHttpActionDescriptor+ActionExecutor+c__DisplayClass10.b__9(System.Object, System.Object[])
000000e62537c8f0 00007ffc1b29a768 System.Web.Http.Controllers.ReflectedHttpActionDescriptor.ExecuteAsync(System.Web.Http.Controllers.HttpControllerContext, System.Collections.Generic.IDictionary`2<System.String,System.Object>, System.Threading.CancellationToken)
000000e62537c950 00007ffc1b29a18e System.Web.Http.Controllers.ApiControllerActionInvoker+d__0.MoveNext()
000000e62537c9c0 00007ffc1b2996ca System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1[[System.__Canon, mscorlib]].Start[[System.Web.Http.Controllers.ApiControllerActionInvoker+d__0, System.Web.Http]](d__0 ByRef)
000000e62537ca70 00007ffc1b299611 System.Web.Http.Controllers.ApiControllerActionInvoker.InvokeActionAsyncCore(System.Web.Http.Controllers.HttpActionContext, System.Threading.CancellationToken)
000000e62537cb30 00007ffc1b299535 System.Web.Http.Controllers.ApiControllerActionInvoker.InvokeActionAsync(System.Web.Http.Controllers.HttpActionContext, System.Threading.CancellationToken)
000000e62537cb60 00007ffc1b299504 System.Web.Http.Controllers.ActionFilterResult.b__0(ActionInvoker)
000000e62537cb90 00007ffc1b2994a9 System.Web.Http.Controllers.ActionFilterResult+c__DisplayClass10`1[[System.Web.Http.Controllers.ActionFilterResult+ActionInvoker, System.Web.Http]].b__f()
000000e62537cbe0 00007ffc1b2622f9 System.Web.Http.Filters.ActionFilterAttribute+d__5.MoveNext()
000000e62537cc40 00007ffc1b261cfa System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1[[System.__Canon, mscorlib]].Start[[System.Web.Http.Filters.ActionFilterAttribute+d__5, System.Web.Http]](d__5 ByRef)
000000e62537ccf0 00007ffc1b261bf4 System.Web.Http.Filters.ActionFilterAttribute.CallOnActionExecutedAsync(System.Web.Http.Controllers.HttpActionContext, System.Threading.CancellationToken, System.Func`1<System.Threading.Tasks.Task`1<System.Net.Http.HttpResponseMessage>>)
000000e62537cdd0 00007ffc1b2584d4 System.Web.Http.Filters.ActionFilterAttribute+d__0.MoveNext()

到了这里之后,甭管有没有问题,反正嫌疑很大,接下来就得研究
GetParentDeptName_All
方法。

3. GetParentDeptName_All 有问题吗

要想知道有没有问题,我们用 ILSpy观察其源码,由于客户隐私的问题,这里就稍微模糊一下,截图如下:

从卦中看,尼玛,还真有一个 while 逻辑,一下子就提起了我的兴趣,看样子八九不离十,接下来到线程栈上 GetParentDeptName_All 方法的
listDept

deptId
参数给挖出来,我们从35号线程入手,使用
!clrstack -a
参数观察输出结果。


0:035> !clrstack -a
OS Thread Id: 0x26a8 (35)
        Child SP               IP Call Site
000000e62537b048 00007ffc879a64a4 [HelperMethodFrame: 000000e62537b048] 
000000e62537b190 00007ffc5e68e04a System.String.Concat(System.String, System.String) [f:\dd\ndp\clr\src\BCL\system\string.cs @ 3207]
    PARAMETERS:
        str0 (<CLR reg>) = 0x000002c05816f360
        str1 (<CLR reg>) = 0x000002c3e21eaf88
    LOCALS:
        <no data>
        <no data>

000000e62537b1e0 00007ffc1dbe85eb xxx.GetParentDeptName_All(System.Collections.Generic.List`1<xxxDepts>, Int64)
    PARAMETERS:
        this (0x000000e62537b2c0) = 0x000002c059898590
        listDept (0x000000e62537b2c8) = 0x000002c159803390
        deptId (0x000000e62537b2d0) = 0x00000000000324fb

拿到了 listDept 的地址之后,用
.logopen

!mdt -e:2 000002c159803390
的输出结果全部记录到文本文件,为了保护客户隐私,这里就不截图了,直接上文本。


[20828] 000002c059802b68 (xxxtDepts)
    <DeptId>k__BackingField:0x324fb (System.Int32)
    <ParentDeptId>k__BackingField:0x31347 (System.Int32)
    ...

[20643] 000002c0597f0e30 (xxxtDepts)
    <DeptId>k__BackingField:0x2f240 (System.Int32)
    <ParentDeptId>k__BackingField:0x31347 (System.Int32)
    ...

[20663] 000002c0597f2d18 (xxxtDepts)
    <DeptId>k__BackingField:0x2f254 (System.Int32)
    <ParentDeptId>k__BackingField:0x2f240 (System.Int32)
    ...

从卦中数据看,listDept数组的第 20643 和 20663 项的子节点和父节点是循环的,这自然就导致了死循环,所以这次生产事故本质上是数据导致的,将结果反馈给朋友,也得到了确认。

问题找到了之后,解决办法也比较简单。

  1. 校正数据。

  2. 可以设置循环上限,如果超过某个阈值,直接抛出异常,这样可以避免出现CPU爆高导致的机器级别故障

PS: .net core 版本的 Dictionary 就是这么干的,参考代码如下:

在 .net framework 中就会出现傻乎乎打爆的严重事件。

三:总结

我的学员没有分析出来,我觉得应该是被 Task.Result 给误导了,真实的dump分析可能会真真假假,假假真真,就像这个社会一样,需要更多的实践历练吧。
图片名称

大家好,我是汤师爷~

订单模型作为整个交易系统的核心,支撑着所有交易环节。

订单域核心概念模型

如图所示,为订单核心概念模型。

1、订单

在实际交易业务处理中,订单会根据不同的业务规则(如店铺、收货地址、配送方式等)拆分成多个子订单,形成一个父订单对应多个子订单的结构。这种拆分机制便于后续的订单履约和商家结算。订单包含以下核心字段:

  • 租户ID:标识订单所属的租户
  • 订单ID:订单的唯一标识
  • 父订单ID:关联的父级订单ID(如有)
  • 商家ID:关联的商家标识
  • 订单类型:区分普通订单、预售订单、跨境订单等类型
  • 下单时间:订单创建的具体时间
  • 支付方式:如现金、银行卡、移动支付、标记支付等
  • 订单状态:反映订单当前所处环节
  • 订单备注:存储订单相关的补充说明
  • 合计金额:订单商品的原始总金额
  • 实付金额:扣除优惠后的实际支付金额
  • 优惠金额:各类促销、优惠券等减免金额

2、订单明细

订单明细是订单中每一个具体商品的详细信息记录,包含以下关键信息:

  • 订单明细ID:每条明细的唯一标识
  • 订单ID:关联的订单编号
  • 商品ID和SKU ID:标识具体商品及其规格
  • 商品编码:商品的内部编码标识
  • 商品数量:购买的数量
  • 商品原价:单个商品价格
  • 商品金额:该商品的总金额
  • 商品实付:
  • 明细类型:区分普通商品、赠品等
  • 优惠金额:应用到该商品的优惠金额

2、渠道信息

记录订单的购买渠道,如电商平台、O2O平台或门店。这些信息有助于分析销售策略和客户购买行为。

3、客户信息

客户的基本信息,包括客户名称、客户类型、客户生日等,用于支持后续的营销活动和客户服务。

4、营销信息

包括订单中应用的促销活动、折扣、积分使用等信息。这些数据帮助商家追踪促销效果并了解客户购买偏好。

5、收货信息

详细记录商品买家的收货信息,包括收货人姓名、手机号码、联系电话和收货地址。

6、支付信息

记录客户的支付方式、支付状态、支付时间和支付金额。这些信息对财务管理和订单结算流程至关重要。

7、交付信息

记录了商品交付的详细情况,包括:预计送货时间、预计送达时间、预约送达时间范围(开始和结束时间)、预约自提时间范围(开始和结束时间)、自提位置、配送方式等。

8、费用明细

费用明细记录订单中的各项费用,包括运费、打包费、服务费等额外收费项目。

订单拆单场景

订单拆单是指在订单创建时,根据预设的业务规则,将一个完整订单分解为多个子订单的过程。规则通常涉及多个维度,包括商家主体、交易模式、商品属性和物流要求等。

通过拆单,系统能够满足复杂的业务需求,提高订单处理效率。以下为常见拆单场景与案例说明:

1、按商家(店铺)拆分

当订单包含多个商家的商品时,系统会按商家将其拆分为独立子订单。例如,你同时购买了服装店和数码配件店的商品,系统会将订单拆分为两个子订单,分别由各自商家备货和发货。

这样拆分可以让每个商家能独立处理订单履约、售后服务和财务结算,确保账务清晰,避免纠纷。

2、按交易模式拆分

不同交易模式往往有特定的资质要求和结算方式。以海外购为例,商品需通过跨境资质认证和海关申报,并计算关税。如果订单中既有普通国内商品,又有海外购商品,系统会将海外购部分独立成子订单,以便专门处理相关资质与税费事宜。

3、按品类拆分(如药品、生鲜)

不同品类的商品处理要求大不相同。药品需要严格遵循处方审核和药剂师复核流程。如果订单中既有处方药,也有普通日用品,系统将药品部分独立拆分,确保合规处理。

同样,生鲜食品需冷链运输。当订单同时包含生鲜与普通商品时,系统会将生鲜部分单独划分成子订单,确保冷链物流的顺利执行,避免商品的新鲜度。

4、按收货地址拆分

当用户想将订单中的商品分别送给不同城市的亲友,并且在一个订单中享受最多的优惠,系统会按收货地址进行拆分。例如,一份订单需分别配送至北京、上海和广州。系统会生成三个子订单,每个子订单对应一个收货地址,确保为各自的配送线路选择合适的物流服务。

通过合理的拆单策略,交易系统可以更灵活地满足复杂的业务场景,实现精确的分工与高效的履约。

订单状态机

订单状态机是指在订单处理流程中,用类似有限状态机的方式来定义和管理订单所经历的不同状态,以及状态间转换逻辑的机制。

通过状态机的设计,可以清晰、结构化地定义订单从生成到最终完成或取消的整个生命周期,明确订单在每个阶段所处的状态,以及不同状态之间的转换条件和业务规则。

订单金额计算

订单金额计算是电商交易系统中的一个核心环节,它不仅涉及基础的商品价格计算,还需要处理各类优惠、折扣、运费等多个维度的金额。下面我们通过具体示例,详细分析订单金额的计算方法和优惠分摊机制。

1、订单金额的计算示例

让我们设定一个简单场景。用户在购物车中添加了 2 个吐司面包,每个售价 20 元,共计 40 元。该订单满足两个优惠活动:

  • 当购买吐司面包时,满 40 元减免 5 元。
  • 订单金额满30,可减免 6 元配送费。

另外,还需支付 1 元的包装费用,订单的各金额计算如图所示。

  • 订单合计金额为 47 元(商品总价 40 元 + 配送费 6 元 + 包装费 1 元)。
  • 优惠金额为 11 元(包括 5 元商品减免与 6 元配送费减免)。
  • 最终用户支付金额为 36 元(47 元 - 11 元 = 36 元)。

2、订单优惠分摊示例

优惠分摊是指将订单中的整体优惠,合理分配到各个商品上。优惠分摊非常重要,主要作用有:

  • 准确计算每件商品在享受优惠后的实付金额,用于后续计算商品额实际收入和利润。
  • 出现售后时,可快速计算出应退还的合理金额,避免计算误差与纠纷。

下面是一个更复杂的案例,展示优惠分摊的计算过程。假设顾客的购物清单为:

  • 吐司面包 2 个,合计 40 元
  • 草莓蛋糕 1 个,合计 150 元

上述商品总计 190 元,并参与 "满 180 减 20 元" 的促销活动。此外,订单还包含 20 元的配送费和 6 元的包装费。

此时需要将 20 元的总优惠分配到各商品上,分摊逻辑是根据商品金额占比来计算:

  • 吐司面包(2 个)的分摊金额:20 元 × (40 元 / 190 元) = 4.21 元
  • 草莓蛋糕的分摊金额:20 元 × (150 元 / 190 元) = 15.79 元

当出现除不尽的情况时,系统会将最后一分钱的差额计入订单中的最后一个商品,从而保证商品的优惠金额总和与实际优惠金额完全一致。

分摊完成后,订单金额为 216 元(商品 190 元 + 配送费 20 元 + 包装费 6 元),减去 20 元优惠,顾客实际支付 196 元。

本文已收录于,我的技术网站:
tangshiye.cn
里面有,算法Leetcode详解,面试八股文、BAT面试真题、简历模版、架构设计,等经验分享。

上一篇介绍了
文字相关
的创建和销毁动画,本篇介绍几个用于
几何图形
的创建和销毁动画效果类。

  1. Create
    :用于在场景中生成一个完整的
    Mobject
    (可渲染对象)
  2. Uncreate
    :是
    Create
    的逆操作,用于将已经存在于场景中的
    Mobject
    从场景中移除
  3. DrawBorderThenFill
    :用于分两步展示一个图形对象
  4. ShowIncreasingSubsets
    :用于展示一个包含多个子对象的父对象中的子集逐步增加的过程
  5. ShowSubmobjectsOneByOne
    :专注于逐个显示一个复杂对象中的子对象
  6. SpiralIn
    :使对象以螺旋式的路径进入场景

1. 动画函数概要

上面的几个动画函数基本都是创建元素用的,用于销毁元素的只有
Uncreate
函数。

1.1. Create

Create
动画效果的核心作用是将一个
Mobject
在场景中瞬间生成并显示出来。

它就像是一个开关,从不可见状态切换到可见状态,用于在场景中引入新的元素,如几何图形、文本对象等。

它的主要参数有:

参数名称 类型 说明
mobject VMobject 要创建的动画对象
lag_ratio float 用于控制动画中对象出现的延迟比例
introducer bool 指定一个用于引入或引导动画对象的动画效果

introducer
参数不常用,它的具体用法和效果可能因版本而异。

1.2. Uncreate


Create
相反,
Uncreate
的主要作用是将场景中已经存在的
Mobject
从可见状态转换为不可见状态,实现对象的移除效果。

它的主要参数有:

参数名称 类型 说明
mobject VMobject 要销毁的动画对象

1.3. DrawBorderThenFill

DrawBorderThenFill
适用于绘制有填充效果的图形。

比如在展示一个复杂的多边形或者自定义形状时,先绘制轮廓可以让观众清楚地看到图形的边界,然后再填充图形中间的部分。

它的主要参数有:

参数名称 类型 说明
mobject VMobject 要创建的动画对象
run_time float 动画的持续时间
rate_func func 动画的速率函数
draw_border_animation_config dict 绘制边框的动画配置
fill_animation_config dict 填充内部的动画配置

1.4. ShowIncreasingSubsets

ShowIncreasingSubsets
可以按照一定的顺序(通常是根据对象在集合中的顺序)逐个或者逐组地显示对象,具有很好的层次感和递进感,能够引导观众逐步理解整体对象是如何由各个部分组成的。

它的主要参数有:

参数名称 类型 说明
group VMobject 要创建的动画对象(一般由多个子对象组成)

1.5. ShowSubmobjectsOneByOne

ShowSubmobjectsOneByOne
类似于
ShowIncreasingSubsets
,但更侧重于逐个显示一个复杂对象中的子对象。

比如在展示一个分层的图形结构(如多层嵌套的几何图形)或者一个具有多个组成部分的动画角色时,逐个显示子对象可以详细地展示其内部结构。

它的主要参数有:

参数名称 类型 说明
group VMobject 要创建的动画对象(一般由多个子对象组成)

1.6. SpiralIn

SpiralIn
以其独特的螺旋式进入方式来吸引观众的注意力,使对象的出现更具动态感和空间感。

这种动画效果可以让场景看起来更加生动和富有创意。

它的主要参数有:

参数名称 类型 说明
shapes VMobject 要创建的动画对象
scale_factor float 控制对象沿螺旋路径进入场景时的缩放比例
fade_in_fraction float 控制对象在沿螺旋路径进入场景时的淡入效果

2. 使用示例

下面还是通过示例来演示各种动画效果及其主要参数。

2.1. Create和Uncreate

这个示例通过设置速率函数演示元素的创建和销毁方式,示例中有3种速度,分别为匀速,逐渐变慢(按照时间的
平方根
来渲染)和逐渐变快(按照时间的
平方
来渲染)。

s1 = Square(color=BLUE)
s2 = Square(color=GREEN)
s3 = Square(color=YELLOW)

VGroup(s1, s2, s3).scale(0.8).arrange(RIGHT, buff=0.5)
self.play(
    Create(s1, rate_func=lambda x: x),
    Create(s2, rate_func=lambda x: np.sqrt(x)),
    Create(s3, rate_func=lambda x: x**2),
    run_time=2,
)
self.wait()
self.play(
    Uncreate(s3, rate_func=lambda x: x),
    Uncreate(s2, rate_func=lambda x: np.sqrt(x)),
    Uncreate(s1, rate_func=lambda x: x**2),
    run_time=2,
)

2.2. DrawBorderThenFill

DrawBorderThenFill
动画的特点是先渲染边框,再填充内部,这个示例通过绘制一个简单的儿童画,来演示其特点。

t = Triangle(fill_opacity=1, fill_color=BLUE_D)
s = Square(side_length=1.5, fill_opacity=1, fill_color=ORANGE)
r = Rectangle(height=1, width=0.5, fill_opacity=1, fill_color=PURPLE)

c1 = Circle()
c2 = Circle()
c3 = Circle()

self.play(DrawBorderThenFill(t), run_time=run_time)
self.play(DrawBorderThenFill(s), run_time=run_time)
self.play(DrawBorderThenFill(r), run_time=run_time)
self.play(
    DrawBorderThenFill(c1),
    DrawBorderThenFill(c2),
    DrawBorderThenFill(c3),
    run_time=run_time,
)

2.3. ShowIncreasingSubsets和ShowSubmobjectsOneByOne

这个示例演示
ShowIncreasingSubsets

ShowSubmobjectsOneByOne
的区别,

ShowIncreasingSubsets
是逐步渲染一个个子对象,已经渲染的子对象会保留下来;


ShowSubmobjectsOneByOne
虽然也是逐步渲染一个个子对象,但是渲染下一个子对象时,会清理上一个子对象。

所以,使用
ShowSubmobjectsOneByOne
时,始终只有一个子对象被显示出来。

# 创建一个由多个小正方形组成的大正方形
squares = VGroup()
colors = [BLUE, GREEN, YELLOW]
for x in range(3):
    for y in range(3):
        square = Square(
            side_length=0.5,
            stroke_width=1,
            stroke_color=RED,
            fill_opacity=0.5,
            fill_color=colors[y],
        ).shift(x * 0.5 * RIGHT + y * 0.5 * UP)
        squares.add(square)

vg = VGroup(squares, squares.copy())
vg.arrange(RIGHT, buff=1)

# 使用ShowIncreasingSubsets动画展示
# 使用ShowSubmobjectsOneByOne动画展示
self.play(
    ShowIncreasingSubsets(vg[0]),
    ShowSubmobjectsOneByOne(vg[1]),
    run_time=3,
)

2.4. SpiralIn

这个示例演示了
SpiralIn
函数通过旋转方式创建元素的方式,第一次的2个图形以默认的参数旋转进场;

第二次的3个图形则以更小的旋转半径(通过
scale_factor
参数)旋转进场。

c = Circle(color=GREEN_C, fill_opacity=1).shift(LEFT)
s = Square(color=BLUE_D, fill_opacity=1).shift(UP)
shapes = VGroup(c, s)
self.play(SpiralIn(shapes))
self.wait()
self.remove(shapes)

t = Triangle(color=RED_D, fill_opacity=1)
shapes = VGroup(c, s, t)
self.play(SpiralIn(shapes, scale_factor=0.5))

3. 附件

文中的代码只是关键部分的截取,完整的代码共享在网盘中(
graph.py
),

下载地址:
完整代码
(访问密码: 6872)