package com.tcm.training.admin.points.service; import cn.idev.excel.FastExcel; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.tcm.training.admin.common.DateTimes; import com.tcm.training.admin.common.PhoneMasker; import com.tcm.training.admin.points.entity.PointsLedger; import com.tcm.training.admin.points.excel.PointsLedgerExcelRow; import com.tcm.training.admin.points.mapper.PointsLedgerMapper; import com.tcm.training.admin.points.row.PointsLedgerRow; import com.tcm.training.admin.points.row.PointsLedgerSummaryRow; import com.tcm.training.admin.points.row.PointsLedgerSyncExchangeRow; import com.tcm.training.admin.points.row.PointsLedgerSyncQuizRow; import com.tcm.training.admin.points.row.PointsRegionOptionRow; import com.tcm.training.admin.points.vo.PointsLedgerItemVO; import com.tcm.training.admin.points.vo.PointsLedgerPageVO; import com.tcm.training.admin.points.vo.PointsLedgerSummaryVO; import com.tcm.training.admin.points.vo.PointsRegionOptionVO; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.net.URLEncoder; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; @Service public class PointsLedgerAppService { public static final String TYPE_INCOME = "INCOME"; public static final String TYPE_EXPENSE = "EXPENSE"; public static final String BUSINESS_LIVE_WATCH = "LIVE_WATCH"; public static final String BUSINESS_POINTS_EXCHANGE = "POINTS_EXCHANGE"; public static final String BUSINESS_POINTS_SETTLEMENT = "POINTS_SETTLEMENT"; public static final String SOURCE_LIVE_QUIZ = "LIVE_QUIZ"; public static final String SOURCE_EXCHANGE_ITEM = "EXCHANGE_ITEM"; public static final String SOURCE_POINTS_SETTLEMENT = "POINTS_SETTLEMENT"; public static final String REF_LIVE_COURSE = "LIVE_COURSE"; public static final String REF_EXCHANGE_ITEM = "EXCHANGE_ITEM"; public static final String REF_POINTS_CONFIG = "POINTS_CONFIG"; private final PointsLedgerMapper pointsLedgerMapper; public PointsLedgerAppService(PointsLedgerMapper pointsLedgerMapper) { this.pointsLedgerMapper = pointsLedgerMapper; } public PointsLedgerPageVO page(int page, int pageSize, String userPhone, String regionCode, String clinicName, String incomeExpenseType, String businessType, String contentKeyword, String referenceNo, LocalDateTime operatedFrom, LocalDateTime operatedTo) { syncLedger(); IPage result = pointsLedgerMapper.selectLedgerPage( new Page(page, pageSize), trimToNull(userPhone), trimToNull(regionCode), trimToNull(clinicName), trimToNull(incomeExpenseType), trimToNull(businessType), trimToNull(contentKeyword), trimToNull(referenceNo), operatedFrom, operatedTo ); PointsLedgerPageVO vo = new PointsLedgerPageVO(); vo.setList(toItems(result.getRecords())); vo.setTotal(result.getTotal()); vo.setPage(result.getCurrent()); vo.setPageSize(result.getSize()); vo.setSummary(buildSummary(userPhone, regionCode, clinicName, incomeExpenseType, businessType, contentKeyword, referenceNo, operatedFrom, operatedTo)); return vo; } public void export(HttpServletResponse response, String userPhone, String regionCode, String clinicName, String incomeExpenseType, String businessType, String contentKeyword, String referenceNo, LocalDateTime operatedFrom, LocalDateTime operatedTo) throws IOException { syncLedger(); response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); response.setCharacterEncoding("utf-8"); String filename = URLEncoder.encode("积分收支明细", "UTF-8").replace("+", "%20"); response.setHeader("Content-Disposition", "attachment;filename*=utf-8''" + filename + ".xlsx"); List rows = pointsLedgerMapper.selectLedgerList( trimToNull(userPhone), trimToNull(regionCode), trimToNull(clinicName), trimToNull(incomeExpenseType), trimToNull(businessType), trimToNull(contentKeyword), trimToNull(referenceNo), operatedFrom, operatedTo ); List exports = new ArrayList(); for (int i = 0; i < rows.size(); i++) { PointsLedgerRow row = rows.get(i); PointsLedgerExcelRow export = new PointsLedgerExcelRow(); export.setSerialNo(i + 1); export.setUserNickname(defaultText(row.getUserNickname())); export.setUserPhone(PhoneMasker.mask(row.getUserPhone())); export.setClinicRegion(defaultText(row.getClinicRegion())); export.setClinicName(defaultText(row.getClinicName())); export.setIncomeExpenseType(incomeExpenseText(row.getIncomeExpenseType())); export.setBusinessType(businessTypeText(row.getBusinessType())); export.setContentName(defaultText(row.getContentName())); export.setReferenceNo(defaultText(row.getReferenceNo())); export.setPointsDelta(formatSignedPoints(row.getPointsDelta())); export.setOperatedAt(formatMinute(row.getOperatedAt())); exports.add(export); } FastExcel.write(response.getOutputStream(), PointsLedgerExcelRow.class) .sheet("积分收支明细") .doWrite(exports); } public List regionOptions() { List rows = pointsLedgerMapper.selectRegionOptions(); List list = new ArrayList(); for (PointsRegionOptionRow row : rows) { list.add(new PointsRegionOptionVO(row.getRegionCode(), row.getRegionName())); } return list; } @Transactional public void syncLedger() { syncQuizLedgers(); syncExchangeLedgers(); } @Transactional public void createExchangeExpenseLedger(Long sourceId, Long appUserId, String userNickname, String userPhone, Long clinicId, String clinicName, String provinceCode, String provinceName, String cityCode, String cityName, String districtCode, String districtName, String contentName, Long referenceId, String referenceNo, Integer pointsDelta, LocalDateTime operatedAt) { if (sourceId == null) { return; } PointsLedger exists = pointsLedgerMapper.selectOne(new LambdaQueryWrapper() .eq(PointsLedger::getDeleted, 0) .eq(PointsLedger::getSourceType, SOURCE_EXCHANGE_ITEM) .eq(PointsLedger::getSourceId, sourceId) .last("limit 1")); if (exists != null) { return; } PointsLedger ledger = buildBaseLedger(appUserId, userNickname, userPhone, clinicId, clinicName, provinceCode, provinceName, cityCode, cityName, districtCode, districtName); ledger.setIncomeExpenseType(TYPE_EXPENSE); ledger.setBusinessType(BUSINESS_POINTS_EXCHANGE); ledger.setContentName(contentName); ledger.setReferenceType(REF_EXCHANGE_ITEM); ledger.setReferenceId(referenceId); ledger.setReferenceNo(referenceNo); ledger.setPointsDelta(pointsDelta == null ? 0 : -Math.abs(pointsDelta)); ledger.setOperatedAt(operatedAt == null ? LocalDateTime.now() : operatedAt); ledger.setSourceType(SOURCE_EXCHANGE_ITEM); ledger.setSourceId(sourceId); pointsLedgerMapper.insert(ledger); } /** * 记录积分活动结束后未达标用户被系统清零的支出流水。 */ @Transactional public void createSettlementClearLedger(Long sourceId, Long appUserId, String userNickname, String userPhone, Long clinicId, String clinicName, String provinceCode, String provinceName, String cityCode, String cityName, String districtCode, String districtName, String contentName, Long referenceId, String referenceNo, Integer pointsDelta, LocalDateTime operatedAt) { if (sourceId == null) { return; } PointsLedger exists = pointsLedgerMapper.selectOne(new LambdaQueryWrapper() .eq(PointsLedger::getDeleted, 0) .eq(PointsLedger::getSourceType, SOURCE_POINTS_SETTLEMENT) .eq(PointsLedger::getSourceId, sourceId) .last("limit 1")); if (exists != null) { return; } PointsLedger ledger = buildBaseLedger(appUserId, userNickname, userPhone, clinicId, clinicName, provinceCode, provinceName, cityCode, cityName, districtCode, districtName); ledger.setIncomeExpenseType(TYPE_EXPENSE); ledger.setBusinessType(BUSINESS_POINTS_SETTLEMENT); ledger.setContentName(contentName); ledger.setReferenceType(REF_POINTS_CONFIG); ledger.setReferenceId(referenceId); ledger.setReferenceNo(referenceNo); ledger.setPointsDelta(pointsDelta == null ? 0 : -Math.abs(pointsDelta)); ledger.setOperatedAt(operatedAt == null ? LocalDateTime.now() : operatedAt); ledger.setSourceType(SOURCE_POINTS_SETTLEMENT); ledger.setSourceId(sourceId); pointsLedgerMapper.insert(ledger); } private void syncQuizLedgers() { List rows = pointsLedgerMapper.selectMissingQuizLedgerRows(); for (PointsLedgerSyncQuizRow row : rows) { PointsLedger ledger = buildBaseLedger(row.getAppUserId(), row.getUserNickname(), row.getUserPhone(), row.getClinicId(), row.getClinicName(), row.getProvinceCode(), row.getProvinceName(), row.getCityCode(), row.getCityName(), row.getDistrictCode(), row.getDistrictName()); ledger.setIncomeExpenseType(TYPE_INCOME); ledger.setBusinessType(BUSINESS_LIVE_WATCH); ledger.setContentName(row.getContentName()); ledger.setReferenceType(REF_LIVE_COURSE); ledger.setReferenceId(row.getReferenceId()); ledger.setReferenceNo(row.getReferenceNo()); ledger.setPointsDelta(Math.abs(row.getPointsDelta() == null ? 0 : row.getPointsDelta())); ledger.setOperatedAt(row.getOperatedAt()); ledger.setSourceType(SOURCE_LIVE_QUIZ); ledger.setSourceId(row.getSourceId()); pointsLedgerMapper.insert(ledger); } } private void syncExchangeLedgers() { List rows = pointsLedgerMapper.selectMissingExchangeLedgerRows(); for (PointsLedgerSyncExchangeRow row : rows) { createExchangeExpenseLedger( row.getSourceId(), row.getAppUserId(), row.getUserNickname(), row.getUserPhone(), row.getClinicId(), row.getClinicName(), row.getProvinceCode(), row.getProvinceName(), row.getCityCode(), row.getCityName(), row.getDistrictCode(), row.getDistrictName(), row.getContentName(), row.getReferenceId(), row.getReferenceNo(), row.getPointsDelta(), row.getOperatedAt() ); } } private PointsLedgerSummaryVO buildSummary(String userPhone, String regionCode, String clinicName, String incomeExpenseType, String businessType, String contentKeyword, String referenceNo, LocalDateTime operatedFrom, LocalDateTime operatedTo) { PointsLedgerSummaryRow row = pointsLedgerMapper.selectLedgerSummary( trimToNull(userPhone), trimToNull(regionCode), trimToNull(clinicName), trimToNull(incomeExpenseType), trimToNull(businessType), trimToNull(contentKeyword), trimToNull(referenceNo), operatedFrom, operatedTo ); PointsLedgerSummaryVO vo = new PointsLedgerSummaryVO(); vo.setIncomeTotal(row == null || row.getIncomeTotal() == null ? 0 : row.getIncomeTotal()); vo.setExpenseTotal(row == null || row.getExpenseTotal() == null ? 0 : row.getExpenseTotal()); vo.setLiveWatchIncomeTotal(row == null || row.getLiveWatchIncomeTotal() == null ? 0 : row.getLiveWatchIncomeTotal()); vo.setExchangeExpenseTotal(row == null || row.getExchangeExpenseTotal() == null ? 0 : row.getExchangeExpenseTotal()); return vo; } private List toItems(List rows) { List list = new ArrayList(); for (PointsLedgerRow row : rows) { PointsLedgerItemVO item = new PointsLedgerItemVO(); item.setId(row.getId()); item.setUserNickname(row.getUserNickname()); item.setUserPhone(PhoneMasker.mask(row.getUserPhone())); item.setClinicRegion(row.getClinicRegion()); item.setClinicName(row.getClinicName()); item.setIncomeExpenseType(incomeExpenseText(row.getIncomeExpenseType())); item.setBusinessType(businessTypeText(row.getBusinessType())); item.setContentName(row.getContentName()); item.setReferenceNo(row.getReferenceNo()); item.setPointsDelta(row.getPointsDelta()); item.setOperatedAt(row.getOperatedAt()); item.setReferencePath(row.getReferencePath()); item.setCanJump(row.getCanJump() != null && row.getCanJump() == 1); list.add(item); } return list; } private PointsLedger buildBaseLedger(Long appUserId, String userNickname, String userPhone, Long clinicId, String clinicName, String provinceCode, String provinceName, String cityCode, String cityName, String districtCode, String districtName) { PointsLedger ledger = new PointsLedger(); ledger.setAppUserId(appUserId); ledger.setUserNickname(userNickname); ledger.setUserPhone(userPhone); ledger.setClinicId(clinicId); ledger.setClinicName(clinicName); ledger.setProvinceCode(provinceCode); ledger.setProvinceName(provinceName); ledger.setCityCode(cityCode); ledger.setCityName(cityName); ledger.setDistrictCode(districtCode); ledger.setDistrictName(districtName); return ledger; } private String incomeExpenseText(String value) { if (TYPE_INCOME.equals(value)) { return "收入"; } if (TYPE_EXPENSE.equals(value)) { return "支出"; } return "-"; } private String businessTypeText(String value) { if (BUSINESS_LIVE_WATCH.equals(value)) { return "观看直播"; } if (BUSINESS_POINTS_EXCHANGE.equals(value)) { return "积分兑换"; } if (BUSINESS_POINTS_SETTLEMENT.equals(value)) { return "活动结算"; } return "-"; } private String formatSignedPoints(Integer value) { int points = value == null ? 0 : value; return points > 0 ? "+" + points : String.valueOf(points); } private String formatMinute(LocalDateTime value) { if (value == null) { return "-"; } return value.format(DateTimes.localDateTimeFormatter()).substring(0, 16); } private String trimToNull(String text) { if (text == null) { return null; } String value = text.trim(); return value.isEmpty() ? null : value; } private String defaultText(String text) { if (text == null || text.trim().isEmpty()) { return "-"; } return text; } }