老房改造系列-81万行Java代码的老系统如何重构

前言

优酷CRP系统-内容采购版权管理系统,是个存在10年的老系统,技术框架上比较老旧;再加上”人来人往“,必然存在很多”不合理但是能跑“和”不敢改,所以ifelse“等等经典代码,一共81w行java代码,17w的jsp代码。我在今年全面接手CRP-财务部分,整体目标就是全面推进CRP财务的业财一体进程。而这些遗留的技术问题都是推进进程的挑战,所以CRP财务本财年的技术主题就是“老系统重构”。

根据以往的工作经验,面对这样的系统,大开大合的重构改版,带来的往往是更加灾难性的”业务不可用“;所以我们的策略,还是要秉着业务优先的原则,跟随业务新需求来逐步重构。但无论产品还是研发心中都要有同一张大图,我们最终要做成什么样子。然后根据大图划清各个业务模块的边界,在保证不会影响其他模块的运行的前提下,进行重构。

图片

81w行java代码中,其实大部分都是废代码,比如:功能和服务还在但是没有人使用;数据都已经迁移到其他系统,下游也不在实际使用,但依赖还在;有很多job还在运行,但并没有实际的数据产出使用方。之前重构的时候跟组里同学开玩笑说“如果随机注释掉一个service中是所有方法实现,系统大概率还是work的”,虽然我们不会这样做,但可能是真的。对于这样的系统,重构的策略如果是重新梳理所有服务的使用情况,无疑是成本特别高的,roi很低。所以应该按需重构和迁移,并保证下游依赖方的不需要做任何改动。

本篇文章会以其中一个模块“付款”来作为示例,原因有二:

一、本财年付款的改版业务述求比较高,这个S的重构进程较其他模块更快一些;

二、想表达的主题更专注在代码重构方向。付款作为整个优酷运营中比较末端的商业行为,在系统上对于付款依赖的下游系统和模块较少。如果是写“合同迁移和改造”,会更偏架构重构和老系统、数据的迁移方案。

付款模块一共涉及大概3w行左右的代码,首先保证下游依赖的接口都不变,还在原有工程服务,并且将老代码迁移到新的工程下。是否迁移工程取决于与迁移的ROI,我们的老工程的前端是用jsp实现的,现在要做前后端分离,所以老代码迁移到新的工程下。

付款

重构的第一原则是以业务为中心,不要为了重构而重构。先来了解一下付款的业务和业务的痛点。

图片

付款要解决的业务问题

付款主要解决俩个问题:1、0资损;2、流程效率

我通过MECE的从下而上的归纳整理后,审慎判断想法建议的“最小公倍数”的方法,对付款进行梳理,先了解一下付款在做一件什么事,以及如何完成目标?

  • 给谁付:收款人是谁?是否有财务或者法务上的风险?以及需要验证对方提供的发票
  • 为谁付:决定了付款的成本归属,归属到节目、部门或者财务口径的入账科目上
  • 付多少钱:是否存在应收款和应付款可以互抵的情况?付款依据是什么?税费如何计算?
  • 怎么付:通过什么方式支付,先票后款还是先款后票,是否支持预约付款?
  • 能不能付:根据不同业务场景以及金额,流转到不同的审批人进行审批。

将这些要解决的业务问题向上抽象总结,付款要想做到

0资损:
  • 信息校验:很多基础信息的校验,最基本的不能付错人
  • 风险拦截:包括风险供应商拦截和风险金额的拦截
  • 金额精准:依据合同、账单、项目等计算出应付金额,然后进行对抵和税费计算(如有)
  • 金额依据状态一致:既然金额的精准决定了最多付多少钱,就要保证金额依据与付款单的状态一致性
提高流程效率:
  • 自动凭证入账
  • 多种付款方式的支持
  • 快捷的流程审批

到这里应该可以看出来,付款不是一个复杂业务流程的模块,它的核心述求是“稳定”与“可扩展”。从这个季度的需求也可以验证这点。

