统计方法耗时

This commit is contained in:
Guoqs
2024-11-21 18:49:22 +08:00
parent 365466c82e
commit 1c42f253d0
8 changed files with 515 additions and 38 deletions

View File

@@ -1,41 +1,54 @@
package com.jsowell.framework.aspectj;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
@Aspect
@Order(1)
@Component// 使用spring容器进行管理
@Slf4j
public class CostTimeAspect {
private static final Logger logger = LoggerFactory.getLogger(CostTimeAspect.class);
/**
* 首先定义一个切点
*/
// @org.aspectj.lang.annotation.Pointcut("@annotation(com.counttime.annotation.entity.CostTime)")
@org.aspectj.lang.annotation.Pointcut("@annotation(com.jsowell.common.annotation.CostTime)")
public void countTime() {
// @org.aspectj.lang.annotation.Pointcut("@annotation(com.jsowell.common.annotation.CostTime)")
// public void countTime() {
//
// }
}
// @Around("countTime()")
// public Object doAround(ProceedingJoinPoint joinPoint) {
// Object obj = null;
// try {
// long beginTime = System.currentTimeMillis();
// obj = joinPoint.proceed();
// // 获取方法名称
// String methodName = joinPoint.getSignature().getName();
// // 获取类名称
// String className = joinPoint.getSignature().getDeclaringTypeName();
// log.info("统计方法耗时, 类:[{}], 方法:[{}], 耗时时间为:[{}ms]", className, methodName, (System.currentTimeMillis() - beginTime));
// } catch (Throwable throwable) {
// throwable.printStackTrace();
// }
// return obj;
// }
@Around("countTime()")
public Object doAround(ProceedingJoinPoint joinPoint) {
Object obj = null;
@Around("@annotation(com.jsowell.common.annotation.CostTime)")
public Object aroundMethod(ProceedingJoinPoint joinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
try {
long beginTime = System.currentTimeMillis();
obj = joinPoint.proceed();
// 获取方法名称
String methodName = joinPoint.getSignature().getName();
// 获取类名称
String className = joinPoint.getSignature().getDeclaringTypeName();
log.info("统计方法耗时, 类:[{}], 方法:[{}], 耗时时间为:[{}ms]", className, methodName, (System.currentTimeMillis() - beginTime));
} catch (Throwable throwable) {
throwable.printStackTrace();
return joinPoint.proceed();
} finally {
long endTime = System.currentTimeMillis();
long timeConsumed = endTime - startTime;
logger.info("方法 {} 耗时 {} 毫秒", joinPoint.getSignature().toShortString(), timeConsumed);
}
return obj;
}
}

View File

@@ -0,0 +1,39 @@
package com.jsowell.netty.factory;
import com.jsowell.netty.handler.yunkuaichongV2.AbstractYkcHandlerV2;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Map;
/**
* 工厂设计模式
* 云快充操作
*/
@Component
public class YKCOperateFactoryV2 {
@Autowired
private Map<String, AbstractYkcHandlerV2> strategyMap;
/**
* 注册
* @param str
* @param handler
*/
// public void register(String str, AbstractYkcHandlerV2 handler) {
// if (StringUtils.isBlank(str) || Objects.isNull(handler)) {
// return;
// }
// strategyMap.put(str, handler);
// }
/**
* 获取
* @param name
* @return
*/
public AbstractYkcHandlerV2 getInvokeStrategy(String name) {
return strategyMap.get(name);
}
}

View File

@@ -1,7 +1,6 @@
package com.jsowell.netty.handler.yunkuaichong;
import com.google.common.primitives.Bytes;
import com.jsowell.common.annotation.CostTime;
import com.jsowell.common.constant.CacheConstants;
import com.jsowell.common.core.domain.ykc.YKCDataProtocol;
import com.jsowell.common.core.domain.ykc.YKCFrameTypeCode;
@@ -25,27 +24,10 @@ public abstract class AbstractYkcHandler implements InitializingBean {
private static final String REQUEST_PREFIX = "Request_";
/**
* 执行逻辑
* 有应答
*/
// public byte[] supplyProcess(YKCDataProtocol ykcDataProtocol, Channel channel) {
// throw new UnsupportedOperationException();
// }
public byte[] supplyProcess(YKCDataProtocol ykcDataProtocol, ChannelHandlerContext channel) {
throw new UnsupportedOperationException();
}
/**
* 执行逻辑
* 不需要应答
*/
// public void pushProcess() {
// throw new UnsupportedOperationException();
// }
/**
* 组装应答的结果
* @param ykcDataProtocol 请求数据
@@ -93,7 +75,6 @@ public abstract class AbstractYkcHandler implements InitializingBean {
* 阻止重复帧
* @return true 重复
*/
@CostTime
protected boolean verifyTheDuplicateRequest(YKCDataProtocol ykcDataProtocol, ChannelHandlerContext ctx) {
// 获取序列号域
int serialNumber = BytesUtil.bytesToIntLittle(ykcDataProtocol.getSerialNumber());

View File

@@ -0,0 +1,84 @@
package com.jsowell.netty.handler.yunkuaichongV2;
import com.google.common.primitives.Bytes;
import com.jsowell.common.constant.CacheConstants;
import com.jsowell.common.core.domain.ykc.YKCDataProtocol;
import com.jsowell.common.core.domain.ykc.YKCFrameTypeCode;
import com.jsowell.common.core.redis.StaticRedisCache;
import com.jsowell.common.enums.ykc.PileChannelEntity;
import com.jsowell.common.util.BytesUtil;
import com.jsowell.common.util.CRC16Util;
import com.jsowell.common.util.DateUtils;
import com.jsowell.common.util.YKCUtils;
import io.netty.channel.ChannelHandlerContext;
/**
* 模板方法模式
*/
public interface AbstractYkcHandlerV2 {
String REQUEST_PREFIX = "Request_";
byte[] supplyProcess(YKCDataProtocol ykcDataProtocol, ChannelHandlerContext channel);
/**
* 组装应答的结果
* @param ykcDataProtocol 请求数据
* @param messageBody 消息体
* @return 应答结果
*/
default byte[] getResult(YKCDataProtocol ykcDataProtocol, byte[] messageBody) {
// 起始标志
byte[] head = ykcDataProtocol.getHead();
// 序列号域
byte[] serialNumber = ykcDataProtocol.getSerialNumber();
// 加密标志
byte[] encryptFlag = ykcDataProtocol.getEncryptFlag();
// 请求帧类型
byte[] requestFrameType = ykcDataProtocol.getFrameType();
// 应答帧类型
byte[] responseFrameType = YKCFrameTypeCode.PlatformAnswersRelation.getResponseFrameTypeBytes(requestFrameType);
// 数据域 值为“序列号域+加密标志+帧类型标志+消息体”字节数之和
byte[] dataFields = Bytes.concat(serialNumber, encryptFlag, responseFrameType, messageBody);
// 计算crc 从序列号域到数据域的 CRC 校验
int crc16 = CRC16Util.calcCrc16(dataFields);
return Bytes.concat(head, BytesUtil.intToBytes(dataFields.length, 1), dataFields, BytesUtil.intToBytes(crc16));
}
/**
* 保存桩最后链接到平台的时间
* @param pileSn 桩编号
*/
default void saveLastTimeAndCheckChannel(String pileSn, ChannelHandlerContext ctx) {
String redisKey = CacheConstants.PILE_LAST_CONNECTION + pileSn;
StaticRedisCache.staticRedisCache.setCacheObject(redisKey, DateUtils.getDateTime(), CacheConstants.cache_expire_time_30d);
// 保存桩号和channel的关系
PileChannelEntity.checkChannel(pileSn, ctx);
}
/**
* 阻止重复帧
* @return true 重复
*/
default boolean verifyTheDuplicateRequest(YKCDataProtocol ykcDataProtocol, ChannelHandlerContext ctx) {
// 获取序列号域
int serialNumber = BytesUtil.bytesToIntLittle(ykcDataProtocol.getSerialNumber());
// 获取帧类型
String frameTypeStr = YKCUtils.frameType2Str(ykcDataProtocol.getFrameType());
// 获取channelId
String channelId = ctx.channel().id().asShortText();
String redisKey = REQUEST_PREFIX + channelId + "_" + frameTypeStr;
Boolean result = StaticRedisCache.staticRedisCache.setnx(redisKey, ykcDataProtocol.getHEXString(), 30);
// result返回false说明没有设置成功就是说已经有相同请求了所以返回true重复
return !result;
}
}

View File

@@ -0,0 +1,68 @@
package com.jsowell.netty.handler.yunkuaichongV2;
import com.google.common.primitives.Bytes;
import com.jsowell.common.constant.Constants;
import com.jsowell.common.core.domain.ykc.YKCDataProtocol;
import com.jsowell.common.core.domain.ykc.YKCFrameTypeCode;
import com.jsowell.common.util.BytesUtil;
import com.jsowell.common.util.YKCUtils;
import com.jsowell.pile.service.PileBillingTemplateService;
import com.jsowell.pile.service.YKCPushCommandService;
import io.netty.channel.ChannelHandlerContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* 计费模板验证请求Handler
*
* @author JS-ZZA
* @date 2022/9/17 14:10
*/
@Slf4j
@Service
public class BillingTemplateValidateRequestHandlerV2 implements AbstractYkcHandlerV2 {
private final String type = YKCUtils.frameType2Str(YKCFrameTypeCode.BILLING_TEMPLATE_VALIDATE_CODE.getBytes());
@Autowired
private PileBillingTemplateService pileBillingTemplateService;
@Autowired
private YKCPushCommandService ykcPushCommandService;
// @Override
// public void afterPropertiesSet() throws Exception {
// YKCOperateFactory.register(type, this);
// }
@Override
public byte[] supplyProcess(YKCDataProtocol ykcDataProtocol, ChannelHandlerContext channel) {
// log.info("[===执行计费模板验证请求逻辑===] param:{}, channel:{}", JSON.toJSONString(ykcDataProtocol), channel.toString());
// 获取消息体
byte[] msgBody = ykcDataProtocol.getMsgBody();
int startIndex = 0;
int length = 7;
// 桩号
byte[] pileSnByte = BytesUtil.copyBytes(msgBody, startIndex, length);
String pileSn = BytesUtil.binary(pileSnByte, 16);
// 保存时间
saveLastTimeAndCheckChannel(pileSn, channel);
// 计费模型编码
startIndex += length;
length = 2;
byte[] billingTemplateCodeByte = BytesUtil.copyBytes(msgBody, startIndex, length);
String billingTemplateCode = BytesUtil.binary(billingTemplateCodeByte, 16);
// byte[] flag = Constants.oneByteArray; 0x00 桩计费模型与平台一致 0x01桩计费模型与平台不一致
byte[] flag = Constants.zeroByteArray;
// 消息体
byte[] messageBody = Bytes.concat(pileSnByte, billingTemplateCodeByte, flag);
return getResult(ykcDataProtocol, messageBody);
}
}

View File

@@ -0,0 +1,284 @@
package com.jsowell.netty.handler.yunkuaichongV2;
import com.alibaba.fastjson2.JSON;
import com.google.common.collect.Lists;
import com.google.common.primitives.Bytes;
import com.jsowell.common.annotation.CostTime;
import com.jsowell.common.constant.Constants;
import com.jsowell.common.core.domain.ykc.LoginRequestData;
import com.jsowell.common.core.domain.ykc.YKCDataProtocol;
import com.jsowell.common.core.domain.ykc.YKCFrameTypeCode;
import com.jsowell.common.util.BytesUtil;
import com.jsowell.common.util.StringUtils;
import com.jsowell.common.util.YKCUtils;
import com.jsowell.common.util.spring.SpringUtils;
import com.jsowell.pile.domain.PileBasicInfo;
import com.jsowell.pile.domain.ykcCommond.IssueQRCodeCommand;
import com.jsowell.pile.domain.ykcCommond.ProofreadTimeCommand;
import com.jsowell.pile.domain.ykcCommond.PublishPileBillingTemplateCommand;
import com.jsowell.pile.service.PileBasicInfoService;
import com.jsowell.pile.service.PileBillingTemplateService;
import com.jsowell.pile.service.PileMsgRecordService;
import com.jsowell.pile.service.YKCPushCommandService;
import com.jsowell.pile.vo.web.BillingTemplateVO;
import io.netty.channel.ChannelHandlerContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.util.List;
import java.util.concurrent.CompletableFuture;
@Slf4j
@Service
public class LoginRequestHandlerV2 implements AbstractYkcHandlerV2 {
private final String type = YKCUtils.frameType2Str(YKCFrameTypeCode.LOGIN_CODE.getBytes());
@Autowired
private PileBasicInfoService pileBasicInfoService;
@Autowired
private YKCPushCommandService ykcPushCommandService;
@Autowired
private PileMsgRecordService pileMsgRecordService;
@Autowired
private PileBillingTemplateService pileBillingTemplateService;
private List<String> newProgramVersionList = Lists.newArrayList("c6-30");
// 引入线程池
private ThreadPoolTaskExecutor executor = SpringUtils.getBean("threadPoolTaskExecutor");
// @Override
// public void afterPropertiesSet() throws Exception {
// YKCOperateFactory.register(type, this);
// }
public static void main(String[] args) {
String msg = "8800000000009000020f56312e323035303000898604940121c138531304";
byte[] msgBody = BytesUtil.str2Bcd(msg);
int startIndex = 0;
int length = 7;
// 桩编码
byte[] pileSnByte = BytesUtil.copyBytes(msgBody, startIndex, length);
String pileSn = BytesUtil.binary(pileSnByte, 16);
// log.info("桩号:{}", pileSn);
// 桩类型 0 表示直流桩, 1 表示交流桩
startIndex += length;
length = 1;
byte[] pileTypeByteArr = BytesUtil.copyBytes(msgBody, startIndex, length);
String pileType = BytesUtil.bcd2Str(pileTypeByteArr);
// 充电枪数量
startIndex += length;
byte[] connectorNumByteArr = BytesUtil.copyBytes(msgBody, startIndex, length);
String connectorNum = BytesUtil.bcd2Str(connectorNumByteArr);
// 通信协议版本 版本号乘 10v1.0 表示 0x0A
startIndex += length;
byte[] communicationVersionByteArr = BytesUtil.copyBytes(msgBody, startIndex, length);
// int i = Integer.parseInt(BytesUtil.bcd2Str(communicationVersionByteArr)); // 0F --> 15
BigDecimal bigDecimal = new BigDecimal(BytesUtil.bcd2Str(communicationVersionByteArr));
BigDecimal communicationVersionTemp = bigDecimal.divide(new BigDecimal(10));
String communicationVersion = "v" + communicationVersionTemp;
// 程序版本
startIndex += length;
length = 8;
byte[] programVersionByteArr = BytesUtil.copyBytes(msgBody, startIndex, length);
String programVersion = BytesUtil.ascii2Str(programVersionByteArr);
log.info("程序版本:{} length:{}", programVersion, programVersion.length());
// 网络连接类型 0x00 SIM 卡 0x01 LAN 0x02 WAN 0x03 其他
startIndex += length;
length = 1;
byte[] internetConnectionTypeByteArr = BytesUtil.copyBytes(msgBody, startIndex, length);
String internetConnection = BytesUtil.bcd2Str(internetConnectionTypeByteArr);
// sim卡
startIndex += length;
length = 10;
byte[] simCardNumByteArr = BytesUtil.copyBytes(msgBody, startIndex, length);
String iccid = BytesUtil.bin2HexStr(simCardNumByteArr);
// 运营商 0x00 移动 0x02 电信 0x03 联通 0x04 其他
startIndex += length;
length = 1;
byte[] businessTypeByteArr = BytesUtil.copyBytes(msgBody, startIndex, length);
String business = BytesUtil.bcd2Str(businessTypeByteArr);
}
@CostTime
@Override
public byte[] supplyProcess(YKCDataProtocol ykcDataProtocol, ChannelHandlerContext ctx) {
if (verifyTheDuplicateRequest(ykcDataProtocol, ctx)) {
// 阻止重复帧
return null;
}
// 获取消息体
byte[] msgBody = ykcDataProtocol.getMsgBody();
int startIndex = 0;
int length = 7;
// 桩编码
byte[] pileSnByte = BytesUtil.copyBytes(msgBody, startIndex, length);
String pileSn = BytesUtil.binary(pileSnByte, 16);
// 保存时间
saveLastTimeAndCheckChannel(pileSn, ctx);
// 桩类型 0 表示直流桩, 1 表示交流桩
startIndex += length;
length = 1;
byte[] pileTypeByteArr = BytesUtil.copyBytes(msgBody, startIndex, length);
String pileType = BytesUtil.bcd2Str(pileTypeByteArr);
// 充电枪数量
startIndex += length;
byte[] connectorNumByteArr = BytesUtil.copyBytes(msgBody, startIndex, length);
String connectorNum = BytesUtil.bcd2Str(connectorNumByteArr);
// 通信协议版本 版本号乘 10v1.0 表示 0x0A
startIndex += length;
byte[] communicationVersionByteArr = BytesUtil.copyBytes(msgBody, startIndex, length);
BigDecimal bigDecimal = new BigDecimal(BytesUtil.bcd2Str(communicationVersionByteArr));
BigDecimal communicationVersionTemp = bigDecimal.divide(new BigDecimal(10));
String communicationVersion = "v" + communicationVersionTemp;
// 程序版本
startIndex += length;
length = 8;
byte[] programVersionByteArr = BytesUtil.copyBytes(msgBody, startIndex, length);
String programVersion = BytesUtil.ascii2Str(programVersionByteArr);
// log.info("程序版本:{} length:{}", programVersion, programVersion.length());
// 网络连接类型 0x00 SIM 卡 0x01 LAN 0x02 WAN 0x03 其他
startIndex += length;
length = 1;
byte[] internetConnectionTypeByteArr = BytesUtil.copyBytes(msgBody, startIndex, length);
String internetConnection = BytesUtil.bcd2Str(internetConnectionTypeByteArr);
// sim卡
startIndex += length;
length = 10;
byte[] simCardNumByteArr = BytesUtil.copyBytes(msgBody, startIndex, length);
String iccid = BytesUtil.bin2HexStr(simCardNumByteArr);
// 运营商 0x00 移动 0x02 电信 0x03 联通 0x04 其他
startIndex += length;
length = 1;
byte[] businessTypeByteArr = BytesUtil.copyBytes(msgBody, startIndex, length);
String business = BytesUtil.bcd2Str(businessTypeByteArr);
// *********************** 字段解析完成,下面进行逻辑处理 *********************** //
LoginRequestData loginRequestData = LoginRequestData.builder()
.pileSn(pileSn)
.pileType(pileType)
.connectorNum(connectorNum)
.communicationVersion(communicationVersion)
.programVersion(programVersion)
.internetConnection(internetConnection)
.iccid(iccid)
.business(business)
.build();
// 结果(默认 0x01:登录失败)
byte[] flag = Constants.oneByteArray;
// 通过桩编码SN查询数据库如果有数据则登录成功否则登录失败
PileBasicInfo pileBasicInfo = null;
try {
pileBasicInfo = pileBasicInfoService.selectPileBasicInfoBySN(pileSn);
} catch (Exception e) {
log.error("selectPileBasicInfoBySN发生异常", e);
}
if (pileBasicInfo != null) {
flag = Constants.zeroByteArray;
// 异步修改充电桩状态
CompletableFuture.runAsync(() -> {
try {
// 更改桩和该桩下的枪口状态分别为 在线、空闲 公共方法修改状态
pileBasicInfoService.updateStatus(BytesUtil.bcd2Str(ykcDataProtocol.getFrameType()), pileSn, null, null, null);
} catch (Exception e) {
e.printStackTrace();
}
}, executor);
// 异步发送对时指令
CompletableFuture.runAsync(() -> {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 对时
ProofreadTimeCommand command = ProofreadTimeCommand.builder().pileSn(pileSn).build();
ykcPushCommandService.pushProofreadTimeCommand(command);
}, executor);
// log.info("下面进行下发二维码 pileSn:{}, thread:{}", pileSn, Thread.currentThread().getName());
// 异步发送下发二维码指令
CompletableFuture.runAsync(() -> {
try {
Thread.sleep(600);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 下发二维码
IssueQRCodeCommand issueQRCodeCommand = IssueQRCodeCommand.builder().pileSn(pileSn).build();
ykcPushCommandService.pushIssueQRCodeCommand(issueQRCodeCommand);
}, executor);
// 异步发送0x58
CompletableFuture.runAsync(() -> {
// 查询计费模板
BillingTemplateVO billingTemplateVO = pileBillingTemplateService.selectBillingTemplateDetailByPileSn(pileSn);
PublishPileBillingTemplateCommand command = PublishPileBillingTemplateCommand.builder()
.pileSn(pileSn)
.billingTemplateVO(billingTemplateVO)
.build();
// 发送请求
ykcPushCommandService.pushPublishPileBillingTemplate(command);
}, executor);
if (StringUtils.equals("00", internetConnection)) {
CompletableFuture.runAsync(() -> {
// 充电桩使用的sim卡把信息存库
try {
// pileBasicInfoService.updatePileSimInfo(pileSn, iccid);
// pileBasicInfoService.updatePileSimInfoV2(pileSn, iccid);
pileBasicInfoService.bindPileSimCard(pileSn, iccid);
} catch (Exception e) {
log.error("更新充电桩sim卡信息失败pileSn:{}, iccid:{}", pileSn, iccid, e);
}
}, executor);
}
}
// 异步保持登录报文
CompletableFuture.runAsync(() -> {
// 保存报文 没有登录认证通过还要不要保存报文?
try {
String jsonMsg = JSON.toJSONString(loginRequestData);
pileMsgRecordService.save(pileSn, pileSn, type, jsonMsg, ykcDataProtocol.getHEXString());
} catch (Exception e) {
log.error("保存报文失败pileSn:{}", pileSn, e);
}
}, executor);
// 消息体
byte[] messageBody = Bytes.concat(pileSnByte, flag);
return getResult(ykcDataProtocol, messageBody);
}
}

View File

@@ -7,6 +7,7 @@ import com.jsowell.common.enums.ykc.PileConnectorDataBaseStatusEnum;
import com.jsowell.common.util.StringUtils;
import com.jsowell.common.util.YKCUtils;
import com.jsowell.netty.factory.YKCOperateFactory;
import com.jsowell.netty.factory.YKCOperateFactoryV2;
import com.jsowell.netty.handler.yunkuaichong.AbstractYkcHandler;
import com.jsowell.netty.service.yunkuaichong.YKCBusinessService;
import com.jsowell.pile.service.OrderBasicInfoService;
@@ -31,6 +32,9 @@ public class YKCBusinessServiceImpl implements YKCBusinessService {
@Autowired
private OrderBasicInfoService orderBasicInfoService;
@Autowired
private YKCOperateFactoryV2 ykcOperateFactoryV2; // 使用注解注入
@Override
public byte[] process(byte[] msg, ChannelHandlerContext ctx) {
if (!YKCUtils.checkMsg(msg)) {
@@ -42,6 +46,7 @@ public class YKCBusinessServiceImpl implements YKCBusinessService {
String frameType = YKCUtils.frameType2Str(ykcDataProtocol.getFrameType());
// 获取业务处理handler
AbstractYkcHandler invokeStrategy = YKCOperateFactory.getInvokeStrategy(frameType);
// AbstractYkcHandlerV2 invokeStrategy = ykcOperateFactoryV2.getInvokeStrategy("loginRequestHandlerV2");
return invokeStrategy.supplyProcess(ykcDataProtocol, ctx);
}

View File

@@ -26,7 +26,10 @@ import com.jsowell.pile.transaction.service.TransactionService;
import com.jsowell.pile.util.UserUtils;
import com.jsowell.pile.vo.uniapp.customer.BillingPriceVO;
import com.jsowell.pile.vo.uniapp.customer.CurrentTimePriceDetails;
import com.jsowell.pile.vo.web.*;
import com.jsowell.pile.vo.web.BillingDetailVO;
import com.jsowell.pile.vo.web.BillingTemplateVO;
import com.jsowell.pile.vo.web.EchoBillingTemplateVO;
import com.jsowell.pile.vo.web.MemberGroupVO;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.beans.BeanUtils;