整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

订单及其状态机的设计实现

订单及其状态机的设计实现

态机简介:

状态机是有限状态自动机的简称,是现实事物运行规则抽象而成的一个数学模型。【规则的抽象】

有限状态机一般都有以下特点:

(1)可以用状态来描述事物,并且任一时刻,事物总是处于一种状态;

(2)事物拥有的状态总数是有限的;

(3)通过触发事物的某些行为,可以导致事物从一种状态过渡到另一种状态;

(4)事物状态变化是有规则的,A状态可以变换到B,B可以变换到C,A却不一定能变换到C;

(5)同一种行为,可以将事物从多种状态变成同种状态,但是不能从同种状态变成多种状态。

状态机这种描述客观世界的方式就是将事物抽象成若干状态,然后所有的事件和规则导致事物在这些状态中游走。最终使得事物“自圆其说”。

很多通信协议的开发都必须用到状态机;一个健壮的状态机可以让你的程序,不论发生何种突发事件都不会突然进入一个不可预知的程序分支。

  • 状态机示例:

四大概念:


状态(state)

一个状态机至少要包含两个状态。

分为:现态(源状态)、次态(目标状态)

状态可以理解为一种结果,一种稳态形式,没有扰动会保持不变的。

状态命名形式:

1.副词+动词;例如:待审批、待支付、待收货

这种命名方式体现了:状态机就是事件触发状态不断迁徙的本质。表达一种待触发的感觉。

2.动词+结果;例如:审批完成、支付完成

3.已+动词形式;例如:已发货、已付款

以上两种命名方式体现了:状态是一种结果或者稳态的本质。表达了一种已完成的感觉。

角色很多的时候,为了表示清晰,可以加上角色名:例如:待财务审批、主管批准

命名考虑从用户的理解的角度出发。



事件(event)

or

触发条件

又称为“条件”,就是某个操作动作的触发条件或者口令。当一个条件满足时,就会触发一个动作,或者执行一次状态迁徙

这个事件可以是外部调用、监听到消息、或者各种定时到期等触发的事件。

对于灯泡,“打开开关”就是一个事件。

条件命名形式:动词+结果;例如:支付成功、下单时间>5分钟


动作(action)

事件发生以后要执行动作。例如:事件=“打开开关指令”,动作=“开灯”。一般就对应一个函数。

条件满足后执行动作。动作执行完毕后,可以迁移到新的状态,也可以仍旧保持原状态。

动作不是必需的,当条件满足后,也可以不执行任何动作,直接迁移到新状态。

那么如何区分“动作”和“状态”?

“动作”是不稳定的,即使没有条件的触发,“动作”一旦执行完毕就结束了;

而“状态”是相对稳定的,如果没有外部条件的触发,一个状态会一直持续下去。


变换(transition)

即从一个状态变化到另外一个状态

例如:“开灯过程”就是一个变化



状态机其他表达方式:

状态机的设计:

信息系统中有很多状态机,例如:业务订单的状态。

状态机的设计存在的问题:什么是状态?到底有多少个状态?要细分到什么程度?

  信息系统是现实世界的一种抽象和描述。而业务领域中那些已经发生的事件就是事实信息系统就是将这些事实以信息的形式存储到数据库中,即:信息就是一组事实

信息系统就是存储这些事实,对这些事实进行管理与追踪,进而起到提高工作效率的作用。

信息系统就是记录已经发生的事实,信息系统中的状态基本和事实匹配。即:标识某个事实的完成度。

业务系统,根据实际业务,具体会有哪些发生的事实需要记录,基本上这些事实就至少对应一个状态。需要记录的事实就是一种稳态,一种结果。

例如:【待支付】->【已支付】->【已收货】->【已评价】

这些都是系统需要记录的已发生的客观事实。而这些事实就对应了状态,而发生这些事实的事件就对应了触发状态机的转换的事件。

根据自己的业务实际进行分析,并画出状态图即可。

状态机实现方式:状态模式

下面是经典的自动贩卖机例子来说明状态模式的用法,状态图如下:

分析一个这个状态图:

a、包含4个状态(我们使用4个int型常量来表示)
b、包含3个暴露在外的方法(投币、退币、转动手柄、(发货动作是内部方法,售卖机未对外提供方法,售卖机自动调用))
c、我们需要处理每个状态下,用户都可以触发这三个动作。

我们可以做没有意义的事情,在【未投币】状态,试着退币,或者同时投币两枚,此时机器会提示我们不能这么做。


实现逻辑:

    任何一个可能的动作,我们都要检查,看看我们所处的状态和动作是否合适。

状态机使用if-else或switch实现

测试自动售卖机

使用if-else/switch的方式实现状态有如下问题:

  • 没有遵守【开闭】原则,没有【封装变化】,所以没有弹性,应对需求变更非常吃力。

   例如:现在增加一个状态。每个方法都需要添加if-else语句。

  • 状态如何转换看得不是很清楚,隐藏在if-else/switch逻辑中。

升级策略:

【封装变化】,局部化每个状态的行为,将每个状态的行为放到各自类中,每个状态只要实现自己的动作就可以了。

贩卖机只要将动作委托给代表当前状态的状态对象即可。


public interface State
{
    /**
     * 放钱
     */
    public void insertMoney();
    /**
     * 退钱
     */
    public void backMoney();
    /**
     * 转动曲柄
     */
    public void turnCrank();
    /**
     * 出商品
     */
    public void dispense();
}
public class NoMoneyState implements State
{
 