付款的技术痛点

  • 代码臃肿,扩展性低

付款有个特点,没有很复杂的业务流程,但是涉及到资金,在付款之前需要做很多的金额计算和风险校验。而且另外一个特点,付款作为一个工具性质的模块,会接入很多业务方。不同的业务,在金额计算、风险校验等流程上基本一致,但实际接入实现的时候,会有或多或少的差别(比如,付款金额的依据上,主客和OTT会有不同类型的账单)。可以看出付款这部分对于复用性、扩展性要求是比较高的。现在要接入OTT的付款,我们先来看一下如果继续在老代码上升级,会有哪些问题。

@Override
    @Transactional(rollbackFor = Exception.class,transactionManager = "transactionManager2")
    public Payment submitPayment(PaymentDto paymentDto, User user) {
    **只保留能说明问题的关键代码或者注释,省去前整个方法600行左右**


    ***payment对象初始化代码***


    ...省去60行代码...


    Integer r = paymentDao.insertPayment(paymentDto);


***payment付款依赖对象初始化代码***


    //保存关联节目
    playComponent.dealPaymentPlay(paymentDto.getId(), ListUtils.emptyIfNull(paymentDto.getPaymentPlayDtoList()), user);
//保存文件
appendixComponent.dealFile(paymentDto.getId(), ListUtils.emptyIfNull(paymentDto.getFileDtoList()), user);
//保存账单
paymentAssociatedBillComponent.dealBill(paymentDto.getId(), ListUtils.emptyIfNull(paymentDto.getBillDtoList()).stream().map(AssociatedBillDto::getBillId).collect(Collectors.toList()));
//保存责任人和其他操作人
comPermissionComponent.saveComPermission(paymentDto, "ALL");


**第一步做金额和风险校验,为简单只保留注释,省去实现代码**


    //1.校验重复提交


    ...省去5行代码...


    //2.提交前校验


    ...省去20行代码...


    //3.校验账单金额&&所属公司


    ...省去5行代码...


    //4.校验娱乐宝账号


    ...省去1行代码...


    //5.校验付款条件


    checkPayCondition(payment);


//6.校验节目金额
if (paymentComponent.needPaymentToPlay(payment.getType())) {
    checkPaymentSubject(payment);
}


    **校验过程中混入payment对象初始化代码**
    CrpContract contract = crpContractDao.getContractById(payment.getContractId());
Integer operationFlow = contract.getOperationFlow();
payment.setContractOperationFlow(operationFlow);


//7.校验本次申请金额是否超过预期


....省去40行代码...


    //8.仅版权采购合同支持预约付款
    if (){
    throw new RuntimeException("仅版权采购合同支持预约付款!");
}
//8.校验预约付款不能选择先款后票
if (){
    throw new RuntimeException("预约付款仅支持先收票后付款!");
}
    //9.版权采购&&收款账户国家为CN&&签约币种为RMB 才可以使用预约付款


    ...省去10行代码...


    **payment对象初始化代码**
    payment.setApplyDate(new Date());


...省去40行代码...


    **多了一次没有必要的数据库update**
    paymentDao.updatePayment(payment);


**payment对象初始化代码**
    String actualApplyWorkNo = payment.getActualApplyWorkNo();


...省去10行代码...


    paymentDao.updatePayment(payment);


//异步提交审批流
BpmsDto bpmsDto = new BpmsDto();


...省去10行代码...


    return payment;
}


private xxx(){}

比较典型的“流水账”代码,最直观会导致的问题就是维护困难,比如想查一个字段不正确的bug,最差情况要通读600+代码(还有部分private方法)。在升级的时候,最容易想到的办法就是继续盖楼(比如代码中调用了俩次        paymentDao.updatePayment(payment),应该就是盖楼的时候,代码复制多了),从而使“泥丸”越滚越大。

