商品上新业务状态机接入实践

一、商品上新业务介绍

001.png

商品上新即为在得物平台上架一个新的商品,一个完整的商品上新流程从各种不同的来源渠道提交新品申请开始,需要历经多轮不同角色的审核,主要包括:

  • 选品审核:根据新品申请提交的资料信息判定是否符合上架要求;
  • 商品资料审核:对商品资料正确和完整性的审核,包含商管、风控、法务的多轮审核;
  • 商研审核:商研审核是针对该商品在平台鉴别支持能力的判断,这也是得物业务的特色之处。

这几轮审核中,选品审核与商研审核特定归属为新品来样流程,仅在商品上新业务中出现,他决定了商品是否可在得物平台售卖;商品资料审核归属于商品资料处理流程,他决定了当前商品资料是否符合在C端展示的要求。

因此,在系统实现中,必然涉及新品来样流程和商品资料处理流程的状态流转,前者涉及新品来样表,后者主要为商品SPU主表,本文重点讨论新品来样流程的流转与状态机接入,新品来样流程的来源渠道属性非常明显,不同的渠道业务逻辑与流程都存在或大或小的区别。

二、为什么考虑接入状态机

  • 状态枚举值个数较多,且相互间的流转条件不明确,了解业务流程必须仔细研究代码,上手和维护成本高。
  • 状态的转移完全由代码随意指定,状态间随意流转存在风险。
  • 部分状态流转不支持幂等,重复操作可能造成不符合预期的后果。
  • 新增状态、修改状态流转成本高、风险大,代码修改范围不可控,测试需要全流程回归。

三、商品上新流程中涉及的状态

新品来样状态枚举

对应新品来样表的status字段,包含如下枚举值(为方便说明,进行了适度简化):

public enum NewProductShowEnum {
    DRAFT(0, "草稿"),
    CHECKING(1, "选品中"),
    UNPUT_ON_SALE_UNPASS(2, "选品不通过"),
    UNPUT_ON_SALE_PASSED(3, "商研审核中"),
    UNPUT_ON_SALE_PASSED_UNSEND(4, "商品资料待审核"),
    UNPUT_ON_SALE_PASSED_UNSEND_NOT_PUT(5, "鉴别不通过"),
    UNPUT_ON_SALE_PASSED_SEND(6, "请寄样"),
    SEND_PRODUCT(7, "商品已寄样"),
    SEND_PASS(8, "寄样鉴别通过"),
    SEND_REJECT(9, "寄样鉴别不通过"),
    GONDOR_INVALID(10, "作废"),
    FINSH_SPU(11, "新品资料审核通过"),
}

SPU状态枚举

对应商品SPU主表的status字段,包含如下枚举值(为方便说明,进行了适度简化):

public enum SpuStatusEnum {
    OFF_SHELF(0, "下架"),
    ON_SHELF(1, "上架"),
    TO_APPROVE(2, "待审核"),
    APPROVED(3, "审核通过"),
    REJECT(4, "审核不通过"),
    TO_RISK_APPROVE(8, "待风控审核"),
    TO_LEGAL_APPROVE(9, "待法务审核"),
}

商品上新业务流程中涉及SPU的状态流转部分,以商品的状态流转为准,商品状态流转也进行了状态机接入(但不是本文讨论的内容),本文将主要讨论新品来样表status的状态流转。

四、新品来样所有事件

  • 保存新品草稿
  • 提交新品申请
  • 选品通过
  • 选品不通过
  • 选品驳回后重新提交
  • 发起商研审核
  • 商研审核-支持鉴别
  • 商研审核-不支持鉴别
  • 商研审核-商品信息有误
  • SPU审核驳回超过X天
  • 发起寄样
  • 寄样进度更新

共12个。

五、新品来样状态流转

上文提到,不同的商品来源渠道对应的上新流程有所差别,这意味着不同渠道的状态流转也是不同的,以下为B端卖家渠道示意:

002.png

图中橙色方框代表新品来样状态,绿色方框代表SPU状态,蓝色圆角框代表触发状态变更的事件,有箭头连线的地方代表可以从当前状态流转到下一状态。

注意某些事件触发时,需要流转到的目标状态不是固定的,需要经过一系列的逻辑判断,才能决定最终要流转到的目标状态。

六、状态机技术选型

选择Spring StateMachine作为实际使用的状态机框架,具体过程和细节可参考这篇文章得物商品状态体系介绍,本文不再详述。

七、状态机接入面临的困难

目前新品来样的代码中还面临着不同渠道之间代码耦合的问题,需要在本次接入中一起解决,否则状态机接入的成本会很高,质量也难以保证,后续维护更加困难。即使理想状态下经过了上述的状态机的改造,不进行其他改造,还会存在两方面的问题:

  • 对目标状态判断逻辑的耦合;
  • 实际执行动作的耦合。

可以简单理解为状态机的guard(判断是否满足执行前提条件)和action(实际执行的动作)的实现里有一个超大的接口,里面包含了所有渠道间不同的判断目标状态、执行不同的action的代码,想从中了解到某个渠道具体做了什么事阅读起来非常困难。

问题集中反映在新品来样的选品审核、商研审核接口的代码中(这部分也是新品来样业务逻辑最多最复杂的部分),它夹杂了所有渠道所有通过不通过的逻辑、选品和商研的逻辑,全部糅合在一起,代码冗长且可读性不好,同时还存在大事务的问题(事务中多次RPC调用),因此在状态机接入的同时需要将这些代码进行拆分和合并,具体包括:

  • 不同渠道的代码使用策略模式拆分;
  • 不同状态、不同的操作事件处理逻辑归纳到状态机不同状态&事件的guard和action类中;
  • 对不同渠道中相同的代码处理逻辑封装成一个个的代码模块,在各自渠道中调用。

总体的改造方式如下图所示:

003.png

八、预期收益

从上文可以了解到,虽然是状态机接入,实际上是要完成两方面的改造,一是完成对整个上新流程中分渠道、分操作的业务代码的解耦,这部分的改造,能够:

  • 解决之前新品申请链路中的大事务问题,如:提交报名、新品审核;
  • 各商品来源渠道之间业务隔离,代码变更范围更加可控,更利于测试;
  • 提高代码的可扩展性,降低代码理解门槛,提高日常需求的迭代开发效率。

二是状态机的接入,可以解决新品来样流程中的状态流转问题,包括:

  • 统一集中管理状态变更规则,便于学习上手和后期维护;
  • 避免不合法、重复的状态流转;
  • 新增状态、状态流程之间的顺序调整变得更容易,代码修改更可控。

九、详细设计

按渠道拆分的合理性

从不同的商品来源渠道发起新品来样,是不同的角色通过不同的端来提交新品的过程,角色和端的组合是固定的,并不能随意组合,单独看角色或者端,并不具备共同的业务特征,只有特定的角色X端确定了才能确定一个完整的业务流程。

每个渠道的新品申请的能力也是不同的,比如商家对商品信息的掌握是最完整的,因此新品申请时就可以填写一个完整的商品资料,并且业务流程也比其他渠道多,相比而言App端仅能填写很少的商品信息,一旦申请被拒绝了就不能再修改提交了。因此不同渠道之间的差异是天然存在的,并且受制于渠道本身可能会一直存在下去。

因此在部分操作下按渠道拆分是有一定合理性和必要性的。

业务操作按渠道解耦

业务操作通用接口

新品来样中的很多重要节点的单条记录(批量操作也会转成单条处理)业务操作(比如提交新品申请、选品审核、商研审核)都可以抽象成“请求预处理 -> 操作校验 -> 执行业务逻辑 -> 持久化操作 -> 相关后处理动作”,因此设计一个通用的接口类来承载新品来样不同渠道不同业务操作的执行流程:

public interface NspOperate<C> {

    /**
     * 支持的商品来源渠道
     * @return
     */
    Integer supportApplyType();

    /**
     * 支持的操作类型
     * @return
     */
    String operateCode();

    /**
     * 请求预处理
     * @param context
     */
    void preProcessRequest(C context);

    /**
     * 校验
     * @param context
     */
    void verify(C context);

    /**
     * 执行业务逻辑
     * @param context
     */
    void process(C context);

    /**
     * 执行持久化
     * @param context
     */
    void persistent(C context);

    /**
     * 后处理
     * @param context
     */
    void post(C context);
}

一些说明:

  • 后续状态机的每个事件都与该接口的操作类型一一对应。 此外,还可以定义其他操作类型,用于不涉及状态流转的场景(比如:编辑新品申请、根据新品申请创建SPU)。
  • process方法的定义较为宽泛,在不同的业务操作中,实际执行的内容可能区别很大,比如提交新品审核可能只做一些数据组装的动作,而商研审核中则需要对本次操作后的目标状态进行判断。因此子类可以基于自己的业务需要,再进一步拆分定义新的待实现方法。
  • persistent持久化方法单独定义出来,是为了支持只在该方法上加事务,目前系统的代码中其实也有类似的设计,但事务加的太宽泛,包括了校验、业务处理等整个执行流程,中间可能包含了各种RPC调用,这也是导致大事务的其中一个重要原因,因此这里明确该方法的实现只有读写DB操作,不包含任何业务逻辑。
  • 每一个该接口的实现以“商品来源渠道+操作类型”形成唯一键进行Spring Bean的管理,同时为了兼顾有些操作是不区分商品来源的,故允许定义一个特殊的applyType(比如-1)代表当前实现支持所有渠道。在获取实现时,优化获取当前渠道的实现,找不到则尝试查找全渠道的实现:
public NspOperate getNspOperate(Integer applyType, String operateCode) {
    String key = buildKey(applyType, operateCode);
    NspOperate nspOperate = operateMap.get(key);
    if (Objects.isNull(nspOperate)) {
        String generalKey = buildKey(-1, operateCode);
        nspOperate = operateMap.get(generalKey);
    }
    AssertUtils.throwIf(Objects.isNull(nspOperate), "NspOperate not found! key = " + key);
    return nspOperate;
}

业务操作实现类

根据目前的业务场景,为了便于部分代码的重用,对业务操作的实现最多有3层继承关系:

004.png

  • 第一层:对操作类型(业务事件)聚合的维度,比如商研审核,可以在这里定义商研审核中共用的代码、自定义方法,比如:商研审核通用的入参校验,字段非空之类。
  • 第二层:具体到操作类型维度(业务事件),比如商研审核-支持鉴别、商研审核-不支持鉴别等,这里可以定义操作类型维度下所有商品来源渠道的公共代码。比如:不支持鉴别时原因必填,商研审核调用多个系统的一连串的判断逻辑。
  • 第三层:具体到商品来源渠道级别的具体实现,可以复用父类中的代码。

并不是每种业务操作都需要有这3层实现,实际使用中三种情况都会出现,比如:

  • 只有一层:新品来样作废,与商品来源渠道无关,所有渠道都使用相同逻辑,只有一个实现类即可。
  • 只有两层:提交新品申请,区分到不同的商品来源渠道即可。
  • 有三层:新品商研审核,商研审核下还分多种操作类型(业务事件),如:商研审核-支持鉴别、商研审核-不支持鉴别、商研审核-发起寄样等,每种操作类型下各个商品来源渠道有各自的实现。

状态机接入

状态机定义

从上文的状态流转图来看,新品来样的状态流转还是比较清楚的,但实际上每个渠道的状态流程都会出现一些细小的差别,为避免来源渠道拆分的不彻底,也综合考虑到状态机配置的成本不高,因此决定每个渠道构建自己的状态机配置。

以C端渠道为例,状态机的配置如下:

@Configuration
@Slf4j
@EnableStateMachineFactory(name = "newSpuApplyStateMachineFactory")
public class NewSpuApplyStateMachineConfig extends EnumStateMachineConfigurerAdapter<NewProductShowEnum, NewSpuApplyStateMachineEventsEnum> {

    public final static String DEFAULT_MACHINEID = "spring/machine/commodity/newspuapply";

    @Resource
    private NewSpuApplyStateMachinePersist newSpuApplyStateMachinePersist;

    @Resource
    private NspNewApplyAction nspNewApplyAction;

    @Resource
    private NspNewApplyGuard nspNewApplyGuard;

    @Bean
    public StateMachinePersister<NewProductShowEnum, NewSpuApplyStateMachineEventsEnum, NewSpuApplySendEventContext> newSpuApplyMachinePersister() {
        return new DefaultStateMachinePersister<>(newSpuApplyStateMachinePersist);
    }

    @Override
    public void configure(StateMachineConfigurationConfigurer<NewProductShowEnum, NewSpuApplyStateMachineEventsEnum> config) throws Exception {
        config.withConfiguration().machineId(DEFAULT_MACHINEID);
    }

    @Override
    public void configure(StateMachineStateConfigurer<NewProductShowEnum, NewSpuApplyStateMachineEventsEnum> config) throws Exception {
        config.withStates()
                .initial(NewProductShowEnum.STM_INITIAL)
                .state(NewProductShowEnum.CHECKING)
                .state(NewProductShowEnum.UNPUT_ON_SALE_UNPASS)
                .state(NewProductShowEnum.UNPUT_ON_SALE_PASSED_UNSEND)
                .state(NewProductShowEnum.UNPUT_ON_SALE_PASSED)
                .choice(NewProductShowEnum.STM_UNPUT_ON_SALE_PASSED_UNSEND)
                .choice(NewProductShowEnum.STM_UNPUT_ON_SALE_PASSED)
                .state(NewProductShowEnum.UNPUT_ON_SALE_PASSED_UNSEND_NOT_PUT)
                .state(NewProductShowEnum.OTHER_UNPASS_FOR_SPU_STUDYER)
                .state(NewProductShowEnum.FINSH_SPU)
                .state(NewProductShowEnum.GONDOR_INVALID)
                .states(EnumSet.allOf(NewProductShowEnum.class));
    }

    @Override
    public void configure(StateMachineTransitionConfigurer<NewProductShowEnum, NewSpuApplyStateMachineEventsEnum> transitions) throws Exception {

        transitions.withExternal()
                //提交新的新品申请
                .source(NewProductShowEnum.STM_INITIAL)
                .target(NewProductShowEnum.CHECKING)
                .event(NewSpuApplyStateMachineEventsEnum.NEW_APPLY)
                .guard(nspNewApplyGuard)
                .action(nspNewApplyAction)

                //选品不通过
                .and().withExternal()
                .source(NewProductShowEnum.CHECKING)
                .target(NewProductShowEnum.UNPUT_ON_SALE_UNPASS)
                .event(NewSpuApplyStateMachineEventsEnum.OM_PICK_REJECT)
                .guard(nspOmRejectGuard)
                .action(nspOmRejectAction)

                //选品通过
                .and().withExternal()
                .source(NewProductShowEnum.CHECKING)
                .target(NewProductShowEnum.UNPUT_ON_SALE_PASSED_UNSEND)
                .event(NewSpuApplyStateMachineEventsEnum.OM_PICK_PASS)
                .guard(nspOmPassGuard)
                .action(nspOmPassAction)

                //发起商研审核
                .and().withExternal()
                .source(NewProductShowEnum.UNPUT_ON_SALE_PASSED_UNSEND)
                .target(NewProductShowEnum.STM_UNPUT_ON_SALE_PASSED_UNSEND)
                .event(NewSpuApplyStateMachineEventsEnum.START_BR_AUDIT)

                .and().withChoice()
                .source(NewProductShowEnum.STM_UNPUT_ON_SALE_PASSED_UNSEND)
                .first(NewProductShowEnum.UNPUT_ON_SALE_PASSED, nspStartBrAuditWaitAuditStatusDecide, nspStartBrAuditWaitAuditChoiceAction)
                .then(NewProductShowEnum.UNPUT_ON_SALE_PASSED_UNSEND_NOT_PUT, nspStartBrAuditRejctStatusDecide, nspStartBrAuditRejctChoiceAction)
                .last(NewProductShowEnum.FINSH_SPU, nspStartBrAuditFinishChoiceAction)

                //商研审核-支持鉴别
                .and().withExternal()
                .source(NewProductShowEnum.UNPUT_ON_SALE_PASSED)
                .target(NewProductShowEnum.FINSH_SPU)
                .event(NewSpuApplyStateMachineEventsEnum.BR_HUMAN_AUDIT_SUPPORT_ALL)
                .guard(nspBrAuditSupportAllGuard)
                .action(nspBrAuditSupportAllAction)

                //商研审核-商品信息有误
                .and().withExternal()
                .source(NewProductShowEnum.UNPUT_ON_SALE_PASSED)
                .target(NewProductShowEnum.OTHER_UNPASS_FOR_SPU_STUDYER)
                .event(NewSpuApplyStateMachineEventsEnum.BR_HUMAN_AUDIT_WRONG_INFO)
                .guard(nspBrAuditWrongInfoGuard)
                .action(nspBrAuditWrongInfoAction)

                //商研审核-不支持鉴别
                .and().withExternal()
                .source(NewProductShowEnum.UNPUT_ON_SALE_PASSED)
                .target(NewProductShowEnum.UNPUT_ON_SALE_PASSED_UNSEND_NOT_PUT)
                .event(NewSpuApplyStateMachineEventsEnum.BR_HUMAN_AUDIT_SUPPORT_NONE)
                .guard(nspBrAuditRejectGuard)
                .action(nspBrAuditRejectAction)
        ;
    }
}

状态机的状态与新品来样DB表中的status字段完全映射,状态机事件与上文图中的事件完全匹配。 新品来样中有一些收到事件后需要经过一系列逻辑判断才能得出目标状态的场景,这里会借助状态机的Choice State,完成对目标状态的判断和流转。

明确一下状态机相关的元素哪些是独立拆分的,哪些是共用的:

005.png

可以看到只有状态机的配置类是每个渠道不同的,因此成本不高。guard和action的实现类如何实现所有渠道共用会在下文说明。

Guard与Action的实现

从上文状态机的具体配置中可以看到,新品来样流程中涉及两类状态流转:

  • 触发事件后的目标状态是固定的,比如选品审核时触发了选品不通过事件,新品申请的目标状态将确定为选品不通过;
  • 触发事件后的目标状态需要经过代码逻辑判断,为此状态机配置中引入了choice state,比如发起商研审核的事件,新品申请的目标状态可能是直接不支持鉴别,也可能是新品申请直接通过,也可能是需要人工审核。

在Spring状态机的设计中,这两类状态流转,gurad和action承担的职责会有所差异:

006.png

因此这两类guard和action的实现逻辑会有所不同。

然而,对于同一个事件/Choice state下的guard和action,不同商品来源渠道之间是可以共用的,因为已经实现了按商品来源渠道的业务代码拆分,只需要在实现中路由到具体的NspOperate业务实现类即可。下面给出示例:

目标状态固定的guard:

@Component
public class NspNewApplyGuard extends AbstractGuard<NewProductShowEnum, NewSpuApplyStateMachineEventsEnum, NewSpuApplySendEventContext> {

    @Resource
    private NewSpuApplyOperateHelper newSpuApplyOperateHelper;

    @Override
    protected boolean process(StateContext<NewProductShowEnum, NewSpuApplyStateMachineEventsEnum> context) {

        final CatetorySendEventContextRequest<NewSpuApplyContext> request = getSendEventContext(context).getRequest();
        NewSpuApplyContext ctx = request.getParams();
        Integer applyType = ctx.getApplyType();     //从业务数据中取出商品来源
        
        NspOperate<NewSpuApplyContext> nspOperate = newSpuApplyOperateHelper.getNspOperate(applyType, NewSpuApplyStateMachineEventsEnum.NEW_APPLY.getCode());   //固定的事件code
        //做请求的预处理
        nspOperate.preProcessRequest(ctx);
        //对业务数据做校验,校验不通过即抛出异常
        nspOperate.verify(ctx);

        //正常执行完上述2个方法,代表是可以执行的
        return Boolean.TRUE;
    }
}

guard中只需根据商品来源和固定的事件code获取到NspOperate实现类,并调用NspOperate的preProcessRequest和verify方法完成校验即可。

目标状态固定的action:

@Component
public class NspNewApplyAction extends AbstractSuccessAction<NewProductShowEnum, NewSpuApplyStateMachineEventsEnum, CategorySendEventContext> {

    @Resource
    private NewSpuApplyOperateHelper newSpuApplyOperateHelper;

    @Override
    protected void process(StateContext<NewProductShowEnum, NewSpuApplyStateMachineEventsEnum> context) {
        final CatetorySendEventContextRequest<NewSpuApplyContext> request = getSendEventContext(context).getRequest();
        NewSpuApplyContext ctx = request.getParams();
        Integer applyType = ctx.getApplyType();    //从业务数据中取出商品来源
        
        NspOperate<NewSpuApplyContext> nspOperate = newSpuApplyOperateHelper.getNspOperate(applyType, NewSpuApplyStateMachineEventsEnum.NEW_APPLY.getCode());   //固定的事件code

        //执行业务逻辑
        nspOperate.process(ctx);
        //持久化
        nspOperate.persistent(ctx);
        //后处理
        nspOperate.post(ctx);
    }
}

action中同样根据商品来源和固定的事件code获取到NspOperate实现类,并调用NspOperate的后几个方法完成业务操作。

Choice state中的guard:

guard需要根据当前渠道和事件做目标状态的判定,这里单独抽象出一个接口供guard实现调用,NspOperate中如果需要用到类似逻辑也可以引用这个单独的接口,因此不会有代码重复:

public interface NspStatusDecider<C, R> {

    /**
     * 支持的商品来源渠道
     * @return
     */
    Integer supportApplyType();

    /**
     * 支持的操作类型
     * @return
     */
    String operateCode();

    /**
     * 判定目标状态
     * @param context
     */
    R decideStatus(C context);
}
@Component
public class NspBrAuditNoIdentifyGuard extends AbstractGuard<NewProductShowEnum, NewSpuApplyStateMachineEventsEnum, NewSpuApplySendEventContext> {

    @Resource
    private NewSpuApplyOperateHelper newSpuApplyOperateHelper;

    @Override
    protected boolean process(StateContext<NewProductShowEnum, NewSpuApplyStateMachineEventsEnum> context) {

        final CatetorySendEventContextRequest<NewSpuApplyContext> request = getSendEventContext(context).getRequest();
        NewSpuApplyContext ctx = request.getParams();
        Integer applyType = ctx.getApplyType();     //从业务数据中取出商品来源

        NspStatusDecider<NewSpuApplyContext, Result> nspStatusDecider = newSpuApplyOperateHelper.getNspStatusDecider(applyType, NewSpuApplyStateMachineEventsEnum.BR_HUMAN_AUDIT_SUPPORT_NONE.getCode());   //固定的事件code
        //判定目标状态
        Result result = nspStatusDecider.decideStatus(ctx);
        ctx.setResult(result);  //将判定结果放入上下文,其他的guard可以引用结果,避免重复判断
        
        return Result.isSuccess(result);    //根据判定结果决定是否匹配当前guard对应的目标状态
    }
}

Choice state中的action:

@Component
public class NspBrAuditNoIdentifyAction extends AbstractSuccessAction<NewProductShowEnum, NewSpuApplyStateMachineEventsEnum, CategorySendEventContext> {

    @Resource
    private NewSpuApplyOperateHelper newSpuApplyOperateHelper;

    @Override
    protected void process(StateContext<NewProductShowEnum, NewSpuApplyStateMachineEventsEnum> context) {
        final CatetorySendEventContextRequest<NewSpuApplyContext> request = getSendEventContext(context).getRequest();
        NewSpuApplyContext ctx = request.getParams();
        Integer applyType = ctx.getApplyType();    //从业务数据中取出商品来源

        NspOperate<NewSpuApplyContext> nspOperate = newSpuApplyOperateHelper.getNspOperate(applyType, NewSpuApplyStateMachineEventsEnum.BR_HUMAN_AUDIT_SUPPORT_NONE.getCode());   //固定的事件code

        //做请求的预处理
        nspOperate.preProcessRequest(ctx);
        //对业务数据做校验
        nspOperate.verify(ctx);
        //执行业务逻辑
        nspOperate.process(ctx);
        //持久化
        nspOperate.persistent(ctx);
        //后处理
        nspOperate.post(ctx);
    }
}

与目标状态固定的action的唯一不同在于多执行了NspOperate的preProcessRequest和verify方法。

不根据不同渠道间使用不同的guard和action实现,而使用单独的策略类来划分不同的渠道实现,出于下面两点考虑:

  • 有更换状态机实现的可能,因此不希望状态机实现相关的代码与业务逻辑代码耦合;
  • 不涉及状态机的场景,同样存在按渠道拆分逻辑的需要,比如新品申请编辑等等。

商品上新过程中与SPU状态流转的联动

当新品来样进入“商品资料待审核”状态之后,将由SPU状态机流程接管后续SPU的状态流转,直至SPU状态抵达“审核通过”后,新品来样状态流转到商研审核阶段。在这期间,SPU的每次信息和状态变更都需要通知到新品来样(通过MQ或应用内event),再对新品来样记录做对应的业务处理。

后续扩展分析

对于日后新品申请流程中可能涉及的变更,评估本次改造的扩展性。

新增商品来源渠道

配置新的状态机,针对新渠道实现各种业务操作和事件的实现即可,不会影响到现有渠道。

新品来样新增状态节点

修改状态机配置,增加新的事件和对应的实现类即可。

新品来样调整状态间顺序

修改状态机配置,评估涉及的业务操作实现类的修改,修改范围是明确和可控的。

十、小结

我们通过策略模式将不同商品来源渠道的业务逻辑解耦,保留共性,各自实现自己的差异化逻辑,为未来的业务需求变更提供扩展性;通过状态机的引入明确和规范了新品流程中的状态流转,确保状态正确、合法地流转,同时为未来的业务流程的变更打下坚实的基础。

本次改造一方面解决了目前实现中的顽疾,降低了现有代码的上手难度,另一方面也兼顾了开发效率,后续不管是新增来源渠道或是修改业务流程,都可以保障代码修改范围的可控、可测,也不会增加额外的工作量,能够更有效、更安全稳定地支撑业务。

*文/甜橙

本文属得物技术原创,更多精彩文章请看:得物技术官网