    private VendingMachine machine;
 
    public NoMoneyState(VendingMachine machine)
    {
        this.machine=machine;
        
    }
    
    @Override
    public void insertMoney()
    {
        System.out.println("投币成功");
        machine.setState(machine.getHasMoneyState());
    }
 
    @Override
    public void backMoney()
    {
        System.out.println("您未投币,想退钱?...");
    }
 
    @Override
    public void turnCrank()
    {
        System.out.println("您未投币,想拿东西么?...");
    }
 
    @Override
    public void dispense()
    {
        throw new IllegalStateException("非法状态!");
    }
 
}
public class HasMoneyState implements State
{
 
    private VendingMachine machine;
 
    public HasMoneyState(VendingMachine machine)
    {
        this.machine=machine;
    }
 
    @Override
    public void insertMoney()
    {
        System.out.println("您已经投过币了,无需再投....");
    }
 
    @Override
    public void backMoney()
    {
        System.out.println("退币成功");
            machine.setState(machine.getNoMoneyState());
    }
 
    @Override
    public void turnCrank()
    {
        System.out.println("你转动了手柄");
        machine.setState(machine.getSoldState());
    }
 
    @Override
    public void dispense()
    {
        throw new IllegalStateException("非法状态!");
    }
 
}
    
public class SoldOutState implements State
{
 
    private VendingMachine machine;
 
    public SoldOutState(VendingMachine machine)
    {
        this.machine=machine;
    }
 
    @Override
    public void insertMoney()
    {
        System.out.println("投币失败,商品已售罄");
    }
 
    @Override
    public void backMoney()
    {
        System.out.println("您未投币,想退钱么?...");
    }
 
    @Override
    public void turnCrank()
    {
        System.out.println("商品售罄,转动手柄也木有用");
    }
 
    @Override
    public void dispense()
    {
        throw new IllegalStateException("非法状态!");
    }
 
}
public class SoldState implements State
{
 
    private VendingMachine machine;
 
    public SoldState(VendingMachine machine)
    {
        this.machine=machine;
    }
 
    @Override
    public void insertMoney()
    {
        System.out.println("正在出货,请勿投币");
    }
 
    @Override
    public void backMoney()
    {
        System.out.println("正在出货,没有可退的钱");
    }
 
    @Override
    public void turnCrank()
    {
        System.out.println("正在出货,请勿重复转动手柄");
    }
 
    @Override
    public void dispense()
    {
        machine.releaseBall();
        if (machine.getCount() > 0)
        {
            machine.setState(machine.getNoMoneyState());
        } else
        {
            System.out.println("商品已经售罄");
            machine.setState(machine.getSoldOutState());
        }
    }
}
public class VendingMachine
{
    private State noMoneyState;
    private State hasMoneyState;
    private State soldState;
    private State soldOutState;
    private State winnerState ; 


    private int count=0;
    private State currentState=noMoneyState;
 
    public VendingMachine(int count)
    {
        noMoneyState=new NoMoneyState(this);
        hasMoneyState=new HasMoneyState(this);
        soldState=new SoldState(this);
        soldOutState=new SoldOutState(this);
        winnerState=new WinnerState(this);
 
              if (count > 0)
             {
            this.count=count;
            currentState=noMoneyState;
             }
    }
 
       //将这些动作委托给当前状态.
    public void insertMoney()
    {
        currentState.insertMoney();
    }
 
    public void backMoney()
    {
        currentState.backMoney();
    }
       
        // 机器不用提供dispense动作,因为这是一个内部动作.用户不可以直 
        //接要求机器发放糖果.我们在状态对象的turnCrank()方法中调用 
        //dispense方法;

       //dispense无论如何,即使在nomoney状态也会被执行.
       //让不合法的情形下,dispense抛出异常处理。
    public void turnCrank()
    {
        currentState.turnCrank();
            currentState.dispense();
    }
 

    public void releaseBall()
    {
        System.out.println("发出一件商品...");
        if (count !=0)
        {
            count -=1;
        }
    }
 
    public void setState(State state)
    {
        this.currentState=state;
    }
 
    //getter setter omitted ...
 
}

我们之前说过,if-else/switch实现方式没有弹性,那现在按照这种实现模式,需求变更修改起来会轻松点吗?

红色部分标记了我们的需求变更:当用户每次转动手柄的时候,有10%的几率赠送一瓶。

实现方式:

我们遵守了【开闭】原则,只要新建一个WinnerState的类即可。然后有限的修改has_money的转向即可。

为什么WinnerState要独立成一个状态,其实它和sold状态一模一样。我把代码写在SoldState中不行吗?

  • 第一个原因就是上面说的信息系统的本质就是记录【事实】,中奖是需要记录的事实,它应该是一个状态。
  • 第二个原因:【单一职责】问题,我们一个类的责任是明确的。

   如果sold需求变化不一定影响到winner代码实现,winner需求变化时,也不一定要修改sold,比如促销方案结束了,中奖概率变了等。

   如果他们的变化不是一定互相影响到彼此的,那我们就该将他们分离,即是【隔离变化】也是遵守【单一职责】的原则。


public class WinnerState implements State
{
 
    private VendingMachine machine;
 
    public WinnerState(VendingMachine machine)
    {
        this.machine=machine;
    }
 