第二个问题,扩展性不好。比如接入OTT的时候,账单的数据库表和开放平台的不一致。按照原有的方式,最简单的就是在保存账单的时候用ifelse判断一下,如果是ott的付款单,则保存到ottPaymentAssociatedBill中;或者变化特别大的话,干脆ctrl+c -> ctrl+v ,复制一下类改名叫OttPaymentService,又多了一个600+的大方法,显然不妥。

--解决方案:从上而下的业务流程拆解

所以我们需要对付款的保存提交进行重构,先根据金字塔原理,将付款流程分解为一个有层级结构的金字塔结构。从上而下的进行拆解:

图片

按照这个结构来重新组织代码结构:

图片

付款保存Command :PaymentSaveCmdExe

@Service
public class PaymentSaveCmdExe {


    @Autowired
    SubmitContextInitPhase initPhase;


    @Autowired
    SaveValidatePhase validatePhase;


    @Autowired
    SaveProcessPhase processPhase;


    @Transactional(rollbackFor = Exception.class,transactionManager = "transactionManager2")
    public PaymentSubmitContext execute(PaymentSubmitCmd cmd){
        PaymentSubmitContext context = init(cmd);
        validate(context);
        process(context);
        return context;
    }


    private void process(PaymentSubmitContext context) {
        processPhase.process(context);
    }


    private void validate(PaymentSubmitContext context) {
        validatePhase.validate(context);
    }


    private PaymentSubmitContext init(PaymentSubmitCmd cmd) {
        return initPhase.init(cmd);
    }


}

付款提交Command :PaymentSubmitCmdExe

@Service
public class PaymentSubmitCmdExe {


    @Autowired
    SubmitContextInitPhase initPhase;


    @Autowired
    SubmitValidatePhase validatePhase;


    @Qualifier("submitProcessPhase")
    @Autowired
    SubmitProcessPhase processPhase;


    @Autowired
    PaymentSaveCmdExe saveCmdExe;


    @Transactional(rollbackFor = Exception.class,transactionManager = "transactionManager2")
    public Payment execute(PaymentSubmitCmd cmd){
        PaymentSubmitContext context = saveCmdExe.execute(cmd);
        validate(context);
        process(context);
        return context.getPayment();
    }


    private void process(PaymentSubmitContext context) {
        processPhase.process(context);
    }


    private void validate(PaymentSubmitContext context) {
        validatePhase.validate(context);
    }




}

这样我们就把程序入口的逻辑写清楚,然后再去拆解phase中的不同步骤,以submitValidatePhase为例:

public class SubmitValidatePhase {
  public void validate(PaymentSubmitContext context){
        /** validate */
        //重复提交校验
        duplicateSubmissionValidate(context);
        //基础信息校验
        baseInfoValidate(context);
        //关联账单校验
        paymentAssociatedBillValidate(context);
        //付款条件校验
        paymentConditionValidate(context);
        //关联节目校验
        paymentAssociatedPlayValidate(context);
        //款项类型校验
        paymentTypePermissionValidate(context);
        //预约付款校验
        appointmentPayValidate(context);
        //其他校验用于扩展
        otherVaidate(context);
    }
  
     protected void appointmentPayValidate(PaymentSubmitContext context) {...}
     protected void paymentAssociatedBillValidate(PaymentSubmitContext context) {...}


}

整个结构按照金字塔结构来编写,每个类都是对应业务步骤上,运维成本会大幅度下降。

按照这个结构来进行扩展,接入OTT付款的话,只需加个入口Command,和有业务差别的Phase继承原有Phase,并重写差异的方法即可。

@Service
public class BorrowingDeductionPaymentSubmitCmdExe extends PaymentSubmitCmdExe {


    @Autowired
    SubmitContextInitPhase initPhase;


    @Autowired
    SubmitValidatePhase validatePhase;