未经得物技术许可严禁转载,否则依法追究法律责任!

  • 18
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
第一部 MySQL基础入门(21节) 01-老男孩运维DBA课程开班.avi 02-数据库概念介绍.avi 03-数据库种类介绍及关系型数据库原理.avi 04-非关系型数据库介绍.avi 05-非关系型数据库种类介绍.avi 06-关系型数据库产品介绍01.avi 07-关系型数据库产品介绍02.avi 08-非关系型数据库产品介绍01.avi 09-数据库产品排名及发展大事记.avi 10-MySQL数据库产品介绍.avi 11-数据库基础内容小结.avi 12-为什么选择MySQL数据库即MySQL优势介绍.avi 13-MySQL数据库分类与版本升级知识讲解.avi 14-MySQL数据库商业版与社区版区别.avi 15-MySQL数据库的发布版本知识讲解.avi 16-MySQL数据库发展的三条产品线介绍.avi 17-MySQL数据库发布版本命名知识介绍.avi 18-企业生产场景如何选择MySQL产品线产品及对应版本.avi 19-MySQL数据库企业生产常用5种安装方法介绍.avi 20-如何选择MySQL数据库版本及安装方式.avi 21-MySQL5.5编译方式安装实战.avi 第二部 MySQL多实例安装与企业应用场景(10节) 22-MySQL数据库多实例介绍.avi 23-MySQL数据库多实例的优势和问题介绍.avi 24-MySQL数据库多实例的门户企业应用场景.avi 25-MySQL数据库多实例的多种配置方案介绍.avi 26-MySQL数据库多实例安装实战讲解.avi 27-MySQL数据库多实例初始化及服务启动.avi 28-MySQL数据库多实例启动问题排错方法及实战排错.avi 29-MySQL数据库多实例实战排错02.avi 30-增加MySQL数据库第三个实例实战讲解.avi 31-MySQL数据库多实例的其他故障问题说明.avi 第三部:MySQL应用管理及进阶实战操作(29节) 01-MySQL启动与关闭深入知识及原理.avi 02-登录MySQL知识深入讲解.avi 03-学会使用MySQL数据库的帮助.avi 04-修改mysql密码多方法实战及注意-排错.avi 05-找回丢失的mysql密码实战.avi 06-多实例找回丢失的mysql密码.avi 07-SQL语言介绍及分类详解.avi 08-创建不同字符集数据库及企业场景应用讲解.avi 09-显示-连接-删除等数据库的库管理应用讲解.avi 10-创建mysql用户及授权的多种方法实战.avi 11-本地及远程主授权思想及多种方法实战.avi 12-自己动手实战确定MySQL的授权权限列表.avi 13-生产场景如何授权用户及权限讲解.avi 14-MySQL建表语句及表的知识.avi 15-查看表结构以及建表语句.avi 16-索引的知识及索引创建多种方法实战.avi 17-索引生效的基本条件说明.avi 18-主键-唯一-普通索引创建及删除小结.avi 19-DML语句之insert知识讲解.avi 20-DQL之select知识实战讲解.avi 21-DQL之select多表连表查询实战.avi 22-利用explain查看sql语句的执行计划.avi 23-DML之修改表中的记录实战.avi 24-小试牛刀初步增量恢复MySQL数据实战.avi 25-某企业运维全套面试题解答.avi 26-DML之修改表及企业严重故障案例解决实战.avi 27-删除表中的数据企业故障案例及防止方法.avi 28-增删改表的字段实战详解.avi 29-更改表名及删除表实战讲解.avi 第四部 MySQL乱码问题及字符集实战(14节) 01-MySQL数据乱码及多种解决方案实战1.avi 02-MySQL数据乱码及多种解决方案实战2.avi 03-MySQL不乱码5种方法及不乱码根源深入详解.avi 04-下节内容及课后作业说明.avi 05-课前思想-17期学生23万入职BAT公司 06-课前思想-及课前考试题说明.avi 07-MySQL字符集介绍及生产常用字符集说明.avi 08-MySQL不同字符集区别及企业生产选择.avi 09-MySQL乱码原因开始系统影响揭秘.avi 10-MySQL上执行set names到底做了什么?.avi 11-MySQL命令带字符集导入到底做了什么?.avi 12-MySQL数据中文不乱码终极实战.avi 13-MySQL数据中文不乱码终极大总结.avi 14-更改生产线上MySQL数据字符集的方案讲解.avi 第五部 MySQL备份、增量备份及数据恢复基础实战(12节) 01-备份数据的意义及重要性.avi 02-mysqldump多个命令参数备份实战.avi 03-mysqldump逻辑备份的工作原理.avi 04-备份多个库的思想及多种方法讲解.avi 05-mysql单多表的备份实战及脚本批量备份讲解.avi 06-只备份MySQL表结构及只备份MySQL数据.avi 07-企业生产场景不同引擎备份命令参数详解.avi 08-利用source恢复mysql数据讲解及实战.avi 09-分库备份后mysql如何分库恢复实战.avi 10-mysql进程-状态-在线修改参数重要知识讲解.avi 11-mysqlbinlog命令介绍及实战讲解.avi 12-mysqldump-master-data参数答疑详解.avi 第六部 MySQL主从复制原理及实战部署(10节) 01-由架构因为引出主从复制的作用及重要性.avi 02-文件及DB各种同步方案大集合介绍讲解.avi 03-mysql主从复制介绍及分布式数据库架构实现介绍.avi 04-主从同步的应用场景及切换从库不丢数据多方案介绍.avi 05-mysql数据库读写分离介绍及企业生产实现方案.avi 06-根据企业业务拆分业务应用到不同的从库思想.avi 07-mysql主从复制原理画图深入浅出讲解.avi 08-mysql主从复制实战01.avi 09-mysql主从复制实战02.avi 10-主从复制的超简单实现方案说明.avi 第七部 MySQL主从复制企业级深入高级应用实战(20节) 01-MySQL主从复制原理画图口头考试讲解.avi 02-课前思想-企业面试必胜绝招讲解.avi 03-MySQL主从复制故障多个案例实战讲解.avi 04-回顾MySQL主从复制配置步骤及注意事项.avi 05-回顾MySQL主从复制原理要点.avi 06-企业场景MySQL主从复制最牛部署方案.avi 07-一键获取全备及binlog位置多种方案讲解.avi 08-不停主库一键批量配置MySQL主从复制方案.avi 09-MySQL主从复制3个线程状态信息讲解.avi 10-MySQL主从复制读写分离授权多种方案案例详解.avi 11-忽略MySQL主从复制主库授权表同步实战.avi 12-MySQL主从复制指定不同库表同步参数说明.avi 13-MySQL主从复制从库只读案例实战及原理总结.avi 14-企业生产场景MySQL主从复制故障原因及实战解决.avi 15-从库开启BINLOG原因及开启实战讲解1.avi 16-从库开启BINLOG原因及开启实战讲解2.avi 17-企业场景一主多从宕从库切换主继续和从同步过程详解1.avi 18-企业场景一主多从宕从库切换主继续和从同步过程详解2.avi 19-企业场景一主多从宕从库宕解决.avi 20--企业场景-双主及多主同步过程详解及企业选用方案说明.avi 第八部-老男孩MySQL备份与恢复实战案例及生产方案(10节) 01-全量及增量备份概念及优缺点详解 02-不同企业如何选择备份方案及备份需要恢复的场景分析 03-企业场景数据库需要增量恢复的条件分析 04-MySQL数据恢复的必要条件及恢复案例场景图解分析 05-实战模拟企业数据丢失应用的案例场景 06-MySQL数据库增量恢复详细实战 07-更多MySQL数据库增量恢复大讨论及大总结 08-MySQL增量恢复小结及核心恢复思想 09-课后自学能力作业-务必完成并分享讲解 10-内部学员技术大会分享课表说明 第九部-老男孩MySQL服务日志详细介绍及增量恢复命令实践(7节) 01-mysqlbinlog命令介绍及实战讲解 02-mysqldump-master-data参数答疑详解 03-MySQL服务错误日志介绍及实践 04-MySQL服务普通查询日志介绍及实践 05-MySQL服务慢查询日志介绍及实践 06-MySQL服务二进制日志的3种工作模式详细介绍 07-MySQL服务二进制日志的3种工作模式配置方法实战 第十部-老男孩MySQL常用引擎及优缺点-应用场景-调优详解(14节) 01-MySQL服务存储引擎介绍 02-MySQL服务存储引擎体系结构 03-MySQL服务MyISAM引擎介绍及磁盘文件格式 04-MySQL服务事务详细介绍及ACID特性说明 05-MySQL服务MyISAM引擎特点讲解 06-MySQL服务MyISAM引擎适合的生产应用场景 07-MySQL服务MyISAM引擎调优精要 08-MySQL服务InnoDB引擎介绍及磁盘文件格式 09-MySQL服务InnoDB引擎特点讲解 10-MySQL服务InnoDB引擎适合的生产应用场景 11-MySQL服务InnoDB引擎调优及不同引擎功能对比 12-MySQL服务引擎种类功能及对应参数配置说明 13-批量修改MySQL服务引擎的多种方案 14-有关MySQL服务存储引擎的面试题等说明 第十一部 MySQL读写分离开发实现及软件实现-物理备份-高可用(已经包含有5节视频+文档资料) 01-amoeba读写分离实现技术分享.avi 02-mysql-proxy读写分离实现技术分享.avi 03-PHP程序实现读写分离技术分享.avi 04-xtrabackup热备工具技术分享.avi 05-mysql-mmm高可用实现技术分享.avi 文档资料 01-mysql-mmm高可用架构-王雄.rar 02-mysql半主从同步-技术分享--余宏刚.rar 03-xtrabackup安装及操作文档-技术分享--王玉晓.rar 04-mysql主从通过mysql-proxy程序实现读写分离.doc 老男孩linux测试读写分离php网站源代码.zip 第十二部 MySQL高可用工具heartbeat实战(33节) 01-heartbeat介绍与作用.avi 02-Heartbeat的工作原理与服务切换条件.avi 03-Heartbeat心跳连接介绍.avi 04-裂脑的概念讲解.avi 05-裂脑发生的多种原因介绍.avi 06-防止裂脑发生的8种秘籍01.avi 07-防止裂脑发生的8种秘籍02.avi 08-防止裂脑发生的8种秘籍03.avi 09-有关fence设备和仲裁制说明.avi 10-heartbeat的消息类型介绍.avi 11-heartbeat的IP接管和故障转移原理.avi 12-高可用软件的IP配置管理技术讲解.avi 13-heartbeat的默认路径配置介绍.avi 14-heartbeat的软件版本分支说明.avi 15-heartbeat企业应用场景介绍.avi 16-部署heartbeat高可用业务需求描述.avi 17-模拟服务器准备及IP配置说明.avi 18-配置主名及服务器hosts文件解析.avi 19-配置服务器间心跳的连接.avi 20-安装heartbeat软件及配置文件讲解.avi 21-heartbeat的主要配置文件参数讲解.avi 22-实战配置heartbeat配置文件.avi 23-启动heartbeat服务并查看配置的VIP.avi 24-发生裂脑故障及根据前面原理排查思路.avi 25-通过heartbeat日志分析接管过程.avi 26-heartbeat实现web服务高可用实战案例1.avi 27-heartbeat实现web服务高可用实战案例1原理.avi 28-heartbeat实现web服务高可用案例2.avi 29-有关heartbeat调用资源的生产场景应用.avi 30-heartbeat高可用案例拓展.avi 31-heartbeat和keepalived的应用场景区别.avi 32-heartbeat服务生产环境下维护要点.avi 33-heartbeat服务生产环境维护FAQ.avi 第十三部 MySQL高可用工具drbd实战(18节) 01-drbd服务介绍.avi 02-drbd的工作原理介绍.avi 03-drbd的工作原理图.avi 04-drbd的复制协议与应用模式.avi 05-drbd的企业应用场景介绍.avi 06-常见运维同步工具介绍.avi 07-drbd的部署及ip配置规划.avi 08-drbd软件的安装配置准备.avi 09-创建同步的分区实践drbd.avi 10-drbd环境分区说明及分区命令.avi 11-实际安装drbd软件实战.avi 12-drbd相关配置列表说明.avi 13-配置drbd加载到内核模块.avi 14-drbd配置文件参数讲解及实际配置.avi 15-drbd服务初始化数据故障排查过程.avi 16-启动drbd并配置主从同步数据.avi 17-drbd服务的常见故障及排查方法.avi 18-挂载测试数据库同步及查看备节点数据.avi 第十四部 MySQL高可用综合实战(完整版32节) 01-重要课前思想mp4 02-MySQL高可用需求与架构讲解mp4 03-大规模集群架构中MySQL高可用的位置和架构mp4 04-MySQL高可用实战模拟环境描述mp4 05-MySQL高可用生产场景配置及网络地址规划mp4 06-MySQL高可用实施正式环境准备与裂脑注意mp4 07-快速安装双Centos6.4下heartbeat并配置启动mp4 08-快速安装双Centos6.4下drbd服务准备01mp4 09-快速安装双Centos6.4下drbd服务实战02mp4 10-详解drbd服务状态信息细节mp4 11-启动服务测试DRBD实际同步情况mp4 12--配合heartbeat调试drbd服务配置mp4 13-MySQL服务安装实战安装过程讲解mp4 14-一键安装Mysql多实例实战讲解与演示mp4 15-通过手工测试MySQL的高可用切换mp4 16-MySQL双主高可用自动切换实战实现mp4 17-有关MySQL双主高可用my.cnf的配置注意事项讲解mp4 18-MySQL高可用切换的重要思想原理及故障排除思想、mp4 19-MySQL主从同步一键安装脚本实战mp4 20-MySQL多主多从高可用集群自动切换实现mp4 21-有关MySQL高可用注意及常见故障讲解mp4 22-MySQL集群及高可用的更多思路方案引子mp4 23-常见MySQL集群高可用架构讲座01.mp4 24-常见MySQL集群高可用架构讲座02.mp4 25-常见MySQL集群高可用架构讲座03.mp4 26-常见MySQL集群高可用架构讲座04.mp4 27-常见MySQL集群高可用架构讲座05.mp4 28-门户分布式数据库架构案例分享01.mp4 29-门户分布式数据库架构案例分享02.mp4 30-门户分布式数据库架构案例分享03.mp4 31-门户分布式数据库架构案例分享04.mp4 32-门户分布式数据库架构案例分享05.mp4 第十五部 MySQL数据库优化思想与优化实战(9节) 1-网站打开慢mysql问题多解决方案企业案例.avi 2-MySQL索引优化要点精讲01 3-MySQL索引优化生产案例讲解02 4-linux运维人员必须掌握的核心经验案例.avi MySQL数据库生产场景核心优化精讲 5-MySQL数据库硬件优化生产方案及细节精讲01.avi 6-MySQL数据库系统优化生产方案及细节精讲02.avi 7-MySQL数据库参数索引优化生产方案及细节精讲03.avi 8-MySQL数据库SQL优化生产方案及细节精讲04.avi 9-MySQL数据库架构优化生产方案及细节精讲05.avi 第十六部 MySQL业务变更流程与安全管理思想(7节) 01-安全优化-项目开发流程及数据库更流程.avi 02-DBA参与项目数据库设计及培训开发人员高效设计MYSQL库语句.avi 03-老男孩核心思想-项目周期的制定思想.avi 04-linux进程占用cpu高的解决方案案例.avi 05-数据库账户权限控制多种思路及技巧.avi 06-数据库客户端安全控制策略详解.avi 07-数据库运维的核心管理思想.avi
关于手的一些基础知 手工作原理介绍的一编文章(续四) 手所有软件工作的流程都是在CPU的作用下进行的,具体的划分包括下文所述的5个流程。这些流程都是以软件数据的形式储于手的EEPROM和FLASHROM中. 一、开流程 当手的供电模块检测到电源开关键被按下后,会将手电池的电压转换为适合手电路各部分使用的电压值,供应给相应的电源模块,当时钟电路得到供电电压后产生震荡信号,送入逻辑电路,CPU在得到电压和时钟信号后会执行开程序,首先从ROM中读出引导码,执行逻辑系统的自检。并且使所有的复位信号置高,如果自检通过,则CPU给出看门狗(Watchdog)信号给各模块,然后电源模块在看门狗(Watchdog)信号的作用下,维持开状态。 二、上网流程 手后,既搜索广播控制信号道(BCCH)的载频。因为系统随时都向在小区中的各用户发送出用户广播控制信息。手收集搜索到最强的(BCCH)的载频。对应的载频频率后,读取频率校正信道(FCCH),使手(MS)的频率与同步。所以每一个用户的手在不同上午位置(既不同的小区)的载频是固定的,它是由GSM网络运营商组网时确定,而不是由用户的GSM手来决定。手读取同步信道(SCH)的信息后找出基地站(BTS)的任别码,并同步到超高帖TDMA的帖号上。手在处理呼叫前读取系统的信息。比如:邻近小区的情况、现在所处小区的使用频率及小区是否可以使用移动系统的国家号码和网络号码等等,这些信息都可以在以BCCH上得到手在请求接入信道(RACH)上发出接入请求信息,向系统送SIM卡帐号等信息。系统在鉴权合格后,通过允许接入信道(AGCH)使GSM手接入信道上并分 配到GSM手一个独立专用控制信道(SDCCH)。手在SDDCCH上完成登记。在满速随路控制信道(SACCH)上发出控制指令,然后手返回空闲状态,并监听BCCH和CCCH共控制信道上的信息。此时手已经做好了寻呼的准备工作。 一、 流程 用户监测BCCH时,必须与相近的基站取得同步。通过接收FCCH、SCH 、BCCH信息,用户将被锁定到系统及适应的BCCH上。 二、 呼叫流程 1、手作主叫 我们GSM系统中由手发出呼叫的情况,首先,用户在监测BCCH时,必须与相近的基站取得同步。通过接收FCCH、SCH、BCCH信息,用户将被锁定到系统及适当的BCCH上的。为了发出呼叫,用户首先要拨号,并按压GSM手的发射键。手用锁定它的基站系统的ARFCN来发射RACH数据突发序列。然后基站以CCCH上的AGCH信息来响应,CCCH为手指定一个的信道进行SDSSH连接。正在监测BCCH中T的用户,将从AGCH接收到它的ARFCN和TS安排,并立即转到的ARFCN和TS上,这一的ARFCN和TS分配就是SDCH(不是TCH)。一旦转接到SDCCH,用户首先等待传给它的SCCH(等待最大持续26或120ms)这信息告知手要求的定时提前量和发射功率。基站根据手以前的RACH传输数据能够决定出适合的定时提前量和功率级,并且通过SACCH发送适当的数据供手处理。在接收和处理完SACCH中的定时提前量信息后,用户能够发送正常的、话音业务所要的求的是突发序列消息。当PSTN从拨号端连接到MSC,且MSC将话音路径接入服务基站时,SDCCH检查用户的合法及有效性,随后在手和基站之间发送信息。几秒钟后,基站经由SDSSH告知手转向一个为TCH安排的ARFCN和TS。一旦再次接到TCH,语音信号就在前向链路上传送,呼叫成功建立,SDCCH被腾空。 2、手作被叫 当从PSTN发出呼叫时,其过程与上述过程类似。基站在BCCH适应内的Tso期间,广播一个PCH消息。锁定于相同ARFCN上的手检测对它的寻呼,并回复 一个RACH消息,以确认接收到寻呼。当网络和服务器基站连接后,基站采用CCCH上的AGCH将手分配到一个的物理信道,以便连接SDCCH和 SACCH。一旦用户在SDCCH上建立了定时提前量并获准确认后,基站就在SDCCH上面重分配物理信道,同时也确立了TCH的分配。 三、 关流程 关时,按下开关键,键盘检测模块向数字逻辑部分发出一个关请求信号,CPU既撤消开维持信号,执行关程序,供电模块撤消供电,射频和逻辑电路立即停止工作,从关。如果在开状态下强制关(取下电池)也有可能会造成内部软件故障。另外手还包含其它软件工作流程如充电流程、电池监测、键盘扫描、测试流程等。 手软件的常见故障及维修 软件故障的认识 4•4•1 什么是软件故障 在上面的节章中我们已经大概了解了软件及单片的系统,现在让我们来了解一下什么是软件的故障。在手中,会引发控制系统不正常有两种情况:一方面是软件故障 (如存储器、CPU虚焊、损坏,数据输送通道有问题);另一方面是软件数据丢失或者错乱软件问题。所以,我们定义:一切由CPU、存储器的数据,程序出现问题 (如错乱、损坏、丢失、中毒等等)而引起的各种各样的故障现象,统称为软件故障 4•1•2软件故障的表现 由软件引起的故障是千奇百怪的,但归纳起来主要的有四个方面: 1、 软件的错乱、损坏在手的屏幕显示返厂维修等信息,其主要表现有: A、显示“联系服务商(CONTACT SERVICE)” B、显示“电话无效,联系服务商(PHONE FAILED SERVICE)” C、显示“软件出错(WRONG SOFTWARE)” D、显示“请等待输入八位特别码(Please Wait To Enter Special Code)” E、显示“非法软件下载(illegal software loaded)” 2、用户自行锁但又无法开锁,所有的原厂密码均别改动,出厂开锁密码无效,这种情况也是软件故障。 3、电路正常的情况下,出现不开、不入网、定屏死,无信号,低电告警、无发射故障,也属于软件故障的范畴。 4、它的软件故障。 4•1•3 软件故障的判定 在手的故障中,有些故障是很显而易见是由软件问题而引发的,如手被锁等,只需处理软件OK!有时也可以通过开电流来判断,去手不开,我们一般可以观察 其开电流来大致判断,比如8088开在30mA左右定一会回零。对于定屏死,比如说夏A8大屏不显示,小屏定屏也属软件故障;对于能开的软件故障, 我们一般是先要观其现在的版本号,例如三星手,西门子手、TCL手等,由于老版本程序存在着教多的隐藏故障,一般采用更版进行升级。另外还要注意对应版 本的问题,比如TCL就有不同显示屏的对应版本号,波导 S1000出线“bad software”(软件坏了)此故障多是软件版本不对引起的,只要写对软件版本就可以解决问题。另外,S1000的旧模块和旧LCD的组合要选择合适的软件版本, 否则会出现 “bad software” 故障。例如:旧LCD+旧模块,适用软件是409AM11B。19A,如果错用了412KM12A。21A或412KM12A。20F均会出现“bad software”。旧模块,适用软件是412KM12A.21A,如果错用了409AM11B.19A或409AM11B.19F。均会出现“bad software”等。三星也有不同版本的对应升级程序。当然也有修理出来的软件故障,这是由于厂家对程序进行硬件跟踪对码工作,导致更换相应元件后而出现软件故障不能正常使用。比如NOKIA手从DCT3都一直硬件对码加密措施;现在三星比如V208说也采用了硬件对码技术,对我们的软件维修带来了阻碍。 第二节 手软件故障处理技巧 CPU通过读取到的内部指挥手工作,这就要求软件内容必须正确,不能 丝毫的差错,遇到软件故障应按一下方法进行处理。 4•2•1 缩小故障范围 判断故障需先将故障范围缩小,并要确定故障类型,既属于硬件故障(如存储器本身损坏)这是内部软件故障。若是硬件故障,在却保其无焊接不良的情况下,应对其进行 更换。若是软件故障,必须用软件维修仪来重写软件。维修实践中发现,手的软件绝大多数出在码片,并多数是芯片中的数据丢失或出错 4•2•2 处理故障技巧 手出现软件故障,有免拆和拆两种方式进行处理,下面介绍两种方式的特点。免拆方式就是借助专门的软件维修设备,配合电脑,在不拆的情况下对手软件故 障进行处理。比如目前较好的全功能数码手软件故障维修仪,可处理如锁、显示“联系服务商”、“话坏,请送修”、“输入保密码” 、 “软件环” 、 不识 卡、不入网、黑屏、低电报警故障,用免拆方式处理软件故障很方便,不用拆,不改变手的串号IMEI,对手内部电路也无须影响。 免拆处理方式不是很万能的,如果手不能开或碰到其他一些没有免拆处理程序的品牌手;遇到软件故障只有将手的码片或字库用热风枪吹下,用万能编程器(如LABTOOL-48),配电脑进行重写(电脑中事先应存有各种手的数据资料),即用电脑内已有的正常数据覆盖故障手中的数据,如果找不到故障手同型号数据,则只有自己进行扩充,需先找一台同型号手,将其码片拆下,用编程器读出其中数据,存进电脑即可。拆软件维修仪不适用于码片和版本合二为一的型(早斯的手如摩托罗拉L2000、西门子2588、摩托罗拉T2688等。现在的型手如摩托罗拉系列包括V988、V60等)无论用免拆软件维修仪还是用拆软件维修仪,写资料前都要查看手的版本,即使是同一类型的手,由于其生产日期和产地的不同,其版本号可能不同,所以,在维修手过程中,对软件的处理一定要核对其版本,否则会造成不开或某些功能失效等故障。另外,很多手的字库采用了BGA封装,如果没有十分把握,特别是初学者就千万不要去动他,因为BGA封装的集成电路对焊接技术要求很高,在重装时要使用专门的焊接工具(BGA置锡工具),否则会越修越复杂。 第三节 用软件维修仪对软件故障进行处理的方法 由于软件故障的处理在手维修中的重要性,使得从事手维修仪生产的厂家推出了各种各样的软件故障维修仪。如上所述的维修处理技巧中已提及,从处理的方法来分,主要为以下几大类:一类是将字库或码片拆下来重写,-48以及其它类型的编程器;另一类是免拆的软件修复。而在免拆情况下对手软件的重写,又可分为两种情况:一种情况是用免电脑、免拆的软件维修仪进行修复;另一种情况用电脑、免拆的软件维修仪进行修复。虽然目前处理手软件的仪器工具很多,但总的来说都是利用维修软件及工具将手内单片系统出错的程序资料重写或覆盖并进行调校。 4•3•1 用带电脑需拆软件仪进行维修的方法 这种方法是将正确码片或字库的数据资料读出存放于电脑当中,当手出现软件故障时,将故障的码片或字库取下来,放上万用编程器去重写入软件资料,然后重焊回码片或字库。些方法在早期的数码手维修当中用的较多,原因是早期的手码片或字库多容易取下来读和写,但由于近期生产的手字库大多为BGA封装,不便取下来。而且厂家也在软件上采取了相应的加密措施,使的有一些手的码片或字库不能拆下来读写,使得这种方法存在局限性,将逐渐被弃用。 4•3•2 用免电脑免拆软件仪进行软件维修的方法 在软件故障当中有一些只是软件错乱造成的,如“手被锁”这一类型的故障就只要处理码片资料就可以解决,由于软件数据量不大,采用简单的单片就可以处理。但由于字库数据量较大,采用这种方法一般的单片就无法达到要求,所以在处理字库引起的软件故障中用的很少。这种方法的优点是操作简单,不需要拆。缺点是由于单片功能欠缺家上存储器容量有限,只能处理一些简单故障。如图4-3-2所示的就是用于修改DTC4 串号的免电脑免拆的软件维修仪。 4•3•3 用电脑免拆软件仪进行软件维修的方法 由于存储器数据容量太大和采用BGA封装,所以采用前面的两种方法都不能很好解决问题。存储容量的增大,最好借助于电脑。为了不拆字库,最好用免拆的方法通过手的外部数据接口来对手软件进行修复。这种方法就是带电脑免拆的软件维修方法,在近期的手维修当中用的较多。这种方法的特点是:一方面将手软件存资料入电脑,由于电脑存储器容量大,因此可以将大量的手软件资料存入;另一方面利用手的外部数据接口,配合相应的驱动程序,可以将手和电脑连接在一起进行通信,将软件资料从电脑传输到手,进行软件修复。带电脑免拆的软件故障维修仪很多,但大多数是将手数据软件存放于电脑,然后通过电脑串口输出,经过RS232接口与手进行通信。摩托罗拉手的数据接口与其它手不一样,采用了专用的接口电路,所以数据从串口输出后要经过一个专用接口才能写入手,这种设备我们通常城之为“EMMIBOX”。还有集所有软件仪功能于一体的“BOX王”。上述这几种方法各有其特点,但目前用的最多且最有效的方法是第三种方法。
通信技术发展调研报告 31402145 通信1403 万军 摘要:通信技术和通信产业20世纪80年代以来发展最快的领域之一,这是人类进入信息 社会的重要标志之一。通信就是互通信息,通信在远古的时代就已存在。现代通信技术 建立在计算、半导体和网络技术飞速发展的基础之上,现代通信技术宽带、个性、数 字和智能的特点。只要有信息的交换基本就有通信技术的存在,在计算之间,电话手 程控交换 ,军事,武器,航天航空等都有通信的应用。 关键字:通信技术发展史、现代通信技术发展及特点、通信技术的应用领域、通信技术 发展的前沿动态 -通信技术发展史 纵观通信的发展分为以下三个阶段:第一阶段是语言和文字通信阶段。在这一阶段,通 信方式简单,内容单一。第二阶段是电通信阶段。1837年,莫尔斯发明电报,并设计 莫尔斯电报码。1876年,贝尔发明电话。这样,利用电磁波不仅可以传输文字,还可 以传输语音,由此大大加快了通信的发展进程。1895年,马可尼发明无线电设备,从而 开创了无线电通信发展的道路。第三阶段是电子信息通信阶段。从总体上看,通信技术 实际上就是通信系统和通信网的技术。通信系统是指点对点通所需的全部设施,而通信 网是由许多通信系统组成的多点之间能相互通信的全部设施。而现代的主要通信技术有 数字通信技术,程控交换技术,信息传输技术,通信网络技术,数据通信与数据网,IS DN与ATM技术,宽带IP技术,接入网与接入技术。 通信发展史 分为有线通信和无线通信 有线通信 美国莫尔斯(F.B.Morse):约5km的电报(点,划,空间 字母,数字); 美国贝尔(A.G.Bell):取得电话专利(电信号 语音); 美国普宾:通信电缆; 1972年 日本:公共通信网的数据通信,传真通信业务; 美国:发表贝尔数据网络,英国:图像信息服务实验; 现代 通信系统利用某些集中转接设施 复杂信息网络 "交换功能" 实现任意两点之间信号的传输. 无线通信 1864年 英国麦克斯韦:电磁波的存在设想; 1888年 德国赫兹(H.Hertz):证实电磁波的存在; 1895年 意大利马可尼:传距仅数百米的无线通信; 1901年 意大利马可尼:横渡大西洋的无线通信; 1938年 法国里本斯:PCM方式; 1940年 美国CBS:彩色电视实验广播; 1951年 美国CBS:彩色电视正式广播; 现代 无线通信遍及全球并通向宇宙, 如GPS其精度可达数十米之内. 数学分析方法发展史 一,傅立叶分析 1822年 法国数学家傅立叶(J.Fourier):奠定傅立叶级数理论基础; 泊松(Poisson),高斯(Gauss):应用到电学中; 19世纪末 用于工程实际的电容器 处理各种频率的正弦信号; 20世纪 谐振电路,滤波器,正弦振荡器 扩展应用领域. 二,拉普拉斯变换 19世纪末 英国工程师赫维赛德(O.Heaviside):运算法(算子法)-先驱; 法国数学家拉普拉斯(P.S.Laplace):拉普拉斯变换方法; 20世纪70年代后 CAD求解电路分析方法 替代拉氏变换. 离散等其它系统的发展 三,Z变换 1730年 英国数学家棣莫弗(De Moivre):生成函数-类似; 19世纪 拉普拉斯: 贡献 20世纪 沙尔(H.L.Seal): 贡献; 20世纪50~60年代 抽样数据控制系统 Z变换应用. 数字计算的研究与实践 四,状态方程分析 20世纪50年代 经典的线性系统理论(外特性); 20世纪60年代 现代的线性系统理论(内部特性), 卡尔曼(R.E.Kalman):状态空间方法. -现代通信技术发展及特点 早期的通信形式属于固定点之间的通信,随着人类社会的发展,信息传递日益频繁, 移动通信正是因为具有信息交流灵活,经济效益明显等优势,得到了迅速的发展,所谓 移动通信,就是在运动中实现的通信。其最大的优点是可以在移动的时候进行通信,方 便,灵活。移动通信系统主要有数字移动通信系统(GSM),码多分址蜂窝移动通信系统 (CDMA)。 对于通信网,主要分为电话网,支撑网和智能网。电话网是进行交互型话音通信,开 放电话业务的电信网;一个完整的电信网除了有以传递信息为主的业务网外,还需要有 若干个用以保障业务网正常运行,增强网络功能,提高网络服务质量的支撑网络,这就 是支撑网,支撑网主要包括No。7信令网,数字同步网和电信管理网。而智能网是在原有 的网络基础上,为快速,方便,经济,灵活的生成和实现各种电信业务而建立的附加 网络结构。 在通信领域,信息一般可以分为话音,数据和图像三大类型。数据是具有某种含义的 数字信号的组合,如字母,数字和符号等,传输时这些字母,数字和符号用离散的数字 信号逐一表达出来,数据通信就是将这样的数据信号夹道数据传输信道上传输,到达接 收地点后
第1部分概述 1 1 交易型系统设计的一些原则 2 1.1 高并发原则 3 1.1.1 无状态 3 1.1.2 拆分 3 1.1.3 服务化 4 1.1.4 消息队列 4 1.1.5 数据异构 6 1.1.6 缓存银弹 7 1.1.7 并发化 9 1.2 高可用原则 10 1.2.1 降级 10 1.2.2 限流 11 1.2.3 切流量 12 1.2.4 可回滚 12 1.3 业务设计原则 12 1.3.1 防重设计 13 1.3.2 幂等设计 13 1.3.3 流程可定义 13 1.3.4 状态状态 13 1.3.5 后台系统操作可反馈 14 1.3.6 后台系统审批化 14 1.3.7 文档和注释 14 1.3.8 备份 14 1.4 总结 14 第2部分高可用 17 2 负载均衡与反向代理 18 2.1 upstream配置 20 2.2 负载均衡算法 21 2.3 失败重试 23 2.4 健康检查 24 2.4.1 TCP心跳检查 24 2.4.2 HTTP心跳检查 25 2.5 其他配置 25 2.5.1 域名上游服务器 25 2.5.2 备份上游服务器 26 2.5.3 不可用上游服务器 26 2.6 长连接 26 2.7 HTTP反向代理示例 29 2.8 HTTP动态负载均衡 30 2.8.1 Consul+Consul-template 31 2.8.2 Consul+OpenResty 35 2.9 Nginx四层负载均衡 39 2.9.1 静态负载均衡 39 2.9.2 动态负载均衡 41 参考资料 42 3 隔离术 43 3.1 线程隔离 43 3.2 进程隔离 45 3.3 集群隔离 45 3.4 房隔离 46 3.5 读写隔离 47 3.6 动静隔离 48 3.7 爬虫隔离 49 3.8 热点隔离 50 3.9 资源隔离 50 3.10 使用Hystrix实现隔离 51 3.10.1 Hystrix简介 51 3.10.2 隔离示例 52 3.11 基于Servlet 3实现请求隔离 56 3.11.1 请求解析和业务处理线程池分离 57 3.11.2 业务线程池隔离 58 3.11.3 业务线程池监控/运维/降级 58 3.11.4 如何使用Servlet 3异步化 59 3.11.5 一些Servlet 3异步化压测数据 64 4 限流详解 66 4.1 限流算法 67 4.1.1 令牌桶算法 67 4.1.2 漏桶算法 68 4.2 应用级限流 69 4.2.1 限流总并发/连接/请求数 69 4.2.2 限流总资源数 70 4.2.3 限流某个接口的总并发/请求数 70 4.2.4 限流某个接口的时间窗请求数 70 4.2.5 平滑限流某个接口的请求数 71 4.3 分布式限流 75 4.3.1 Redis+Lua实现 76 4.3.2 Nginx+Lua实现 77 4.4 接入层限流 78 4.4.1 ngx_http_limit_conn_module 78 4.4.2 ngx_http_limit_req_module 80 4.4.3 lua-resty-limit-traffic 88 4.5 节流 90 4.5.1 throttleFirst/throttleLast 90 4.5.2 throttleWithTimeout 91 参考资料 92 5 降级特技 93 5.1 降级预案 93 5.2 自动开关降级 95 5.2.1 超时降级 95 5.2.2 统计失败次数降级 95 5.2.3 故障降级 95 5.2.4 限流降级 95 5.3 人工开关降级 96 5.4 读服务降级 96 5.5 写服务降级 97 5.6 多级降级 98 5.7 配置中心 100 5.7.1 应用层API封装 100 5.7.2 配置文件实现开关配置 101 5.7.3 配置中心实现开关配置 102 5.8 使用Hystrix实现降级 106 5.9 使用Hystrix实现熔断 108 5.9.1 熔断制实现 108 5.9.2 配置示例 112 5.9.3 采样统计 113 6 超时与重试制 117 6.1 简介 117 6.2 代理层超时与重试 119 6.2.1 Nginx 119 6.2.2 Twemproxy 126 6.3 Web容器超时 127 6.4 中间件客户端超时与重试 127 6.5 数据库客户端超时 131 6.6 NoSQL客户端超时 134 6.7 业务超时 135 6.8 前端Ajax超时 135 6.9 总结 136 6.10 参考资料 137 7 回滚制 139 7.1 事务回滚 139 7.2 代码库回滚 140 7.3 部署版本回滚 141 7.4 数据版本回滚 142 7.5 静态资源版本回滚 143 8 压测与预案 145 8.1 系统压测 145 8.1.1 线下压测 146 8.1.2 线上压测 146 8.2 系统优化和容灾 147 8.3 应急预案 148 第3部分高并发 153 9 应用级缓存 154 9.1 缓存简介 154 9.2 缓存命中率 155 9.3 缓存回收策略 155 9.3.1 基于空间 155 9.3.2 基于容量 155 9.3.3 基于时间 155 9.3.4 基于Java对象引用 156 9.3.5 回收算法 156 9.4 Java缓存类型 156 9.4.1 堆缓存 158 9.4.2 堆外缓存 162 9.4.3 磁盘缓存 162 9.4.4 分布式缓存 164 9.4.5 多级缓存 166 9.5 应用级缓存示例 167 9.5.1 多级缓存API封装 167 9.5.2 NULL Cache 170 9.5.3 强制获取最数据 170 9.5.4 失败统计 171 9.5.5 延迟报警 171 9.6 缓存使用模式实践 172 9.6.1 Cache-Aside 173 9.6.2 Cache-As-SoR 174 9.6.3 Read-Through 174 9.6.4 Write-Through 176 9.6.5 Write-Behind 177 9.6.6 Copy Pattern 181 9.7 性能测试 181 9.8 参考资料 182 10 HTTP缓存 183 10.1 简介 183 10.2 HTTP缓存 184 10.2.1 Last-Modified 184 10.2.2 ETag 190 10.2.3 总结 192 10.3 HttpClient客户端缓存 192 10.3.1 主流程 195 10.3.2 清除无效缓存 195 10.3.3 查找缓存 196 10.3.4 缓存未命中 198 10.3.5 缓存命中 198 10.3.6 缓存内容陈旧需重验证 202 10.3.7 缓存内容无效需重执行请求 205 10.3.8 缓存响应 206 10.3.9 缓存头总结 207 10.4 Nginx HTTP缓存设置 208 10.4.1 expires 208 10.4.2 if-modified-since 209 10.4.3 nginx proxy_pass 209 10.5 Nginx代理层缓存 212 10.5.1 Nginx代理层缓存配置 212 10.5.2 清理缓存 215 10.6 一些经验 216 参考资料 217 11 多级缓存 218 11.1 多级缓存介绍 218 11.2 如何缓存数据 220 11.2.1 过期与不过期 220 11.2.2 维度化缓存与增量缓存 221 11.2.3 大Value缓存 221 11.2.4 热点缓存 221 11.3 分布式缓存与应用负载均衡 222 11.3.1 缓存分布式 222 11.3.2 应用负载均衡 222 11.4 热点数据与更缓存 223 11.4.1 单全量缓存+主从 223 11.4.2 分布式缓存+应用本地热点 224 11.5 更缓存与原子性 225 11.6 缓存崩溃与快速修复 226 11.6.1 取模 226 11.6.2 一致性哈希 226 11.6.3 快速恢复 226 12 连接池线程池详解 227 12.1 数据库连接池 227 12.1.1 DBCP连接池配置 228 12.1.2 DBCP配置建议 233 12.1.3 数据库驱动超时实现 234 12.1.4 连接池使用的一些建议 235 12.2 HttpClient连接池 236 12.2.1 HttpClient 4.5.2配置 236 12.2.2 HttpClient连接池源码分析 240 12.2.3 HttpClient 4.2.3配置 241 12.2.4 问题示例 243 12.3 线程池 244 12.3.1 Java线程池 245 12.3.2 Tomcat线程池配置 248 13 异步并发实战 250 13.1 同步阻塞调用 251 13.2 异步Future 252 13.3 异步Callback 253 13.4 异步编排CompletableFuture 254 13.5 异步Web服务实现 257 13.6 请求缓存 259 13.7 请求合并 261 14 如何扩容 266 14.1 单体应用垂直扩容 267 14.2 单体应用水平扩容 267 14.3 应用拆分 268 14.4 数据库拆分 271 14.5 数据库分库分表示例 275 14.5.1 应用层还是中间件层 275 14.5.2 分库分表策略 277 14.5.3 使用sharding-jdbc分库分表 279 14.5.4 sharding-jdbc分库分表配置 279 14.5.5 使用sharding-jdbc读写分离 283 14.6 数据异构 284 14.6.1 查询维度异构 284 14.6.2 聚合数据异构 285 14.7 任务系统扩容 285 14.7.1 简单任务 285 14.7.2 分布式任务 287 14.7.3 Elastic-Job简介 287 14.7.4 Elastic-Job-Lite功能与架构 287 14.7.5 Elastic-Job-Lite示例 288 15 队列术 295 15.1 应用场景 295 15.2 缓冲队列 296 15.3 任务队列 297 15.4 消息队列 297 15.5 请求队列 299 15.6 数据总线队列 300 15.7 混合队列 301 15.8 其他队列 302 15.9 Disruptor+Redis队列 303 15.9.1 简介 303 15.9.2 XML配置 304 15.9.3 EventWorker 305 15.9.4 EventPublishThread 307 15.9.5 EventHandler 308 15.9.6 EventQueue 308 15.10 下单系统水平可扩展架构 311 15.10.1 下单服务 313 15.10.2 同步Worker 313 15.11 基于Canal实现数据异构 314 15.11.1 Mysql主从复制 315 15.11.2 Canal简介 316 15.11.3 Canal示例 318 第4部分案例 323 16 构建需求响应式亿级商品详情页 324 16.1 商品详情页是什么 324 16.2 商品详情页前端结构 325 16.3 我们的性能数据 327 16.4 单品页流量特点 327 16.5 单品页技术架构发展 327 16.5.1 架构1.0 328 16.5.2 架构2.0 328 16.5.3 架构3.0 330 16.6 详情页架构设计原则 332 16.6.1 数据闭环 332 16.6.2 数据维度化 333 16.6.3 拆分系统 334 16.6.4 Worker无状态化+任务化 334 16.6.5 异步化+并发化 335 16.6.6 多级缓存化 335 16.6.7 动态化 336 16.6.8 弹性化 336 16.6.9 降级开关 336 16.6.10 多房多活 337 16.6.11 多种压测方案 338 16.7 遇到的一些坑和问题 339 16.7.1 SSD性能差 339 16.7.2 键值存储选型压测 340 16.7.3 数据量大时JIMDB同步不动 342 16.7.4 切换主从 342 16.7.5 分片配置 342 16.7.6 模板元数据存储HTML 342 16.7.7 库存接口访问量600w/分钟 343 16.7.8 微信接口调用量暴增 344 16.7.9 开启Nginx Proxy Cache性能不升反降 344 16.7.10 配送至读服务因依赖太多,响应时间偏慢 344 16.7.11 网络抖动时,返回502错误 346 16.7.12 器流量太大 346 16.8 其他 347 17 京东商品详情页服务闭环实践 348 17.1 为什么需要统一服务 348 17.2 整体架构 349 17.3 一些架构思路和总结 350 17.3.1 两种读服务架构模式 351 17.3.2 本地缓存 352 17.3.3 多级缓存 353 17.3.4 统一入口/服务闭环 354 17.4 引入Nginx接入层 354 17.4.1 数据校验/过滤逻辑前置 354 17.4.2 缓存前置 355 17.4.3 业务逻辑前置 355 17.4.4 降级开关前置 355 17.4.5 AB测试 356 17.4.6 灰度发布/流量切换 356 17.4.7 监控服务质量 356 17.4.8 限流 356 17.5 前端业务逻辑后置 356 17.6 前端接口服务端聚合 357 17.7 服务隔离 359 18 使用OpenResty开发高性能Web应用 360 18.1 OpenResty简介 361 18.1.1 Nginx优点 361 18.1.2 Lua的优点 361 18.1.3 什么是ngx_lua 361 18.1.4 开发环境 362 18.1.5 OpenResty生态 362 18.1.6 场景 362 18.2 基于OpenResty的常用架构模式 363 18.2.1 负载均衡 363 18.2.2 单闭环 364 18.2.3 分布式闭环 367 18.2.4 接入网关 368 18.2.5 核心接入Nginx功能 369 18.2.6 业务Nginx功能 369 18.2.7 Web应用 370 18.3 如何使用OpenResty开发Web应用 371 18.3.1 项目搭建 371 18.3.2 启停脚本 372 18.3.3 配置文件 372 18.3.4 nginx.conf配置文件 373 18.3.5 Nginx项目配置文件 373 18.3.6 业务代码 374 18.3.7 模板 374 18.3.8 公共Lua库 375 18.3.9 功能开发 375 18.4 基于OpenResty的常用功能总结 375 18.5 一些问题 376 19 应用数据静态化架构高性能单页Web应用 377 19.1 整体架构 378 19.1.1 CMS系统 379 19.1.2 前端展示系统 380 19.1.3 控制系统 380 19.2 数据和模板动态化 381 19.3 多版本制 381 19.4 异常问题 382 20 使用OpenResty开发Web服务 383 20.1 架构 383 20.2 单DB架构 384 20.2.1 DB+Cache/数据库读写分离架构 384 20.2.2 OpenResty+Local Redis+Mysql集群架构 385 20.2.3 OpenResty+Redis集群+Mysql集群架构 386 20.3 实现 387 20.3.1 后台逻辑 388 20.3.2 前台逻辑 388 20.3.3 项目搭建 389 20.3.4 Redis+Twemproxy配置 389 20.3.5 Mysql+Atlas配置 390 20.3.6 Java+Tomcat安装 394 20.3.7 Java+Tomcat逻辑开发 395 20.3.8 Nginx+Lua逻辑开发 401 21 使用OpenResty开发商品详情页 405 21.1 技术选型 407 21.2 核心流程 408 21.3 项目搭建 408 21.4 数据存储实现 410 21.4.1 商品基本信息SSDB集群配置 410 21.4.2 商品介绍SSDB集群配置 413 21.4.3 其他信息Redis配置 417 21.4.4 集群测试 418 21.4.5 Twemproxy配置 419 21.5 动态服务实现 422 21.5.1 项目搭建 422 21.5.2 项目依赖 422 21.5.3 核心代码 423 21.5.4 基本信息服务 424 21.5.5 商品介绍服务 426 21.5.6 其他信息服务 426 21.5.7 辅助工具 427 21.5.8 web.xml配置 428 21.5.9 打WAR包 428 21.5.10 配置Tomcat 428 21.5.11 测试 429 21.5.12 Nginx配置 429 21.5.13 绑定hosts测试 430 21.6 前端展示实现 430 21.6.1 基础组件 430 21.6.2 商品介绍 432 21.6.4 前端展示 434 21.6.5 测试 442

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值