    @Override
    public void insertMoney()
    {
        throw new IllegalStateException("非法状态");
    }
 
    @Override
    public void backMoney()
    {
        throw new IllegalStateException("非法状态");
    }
 
    @Override
    public void turnCrank()
    {
        throw new IllegalStateException("非法状态");
    }
 
    @Override
    public void dispense()
    {
        System.out.println("你中奖了,恭喜你,将得到2件商品");
        machine.releaseBall();
 
    if (machine.getCount()==0)
    {
        System.out.println("商品已经售罄");
        machine.setState(machine.getSoldOutState());
    } else
    {
        machine.releaseBall();
        if (machine.getCount() > 0)
        {
            machine.setState(machine.getNoMoneyState());
        } else
        {
            System.out.println("商品已经售罄");
            machine.setState(machine.getSoldOutState());
        }
        
    }
 
    }
 
}
public class HasMoneyState implements State
{
 
    private VendingMachine machine;
    private Random random=new Random();
 
    public HasMoneyState(VendingMachine machine)
    {
        this.machine=machine;
    }
 
    @Override
    public void insertMoney()
    {
        System.out.println("您已经投过币了,无需再投....");
    }
 
    @Override
    public void backMoney()
    {
        System.out.println("退币成功");
 
    machine.setState(machine.getNoMoneyState());
    }
 
    @Override
    public void turnCrank()
    {
        System.out.println("你转动了手柄");
        int winner=random.nextInt(10);
        if (winner==0 && machine.getCount() > 1)
        {
            machine.setState(machine.getWinnerState());
        } else
        {
            machine.setState(machine.getSoldState());
        }
    }
 
    @Override
    public void dispense()
    {
        throw new IllegalStateException("非法状态!");
    }
 
}

总结状态模式:

状态模式:允许对象在内部状态改变时改变它的行为,对象看起来好像修改了他的类。

解释:

状态模式将状态封装成为独立的类,并将动作委托到代表当前状态的对象。
所以行为会随着内部状态改变而改变。
我们通过组合简单引用不同状态对象来造成类改变的假象.


状态模式策略模式


1.行为封装的n个状态中,不同状态不用行为。

2.context的行为委托到不同状态中。

3.[当前状态]在n个状态中游走,context的行为也随之[当前状态]的改变而改变。

4.用户对context的状态改变浑然不知。

5.客户不会直接和state交互,只能通过context暴露的方法交互,state转换是context内部事情。

6.state可以是接口也可以是抽象类,取决于有没公共功能可以放进抽象类中。抽象类方便,因为可以后续加方法。

可以将重复代码放入抽象类中。例如:"你已投入25元,不能重复投" 这种通用代码放入抽象类中。

7.context可以决定状态流转,如果这个状态流转是固定的,就适合放在context中进行。但是如果状态流转是动态的就适合放在状态中进行。

例如通过商品的剩余数目来决定流向[已售完]或[等待投币],这个时候放在状态类中,因为dispense要根据状态判断流转。

这个写法决定了,有新需求时候,你是改context还是改state类。

8.可以共享所有的state对象,但是需要修改context的时候时候,需要handler中传入context引用

1.context主动指定需要组合的策略对象是哪一个。

2.可以在启动的时候通过工厂动态指定具体是哪个策略对象,但是没有在策略对象之间游走,即:只组合了一个策略对象。

3.策略作为继承之外一种弹性替代方案。因为继承导致子类继承不适用的方法,且每个类都要维护,策略模式通过不同对象组合来改变行为。

4.策略模式聚焦的是互换的算法来创建业务。



状态机典型应用:订单状态控制

建表语句

如上图所示:

一种典型的订单设计。业务订单和支付退款订单组合,他们分别有自己的状态机。

  • 业务订单状态机负责业务逻辑,并和支付退款状态机联动。
  • 一切以业务状态机为主。例如:业务状态已经【关单】,此时收到支付成功通知,需要进行退款
  • 每个状态有自己的过期时间。异常订单的捞取通过过期时间判断。

状态机模式实现订单状态机:

日常开发过程中,状态机模式应用场景之一的就是订单模型中的状态控制。但是区别于状态模式的点有以下几个:

  • 状态模式,所有的操作都在内存。而订单状态机是要落库的。为了防止订单的并发操作,更新订单的时候需要使用乐观锁机制。
  • 状态模式的状态对象是新建状态机的时候初始化进去的。在实际开发中,状态对象要复用,被spring管理。
  • 而订单状态机对象对应了一条数据库中实体的订单,是要每次从数据库中查出来的即时新建对象,所以必须将该新建的订单状态机对象传入到状态对象中。使用状态对象处理该订单状态机对象。

以支付订单为例:

/*
   Title: PaymentInfo Description:
  支付订单状态机
   该类不可被spring管理,需要new出来,一个类就对应一条数据库中支付订单记录
   本文来自博客园,作者:wanglifeng,转载请注明原文链接:https://www.cnblogs.com/wanglifeng717/p/16214122.html
   
   @author wanglifeng
 */


public class PaymentStateMachine {

    // 数据库中当前支付订单实体
    private SapoPayment payment;

    // 当前状态
    private PaymentState currentState;

    // 需要更新入库的支付订单实体。与payment属性配合,payment为当前数据库中订单实体,用于乐观锁的前置内容校验。
    private SapoPayment paymentForUpdate;