    @Qualifier("borrowingDeductionSubmitProcessPhase")
    @Autowired
    SubmitProcessPhase processPhase;
    
}

@Service
@Slf4j
public class BorrowingDeductionSubmitProcessPhase extends SubmitProcessPhase {
    @Autowired
    BpmsEventPublisher eventPublisher;


    @Override
    @Transactional(rollbackFor = Exception.class,transactionManager = "transactionManager2")
    public void process(PaymentSubmitContext context) {
        super.process(context);
    }


    @Override
    public void startBpms(PaymentSubmitContext context) {
        //异步提交审批流
        BpmsDto bpmsDto = new BpmsDto();
        bpmsDto.setPaymentId(context.getPayment().getId());
        bpmsDto.setProcessType(PaymentBpmsEnum.OTT_PAYMENT_COMMON_APPROVAL.getValue());
        bpmsDto.setWorkNo(context.getPayment().getApplyWorkNo());
        BpmsEvent event = new BpmsEvent(bpmsDto);
        eventPublisher.publishEvent(event);
    }
}
  • 逻辑不收敛、复用性低

在所有的业务系统中,实体状态的维护一定是特别重要的一环,付款更甚。由于涉及到往外付钱,所以付款单的状态,以及付款依据的状态(比如说账单是否已付款),都可能会影响到我们是否会重复付款、少付(少付合作方会投诉甚至有法律风险)。

在MVC的架构中,service层是可以引用dao层的,这种方式很灵活,比如在合同的service中,也可以做付款表的状态更新。但这同时也会产生问题,如果我想修改付款单的状态更新逻辑或者加减状态枚举值,我需要找到所有service方法中对于付款单状态的操作,很容易漏掉。甚至我碰见过更夸张的,同一张表的更新sql写在了俩个Mapper中,状态的更新逻辑修改后,漏掉了一个Mapper的sql修改,恰好调用的入口是接mq消息来更新状态,所以发生了非常“诡异”的状态异常。

其实不只是“状态”,任何实体属性都会有一样的问题,导致这个问题的原因就是实体修改逻辑不收敛。有没有一种规范或者架构能帮助开发者避免这个问题。

--解决方案:架构隔离、能力下沉

大家应该都听说过“六边形架构”或者“COLA框架”,具体的概念我就不在这里详述了,我也只是借这着cola的图来解释一下我们重构是要遵循的准则。在App层将executor分为query和command,我们上一节已经通过从上而下的方法将command的结构搭建起来。那接下来我们要遵守的准则是:Command的实现不能穿透Domain层来直接调用dao,而是把所有的逻辑都收敛到domain和domainService里,由domain层来通过依赖反转的方式来操作数据库。而为了应对复杂的查询(如列表分页查询等场景),Query是可以直接访问Infrastructure层调用dao中的select***方法的。为了遵守这个准则,我们可以通过maven的多module的依赖关系来实现,或者直接通过组内约定,通过建package来保证都是可以的。

图片

将逻辑都收敛到domain中无疑是可以增强复用性的,不用再多说;通过实体操作内聚的办法来收敛之后,还有另一个好处,就是代码看起来会更具备业务表达能力。下面代码是收款的时候写的代码↓↓↓↓↓↓

//domainService
@Service
public class CashCollectionReceiptService {
    @Autowired
    private CashCollectionReceiptRepository receiptRepository;


    @Autowired
    private IContractGateway contractGateway;




    /**
     * 确认回款
     */
    public void confirmCollection(CashCollectionReceipt receipt){
        isCollectionBills(receipt.getBillList());
        receipt.canConfirm();
        receiptRepository.confirm(receipt);
    }
 }
 
 //App层Command执行
@Component
public class CollectionConfirmCmdExe {
         List<CashCollectionBill> billList = billGateway.findByIdList(dto.getBillIdList());
            AbstractReceiptAmountProcessor amountProcess = new OttCollectionReceiptAmountProcessor();
            CashCollectionReceipt receipt = CashCollectionReceipt.builder()
                .billList(billList)
                .totalAmount(dto.getTotalAmount())
                .receiptAmountProcessor(amountProcess)
                .build();
            receiptService.confirmCollection(receipt);
 }

