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.app.notice.service.AppSystemNoticeDispatchCommand; import com.tcm.training.admin.app.notice.service.AppSystemNoticeDispatchService; import com.tcm.training.admin.app.notice.service.AppSystemNoticeType; import com.tcm.training.admin.common.DateTimes; import com.tcm.training.admin.common.PageResponse; import com.tcm.training.admin.common.PhoneMasker; import com.tcm.training.admin.points.dto.ExchangeShipRequest; import com.tcm.training.admin.points.entity.PointsExchangeOrder; import com.tcm.training.admin.points.entity.PointsExchangeOrderItem; import com.tcm.training.admin.points.excel.PointsExchangeItemExcelRow; import com.tcm.training.admin.points.excel.PointsExchangeOrderExcelRow; import com.tcm.training.admin.points.mapper.PointsExchangeOrderItemMapper; import com.tcm.training.admin.points.mapper.PointsExchangeOrderMapper; import com.tcm.training.admin.points.row.PointsExchangeItemRow; import com.tcm.training.admin.points.row.PointsExchangeOrderRow; import com.tcm.training.admin.points.vo.PointsExchangeItemDetailVO; import com.tcm.training.admin.points.vo.PointsExchangeItemListItemVO; import com.tcm.training.admin.points.vo.PointsExchangeOrderListItemVO; 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.LinkedHashMap; import java.util.List; import java.util.Map; @Service public class PointsExchangeAppService { private final PointsExchangeOrderMapper pointsExchangeOrderMapper; private final PointsExchangeOrderItemMapper pointsExchangeOrderItemMapper; private final AppSystemNoticeDispatchService appSystemNoticeDispatchService; public PointsExchangeAppService(PointsExchangeOrderMapper pointsExchangeOrderMapper, PointsExchangeOrderItemMapper pointsExchangeOrderItemMapper, AppSystemNoticeDispatchService appSystemNoticeDispatchService) { this.pointsExchangeOrderMapper = pointsExchangeOrderMapper; this.pointsExchangeOrderItemMapper = pointsExchangeOrderItemMapper; this.appSystemNoticeDispatchService = appSystemNoticeDispatchService; } public PageResponse pageOrders(int page, int pageSize, String giftName, String receiverPhone, Integer status, LocalDateTime createdFrom, LocalDateTime createdTo) { IPage result = pointsExchangeOrderMapper.selectOrderPage( new Page(page, pageSize), trimToNull(giftName), trimToNull(receiverPhone), status, createdFrom, createdTo ); return PageResponse.of(toOrderList(result.getRecords()), result.getTotal(), result.getCurrent(), result.getSize()); } public PageResponse pageItems(int page, int pageSize, Long orderId, String orderNo, String buyerPhone, String giftName, String receiverPhone, Integer status, LocalDateTime createdFrom, LocalDateTime createdTo) { IPage result = pointsExchangeOrderItemMapper.selectItemPage( new Page(page, pageSize), orderId, trimToNull(orderNo), trimToNull(buyerPhone), trimToNull(giftName), trimToNull(receiverPhone), status, createdFrom, createdTo ); return PageResponse.of(toItemList(result.getRecords()), result.getTotal(), result.getCurrent(), result.getSize()); } public PointsExchangeItemDetailVO itemDetail(Long id) { PointsExchangeItemRow row = pointsExchangeOrderItemMapper.selectItemDetail(id); if (row == null) { throw new IllegalArgumentException("兑换明细不存在"); } PointsExchangeItemDetailVO vo = new PointsExchangeItemDetailVO(); vo.setId(row.getId()); vo.setOrderId(row.getOrderId()); vo.setOrderNo(row.getOrderNo()); vo.setStatus(row.getStatus()); vo.setTrackingNo(row.getTrackingNo()); vo.setShippedAt(row.getShippedAt()); vo.setCompletedAt(row.getCompletedAt()); vo.setCancelledAt(row.getCancelledAt()); vo.setCreatedAt(row.getCreatedAt()); PointsExchangeItemDetailVO.BuyerVO buyer = new PointsExchangeItemDetailVO.BuyerVO(); buyer.setUserId(row.getAppUserId()); buyer.setNickname(row.getBuyerNickname()); buyer.setPhone(row.getBuyerPhone()); buyer.setClinicName(row.getClinicName()); buyer.setSettledPoints(row.getSettledPoints()); buyer.setCreatedAt(row.getBuyerCreatedAt()); vo.setBuyer(buyer); PointsExchangeItemDetailVO.ReceiverVO receiver = new PointsExchangeItemDetailVO.ReceiverVO(); receiver.setName(row.getReceiverName()); receiver.setPhone(row.getReceiverPhone()); receiver.setAddress(row.getReceiverAddress()); vo.setReceiver(receiver); PointsExchangeItemDetailVO.GiftVO gift = new PointsExchangeItemDetailVO.GiftVO(); gift.setCover(row.getGiftCover()); gift.setName(row.getGiftName()); gift.setRequiredPoints(row.getRequiredPoints()); gift.setActivityStartAt(row.getActivityStartAt()); gift.setActivityEndAt(row.getActivityEndAt()); vo.setGift(gift); return vo; } @Transactional public void shipOrder(Long id, ExchangeShipRequest request) { PointsExchangeOrder order = requireOrder(id); validatePendingStatus(order.getStatus()); LocalDateTime now = LocalDateTime.now(); List items = pointsExchangeOrderItemMapper.selectList(new LambdaQueryWrapper() .eq(PointsExchangeOrderItem::getDeleted, 0) .eq(PointsExchangeOrderItem::getOrderId, id)); for (PointsExchangeOrderItem item : items) { if (item.getStatus() != null && item.getStatus() == 0) { item.setStatus(1); item.setTrackingNo(request.getTrackingNo().trim()); item.setShippedAt(now); pointsExchangeOrderItemMapper.updateById(item); } } order.setStatus(1); order.setTrackingNo(request.getTrackingNo().trim()); order.setShippedAt(now); pointsExchangeOrderMapper.updateById(order); dispatchOrderShippedNotice(order); } @Transactional public void shipItem(Long id, ExchangeShipRequest request) { PointsExchangeOrderItem item = requireItem(id); validatePendingStatus(item.getStatus()); LocalDateTime now = LocalDateTime.now(); item.setStatus(1); item.setTrackingNo(request.getTrackingNo().trim()); item.setShippedAt(now); pointsExchangeOrderItemMapper.updateById(item); refreshOrderStatus(item.getOrderId(), request.getTrackingNo().trim(), now); dispatchOrderShippedNotice(requireOrder(item.getOrderId())); } /** * 在订单发货后为诊所用户发送订单状态更新通知。 */ private void dispatchOrderShippedNotice(PointsExchangeOrder order) { if (order == null || order.getId() == null || order.getClinicAppUserId() == null) { return; } AppSystemNoticeDispatchCommand command = new AppSystemNoticeDispatchCommand(); command.setAppUserId(order.getClinicAppUserId()); command.setNoticeTypeCode(AppSystemNoticeType.ORDER_STATUS_UPDATE); command.setTitle("订单状态更新"); command.setContent("您的礼品兑换订单已发货"); command.setReferenceType("POINTS_EXCHANGE_ORDER_SHIPPED"); command.setReferenceId(order.getId()); command.setRequireUserSetting(Boolean.TRUE); Map payload = new LinkedHashMap(); payload.put("orderId", String.valueOf(order.getId())); payload.put("orderNo", defaultText(order.getOrderNo())); payload.put("trackingNo", defaultText(order.getTrackingNo())); command.setPayload(payload); appSystemNoticeDispatchService.createAndPush(command); } @Transactional public void deleteOrder(Long id) { PointsExchangeOrder order = requireOrder(id); if (order.getCancelledAt() == null) { throw new IllegalArgumentException("当前状态不允许删除"); } List items = pointsExchangeOrderItemMapper.selectList(new LambdaQueryWrapper() .eq(PointsExchangeOrderItem::getDeleted, 0) .eq(PointsExchangeOrderItem::getOrderId, id)); for (PointsExchangeOrderItem item : items) { pointsExchangeOrderItemMapper.deleteById(item.getId()); } pointsExchangeOrderMapper.deleteById(id); } @Transactional public void deleteItem(Long id) { PointsExchangeOrderItem item = requireItem(id); if (item.getCancelledAt() == null) { throw new IllegalArgumentException("当前状态不允许删除"); } Long orderId = item.getOrderId(); pointsExchangeOrderItemMapper.deleteById(id); if (orderId != null) { refreshOrderStatus(orderId, null, null); } } public void exportOrders(HttpServletResponse response, String giftName, String receiverPhone, Integer status, LocalDateTime createdFrom, LocalDateTime createdTo) throws IOException { 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 = pointsExchangeOrderMapper.selectOrderList( trimToNull(giftName), trimToNull(receiverPhone), status, createdFrom, createdTo ); List exports = new ArrayList(); for (int i = 0; i < rows.size(); i++) { PointsExchangeOrderRow row = rows.get(i); PointsExchangeOrderExcelRow export = new PointsExchangeOrderExcelRow(); export.setSerialNo(i + 1); export.setOrderNo(row.getOrderNo()); export.setGiftName(defaultText(row.getGiftName())); export.setRequiredPoints(row.getRequiredPoints()); export.setActivityRange(formatRange(row.getActivityStartAt(), row.getActivityEndAt())); export.setClinicName(defaultText(row.getClinicName())); export.setReceiverClinicName(defaultText(row.getReceiverClinicName())); export.setReceiverPhone(PhoneMasker.mask(row.getReceiverPhone())); export.setReceiverAddress(defaultText(row.getReceiverAddress())); export.setItemCount(row.getItemCount()); export.setTotalPoints(row.getTotalPoints()); export.setStatus(orderStatusText(row.getStatus())); export.setTrackingNo(defaultText(row.getTrackingNo())); export.setCreatedAt(DateTimes.format(row.getCreatedAt())); exports.add(export); } FastExcel.write(response.getOutputStream(), PointsExchangeOrderExcelRow.class) .sheet("积分兑换汇总订单") .doWrite(exports); } public void exportItems(HttpServletResponse response, Long orderId, String orderNo, String buyerPhone, String giftName, String receiverPhone, Integer status, LocalDateTime createdFrom, LocalDateTime createdTo) throws IOException { 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 = pointsExchangeOrderItemMapper.selectItemList( orderId, trimToNull(orderNo), trimToNull(buyerPhone), trimToNull(giftName), trimToNull(receiverPhone), status, createdFrom, createdTo ); List exports = new ArrayList(); for (int i = 0; i < rows.size(); i++) { PointsExchangeItemRow row = rows.get(i); PointsExchangeItemExcelRow export = new PointsExchangeItemExcelRow(); export.setSerialNo(i + 1); export.setOrderNo(row.getOrderNo()); export.setBuyerNickname(defaultText(row.getBuyerNickname())); export.setBuyerPhone(PhoneMasker.mask(row.getBuyerPhone())); export.setGiftName(defaultText(row.getGiftName())); export.setSettledPoints(row.getSettledPoints()); export.setReceiverName(defaultText(row.getReceiverName())); export.setReceiverPhone(PhoneMasker.mask(row.getReceiverPhone())); export.setReceiverAddress(defaultText(row.getReceiverAddress())); export.setStatus(itemStatusText(row.getStatus())); export.setTrackingNo(defaultText(row.getTrackingNo())); export.setCreatedAt(DateTimes.format(row.getCreatedAt())); exports.add(export); } FastExcel.write(response.getOutputStream(), PointsExchangeItemExcelRow.class) .sheet("积分兑换明细订单") .doWrite(exports); } private void refreshOrderStatus(Long orderId, String trackingNo, LocalDateTime shippedAt) { if (orderId == null) { return; } PointsExchangeOrder order = requireOrder(orderId); List items = pointsExchangeOrderItemMapper.selectList(new LambdaQueryWrapper() .eq(PointsExchangeOrderItem::getDeleted, 0) .eq(PointsExchangeOrderItem::getOrderId, orderId)); if (items.isEmpty()) { pointsExchangeOrderMapper.deleteById(orderId); return; } boolean allCompleted = true; boolean anyShipped = false; for (PointsExchangeOrderItem item : items) { int status = item.getStatus() == null ? 0 : item.getStatus(); if (status != 2) { allCompleted = false; } if (status == 1 || status == 2) { anyShipped = true; } } if (allCompleted) { order.setStatus(2); } else if (anyShipped) { order.setStatus(1); if (trackingNo != null) { order.setTrackingNo(trackingNo); } if (shippedAt != null) { order.setShippedAt(shippedAt); } } else { order.setStatus(0); } pointsExchangeOrderMapper.updateById(order); } private PointsExchangeOrder requireOrder(Long id) { PointsExchangeOrder order = pointsExchangeOrderMapper.selectOne(new LambdaQueryWrapper() .eq(PointsExchangeOrder::getId, id) .eq(PointsExchangeOrder::getDeleted, 0) .last("limit 1")); if (order == null) { throw new IllegalArgumentException("汇总订单不存在"); } return order; } private PointsExchangeOrderItem requireItem(Long id) { PointsExchangeOrderItem item = pointsExchangeOrderItemMapper.selectOne(new LambdaQueryWrapper() .eq(PointsExchangeOrderItem::getId, id) .eq(PointsExchangeOrderItem::getDeleted, 0) .last("limit 1")); if (item == null) { throw new IllegalArgumentException("兑换明细不存在"); } return item; } private void validatePendingStatus(Integer status) { if (status == null || status != 0) { throw new IllegalArgumentException("当前订单状态不允许发货"); } } private List toOrderList(List rows) { List list = new ArrayList(); for (PointsExchangeOrderRow row : rows) { PointsExchangeOrderListItemVO item = new PointsExchangeOrderListItemVO(); item.setId(row.getId()); item.setOrderNo(row.getOrderNo()); item.setGiftName(row.getGiftName()); item.setRequiredPoints(row.getRequiredPoints()); item.setActivityStartAt(row.getActivityStartAt()); item.setActivityEndAt(row.getActivityEndAt()); item.setClinicName(row.getClinicName()); item.setReceiverClinicName(row.getReceiverClinicName()); item.setReceiverPhone(PhoneMasker.mask(row.getReceiverPhone())); item.setReceiverAddress(row.getReceiverAddress()); item.setItemCount(row.getItemCount()); item.setTotalPoints(row.getTotalPoints()); item.setStatus(row.getStatus()); item.setTrackingNo(row.getTrackingNo()); item.setShippedAt(row.getShippedAt()); item.setCreatedAt(row.getCreatedAt()); list.add(item); } return list; } private List toItemList(List rows) { List list = new ArrayList(); for (PointsExchangeItemRow row : rows) { PointsExchangeItemListItemVO item = new PointsExchangeItemListItemVO(); item.setId(row.getId()); item.setOrderId(row.getOrderId()); item.setOrderNo(row.getOrderNo()); item.setBuyerNickname(row.getBuyerNickname()); item.setBuyerPhone(PhoneMasker.mask(row.getBuyerPhone())); item.setClinicName(row.getClinicName()); item.setSettledPoints(row.getSettledPoints()); item.setGiftName(row.getGiftName()); item.setRequiredPoints(row.getRequiredPoints()); item.setReceiverName(row.getReceiverName()); item.setReceiverPhone(PhoneMasker.mask(row.getReceiverPhone())); item.setReceiverAddress(row.getReceiverAddress()); item.setStatus(row.getStatus()); item.setTrackingNo(row.getTrackingNo()); item.setShippedAt(row.getShippedAt()); item.setCreatedAt(row.getCreatedAt()); list.add(item); } return list; } private String trimToNull(String text) { if (text == null) { return null; } String value = text.trim(); return value.isEmpty() ? null : value; } private String formatRange(LocalDateTime start, LocalDateTime end) { return defaultText(DateTimes.format(start)) + " - " + defaultText(DateTimes.format(end)); } private String orderStatusText(Integer status) { if (status == null) { return "-"; } if (status == 0) { return "待发货"; } if (status == 1) { return "已发货"; } if (status == 2) { return "已收货"; } return "-"; } private String itemStatusText(Integer status) { if (status == null) { return "-"; } if (status == 0) { return "待发货"; } if (status == 1) { return "待取货"; } if (status == 2) { return "兑换完成"; } return "-"; } private String defaultText(String text) { if (text == null || text.trim().isEmpty()) { return "-"; } return text; } }