    /* 将最新内容(含状态)更新入库,并当前状态机状态 */
    public void updateStateMachine() {

        // 从Spring容器中获取操作数据的dao
        SapoDao dao=SpringUtil.getBean(SapoDao.class);

        // 更新数据库,乐观锁机制:带前置内容数据校验,其中payment为前置内容,paymentForUpdate为要更新的内容,如果更新结果=0,说明该订单被其他线程修改过。抛异常,放弃此次修改。
        dao.updateSapoPaymentByNull(paymentForUpdate, payment);

        // 记录订单操作流水
        dao.insertSapoOrderStatusLog(SapoOrderStatusLog.getInstance().setOrderId(paymentForUpdate.getId())
                .setOrderType(SapoOrderStatusLog.ORDER_TYPE_PAYMENT).setStatus(paymentForUpdate.getStatus()));

        // 更新当前PaymentStateMachine状态机
        this.setPayment(paymentForUpdate);
        this.setCurrentState(paymentForUpdate.getStatus());
    }

    // 通过条件获取一个支付订单PaymentStateMachine实体
    public static PaymentStateMachine getInstance(SapoPayment sapoPaymentForQuery) {
        // 1.从spring容器中获取dao;
        SapoDao dao=SpringUtil.getBean(SapoDao.class);

        // 2.查出该支付订单
        SapoPayment paymentResult=dao.getSapoPayment(sapoPaymentForQuery);

        // 3.初始化订单状态机
        PaymentStateMachine paymentStateMachine=new PaymentStateMachine();
        paymentStateMachine.setPayment(paymentResult);
        paymentStateMachine.setCurrentState(paymentResult.getStatus());
        paymentStateMachine.setPaymentForUpdate(SapoPayment.getInstance(paymentResult));

        return paymentStateMachine;
    }

    // 设置当前状态机的状态。输入数据库中status字段,映射成对应的状态类实体。
    public void setCurrentState(Integer status) {

        PaymentState currentState=null;

        // status数字,映射成对应的状态类实体
        if (SapoPayment.STATUS_APPLY.equals(status)) {
            currentState=SpringUtil.getBean(PaymentStateApply.class);
        } else if (SapoPayment.STATUS_WAIT_PAY.equals(status)) {
            currentState=SpringUtil.getBean(PaymentStateWaitPay.class);
        } else if (SapoPayment.STATUS_PAY_FINISH.equals(status)) {
            currentState=SpringUtil.getBean(PaymentStatePayFinish.class);
        } else if (SapoPayment.STATUS_FAIL.equals(status)) {
            currentState=SpringUtil.getBean(PaymentStateFail.class);
        } else if (SapoPayment.STATUS_CANCEL.equals(status)) {
            currentState=SpringUtil.getBean(PaymentStateCancel.class);
        } else {
            throw new BusinessException(ResultInfo.SYS_INNER_ERROR.getCode(),
                    "status not in state machine ,status: " + status);
        }

        this.currentState=currentState;
    }

    // TODO 待实现,申请支付订单
    public void apply() {
        // 委托给当前状态执行,将当前订单状态机对象传进去,使用状态对象处理订单
        currentState.apply(this);
    }

    // TODO 待实现,通知支付结果
    public void resultNotify() {
        // 委托给当前状态执行
        currentState.resultNotify(this);
    }

    // TODO 同步给当前状态执行
    public void sync() {
        // 委托给当前状态执行
        currentState.sync(this);
    }

    // 取消订单
    public void cancel() {
        // 委托给当前状态执行
        currentState.cancel(this);
    }


}


public  interface PaymentState {

    public void apply(PaymentStateMachine paymentStateMachine);

    public void resultNotify(PaymentStateMachine paymentStateMachine);
    
    public void sync(PaymentStateMachine paymentStateMachine);
    
    public void cancel(PaymentStateMachine paymentStateMachine);

}
@Service
public class PaymentStateApply extends BaseLogger implements PaymentState {

    @Autowired
    FmPayClientService fmPayClientService;

    @Autowired
    SapoDao dao;

    @Autowired
    private JacksonComponent jacksonComponent;

    public void apply(PaymentStateMachine paymentStateMachine) {

       

    }

    public void sync(PaymentStateMachine paymentStateMachine) {

    }

    public void resultNotify(PaymentStateMachine paymentStateMachine) {
        // TODO Auto-generated method stub

    }

    public void cancel(PaymentStateMachine paymentStateMachine) {
        
        SapoPayment sapoPaymentForUpdate=paymentStateMachine.getPaymentForUpdate();
        sapoPaymentForUpdate.setStatus(SapoPayment.STATUS_CANCEL);
        sapoPaymentForUpdate.setExpireTime(null);
        
        paymentStateMachine.updateStateMachine();
        
        

    }

}
@Service
public class PaymentStateCancel extends BaseLogger implements PaymentState {

    public void apply(PaymentStateMachine paymentStateMachine) {
        // TODO Auto-generated method stub

    }

    public void resultNotify(PaymentStateMachine paymentStateMachine) {
        // TODO Auto-generated method stub

    }

    public void sync(PaymentStateMachine paymentStateMachine) {
        // TODO Auto-generated method stub

    }

    public void cancel(PaymentStateMachine paymentStateMachine) {
        // TODO Auto-generated method stub

    }

}
@Service
public class PaymentStateFail extends BaseLogger implements PaymentState {

    public void apply(PaymentStateMachine paymentStateMachine) {
        // TODO Auto-generated method stub

    }