懂行的一定能看出来我马上要提到DDD了,是的!DDD的整个使用过程是要先通过事件风暴或者use case出发,抽象出用到的实体以及他们之间的关系,然后来进行领域划分。但我们这是在重构老系统,如果我们完全按照DDD的方式来重构,那就回到了最开始我们担心的问题,推倒重来只会带来更灾难的“业务不可用”。所以在重构老系统的时候,我们应该怎么使用DDD?

我特别同意COLA作者张建飞大佬的观点,不要为了DDD而DDD。

COLA可以称其为分层框架但并也不是DDD框架,Domain层使用全部或者部分DDD标准都是可以的,只要Coworker拉通统一即可。DDD只是一个规范标准,是手段不是目标,不管通过什么样的方式,只要能保证能力都是内聚可复用就可以。

在重构的时候,我们面临的状况是已经有大量的逻辑代码,我并不提倡把service中所有方法全部梳理,然后将这些方法全部复制粘贴到重新定义的domain或者domainService中,这样会增加重构的风险和测试成本,ROI很低。我们只需合并同类项,将出现的重复代码,作为通用能力下沉到domain层。

指导下沉有两个关键指标:代码的复用性和内聚性。

复用性是告诉我们When(什么时候该下沉了),即有重复代码的时候。内聚性是告诉我们How(要下沉到哪里),功能有没有内聚到恰当的实体上,有没有放到合适的层次上(因为Domain层的能力也是有两个层次的,一个是Domain Service这是相对比较粗的粒度,另一个是Domain的Model这个是最细粒度的复用)。

图片

按照这个原则在重构付款代码,截止目前为止(重构没有完全完成),也只有俩个方法下沉到了Domain中。而其他的实体也并没有放到聚合根里,比如说付款关联账单等,还是使用之前的实现方式,所有的方法都收敛在各自的service类中,比如:PaymentAssociatedBillComponent。

Payment{
  ***省略属性定义***
  
    public BigDecimal getPaymentAmountRmb() {
        return BigDecimals.multiply(paymentAmount, expectExchangeRate);
    }


    /**
     * 综合付款状态
     * @return
     */
    public void initUnionStatusEnum(){...}
  }
  • 审批流技术框架太老

前言介绍过CRP是一个存在了10年的老系统,系统的工作流审批框架用的不是集团的bpms,而是Activity5(2010年发布,怎么说呢,比我工作年限还要长😂)。由于activity只管流程编排,几乎所有的动作实现都要使用者做开发,再加上“前任”们没有做抽象和解耦,审批逻辑和业务逻辑全都耦合在同一个类中。带大家近距离感受一下历代“继承人”的绝望。

图片

一个service中4000行代码,641个if else判断;你以为这就完了?同样的类还有10+个,刚举的例子只是bottom。

图片

--解决方案:复用轮子,用好设计模式

复用已有的服务,重构后,审批流迁移到了集团的bpms,并且对动作和回调做了进一步的服务封装。审批流只需要在bpms里配置,并在数据库中注册一下,异步提交,而回调只需要通过hsfprovider的方式部署,加上注册的服务版本即可。

