|
@@ -0,0 +1,676 @@
|
|
|
|
|
+package shop.alien.store.service.analytics.impl;
|
|
|
|
|
+
|
|
|
|
|
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
|
|
|
+import lombok.RequiredArgsConstructor;
|
|
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
|
|
+import org.springframework.dao.DuplicateKeyException;
|
|
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
|
|
+import org.springframework.transaction.annotation.Transactional;
|
|
|
|
|
+import org.springframework.util.StringUtils;
|
|
|
|
|
+import shop.alien.entity.analytics.*;
|
|
|
|
|
+import shop.alien.entity.analytics.dto.*;
|
|
|
|
|
+import shop.alien.mapper.*;
|
|
|
|
|
+import shop.alien.store.service.analytics.AnalyticsTrackService;
|
|
|
|
|
+import shop.alien.store.util.analytics.AnalyticsDateUtil;
|
|
|
|
|
+import shop.alien.store.util.analytics.AnalyticsFrontHelper;
|
|
|
|
|
+
|
|
|
|
|
+import javax.servlet.http.HttpServletRequest;
|
|
|
|
|
+import java.util.Date;
|
|
|
|
|
+import java.util.UUID;
|
|
|
|
|
+
|
|
|
|
|
+@Slf4j
|
|
|
|
|
+@Service
|
|
|
|
|
+@RequiredArgsConstructor
|
|
|
|
|
+public class AnalyticsTrackServiceImpl implements AnalyticsTrackService {
|
|
|
|
|
+
|
|
|
|
|
+ private final AnalyticsEventMapper eventMapper;
|
|
|
|
|
+ private final AnalyticsAiRequestMapper aiRequestMapper;
|
|
|
|
|
+ private final AnalyticsUserStatMapper userStatMapper;
|
|
|
|
|
+ private final AnalyticsMerchantStatMapper merchantStatMapper;
|
|
|
|
|
+ private final AnalyticsContentStatMapper contentStatMapper;
|
|
|
|
|
+ private final AnalyticsAiChatStatMapper aiChatStatMapper;
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ @Transactional(rollbackFor = Exception.class)
|
|
|
|
|
+ public void trackEvent(AnalyticsTrackEventDTO dto, HttpServletRequest request) {
|
|
|
|
|
+ if (!StringUtils.hasText(dto.getEventCode())) {
|
|
|
|
|
+ throw new IllegalArgumentException("eventCode 不能为空");
|
|
|
|
|
+ }
|
|
|
|
|
+ AnalyticsFrontHelper.enrichTrackEvent(dto, request);
|
|
|
|
|
+ insertEvent(dto);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ @Transactional(rollbackFor = Exception.class)
|
|
|
|
|
+ public void trackFromFront(AnalyticsFrontReportDTO dto, HttpServletRequest request) {
|
|
|
|
|
+ AnalyticsTrackEventDTO eventDto = convertFrontReport(dto);
|
|
|
|
|
+ AnalyticsFrontHelper.enrichTrackEvent(eventDto, request);
|
|
|
|
|
+ insertEvent(eventDto);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ @Transactional(rollbackFor = Exception.class)
|
|
|
|
|
+ public void batchTrackFromFront(AnalyticsBatchTrackDTO dto, HttpServletRequest request) {
|
|
|
|
|
+ if (dto.getEvents() == null || dto.getEvents().isEmpty()) {
|
|
|
|
|
+ throw new IllegalArgumentException("events 不能为空");
|
|
|
|
|
+ }
|
|
|
|
|
+ if (dto.getEvents().size() > 50) {
|
|
|
|
|
+ throw new IllegalArgumentException("单次最多上报50条");
|
|
|
|
|
+ }
|
|
|
|
|
+ for (AnalyticsFrontReportDTO item : dto.getEvents()) {
|
|
|
|
|
+ trackFromFront(item, request);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ @Transactional(rollbackFor = Exception.class)
|
|
|
|
|
+ public void trackHeartbeat(AnalyticsHeartbeatDTO dto, HttpServletRequest request) {
|
|
|
|
|
+ if (dto.getDurationMs() == null || dto.getDurationMs() <= 0) {
|
|
|
|
|
+ throw new IllegalArgumentException("durationMs 必须大于0");
|
|
|
|
|
+ }
|
|
|
|
|
+ AnalyticsFrontReportDTO report = new AnalyticsFrontReportDTO();
|
|
|
|
|
+ report.setScene(AnalyticsScene.USER_HEARTBEAT.getScene());
|
|
|
|
|
+ report.setUserId(dto.getUserId());
|
|
|
|
|
+ report.setDurationMs(dto.getDurationMs());
|
|
|
|
|
+ report.setDeviceType(dto.getDeviceType());
|
|
|
|
|
+ report.setChannel(dto.getChannel());
|
|
|
|
|
+ report.setCity(dto.getCity());
|
|
|
|
|
+ trackFromFront(report, request);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private void insertEvent(AnalyticsTrackEventDTO dto) {
|
|
|
|
|
+ AnalyticsEvent event = new AnalyticsEvent();
|
|
|
|
|
+ event.setEventId(StringUtils.hasText(dto.getEventId()) ? dto.getEventId() : UUID.randomUUID().toString().replace("-", ""));
|
|
|
|
|
+ event.setEventCode(dto.getEventCode());
|
|
|
|
|
+ event.setUserId(dto.getUserId());
|
|
|
|
|
+ event.setMerchantId(dto.getMerchantId());
|
|
|
|
|
+ event.setTargetId(dto.getTargetId());
|
|
|
|
|
+ event.setContentType(dto.getContentType());
|
|
|
|
|
+ event.setAmount(dto.getAmount());
|
|
|
|
|
+ event.setDurationMs(dto.getDurationMs());
|
|
|
|
|
+ event.setDeviceType(dto.getDeviceType());
|
|
|
|
|
+ event.setChannel(dto.getChannel());
|
|
|
|
|
+ event.setCity(dto.getCity());
|
|
|
|
|
+ event.setEventTime(dto.getEventTime() != null ? dto.getEventTime() : new Date());
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ eventMapper.insert(event);
|
|
|
|
|
+ } catch (DuplicateKeyException e) {
|
|
|
|
|
+ log.debug("埋点事件已存在,跳过: eventId={}", event.getEventId());
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private AnalyticsTrackEventDTO convertFrontReport(AnalyticsFrontReportDTO dto) {
|
|
|
|
|
+ if (!StringUtils.hasText(dto.getScene())) {
|
|
|
|
|
+ throw new IllegalArgumentException("scene 不能为空");
|
|
|
|
|
+ }
|
|
|
|
|
+ AnalyticsScene scene = AnalyticsScene.of(dto.getScene());
|
|
|
|
|
+ if (scene == null) {
|
|
|
|
|
+ throw new IllegalArgumentException("未知 scene: " + dto.getScene());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ AnalyticsTrackEventDTO eventDto = new AnalyticsTrackEventDTO();
|
|
|
|
|
+ eventDto.setEventId(dto.getEventId());
|
|
|
|
|
+ eventDto.setEventCode(scene.getEventCode());
|
|
|
|
|
+ eventDto.setUserId(dto.getUserId());
|
|
|
|
|
+ eventDto.setMerchantId(dto.getMerchantId());
|
|
|
|
|
+ eventDto.setTargetId(dto.getTargetId());
|
|
|
|
|
+ eventDto.setContentType(dto.getContentType());
|
|
|
|
|
+ eventDto.setAmount(dto.getAmount());
|
|
|
|
|
+ eventDto.setDurationMs(dto.getDurationMs());
|
|
|
|
|
+ eventDto.setDeviceType(dto.getDeviceType());
|
|
|
|
|
+ eventDto.setChannel(dto.getChannel());
|
|
|
|
|
+ eventDto.setCity(dto.getCity());
|
|
|
|
|
+ eventDto.setEventTime(dto.getEventTime());
|
|
|
|
|
+ return eventDto;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ @Transactional(rollbackFor = Exception.class)
|
|
|
|
|
+ public void trackAiRequest(AnalyticsAiRequestDTO dto) {
|
|
|
|
|
+ if (!StringUtils.hasText(dto.getApiName()) || !StringUtils.hasText(dto.getApiUrl())) {
|
|
|
|
|
+ throw new IllegalArgumentException("apiName 与 apiUrl 不能为空");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ AnalyticsAiRequest request = new AnalyticsAiRequest();
|
|
|
|
|
+ request.setRequestId(StringUtils.hasText(dto.getRequestId()) ? dto.getRequestId() : UUID.randomUUID().toString().replace("-", ""));
|
|
|
|
|
+ request.setApiName(dto.getApiName());
|
|
|
|
|
+ request.setApiUrl(dto.getApiUrl());
|
|
|
|
|
+ request.setResponseDurationMs(dto.getResponseDurationMs() != null ? dto.getResponseDurationMs() : 0L);
|
|
|
|
|
+ request.setIsTimeout(dto.getIsTimeout() != null ? dto.getIsTimeout() : 0);
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ aiRequestMapper.insert(request);
|
|
|
|
|
+ } catch (DuplicateKeyException e) {
|
|
|
|
|
+ log.debug("AI请求已存在,跳过: requestId={}", request.getRequestId());
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ @Transactional(rollbackFor = Exception.class)
|
|
|
|
|
+ public void upsertUserDetail(AnalyticsUserDetailDTO dto) {
|
|
|
|
|
+ if (dto.getUserId() == null) {
|
|
|
|
|
+ throw new IllegalArgumentException("userId 不能为空");
|
|
|
|
|
+ }
|
|
|
|
|
+ Date statDate = dto.getStatDateOverride() != null ? AnalyticsDateUtil.truncateToDate(dto.getStatDateOverride())
|
|
|
|
|
+ : (dto.getStatDate() != null ? AnalyticsDateUtil.truncateToDate(dto.getStatDate()) : AnalyticsDateUtil.truncateToDate(new Date()));
|
|
|
|
|
+
|
|
|
|
|
+ AnalyticsUserStat existing = findUserStat(statDate, dto.getUserId());
|
|
|
|
|
+ if (existing == null) {
|
|
|
|
|
+ userStatMapper.insert(buildUserStat(dto, statDate));
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ mergeUserStat(existing, dto, statDate);
|
|
|
|
|
+ userStatMapper.updateById(existing);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ @Transactional(rollbackFor = Exception.class)
|
|
|
|
|
+ public void upsertMerchantDetail(AnalyticsMerchantDetailDTO dto) {
|
|
|
|
|
+ if (dto.getMerchantId() == null) {
|
|
|
|
|
+ throw new IllegalArgumentException("merchantId 不能为空");
|
|
|
|
|
+ }
|
|
|
|
|
+ Date statDate = dto.getStatDateOverride() != null ? AnalyticsDateUtil.truncateToDate(dto.getStatDateOverride())
|
|
|
|
|
+ : (dto.getStatDate() != null ? AnalyticsDateUtil.truncateToDate(dto.getStatDate()) : AnalyticsDateUtil.truncateToDate(new Date()));
|
|
|
|
|
+
|
|
|
|
|
+ AnalyticsMerchantStat existing = findMerchantStat(statDate, dto.getMerchantId());
|
|
|
|
|
+ if (existing == null) {
|
|
|
|
|
+ merchantStatMapper.insert(buildMerchantStat(dto, statDate));
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ mergeMerchantStat(existing, dto);
|
|
|
|
|
+ merchantStatMapper.updateById(existing);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ @Transactional(rollbackFor = Exception.class)
|
|
|
|
|
+ public void upsertContentDetail(AnalyticsContentDetailDTO dto) {
|
|
|
|
|
+ if (dto.getContentId() == null || dto.getContentType() == null) {
|
|
|
|
|
+ throw new IllegalArgumentException("contentId 与 contentType 不能为空");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ LambdaQueryWrapper<AnalyticsContentStat> wrapper = new LambdaQueryWrapper<>();
|
|
|
|
|
+ wrapper.eq(AnalyticsContentStat::getContentType, dto.getContentType())
|
|
|
|
|
+ .eq(AnalyticsContentStat::getContentId, dto.getContentId());
|
|
|
|
|
+ AnalyticsContentStat existing = contentStatMapper.selectOne(wrapper);
|
|
|
|
|
+
|
|
|
|
|
+ if (existing == null) {
|
|
|
|
|
+ contentStatMapper.insert(buildContentStat(dto));
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ mergeContentStat(existing, dto);
|
|
|
|
|
+ contentStatMapper.updateById(existing);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ @Transactional(rollbackFor = Exception.class)
|
|
|
|
|
+ public void upsertAiChatDetail(AnalyticsAiChatDetailDTO dto) {
|
|
|
|
|
+ if (!StringUtils.hasText(dto.getChatId()) || dto.getUserId() == null) {
|
|
|
|
|
+ throw new IllegalArgumentException("chatId 与 userId 不能为空");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ LambdaQueryWrapper<AnalyticsAiChatStat> wrapper = new LambdaQueryWrapper<>();
|
|
|
|
|
+ wrapper.eq(AnalyticsAiChatStat::getChatId, dto.getChatId());
|
|
|
|
|
+ AnalyticsAiChatStat existing = aiChatStatMapper.selectOne(wrapper);
|
|
|
|
|
+
|
|
|
|
|
+ if (existing == null) {
|
|
|
|
|
+ aiChatStatMapper.insert(buildAiChatStat(dto));
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ mergeAiChatStat(existing, dto);
|
|
|
|
|
+ aiChatStatMapper.updateById(existing);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ @Transactional(rollbackFor = Exception.class)
|
|
|
|
|
+ public void trackUserRegister(AnalyticsUserRegisterDTO dto) {
|
|
|
|
|
+ if (dto.getUserId() == null) {
|
|
|
|
|
+ throw new IllegalArgumentException("userId 不能为空");
|
|
|
|
|
+ }
|
|
|
|
|
+ Date registerTime = dto.getRegisterTime() != null ? dto.getRegisterTime() : new Date();
|
|
|
|
|
+
|
|
|
|
|
+ AnalyticsTrackEventDTO eventDto = new AnalyticsTrackEventDTO();
|
|
|
|
|
+ eventDto.setEventId(dto.getEventId());
|
|
|
|
|
+ eventDto.setEventCode(AnalyticsEventCode.USER_REGISTER);
|
|
|
|
|
+ eventDto.setUserId(dto.getUserId());
|
|
|
|
|
+ eventDto.setChannel(dto.getChannel());
|
|
|
|
|
+ eventDto.setCity(dto.getCity());
|
|
|
|
|
+ eventDto.setEventTime(registerTime);
|
|
|
|
|
+ insertEvent(eventDto);
|
|
|
|
|
+
|
|
|
|
|
+ Date statDate = AnalyticsDateUtil.truncateToDate(registerTime);
|
|
|
|
|
+ AnalyticsUserStat existing = findUserStat(statDate, dto.getUserId());
|
|
|
|
|
+ if (existing == null) {
|
|
|
|
|
+ AnalyticsUserStat stat = new AnalyticsUserStat();
|
|
|
|
|
+ stat.setStatDate(statDate);
|
|
|
|
|
+ stat.setUserId(dto.getUserId());
|
|
|
|
|
+ stat.setUserPhone(dto.getUserPhone());
|
|
|
|
|
+ stat.setRegisterTime(registerTime);
|
|
|
|
|
+ stat.setFirstLaunchTime(registerTime);
|
|
|
|
|
+ stat.setLastActiveTime(registerTime);
|
|
|
|
|
+ stat.setChannel(dto.getChannel());
|
|
|
|
|
+ stat.setCity(dto.getCity());
|
|
|
|
|
+ stat.setOnlineDurationMin(0);
|
|
|
|
|
+ userStatMapper.insert(stat);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ applyUserRegisterMerge(existing, dto.getUserPhone(), registerTime, dto.getChannel(), dto.getCity());
|
|
|
|
|
+ userStatMapper.updateById(existing);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ @Transactional(rollbackFor = Exception.class)
|
|
|
|
|
+ public void trackUserLogin(AnalyticsUserLoginDTO dto) {
|
|
|
|
|
+ if (dto.getUserId() == null) {
|
|
|
|
|
+ throw new IllegalArgumentException("userId 不能为空");
|
|
|
|
|
+ }
|
|
|
|
|
+ Date activeTime = dto.getLastActiveTime() != null ? dto.getLastActiveTime() : new Date();
|
|
|
|
|
+ Date firstLaunch = dto.getFirstLaunchTime() != null ? dto.getFirstLaunchTime() : activeTime;
|
|
|
|
|
+
|
|
|
|
|
+ AnalyticsTrackEventDTO eventDto = new AnalyticsTrackEventDTO();
|
|
|
|
|
+ eventDto.setEventId(dto.getEventId());
|
|
|
|
|
+ eventDto.setEventCode(AnalyticsEventCode.USER_LOGIN);
|
|
|
|
|
+ eventDto.setUserId(dto.getUserId());
|
|
|
|
|
+ eventDto.setCity(dto.getCity());
|
|
|
|
|
+ eventDto.setDeviceType(dto.getDeviceName());
|
|
|
|
|
+ eventDto.setEventTime(activeTime);
|
|
|
|
|
+ insertEvent(eventDto);
|
|
|
|
|
+
|
|
|
|
|
+ Date statDate = AnalyticsDateUtil.truncateToDate(activeTime);
|
|
|
|
|
+ AnalyticsUserStat existing = findUserStat(statDate, dto.getUserId());
|
|
|
|
|
+ if (existing == null) {
|
|
|
|
|
+ AnalyticsUserStat stat = new AnalyticsUserStat();
|
|
|
|
|
+ stat.setStatDate(statDate);
|
|
|
|
|
+ stat.setUserId(dto.getUserId());
|
|
|
|
|
+ stat.setFirstLaunchTime(firstLaunch);
|
|
|
|
|
+ stat.setLastActiveTime(activeTime);
|
|
|
|
|
+ stat.setCity(dto.getCity());
|
|
|
|
|
+ stat.setDeviceType(dto.getDeviceName());
|
|
|
|
|
+ stat.setOnlineDurationMin(0);
|
|
|
|
|
+ userStatMapper.insert(stat);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ applyUserSessionMerge(existing, dto.getFirstLaunchTime(), firstLaunch,
|
|
|
|
|
+ activeTime, dto.getCity(), dto.getDeviceName(), null);
|
|
|
|
|
+ userStatMapper.updateById(existing);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ @Transactional(rollbackFor = Exception.class)
|
|
|
|
|
+ public void trackUserLogout(AnalyticsUserLogoutDTO dto) {
|
|
|
|
|
+ if (dto.getUserId() == null) {
|
|
|
|
|
+ throw new IllegalArgumentException("userId 不能为空");
|
|
|
|
|
+ }
|
|
|
|
|
+ if (dto.getOnlineDurationMin() == null || dto.getOnlineDurationMin() < 0) {
|
|
|
|
|
+ throw new IllegalArgumentException("onlineDurationMin 不能为空且不能小于0");
|
|
|
|
|
+ }
|
|
|
|
|
+ Date activeTime = dto.getLastActiveTime() != null ? dto.getLastActiveTime() : new Date();
|
|
|
|
|
+ int sessionMin = dto.getOnlineDurationMin();
|
|
|
|
|
+
|
|
|
|
|
+ AnalyticsTrackEventDTO eventDto = new AnalyticsTrackEventDTO();
|
|
|
|
|
+ eventDto.setEventId(dto.getEventId());
|
|
|
|
|
+ eventDto.setEventCode(AnalyticsEventCode.USER_LOGOUT);
|
|
|
|
|
+ eventDto.setUserId(dto.getUserId());
|
|
|
|
|
+ eventDto.setCity(dto.getCity());
|
|
|
|
|
+ eventDto.setDeviceType(dto.getDeviceName());
|
|
|
|
|
+ eventDto.setDurationMs(sessionMin > 0 ? sessionMin * 60000L : 0L);
|
|
|
|
|
+ eventDto.setEventTime(activeTime);
|
|
|
|
|
+ insertEvent(eventDto);
|
|
|
|
|
+
|
|
|
|
|
+ Date statDate = AnalyticsDateUtil.truncateToDate(activeTime);
|
|
|
|
|
+ AnalyticsUserStat existing = findUserStat(statDate, dto.getUserId());
|
|
|
|
|
+ if (existing == null) {
|
|
|
|
|
+ AnalyticsUserStat stat = new AnalyticsUserStat();
|
|
|
|
|
+ stat.setStatDate(statDate);
|
|
|
|
|
+ stat.setUserId(dto.getUserId());
|
|
|
|
|
+ stat.setFirstLaunchTime(dto.getFirstLaunchTime() != null ? dto.getFirstLaunchTime() : activeTime);
|
|
|
|
|
+ stat.setLastActiveTime(activeTime);
|
|
|
|
|
+ stat.setCity(dto.getCity());
|
|
|
|
|
+ stat.setDeviceType(dto.getDeviceName());
|
|
|
|
|
+ stat.setOnlineDurationMin(sessionMin);
|
|
|
|
|
+ userStatMapper.insert(stat);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ applyUserSessionMerge(existing, dto.getFirstLaunchTime(), activeTime,
|
|
|
|
|
+ activeTime, dto.getCity(), dto.getDeviceName(), sessionMin);
|
|
|
|
|
+ userStatMapper.updateById(existing);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ @Transactional(rollbackFor = Exception.class)
|
|
|
|
|
+ public void trackAiChatEnd(AnalyticsAiChatEndDTO dto) {
|
|
|
|
|
+ if (!StringUtils.hasText(dto.getChatId())) {
|
|
|
|
|
+ throw new IllegalArgumentException("chatId 不能为空");
|
|
|
|
|
+ }
|
|
|
|
|
+ if (dto.getUserId() == null) {
|
|
|
|
|
+ throw new IllegalArgumentException("userId 不能为空");
|
|
|
|
|
+ }
|
|
|
|
|
+ if (dto.getStartTime() == null) {
|
|
|
|
|
+ throw new IllegalArgumentException("startTime 不能为空");
|
|
|
|
|
+ }
|
|
|
|
|
+ if (dto.getMessageCount() == null) {
|
|
|
|
|
+ throw new IllegalArgumentException("messageCount 不能为空");
|
|
|
|
|
+ }
|
|
|
|
|
+ if (dto.getAiResponseDurationMs() == null) {
|
|
|
|
|
+ throw new IllegalArgumentException("aiResponseDurationMs 不能为空");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ Date endTime = new Date();
|
|
|
|
|
+ AnalyticsTrackEventDTO eventDto = new AnalyticsTrackEventDTO();
|
|
|
|
|
+ eventDto.setEventId(dto.getEventId());
|
|
|
|
|
+ eventDto.setEventCode(AnalyticsEventCode.AI_CHAT_END);
|
|
|
|
|
+ eventDto.setUserId(dto.getUserId());
|
|
|
|
|
+ eventDto.setDurationMs(dto.getAiResponseDurationMs());
|
|
|
|
|
+ eventDto.setEventTime(endTime);
|
|
|
|
|
+ insertEvent(eventDto);
|
|
|
|
|
+
|
|
|
|
|
+ AnalyticsAiChatDetailDTO detail = new AnalyticsAiChatDetailDTO();
|
|
|
|
|
+ detail.setChatId(dto.getChatId());
|
|
|
|
|
+ detail.setUserId(dto.getUserId());
|
|
|
|
|
+ detail.setStartTime(dto.getStartTime());
|
|
|
|
|
+ detail.setMessageCount(dto.getMessageCount());
|
|
|
|
|
+ detail.setAiResponseDurationMs(dto.getAiResponseDurationMs());
|
|
|
|
|
+ upsertAiChatDetail(detail);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ @Transactional(rollbackFor = Exception.class)
|
|
|
|
|
+ public void trackContentPublish(AnalyticsContentPublishDTO dto) {
|
|
|
|
|
+ if (dto.getContentId() == null) {
|
|
|
|
|
+ throw new IllegalArgumentException("contentId 不能为空");
|
|
|
|
|
+ }
|
|
|
|
|
+ if (dto.getContentType() == null) {
|
|
|
|
|
+ throw new IllegalArgumentException("contentType 不能为空");
|
|
|
|
|
+ }
|
|
|
|
|
+ if (dto.getAuthorType() == null) {
|
|
|
|
|
+ throw new IllegalArgumentException("authorType 不能为空");
|
|
|
|
|
+ }
|
|
|
|
|
+ if (dto.getAuthorId() == null) {
|
|
|
|
|
+ throw new IllegalArgumentException("authorId 不能为空");
|
|
|
|
|
+ }
|
|
|
|
|
+ if (dto.getPublishTime() == null) {
|
|
|
|
|
+ throw new IllegalArgumentException("publishTime 不能为空");
|
|
|
|
|
+ }
|
|
|
|
|
+ if (dto.getContentType() < 1 || dto.getContentType() > 3) {
|
|
|
|
|
+ throw new IllegalArgumentException("contentType 取值范围 1~3");
|
|
|
|
|
+ }
|
|
|
|
|
+ if (dto.getAuthorType() < 1 || dto.getAuthorType() > 2) {
|
|
|
|
|
+ throw new IllegalArgumentException("authorType 取值范围 1~2");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ AnalyticsTrackEventDTO eventDto = new AnalyticsTrackEventDTO();
|
|
|
|
|
+ eventDto.setEventId(dto.getEventId());
|
|
|
|
|
+ eventDto.setEventCode(AnalyticsEventCode.CONTENT_PUBLISH);
|
|
|
|
|
+ eventDto.setTargetId(dto.getContentId());
|
|
|
|
|
+ eventDto.setContentType(dto.getContentType());
|
|
|
|
|
+ if (dto.getAuthorType() == 1) {
|
|
|
|
|
+ eventDto.setUserId(dto.getAuthorId());
|
|
|
|
|
+ } else {
|
|
|
|
|
+ eventDto.setMerchantId(dto.getAuthorId());
|
|
|
|
|
+ }
|
|
|
|
|
+ eventDto.setEventTime(dto.getPublishTime());
|
|
|
|
|
+ insertEvent(eventDto);
|
|
|
|
|
+
|
|
|
|
|
+ AnalyticsContentDetailDTO detail = new AnalyticsContentDetailDTO();
|
|
|
|
|
+ detail.setContentId(dto.getContentId());
|
|
|
|
|
+ detail.setContentType(dto.getContentType());
|
|
|
|
|
+ detail.setAuthorType(dto.getAuthorType());
|
|
|
|
|
+ detail.setAuthorId(dto.getAuthorId());
|
|
|
|
|
+ detail.setPublishTime(dto.getPublishTime());
|
|
|
|
|
+ upsertContentDetail(detail);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ @Transactional(rollbackFor = Exception.class)
|
|
|
|
|
+ public void trackContentInteract(AnalyticsContentInteractDTO dto) {
|
|
|
|
|
+ if (dto.getContentId() == null) {
|
|
|
|
|
+ throw new IllegalArgumentException("contentId 不能为空");
|
|
|
|
|
+ }
|
|
|
|
|
+ if (dto.getContentType() == null) {
|
|
|
|
|
+ throw new IllegalArgumentException("contentType 不能为空");
|
|
|
|
|
+ }
|
|
|
|
|
+ if (dto.getIncrement() == null || dto.getIncrement() == 0) {
|
|
|
|
|
+ throw new IllegalArgumentException("increment 不能为空且不能为0");
|
|
|
|
|
+ }
|
|
|
|
|
+ if (dto.getContentType() < 1 || dto.getContentType() > 3) {
|
|
|
|
|
+ throw new IllegalArgumentException("contentType 取值范围 1~3");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ Date eventTime = new Date();
|
|
|
|
|
+ AnalyticsTrackEventDTO eventDto = new AnalyticsTrackEventDTO();
|
|
|
|
|
+ eventDto.setEventId(dto.getEventId());
|
|
|
|
|
+ eventDto.setEventCode(AnalyticsEventCode.CONTENT_INTERACT);
|
|
|
|
|
+ eventDto.setUserId(dto.getUserId());
|
|
|
|
|
+ eventDto.setTargetId(dto.getContentId());
|
|
|
|
|
+ eventDto.setContentType(dto.getContentType());
|
|
|
|
|
+ eventDto.setEventTime(eventTime);
|
|
|
|
|
+ insertEvent(eventDto);
|
|
|
|
|
+
|
|
|
|
|
+ addContentInteraction(dto.getContentType(), dto.getContentId(), dto.getIncrement());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ @Transactional(rollbackFor = Exception.class)
|
|
|
|
|
+ public void trackMerchantView(AnalyticsMerchantViewDTO dto) {
|
|
|
|
|
+ if (dto.getMerchantId() == null) {
|
|
|
|
|
+ throw new IllegalArgumentException("merchantId 不能为空");
|
|
|
|
|
+ }
|
|
|
|
|
+ if (dto.getShopType() == null) {
|
|
|
|
|
+ throw new IllegalArgumentException("shopType 不能为空");
|
|
|
|
|
+ }
|
|
|
|
|
+ if (dto.getVisitUv() == null) {
|
|
|
|
|
+ throw new IllegalArgumentException("visitUv 不能为空");
|
|
|
|
|
+ }
|
|
|
|
|
+ if (dto.getVisitPv() == null) {
|
|
|
|
|
+ throw new IllegalArgumentException("visitPv 不能为空");
|
|
|
|
|
+ }
|
|
|
|
|
+ if (dto.getShopType() < 1 || dto.getShopType() > 3) {
|
|
|
|
|
+ throw new IllegalArgumentException("shopType 取值范围 1~3");
|
|
|
|
|
+ }
|
|
|
|
|
+ if (dto.getVisitUv() < 0 || dto.getVisitPv() < 0) {
|
|
|
|
|
+ throw new IllegalArgumentException("visitUv、visitPv 不能为负数");
|
|
|
|
|
+ }
|
|
|
|
|
+ if (dto.getVisitUv() == 0 && dto.getVisitPv() == 0) {
|
|
|
|
|
+ throw new IllegalArgumentException("visitUv 与 visitPv 不能同时为0");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ Date eventTime = new Date();
|
|
|
|
|
+ AnalyticsTrackEventDTO eventDto = new AnalyticsTrackEventDTO();
|
|
|
|
|
+ eventDto.setEventId(dto.getEventId());
|
|
|
|
|
+ eventDto.setEventCode(AnalyticsEventCode.MERCHANT_VIEW);
|
|
|
|
|
+ eventDto.setUserId(dto.getUserId());
|
|
|
|
|
+ eventDto.setMerchantId(dto.getMerchantId());
|
|
|
|
|
+ eventDto.setEventTime(eventTime);
|
|
|
|
|
+ insertEvent(eventDto);
|
|
|
|
|
+
|
|
|
|
|
+ Date statDate = AnalyticsDateUtil.truncateToDate(eventTime);
|
|
|
|
|
+ addMerchantVisit(statDate, dto.getMerchantId(), dto.getShopType(), dto.getVisitUv(), dto.getVisitPv());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private void applyUserRegisterMerge(AnalyticsUserStat target, String userPhone, Date registerTime,
|
|
|
|
|
+ String channel, String city) {
|
|
|
|
|
+ if (StringUtils.hasText(userPhone)) {
|
|
|
|
|
+ target.setUserPhone(userPhone);
|
|
|
|
|
+ }
|
|
|
|
|
+ if (target.getRegisterTime() == null) {
|
|
|
|
|
+ target.setRegisterTime(registerTime);
|
|
|
|
|
+ }
|
|
|
|
|
+ if (target.getFirstLaunchTime() == null) {
|
|
|
|
|
+ target.setFirstLaunchTime(registerTime);
|
|
|
|
|
+ }
|
|
|
|
|
+ target.setLastActiveTime(registerTime);
|
|
|
|
|
+ if (StringUtils.hasText(channel)) {
|
|
|
|
|
+ target.setChannel(channel);
|
|
|
|
|
+ }
|
|
|
|
|
+ if (StringUtils.hasText(city)) {
|
|
|
|
|
+ target.setCity(city);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private void applyUserSessionMerge(AnalyticsUserStat target, Date firstLaunchTime,
|
|
|
|
|
+ Date defaultFirstLaunch, Date lastActiveTime, String city,
|
|
|
|
|
+ String deviceName, Integer addOnlineDurationMin) {
|
|
|
|
|
+ if (firstLaunchTime != null) {
|
|
|
|
|
+ target.setFirstLaunchTime(firstLaunchTime);
|
|
|
|
|
+ } else if (target.getFirstLaunchTime() == null) {
|
|
|
|
|
+ target.setFirstLaunchTime(defaultFirstLaunch);
|
|
|
|
|
+ }
|
|
|
|
|
+ target.setLastActiveTime(lastActiveTime);
|
|
|
|
|
+ if (StringUtils.hasText(city)) {
|
|
|
|
|
+ target.setCity(city);
|
|
|
|
|
+ }
|
|
|
|
|
+ if (StringUtils.hasText(deviceName)) {
|
|
|
|
|
+ target.setDeviceType(deviceName);
|
|
|
|
|
+ }
|
|
|
|
|
+ if (addOnlineDurationMin != null && addOnlineDurationMin > 0) {
|
|
|
|
|
+ target.setOnlineDurationMin(defaultInt(target.getOnlineDurationMin()) + addOnlineDurationMin);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private AnalyticsUserStat findUserStat(Date statDate, Long userId) {
|
|
|
|
|
+ LambdaQueryWrapper<AnalyticsUserStat> wrapper = new LambdaQueryWrapper<>();
|
|
|
|
|
+ wrapper.eq(AnalyticsUserStat::getStatDate, statDate).eq(AnalyticsUserStat::getUserId, userId);
|
|
|
|
|
+ return userStatMapper.selectOne(wrapper);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private AnalyticsMerchantStat findMerchantStat(Date statDate, Long merchantId) {
|
|
|
|
|
+ LambdaQueryWrapper<AnalyticsMerchantStat> wrapper = new LambdaQueryWrapper<>();
|
|
|
|
|
+ wrapper.eq(AnalyticsMerchantStat::getStatDate, statDate).eq(AnalyticsMerchantStat::getMerchantId, merchantId);
|
|
|
|
|
+ return merchantStatMapper.selectOne(wrapper);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private AnalyticsUserStat buildUserStat(AnalyticsUserDetailDTO dto, Date statDate) {
|
|
|
|
|
+ AnalyticsUserStat stat = new AnalyticsUserStat();
|
|
|
|
|
+ stat.setStatDate(statDate);
|
|
|
|
|
+ stat.setUserId(dto.getUserId());
|
|
|
|
|
+ stat.setUserPhone(dto.getUserPhone());
|
|
|
|
|
+ stat.setFirstLaunchTime(dto.getFirstLaunchTime());
|
|
|
|
|
+ stat.setLastActiveTime(dto.getLastActiveTime());
|
|
|
|
|
+ stat.setRegisterTime(dto.getRegisterTime());
|
|
|
|
|
+ stat.setCity(dto.getCity());
|
|
|
|
|
+ stat.setDeviceType(dto.getDeviceType());
|
|
|
|
|
+ stat.setChannel(dto.getChannel());
|
|
|
|
|
+ stat.setOnlineDurationMin(defaultInt(dto.getOnlineDurationMin()));
|
|
|
|
|
+ return stat;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private void mergeUserStat(AnalyticsUserStat target, AnalyticsUserDetailDTO dto, Date statDate) {
|
|
|
|
|
+ target.setStatDate(statDate);
|
|
|
|
|
+ if (StringUtils.hasText(dto.getUserPhone())) target.setUserPhone(dto.getUserPhone());
|
|
|
|
|
+ if (dto.getFirstLaunchTime() != null) target.setFirstLaunchTime(dto.getFirstLaunchTime());
|
|
|
|
|
+ if (dto.getLastActiveTime() != null) target.setLastActiveTime(dto.getLastActiveTime());
|
|
|
|
|
+ if (dto.getRegisterTime() != null) target.setRegisterTime(dto.getRegisterTime());
|
|
|
|
|
+ if (StringUtils.hasText(dto.getCity())) target.setCity(dto.getCity());
|
|
|
|
|
+ if (StringUtils.hasText(dto.getDeviceType())) target.setDeviceType(dto.getDeviceType());
|
|
|
|
|
+ if (StringUtils.hasText(dto.getChannel())) target.setChannel(dto.getChannel());
|
|
|
|
|
+ if (dto.getOnlineDurationMin() != null) target.setOnlineDurationMin(dto.getOnlineDurationMin());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private AnalyticsMerchantStat buildMerchantStat(AnalyticsMerchantDetailDTO dto, Date statDate) {
|
|
|
|
|
+ AnalyticsMerchantStat stat = new AnalyticsMerchantStat();
|
|
|
|
|
+ stat.setStatDate(statDate);
|
|
|
|
|
+ stat.setMerchantId(dto.getMerchantId());
|
|
|
|
|
+ stat.setShopType(dto.getShopType());
|
|
|
|
|
+ stat.setVisitUv(defaultInt(dto.getVisitUv()));
|
|
|
|
|
+ stat.setVisitPv(defaultInt(dto.getVisitPv()));
|
|
|
|
|
+ stat.setVerifyConversionRate(dto.getVerifyConversionRate());
|
|
|
|
|
+ stat.setSettleTime(dto.getSettleTime());
|
|
|
|
|
+ stat.setSettleStatus(dto.getSettleStatus());
|
|
|
|
|
+ return stat;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private void mergeMerchantStat(AnalyticsMerchantStat target, AnalyticsMerchantDetailDTO dto) {
|
|
|
|
|
+ if (dto.getShopType() != null) target.setShopType(dto.getShopType());
|
|
|
|
|
+ if (dto.getVisitUv() != null) target.setVisitUv(dto.getVisitUv());
|
|
|
|
|
+ if (dto.getVisitPv() != null) target.setVisitPv(dto.getVisitPv());
|
|
|
|
|
+ if (dto.getVerifyConversionRate() != null) target.setVerifyConversionRate(dto.getVerifyConversionRate());
|
|
|
|
|
+ if (dto.getSettleTime() != null) target.setSettleTime(dto.getSettleTime());
|
|
|
|
|
+ if (dto.getSettleStatus() != null) target.setSettleStatus(dto.getSettleStatus());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private AnalyticsContentStat buildContentStat(AnalyticsContentDetailDTO dto) {
|
|
|
|
|
+ AnalyticsContentStat stat = new AnalyticsContentStat();
|
|
|
|
|
+ stat.setContentId(dto.getContentId());
|
|
|
|
|
+ stat.setContentType(dto.getContentType());
|
|
|
|
|
+ stat.setAuthorType(dto.getAuthorType() != null ? dto.getAuthorType() : 1);
|
|
|
|
|
+ stat.setAuthorId(dto.getAuthorId() != null ? dto.getAuthorId() : 0L);
|
|
|
|
|
+ stat.setPublishTime(dto.getPublishTime());
|
|
|
|
|
+ stat.setInteractionCount(defaultInt(dto.getInteractionCount()));
|
|
|
|
|
+ stat.setStatus(dto.getStatus());
|
|
|
|
|
+ stat.setAuditUserId(dto.getAuditUserId());
|
|
|
|
|
+ stat.setAuditStatus(dto.getAuditStatus());
|
|
|
|
|
+ stat.setAuditTime(dto.getAuditTime());
|
|
|
|
|
+ return stat;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private void mergeContentStat(AnalyticsContentStat target, AnalyticsContentDetailDTO dto) {
|
|
|
|
|
+ if (dto.getAuthorType() != null) target.setAuthorType(dto.getAuthorType());
|
|
|
|
|
+ if (dto.getAuthorId() != null) target.setAuthorId(dto.getAuthorId());
|
|
|
|
|
+ if (dto.getPublishTime() != null) target.setPublishTime(dto.getPublishTime());
|
|
|
|
|
+ if (dto.getInteractionCount() != null) target.setInteractionCount(dto.getInteractionCount());
|
|
|
|
|
+ if (dto.getStatus() != null) target.setStatus(dto.getStatus());
|
|
|
|
|
+ if (dto.getAuditUserId() != null) target.setAuditUserId(dto.getAuditUserId());
|
|
|
|
|
+ if (dto.getAuditStatus() != null) target.setAuditStatus(dto.getAuditStatus());
|
|
|
|
|
+ if (dto.getAuditTime() != null) target.setAuditTime(dto.getAuditTime());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private void addContentInteraction(Integer contentType, Long contentId, int increment) {
|
|
|
|
|
+ LambdaQueryWrapper<AnalyticsContentStat> wrapper = new LambdaQueryWrapper<>();
|
|
|
|
|
+ wrapper.eq(AnalyticsContentStat::getContentType, contentType)
|
|
|
|
|
+ .eq(AnalyticsContentStat::getContentId, contentId);
|
|
|
|
|
+ AnalyticsContentStat existing = contentStatMapper.selectOne(wrapper);
|
|
|
|
|
+ if (existing == null) {
|
|
|
|
|
+ AnalyticsContentStat stat = new AnalyticsContentStat();
|
|
|
|
|
+ stat.setContentType(contentType);
|
|
|
|
|
+ stat.setContentId(contentId);
|
|
|
|
|
+ stat.setAuthorType(1);
|
|
|
|
|
+ stat.setAuthorId(0L);
|
|
|
|
|
+ stat.setInteractionCount(Math.max(0, increment));
|
|
|
|
|
+ contentStatMapper.insert(stat);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ int next = defaultInt(existing.getInteractionCount()) + increment;
|
|
|
|
|
+ existing.setInteractionCount(Math.max(0, next));
|
|
|
|
|
+ contentStatMapper.updateById(existing);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private void addMerchantVisit(Date statDate, Long merchantId, Integer shopType, int visitUvDelta, int visitPvDelta) {
|
|
|
|
|
+ AnalyticsMerchantStat existing = findMerchantStat(statDate, merchantId);
|
|
|
|
|
+ if (existing == null) {
|
|
|
|
|
+ AnalyticsMerchantStat stat = new AnalyticsMerchantStat();
|
|
|
|
|
+ stat.setStatDate(statDate);
|
|
|
|
|
+ stat.setMerchantId(merchantId);
|
|
|
|
|
+ stat.setShopType(shopType);
|
|
|
|
|
+ stat.setVisitUv(Math.max(0, visitUvDelta));
|
|
|
|
|
+ stat.setVisitPv(Math.max(0, visitPvDelta));
|
|
|
|
|
+ merchantStatMapper.insert(stat);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (shopType != null) {
|
|
|
|
|
+ existing.setShopType(shopType);
|
|
|
|
|
+ }
|
|
|
|
|
+ existing.setVisitUv(defaultInt(existing.getVisitUv()) + visitUvDelta);
|
|
|
|
|
+ existing.setVisitPv(defaultInt(existing.getVisitPv()) + visitPvDelta);
|
|
|
|
|
+ merchantStatMapper.updateById(existing);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private AnalyticsAiChatStat buildAiChatStat(AnalyticsAiChatDetailDTO dto) {
|
|
|
|
|
+ AnalyticsAiChatStat stat = new AnalyticsAiChatStat();
|
|
|
|
|
+ stat.setChatId(dto.getChatId());
|
|
|
|
|
+ stat.setUserId(dto.getUserId());
|
|
|
|
|
+ stat.setStartTime(dto.getStartTime() != null ? dto.getStartTime() : new Date());
|
|
|
|
|
+ stat.setMessageCount(defaultInt(dto.getMessageCount()));
|
|
|
|
|
+ stat.setAiResponseDurationMs(dto.getAiResponseDurationMs() != null ? dto.getAiResponseDurationMs() : 0L);
|
|
|
|
|
+ return stat;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private void mergeAiChatStat(AnalyticsAiChatStat target, AnalyticsAiChatDetailDTO dto) {
|
|
|
|
|
+ if (dto.getStartTime() != null) target.setStartTime(dto.getStartTime());
|
|
|
|
|
+ if (dto.getMessageCount() != null) target.setMessageCount(dto.getMessageCount());
|
|
|
|
|
+ if (dto.getAiResponseDurationMs() != null) target.setAiResponseDurationMs(dto.getAiResponseDurationMs());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private int defaultInt(Integer value) {
|
|
|
|
|
+ return value != null ? value : 0;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|