    public void resultNotify(PaymentStateMachine paymentStateMachine) {
        // TODO Auto-generated method stub

    }

    public void sync(PaymentStateMachine paymentStateMachine) {
        // TODO Auto-generated method stub

    }

    public void cancel(PaymentStateMachine paymentStateMachine) {
        throw new BusinessException(ResultInfo.SYS_INNER_ERROR.getCode(), "fail status can not cancel");

    }

}
@Service
public class PaymentStatePayFinish extends BaseLogger implements PaymentState {

    public void apply(PaymentStateMachine paymentStateMachine) {
        // TODO Auto-generated method stub

    }

    public void resultNotify(PaymentStateMachine paymentStateMachine) {
        // TODO Auto-generated method stub

    }

    public void sync(PaymentStateMachine paymentStateMachine) {
        // TODO Auto-generated method stub

    }

    public void cancel(PaymentStateMachine paymentStateMachine) {
        throw new BusinessException(ResultInfo.SYS_INNER_ERROR.getCode(), "payfinish status can not cancel");

    }

}
@Service
public class PaymentStateWaitPay extends BaseLogger implements PaymentState {

    @Autowired
    FmPayClientService fmPayClientService;

    @Autowired
    SapoDao dao;

    @Autowired
    private JacksonComponent jacksonComponent;

    public void payResultNotify() {
        // TODO implement here
    }

    public void apply(PaymentStateMachine paymentStateMachine) {
        throw new BusinessException(ResultInfo.SYS_INNER_ERROR.getCode(),
                "applyPayPlatform not match payment state machine,currentStatus:"
                        + paymentStateMachine.getPayment().getStatus());

    }

    public void sync(PaymentStateMachine paymentStateMachine) {


        // TODO 过期去统一支付查询

        String payStatus=queryPayResultResponse.getPayStatus();

        // 1:初始化输入 2:支付中 3:支付成功 4:支付失败 5:撤销
        if (QueryPayResultResponse.PAY_STATUS_INIT.equals(payStatus)) {
            throw new BusinessException(ResultInfo.SYS_INNER_ERROR.getCode(),
                    "FMpay queryPay return init status ,we are waitpay");
        }

        if (QueryPayResultResponse.PAY_STATUS_ING.equals(payStatus)) {
            return;
        }

        SapoPayment sapoPaymentForUpdate=paymentStateMachine.getPaymentForUpdate();

        if (QueryPayResultResponse.PAY_STATUS_CANCEL.equals(payStatus)) {
            sapoPaymentForUpdate.setStatus(SapoPayment.STATUS_CANCEL);

        } else if (QueryPayResultResponse.PAY_STATUS_FAIL.equals(payStatus)) {
            sapoPaymentForUpdate.setStatus(SapoPayment.STATUS_FAIL);

        } else if (QueryPayResultResponse.PAY_STATUS_SUCCESS.equals(payStatus)) {
            sapoPaymentForUpdate.setStatus(SapoPayment.STATUS_PAY_FINISH);

        }
        sapoPaymentForUpdate.setExpireTime(null);

        paymentStateMachine.updateStateMachine();

    }

    public void resultNotify(PaymentStateMachine paymentStateMachine) {
        // TODO Auto-generated method stub

    }

    public void cancel(PaymentStateMachine paymentStateMachine) {
        throw new BusinessException(ResultInfo.SYS_INNER_ERROR.getCode(), "wait pay status can not cancel");

    }

}

文章来自https://www.cnblogs.com/wanglifeng717/p/16214122.html

着过年放假在家复习了之前学的JS知识,用原生撸了一个购物车模块,下面我来整理一下我的思路分享给大家。

一、功能和效果图

1.1 废话不多说,首先上个效果图,如下:

购物车功能效果图

1.2 功能介绍:

  1. 点击全选按钮,每一项商品的复选框处于被勾选的状态,同时计算出商品数量和商品总价;
  2. 点击数量切换的按钮,能自动计算出修改数量之后的商品数量和价格;
  3. 商品的总计数量和总价格应该只计算被勾选的商品的数量和金额。

功能介绍完毕,下面开始介绍我写这个购物车的步骤。


二、购物车的页面结构

2.1 HTML代码