//异步提交审批流publishEvent
      protected void startBpms(PaymentSubmitContext context) {
        //异步提交审批流
        BpmsDto bpmsDto = new BpmsDto();
        bpmsDto.setPaymentId(context.getPayment().getId());
        bpmsDto.setProcessType(PaymentBpmsEnum.PAYMENT_COMMON_APPROVAL.getValue());
        bpmsDto.setWorkNo(context.getPayment().getApplyWorkNo());
        BpmsEvent event = new BpmsEvent(bpmsDto);
        eventPublisher.publishEvent(event);
    }
    
    
    //BpmsEventListener
    @Override
    public Result<String> submitBpms(BpmsDto bpmsDto) {
        try {
            Payment payment = paymentDao.getPaymentById(bpmsDto.getPaymentId());
            if (payment == null) {
                return Result.valueOfERROR("付款不存在");
            }
            String billId = billHelper.submitApproval(payment, bpmsDto.getWorkNo(), bpmsDto.getProcessType());
            return Result.valueOfOK(billId);
        } catch (Exception e) {
            log.error("submitBpms_error e={}", e);
        }
        return Result.valueOfERROR("error");
    }
@HSFProvider(serviceInterface = BillCallBackService.class, serviceVersion = "CRP_PAYMENT_BILL_CALL_BACK_1.0.0")
public class PaymentBillCallBackServiceImpl implements BillCallBackService {
    @Override
    public Result<Void> callBackCommit(String billId, String bizId) {...}


    /**
     * 审批不同意
     */
    @Override
    public Result<Void> callBackDisagree(String billId, String bizId) {...}


    /**
     * 审批同意
     */
    @Override
    public Result<Void> callBackAgree(String billId, String bizId) {...}


    /**
     * 审批流终止
     */
    @Override
    public Result<Void> callBackRecall(String billId, String bizId) {...}


    @Override
    public Result<Void> callBackCancel(String billId, String bizId) {... }




    @Override
    public Map<String, String> getProcessInitData(String billId, String bizId) {...}

这样,整个审批流的流转全部有审批单据服务封装,做到了很好的解耦;与业务状态相关action代码都写在回调中,但付款的审批流程特别长,而且对应了很多业务操作,这是600+个ifelse判断的主要来源。这个时候可以使用工厂+策略模式干掉ifelse判断

//审批流执行抽象策略类
public abstract class BpmsAbstractExecutor {


    public abstract void execute();


}


//财务审批
@Service("finaceExecutor")
public class FinanceExecutor extends BpmsAbstractExecutor {
    @Override
    public void execute() {...}
}




//税务审批
@Service("taxExecutor")
public class TaxExecutor extends BpmsAbstractExecutor {
    @Override
    public void execute() {...}
}
//用枚举类注册服务的策略实现类
public enum BpmsExecutorEnum {
    FINANCE("finace", "finaceExecutor", "财务审批"),
    TAX("tax", "taxExecutor", "税务审批");
    
    private final String key;
    private final String executorName;
    private final String desc;
    BpmsExecutorEnum(String key, String executorName, String desc) {
        this.executorName = executorName;
        this.key = key;
        this.desc = desc;
    }
***省略getter***
}
//工厂模式直接调用策略实现类
@Service
public class BpmsExecutorFactory {
    private static final Map<String, String> executorNames = new ConcurrentHashMap<>();
    static {
        BpmsExecutorEnum[] executorEnums = BpmsExecutorEnum.values();
        for (BpmsExecutorEnum executorEnum : executorEnums) {
            executorNames.put(executorEnum.getKey()), executorEnum.getExecutorName());
        }
    }
    @Autowired
    private Map<String, BpmsAbstractExecutor> executorMap;


    public void execute(String groupNameEn) {
        String executorName = executorNames.get(groupNameEn);
        if (StringUtils.isEmpty(executorName)) {
            return;
        }
        BpmsAbstractExecutor executor = executorMap.get(executorName);
        if (Objects.isNull(executor)) {
            return;
        }
        executor.execute();
    }
}

策略+工厂模式比较适用于审批操作的业务处理特别多,并且业务复杂的情况,正好适用于解决4000+行代码,600+ifelse判断的老代码重构。如果只是简单的逻辑重构、ifelse没有很多的话,在service类中extract几个private方法就好了,毕竟策略+工厂模式会引入额外的类和入口,使用不当也会增加程序复杂度。