<table>
        <caption>
            购物车
        </caption>
        <thead>
            <tr>
                <!-- 全选复选框 -->
                <th>
                    <input type="checkbox" name="checkAll" id="check-all" checked /><label for="check-all">全选</label>
                </th>
                <th>图片</th>
                <th>品名</th>
                <th>单位</th>
                <th>单价/元</th>
                <th>数量</th>
                <th>金额/元</th>
            </tr>
        </thead>
        <tbody>
            <tr>
                <td>
                    <input type="checkbox" name="item" value="SN-1020" checked />
                </td>
                <td>
                    <a href=""><img src="images/p1.jpg" alt="" /></a>
                </td>
                <td>iPhone 11</td>
                <td>台</td>
                <td class="price">4799</td>
                <td><input type="number" min="1" value="1" /></td>
                <td class="amount">xxxx</td>
            </tr>
            <tr>
                <td>
                    <input type="checkbox" name="item" value="SN-1020" checked />
                </td>
                <td>
                    <a href=""><img src="images/p2.jpg" alt="" /></a>
                </td>
                <td>小米pro 11</td>
                <td>部</td>
                <td class="price">3999</td>
                <td><input type="number" min="1" value="2" /></td>
                <td class="amount">xxxx</td>
            </tr>
            <tr>
                <td>
                    <input type="checkbox" name="item" value="SN-1030" checked />
                </td>
                <td>
                    <a href=""><img src="images/p3.jpg" alt="" /></a>
                </td>
                <td>MacBook Pro</td>
                <td>台</td>
                <td class="price">18999</td>
                <td><input type="number" min="1" value="1" /></td>
                <td class="amount">xxxx</td>
            </tr>
            <tr>
                <td>
                    <input type="checkbox" name="item" value="SN-1040" checked />
                </td>
                <td>
                    <a href=""><img src="images/p4.jpg" alt="" /></a>
                </td>
                <td>小米75电视</td>
                <td>台</td>
                <td class="price">5999</td>
                <td><input type="number" min="1" value="2" /></td>
                <td class="amount">xxxx</td>
            </tr>
            <tr>
                <td>
                    <input type="checkbox" name="item" value="SN-1050" checked />
                </td>
                <td>
                    <a href=""><img src="images/p5.jpg" alt="" /></a>
                </td>
                <td>Canon 90D单反</td>
                <td>台</td>
                <td class="price">9699</td>
                <td><input type="number" min="1" value="1" /></td>
                <td class="amount">xxxx</td>
            </tr>
        </tbody>
        <tfoot>
            <tr style="font-weight: bolder; font-size: 1.2em">
                <td colspan="5">总计:</td>
                <td id="sum">xxxx</td>
                <td id="total-amount">xxxx</td>
            </tr>
        </tfoot>
    </table>

2.2 CSS代码

table {
    border-collapse: collapse;
    width: 90%;
    text-align: center;
    margin: auto;
}

table caption {
    margin-bottom: 15px;
    font-size: 1.5rem;
}

table th, table td {
    border-bottom: 1px solid #ccc;
    padding: 5px;
    font-weight: normal;
}

table thead tr:first-of-type {
    background-color: #e6e6e6;
    height: 3em;
}

table input[type="checkbox"] {
    width: 1.5em;
    height: 1.5em;
}

table tbody tr {
    border-bottom: 1px solid #ccc;
}

table tbody tr:hover {
    background-color: #f6f6f6;
    cursor: pointer;
}

tbody img {
    width: 3em;
}

tbody input[type="number"] {
    width: 3em;
}

button {
    width: 150px;
    height: 30px;
    outline: none;
    border: none;
    background-color: teal;
    color: white;
    letter-spacing: 5px;
}

button:hover {
    opacity: 0.7;
    cursor: pointer;
}

2.3 效果图

购物车效果图

以上就是一个简单的购物车页面的HTML和CSS样式代码。


三、完成相关JS代码

首先,我们先完成商品的全选与取消全选的功能,所以肯定是需要拿到全选复选框元素和商品前面的复选框元素,代码如下:

// 获取全选复选框,所有的商品都有一个独立的复选框
const checkAll=document.querySelector('#check-all');
const checkItems=document.getElementsByName('item');

拿到全选和每个商品的复选框元素之后,给全选框添加一个change事件,监听它的checked值的变化。此时全选框的checked值可以通过事件监听回调函数中的ev参数下的ev.target.checked拿到。

checkALl.onchange=ev=> {
    // 如果全选框处于选中状态,ev.target.checked的值就为true,反之,为false。
    console.log(ev.target.checked);
};

如果想让全选框的的状态和每个商品前的复选框状态保持一致,那么就使他们的checked值一致即可。因此,我们可以在全选复选框的change事件中遍历每个商品的复选框元素。

checkALl.onchange=ev=> {
    // 如果全选框处于选中状态,ev.target.checked的值就为true,反之,为false。
    console.log(ev.target.checked);
    checkItems.forEach(item=> item.checked=ev.target.checked);
};

这样点击全选框的时候,就可以实现全部选中,和取消全选的功能了。效果如图:

全选与取消全选

全选和取消全选的功能完成之后,下面开始完善逐个勾选商品,直至勾选全部商品,让全选按钮自动变成被选中的状态。

要完成这个功能,我们可以通过对每个商品的复选框添加一个change事件来监听checked的变化。因此需要通过forEach()方法对遍历每一个商品。

checkItems.forEach(item=> item.onchange=ev=> {
    // 在这里处理每一项的checked值
});

此时,我们可以这样考虑:当每个商品的复选框都被勾选,即:所有商品复选框的checked的值全部为true时,全选复选框才会显示被勾选的状态,也就是全选复选框的checked的值也要为true。

由于checkAll的状态依赖于每一项商品的checked值,那么可以利用一个数组函数:Array.every()遍历每一项商品,当所有商品的checked值都为true时,every()方法的返回值就是一个true,然后再赋值给checkAll即可。注意:由于我们拿到的checkItems是一个NodeList数组,需要先将其转换成数组后再进行操作。

checkItems.forEach(item=> item.onchange=ev=> {
    checkAll.checked=Array.from(checkItems).every(checkItem=> checkItem.checked);
});

点击选中每个商品

至此,全选和单选功能全部完成了。下面开始写自动计算金额的和总数的功能。

购物车的数量和金额不仅包含每一项商品的数量和每一项商品的总金额,还包含了计算选中的商品总数,以及所有选中的商品的总金额。

下面首先完成单个商品的总金额计算,总金额=单价 * 数量,根据这个公式,我们首先拿到商品的单价和数量元素。

// 获取单价组成的数组
const priceLists=document.querySelectorAll('.price');
// 获取数量组成的数组
const numberLists=document.querySelectorAll('body input[type=number]');

以上单价(priceLists)数量(numberLists)都是NodeList类型的,需要先将它们转换成数组,由于表单中获取的内容都是string类型,而参与计算的需要的是整型,所以这里需要进行一下转换,使用parseInt()方法即可。

// 获取商品单价组成的数组
const priceLists=document.querySelectorAll('.price');
const priceArr=Array.from(priceLists).map(item=> parseInt(item.textContent)); // [ 4799, 3999, 18999, 5999, 9699 ]
// 获取商品数量组成的数组
const numberLists=document.querySelectorAll('body input[type=number]');
const numbersArr=Array.from(numberLists).map(item=> parseInt(item.value)); // 默认值:[ 1, 1, 1, 1, 1 ]

注意:商品价格和商品数量在取值时有些不同。商品的单价是普通元素直接使用textContent即可拿到它内部的值,而数量这个用的是表单控件,所以需要使用value才可以拿到值。 我刚开始写这个功能的时候懵逼了半天,此处一定要注意。

拿到商品的单价和数量之后就可以按照上面的公式进行计算了,由于商品的价格和商品的数量都是一个数组,并且价格和数量在数组中都是一一对应的关系,因此可以使用JS数组的reduce()方法进行遍历。

let amountArr=[priceArr, numbersArr].reduce((prev, curr)=> {
    return prev.map((item, index)=> {
        return item * curr[index];
    });
});

总感觉上述写法有点怪怪的,是不是可以进行简化呢?根据箭头函数的特征,当只有一条返回语句的时候可以省略掉return关键字和大括号,因此上述方法可以简写成下面这样:

let amountArr=[priceArr, numbersArr].reduce((prev, curr)=> prev.map((item, index)=> item * curr[index]));
console.log(amountArr); // [ 4799, 3999, 18999, 5999, 9699 ]

(PS:上面的方法我一开始也没有发现可以简写,我是把代码发给我朋友看了之后,朋友给我点醒了。还是才疏学浅呀。)

这时已经计算出来了每个商品的总金额,那么我们将其渲染到页面中。

// 获取单个商品总金额的元素数组
const amountDOM=document.querySelectorAll('.amount');
amountDOM.forEach((item, index)=> item.textContent=amountArr[index]);

计算每个商品的金额并渲染到页面中

单个商品的总金额渲染到页面之后,下面就开始计算商品的总数,和总金额了。根据某东、某宝的购物车功能,我们可以发现,总计那里统计的商品总数是一般是我们勾选上的商品总数,总金额也是一样的,那么我们就需要根据商品的状态来进行计算了。

首先声明一个数组,用于存储被选中的商品的状态,如果被选中,值为1,未被选中,则为0。

let  isChecked=[];
checkItems.forEach(item=> isChecked.push(item.checked===true ? 1 : 0));
// 打印出商品状态值
console.log(isChecked);
  • 效果如图:

打印商品状态值

商品的状态已经记录好了,那么现在就需要统计选中的商品对应的数量了。

// 声明一个用于存储商品数量的数组,该数组的作用是用于与对应的商品的状态值的数组进行相乘,得到实际的被选中的商品的数组。
let checkedNumbers=[];
numbersArr.forEach((item, index)=> checkedNumbers.push(item * isChecked[index]));
// 打印被选中的商品的数量
console.log(checkedNumbers);

打印出选中的商品的数量数组

计算出被选中的商品数量的总数并渲染到页面中:

let checkedSum=checkedNumbers.reduce((prev, curr)=> prev + curr);
// 将获取的数量结果渲染到页面中
document.querySelector('#sum').textContent=checkedSum;

效果如上图已经出来了。

下面开始计算被选中的商品的总金额,该总金额等于上面所有被选中的商品的总金额之和。计算出结果之后渲染到页面中。

// 声明一个数组用于存储每一个被选中的商品的总金额
let checkedPrice=[];
checkedNumbers.forEach((item, index)=> checkedPrice.push(item * priceArr[index]));
// 打印被选中的每个被选中的商品总金额
console.log(checkedPrice);
// 计算被选中的商品总金额
let totalAmount=checkedPrice.reduce((prev, curr)=> prev + curr);
// 将选中的商品总金额渲染到页面中
document.querySelector('#total-amount').textContent=totalAmount;
  • 效果图:

将总金额渲染到页面

至此,关于计算单个商品的总金额以及被选中商品的数量和总金额的功能已经全部完成了,但是我们还需要实现在页面加载以及更改某个商品数量时自动计算的功能。那么就需要将上述的计算功能封装成一个函数,以便后面每一次执行计算时使用。

  • 封装后的代码如下:
function autoCalculate() {
    // 获取单价组成的数组
    const priceLists=document.querySelectorAll('.price');
    const priceArr=Array.from(priceLists).map(item=> parseInt(item.textContent));
    // 获取数量组成的数组
    const numberLists=document.querySelectorAll('body input[type=number]');
    const numbersArr=Array.from(numberLists).map(item=> parseInt(item.value));
    console.log(priceArr, numbersArr);
  
    // 由于拿到的表单里的数据都是string类型的,所以需要先将其转换成int类型,因此需要使用`map()`方法操作一下
    let amountArr=[priceArr, numbersArr].reduce((prev, curr)=> prev.map((item, index)=> item * curr[index]));
    console.log(amountArr);
    const amountDOM=document.querySelectorAll('.amount');
    amountDOM.forEach((item, index)=> item.textContent=amountArr[index]);

    // 首先声明一个数组,用于存储被选中的商品的状态,如果被选中,值为1,未被选中,则为0
    let isChecked=[];
    checkItems.forEach(item=> isChecked.push(item.checked===true ? 1 : 0));
    console.log(isChecked);
    // 声明一个用于存储是商品数量的数组,该数组的作用是:如果商品处于被选中的状态,那么就存储它真实的数量值,
    // 如果没有被选中,那么数量就是0
    let checkedNumbers=[];
    numbersArr.forEach((item, index)=> checkedNumbers.push(item * isChecked[index]));
    console.log(checkedNumbers);
    // 此时,被选中的商品的总数为:
    let checkedSum=checkedNumbers.reduce((prev, curr)=> prev + curr);
    console.log(checkedSum);
    // 将获取的数量结果渲染到页面中
    document.querySelector('#sum').textContent=checkedSum;
    // 下面开始计算被选中的商品的总金额,该总金额等于上面所有被选中的商品的总金额之和。
    // 声明一个数组用于存储每一个被选中的商品的总金额
    let checkedPrice=[];
    checkedNumbers.forEach((item, index)=> checkedPrice.push(item * priceArr[index]));
    console.log(checkedPrice);
    // 计算被选中的商品总金额
    let totalAmount=checkedPrice.reduce((prev, curr)=> prev + curr);
    // 将选中的商品总金额渲染到页面中
    document.querySelector('#total-amount').textContent=totalAmount;
}

将代码封装后我们会发现,单个商品的总金额,商品总数以及总金额的值都没了,如下图:

封装代码后的效果

这是因为,代码在第一次加载的时候并没有执行封装后的函数,因此需要加一行代码:

// 页面第一次加载的时候自动执行一次。
window.onload=autoCalculate;

这样页面中的数据在第一次加载的时候就全部都正常了。

下面完成最后一个功能:调整商品的数量,会自动计算总数和金额。该功能还是通过change事件监听某个表单数据的变化来完成。效果图下图:

  • 代码实现:
// 监听某个控件的事件,首先需要拿到控件元素。
const numInput=document.querySelectorAll('body input[type=number]');
// 上面都用了onchange来监听,这里换个方法使用addEventListener。
numInput.forEach(item=> item.addEventListener('change', autoCalculate));

但是我们会发现这里有个小bug,就是如果勾选没有选中的商品,并不会自动计算商品数量和总价,原因很简单,我们在监听单个商品选中和全选的时候根本就没有执行自动计算函数,只需要在二者的事件监听中加上自动计算的函数即可。

checkAll.onchange=ev=> {
    checkItems.forEach(item=> item.checked=ev.target.checked);
    // 解决勾选全选框不会自动计算的bug
    autoCalculate();
};
checkItems.forEach(item=> item.onchange=ev=> {
    checkAll.checked=Array.from(checkItems).every(checkItem=> checkItem.checked);
    // 解决勾选全选框不会自动计算的bug
    autoCalculate();
});

写到这里,我们购物车的所有功能都已经完成了。购物车这个模块看似不难,其实这里面的坑也是不少的,例如:

  1. 在操作获取的元素节点时,我们有时候需要将其转换成一个数组才可以使用数组函数进行操作,因为我们通过document.querySelector获取的元素并不是一个真正的数组,而是一个类数组(NodeList);
  2. 案例中使用了多个数组函数,Array.from()、Array.reduce()、Array.every()等等,由此可见,熟练掌握JS常用的数组也是非常重要的;
  3. 掌握事件监听:addEventListener;
  4. 箭头函数的简写方法;
  5. 表单事件的监听,只能通过onchange方法,千万不要使用onclick。

以上就是我个人在写这个购物车功能的全部新的,由于本人也是新手,可能还有其他更简介方便的写法,如果有问题,请各位大佬批评指正,不胜感激。

如果有刚开始学习JS的同学,想要源码的各位亲,可以关注并私信回复“购物车”即可。

单状态 JavaScript 资产

您可以通过使用 Shopify 后台中的自定义脚本框,将 JavaScript 添加到结帐的订单状态页面(以前称为“感谢”页面)。

Shopify.Checkout.OrderStatus JavaScript 资产可用于向订单状态页面添加多种类型的内容,包括:

-特定产品的备注

-单个发货方式的说明

-数字产品的下载链接。

也可以通过 ScriptTag 访问此 JavaScript 资源。

如何实现

Shopify.Checkout.OrderStatus 包含用于生成新内容的函数:

addContentBox(params)

此函数添加一个内容框,从其中传递给该函数的每个参数都呈现为单独的行。

函数内部支持 HTML5,并且您可以在函数外部使用 liquid。

示例

您可以在使用了特定的发货方式时添加内容:

{% if checkout.shipping_method.title=='Pick-up at the store' %}

Shopify.Checkout.OrderStatus.addContentBox(

'

Pick-up in store

',

'

We are open everyday from 9am to 5pm.

'

)

{% endif %}

(来源:Shopify)

以上内容属作者个人观点,不代表雨果网立场!如有侵权,请联系我们。

相关链接:Shopify后台怎么复制现有订单?Shopify复制现有订单操作一览