这样,通过老技术框架的迁移、服务封装+设计模式进行了重构,4000+行代码其实还在,只不过现在已经拆分到各自单一职责的模块中,而找到他们的入口文件只有不到200行,这样就可以做到清晰可维护了。

如何保证改动的质量问题

有人问到了这个问题,简单整理了一下方案

付款这个功能,如果出现质量问题很有可能会产生资损。为了拆解这个问题还是从业务出发,付款中有俩个非常重要的风险因素,只要卡住这俩个点就不会出大问题,

1、付款单 | 付款凭据 的金额和状态是否正确;

2、下游依赖是否符合预期;

解决方案如下:

1、规则校验这边是用“资损平台”进行规则配置,可以通过接口、sql和binlog变动来做编排,用来监控重点1

2、冒烟卡口主要用在对下游提供服务的hsf服务上,用来监控重点2

3、单测:单元测试在之前“流水账代码”阶段比较难做单测,尤其迭代多了之后,ifelse膨胀,mock工作量巨大;现在改成分层架构+DDD,只把单测用在核心业务逻辑上,mock会更简单也更有效。目前单测也只用在新业务上,整体覆盖率还很低很低。

结尾

这个财年借着CRP-付款模块的改造,总结和抽象了一些老系统改造的方法。重构第一原则是以业务为中心,找到各自业务的痛点与特点,才会有针对性有效的方法。对于付款的问题,1、代码臃肿扩展性低:通过从上而下的流程拆解来解决;2、逻辑不收敛复用性低:通过架构隔离与能力下沉来解决;3、技术框架老旧:通过复用轮子和设计模式的使用来解决。希望能对遇到类似问题的同学有所帮助。

最后的最后,CRP业务包含了合同、结算、财务三大业务,我只是负责其中一块,81w行代码重构不是靠我一个人;复用的审批流封装的服务也是上一任“继承人”留下的特别棒的抽象服务,起这个标题也只是希望大家能关注到多提意见和建议。老系统问题的形成是个历史积累的过程,而后续重构的人最重要的是要有好的心态以及“业务枷锁”下的极致技术追求

  • 31
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
很高兴回答您的问题。以下是一段使用Java个推推送的代码示例: ``` import java.util.List; import java.util.ArrayList; import java.util.Map; import java.util.HashMap; import com.gexin.rp.sdk.base.IPushResult; import com.gexin.rp.sdk.base.impl.SingleMessage; import com.gexin.rp.sdk.base.impl.Target; import com.gexin.rp.sdk.base.impl.AppMessage; import com.gexin.rp.sdk.base.impl.BatchImpl; import com.gexin.rp.sdk.base.impl.Batch; import com.gexin.rp.sdk.base.impl.APNPayload; import com.gexin.rp.sdk.base.impl.APNPayload.DictionaryAlertMsg; import com.gexin.rp.sdk.base.impl.APNPayload.SimpleAlertMsg; import com.gexin.rp.sdk.template.TransmissionTemplate; import com.gexin.rp.sdk.template.style.Style; import com.gexin.rp.sdk.template.style.Style6; import com.gexin.rp.sdk.template.style.Style7; import com.gexin.rp.sdk.template.style.Style8; import com.gexin.rp.sdk.template.style.Style9; import com.gexin.rp.sdk.template.style.Style10; import com.gexin.rp.sdk.template.style.Style11; import com.gexin.rp.sdk.template.style.Style12; import com.gexin.rp.sdk.template.style.Style13; import com.gexin.rp.sdk.template.style.Style14; import com.gexin.rp.sdk.template.style.Style15; import com.gexin.rp.sdk.template.style.Style16; import com.gexin.rp.sdk.template.style.Style17; import com.gexin.rp.sdk.template.style.Style18; import com.gexin.rp.sdk.template.style.Style19; import com.gexin.rp.sdk.template.style.Style20; import com.gexin.rp.sdk.template.style.Style21; import com.gexin.rp.sdk.template.style.Style22; import com.gexin.rp.sdk.template.style.Style23; import com.gexin.rp.sdk.template.style.Style24; import com.gexin.rp.sdk.template.style.Style25; import com.gexin.rp.sdk.template.style.Style26; import com.gexin.rp.sdk.template.style.Style27; import com.gexin.rp.sdk.template.style.Style28; import com.gexin.rp.sdk.template.style.Style29; import com.gexin.rp.sdk.template.style.Style30; import com.gexin.rp.sdk.template.style.Style31; import com.gexin.rp.sdk.template.style.Style32; import com.gexin.rp.sdk.template.style.Style33; import com.gexin.rp.sdk.template.style.Style34; import com.gexin.rp.sdk.template.style.Style35; import com.gexin.rp.sdk.template.style.Style36; import com.gexin.rp.sdk.template.style.Style37; import com.gexin.rp.sdk.template.style.Style38; import com.gexin.rp.sdk.template.style.Style39; import com.gexin.rp.sdk.template.style.Style40; import com.gexin.rp.sdk.template.style.Style41; import com.gexin.rp.sdk.template.style.Style42; import com.gexin.rp.sdk.template.style.Style43; import com.gexin.rp.sdk.template.style.Style44; import com.gexin.rp.sdk.template.style.Style45; import com.gexin.rp.sdk.template.style.Style46; import com.gexin.rp.sdk.template.style.Style47; import com.gexin.rp.sdk.template.style.Style48; import com.gexin.rp.sdk.template.style.Style49; import com.gexin.rp.sdk.template.style.Style50; import com.gexin.rp.sdk.template.style.Style51; import com.gexin.rp.sdk.template.style.Style52; import com.gexin.rp.sdk.template.style.Style53; import com.gexin.rp.sdk.template.style.Style54; import com.gexin.rp.sdk.template.style.Style55; import com.gexin.rp.sdk.template.style.Style56; import com.gexin.rp.sdk.template.style.Style57; import com.gexin.rp.sdk.template.style.Style58; import com.gexin.rp.sdk.template.style.Style59; import com.gexin.rp.sdk.template.style.Style60; import com.gexin.rp.sdk.template.style.Style61; import com.gexin.rp.sdk.template.style.Style62; import com.gexin.rp.sdk.template.style.Style63; import com.gexin.rp.sdk.template.style.Style64; import com.gexin.rp.sdk.template.style.Style65; import com.gexin.rp.sdk.template.style.Style66; import com.gexin.rp.sdk.template.style.Style67; import com.gexin.rp.sdk.template.style.Style68; import com.gexin.rp.sdk.template.style.Style69; import com.gexin.rp.sdk.template.style.Style70; import com.gexin.rp.sdk.template.style.Style71; import com.gexin.rp.sdk.template.style.Style72; import com.gexin.rp.sdk.template.style.Style73; import com.gexin.rp.sdk.template.style.Style74; import com.gexin.rp.sdk.template.style.Style75; import com.gexin.rp.sdk.template.style.Style76; import com.gexin.rp.sdk.template.style.Style77; import com.gexin.rp.sdk.template.style.Style78; import com.gexin.rp.sdk.template.style.Style79; import com.gexin.rp.sdk.template.style.Style80; import com.gexin.rp.sdk.template.style.Style81; import com.gexin.rp.sdk.template.style.Style82; import com.gexin.rp.sdk.template.style.Style83; import com.gexin.rp.sdk.template.style.Style84; import com.gexin.rp.sdk.template.style.Style85; import com.gexin.rp.sdk.template.style.Style86; import com.gexin.rp.sdk.template.style.Style87; import com.gexin.rp.sdk.template.style.Style88; import com.gexin.rp.sdk.template.style.Style89; import com.gexin.rp.sdk.template.style.Style90; import com.gexin.rp.sdk.template.style

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值