package com.tcm.training.admin.app.patient.service; 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.auth.service.AppAuthSessionService; import com.tcm.training.admin.app.patient.mapper.AppMyPatientQueryMapper; import com.tcm.training.admin.app.patient.row.AppMyPatientExchangeRow; import com.tcm.training.admin.app.patient.row.AppMyPatientExchangeOrderItemRow; import com.tcm.training.admin.app.patient.row.AppMyPatientLiveRow; import com.tcm.training.admin.app.patient.row.AppMyPatientRow; import com.tcm.training.admin.app.patient.vo.AppMyPatientExchangeItemVO; import com.tcm.training.admin.app.patient.vo.AppMyPatientExchangeOrderItemVO; import com.tcm.training.admin.app.patient.vo.AppMyPatientItemVO; import com.tcm.training.admin.app.patient.vo.AppMyPatientLiveItemVO; import com.tcm.training.admin.common.PageResponse; import com.tcm.training.admin.member.entity.AppUser; import com.tcm.training.admin.member.mapper.UserMapper; import com.tcm.training.admin.points.entity.PointsExchangeOrder; import com.tcm.training.admin.points.entity.PointsExchangeOrderItem; import com.tcm.training.admin.points.mapper.PointsExchangeOrderItemMapper; import com.tcm.training.admin.points.mapper.PointsExchangeOrderMapper; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; import java.util.ArrayList; import java.util.List; /** * APP 我的患者聚合服务。 */ @Service public class AppMyPatientService { private static final int USER_TYPE_PERSONAL = 0; private static final int USER_TYPE_DOCTOR = 1; private static final int USER_STATUS_ENABLED = 1; private static final String BUSINESS_TYPE_LIVE_WATCH = "LIVE_WATCH"; private static final String INCOME_EXPENSE_TYPE_INCOME = "INCOME"; private static final int EXCHANGE_STATUS_PENDING = 0; private static final int EXCHANGE_STATUS_SHIPPED = 1; private static final int EXCHANGE_STATUS_COMPLETED = 2; private static final int EXCHANGE_ITEM_STATUS_PENDING = 0; private static final int EXCHANGE_ITEM_STATUS_PICKUP = 1; private static final int EXCHANGE_ITEM_STATUS_COMPLETED = 2; private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd"); private final AppAuthSessionService appAuthSessionService; private final UserMapper userMapper; private final AppMyPatientQueryMapper appMyPatientQueryMapper; private final PointsExchangeOrderMapper pointsExchangeOrderMapper; private final PointsExchangeOrderItemMapper pointsExchangeOrderItemMapper; /** * 注入 APP 我的患者聚合所需依赖。 */ public AppMyPatientService(AppAuthSessionService appAuthSessionService, UserMapper userMapper, AppMyPatientQueryMapper appMyPatientQueryMapper, PointsExchangeOrderMapper pointsExchangeOrderMapper, PointsExchangeOrderItemMapper pointsExchangeOrderItemMapper) { this.appAuthSessionService = appAuthSessionService; this.userMapper = userMapper; this.appMyPatientQueryMapper = appMyPatientQueryMapper; this.pointsExchangeOrderMapper = pointsExchangeOrderMapper; this.pointsExchangeOrderItemMapper = pointsExchangeOrderItemMapper; } /** * 分页查询直属患者列表。 */ public PageResponse pagePatients(int page, int pageSize, String keyword) { Long doctorUserId = requireDoctorUserId(); IPage result = appMyPatientQueryMapper.selectPatientPage( new Page(normalizePage(page), normalizePageSize(pageSize)), doctorUserId, trimToNull(keyword) ); return PageResponse.of(toPatientItems(result.getRecords()), result.getTotal(), result.getCurrent(), result.getSize()); } /** * 分页查询直属患者直播积分记录。 */ public PageResponse pageLiveRecords(int page, int pageSize, String keyword, String startDate, String endDate) { Long doctorUserId = requireDoctorUserId(); IPage result = appMyPatientQueryMapper.selectLiveRecordPage( new Page(normalizePage(page), normalizePageSize(pageSize)), doctorUserId, trimToNull(keyword), parseStartDate(startDate), parseEndDate(endDate), BUSINESS_TYPE_LIVE_WATCH, INCOME_EXPENSE_TYPE_INCOME ); return PageResponse.of(toLiveItems(result.getRecords()), result.getTotal(), result.getCurrent(), result.getSize()); } /** * 分页查询直属患者兑换记录。 */ public PageResponse pageExchangeRecords(int page, int pageSize, String keyword, String status) { Long doctorUserId = requireDoctorUserId(); IPage result = appMyPatientQueryMapper.selectExchangeRecordPage( new Page(normalizePage(page), normalizePageSize(pageSize)), doctorUserId, trimToNull(keyword), normalizeExchangeStatus(status) ); return PageResponse.of(toExchangeItems(result.getRecords()), result.getTotal(), result.getCurrent(), result.getSize()); } /** * 分页查询直属患者所属主订单下的子订单列表。 */ public PageResponse pageExchangeOrderItems(Long orderId, int page, int pageSize) { Long doctorUserId = requireDoctorUserId(); requireDoctorOrder(orderId, doctorUserId); IPage result = appMyPatientQueryMapper.selectExchangeOrderItemPage( new Page(normalizePage(page), normalizePageSize(pageSize)), doctorUserId, orderId ); return PageResponse.of(toExchangeOrderItemList(result.getRecords()), result.getTotal(), result.getCurrent(), result.getSize()); } /** * 确认当前医生可查看的兑换主订单已收货。 */ @Transactional public void confirmExchangeOrderReceived(Long orderId) { Long doctorUserId = requireDoctorUserId(); PointsExchangeOrder order = requireDoctorOrder(orderId, doctorUserId); if (order.getStatus() == null || order.getStatus().intValue() != EXCHANGE_STATUS_SHIPPED) { throw new IllegalArgumentException("当前订单不允许确认收货"); } LocalDateTime now = LocalDateTime.now(); order.setStatus(EXCHANGE_STATUS_COMPLETED); order.setCompletedAt(now); pointsExchangeOrderMapper.updateById(order); List items = pointsExchangeOrderItemMapper.selectList(new LambdaQueryWrapper() .eq(PointsExchangeOrderItem::getDeleted, 0) .eq(PointsExchangeOrderItem::getOrderId, orderId) .isNull(PointsExchangeOrderItem::getCancelledAt)); for (PointsExchangeOrderItem item : items) { if (item.getStatus() == null || item.getStatus().intValue() == EXCHANGE_ITEM_STATUS_COMPLETED) { continue; } item.setStatus(EXCHANGE_ITEM_STATUS_PICKUP); pointsExchangeOrderItemMapper.updateById(item); } } /** * 校验当前登录用户是启用中的医生账号。 */ private Long requireDoctorUserId() { Long userId = appAuthSessionService.requireLoginUserId(); AppUser user = userMapper.selectById(userId); if (user == null || user.getDeleted() == null || user.getDeleted().intValue() != 0) { throw new IllegalArgumentException("用户不存在"); } if (user.getStatus() == null || user.getStatus().intValue() != USER_STATUS_ENABLED) { throw new IllegalArgumentException("用户已禁用"); } if (user.getType() == null || user.getType().intValue() != USER_TYPE_DOCTOR) { throw new IllegalArgumentException("当前用户无权查看患者数据"); } return userId; } /** * 将患者列表查询结果转换为 APP 页面展示结构。 */ private List toPatientItems(List rows) { List items = new ArrayList(); if (rows == null) { return items; } for (AppMyPatientRow row : rows) { AppMyPatientItemVO item = new AppMyPatientItemVO(); item.setId(row.getId()); item.setNickname(resolveNickname(row.getNickname(), row.getPhone())); item.setAvatar(trimToEmpty(row.getAvatar())); item.setCreatedAt(row.getCreatedAt()); items.add(item); } return items; } /** * 将直播积分记录转换为 APP 页面展示结构。 */ private List toLiveItems(List rows) { List items = new ArrayList(); if (rows == null) { return items; } for (AppMyPatientLiveRow row : rows) { AppMyPatientLiveItemVO item = new AppMyPatientLiveItemVO(); item.setId(row.getId()); item.setPatientId(row.getPatientId()); item.setNickname(resolveNickname(row.getPatientNickname(), row.getPatientPhone())); item.setAvatar(trimToEmpty(row.getPatientAvatar())); item.setCourseTitle(trimToEmpty(row.getCourseTitle())); item.setOperatedAt(row.getOperatedAt()); item.setPoints(normalizeNumber(row.getPoints())); items.add(item); } return items; } /** * 将兑换数据查询结果转换为 APP 页面展示结构。 */ private List toExchangeItems(List rows) { List items = new ArrayList(); if (rows == null) { return items; } for (AppMyPatientExchangeRow row : rows) { AppMyPatientExchangeItemVO item = new AppMyPatientExchangeItemVO(); item.setId(row.getId()); item.setOrderNo(trimToEmpty(row.getOrderNo())); item.setGiftName(trimToEmpty(row.getGiftName())); item.setGiftCover(trimToEmpty(row.getGiftCover())); item.setNickname(trimToEmpty(row.getNickname())); item.setActivityPeriodText(buildActivityPeriodText(row.getActivityStartAt(), row.getActivityEndAt())); item.setStatus(row.getStatus()); item.setStatusText(resolveExchangeStatusText(row.getStatus())); item.setCount(normalizeNumber(row.getCount())); item.setCanConfirmReceived(Boolean.valueOf(Boolean.TRUE.equals(row.getCanConfirmReceived()))); items.add(item); } return items; } /** * 将主订单下子订单列表转换为 APP 页面展示结构。 */ private List toExchangeOrderItemList(List rows) { List items = new ArrayList(); if (rows == null) { return items; } for (AppMyPatientExchangeOrderItemRow row : rows) { AppMyPatientExchangeOrderItemVO item = new AppMyPatientExchangeOrderItemVO(); item.setId(row.getId()); item.setOrderId(row.getOrderId()); item.setPatientId(row.getPatientId()); item.setPatientName(trimToEmpty(row.getPatientName())); item.setPatientPhone(trimToEmpty(row.getPatientPhone())); item.setSettledPoints(normalizeNumber(row.getSettledPoints())); item.setStatus(row.getStatus()); item.setStatusText(resolveExchangeItemStatusText(row.getStatus())); item.setCreatedAt(row.getCreatedAt()); items.add(item); } return items; } /** * 将兑换页状态筛选值转换为数据库状态码。 */ private Integer normalizeExchangeStatus(String status) { String normalized = trimToNull(status); if (normalized == null || "all".equalsIgnoreCase(normalized)) { return null; } if ("pending".equalsIgnoreCase(normalized)) { return Integer.valueOf(EXCHANGE_STATUS_PENDING); } if ("received".equalsIgnoreCase(normalized)) { return Integer.valueOf(EXCHANGE_STATUS_SHIPPED); } throw new IllegalArgumentException("兑换状态不合法"); } /** * 解析列表日期筛选的开始时间。 */ private LocalDateTime parseStartDate(String value) { String normalized = trimToNull(value); if (normalized == null) { return null; } try { return LocalDate.parse(normalized, DATE_FORMATTER).atStartOfDay(); } catch (DateTimeParseException ex) { throw new IllegalArgumentException("开始日期格式不正确"); } } /** * 解析列表日期筛选的结束时间,并自动补到当天结束。 */ private LocalDateTime parseEndDate(String value) { String normalized = trimToNull(value); if (normalized == null) { return null; } try { return LocalDate.parse(normalized, DATE_FORMATTER).plusDays(1).atStartOfDay().minusSeconds(1); } catch (DateTimeParseException ex) { throw new IllegalArgumentException("结束日期格式不正确"); } } /** * 拼接兑换活动期数字段展示文案。 */ private String buildActivityPeriodText(LocalDateTime activityStartAt, LocalDateTime activityEndAt) { String startText = formatDate(activityStartAt); String endText = formatDate(activityEndAt); if (startText.isEmpty() && endText.isEmpty()) { return ""; } return startText + "-" + endText; } /** * 将日期时间格式化为页面期数字段所需的 yyyy.MM.dd。 */ private String formatDate(LocalDateTime value) { if (value == null) { return ""; } return value.toLocalDate().toString().replace('-', '.'); } /** * 解析兑换记录状态文案。 */ private String resolveExchangeStatusText(Integer status) { if (status != null && status.intValue() == EXCHANGE_STATUS_PENDING) { return "待发货"; } if (status != null && status.intValue() == EXCHANGE_STATUS_SHIPPED) { return "待收货"; } return "已收货"; } /** * 解析兑换子订单状态文案。 */ private String resolveExchangeItemStatusText(Integer status) { if (status != null && status.intValue() == EXCHANGE_ITEM_STATUS_PENDING) { return "待发货"; } if (status != null && status.intValue() == EXCHANGE_ITEM_STATUS_PICKUP) { return "待取货"; } return "兑换完成"; } /** * 校验并返回当前医生可操作的兑换主订单。 */ private PointsExchangeOrder requireDoctorOrder(Long orderId, Long doctorUserId) { if (orderId == null) { throw new IllegalArgumentException("兑换订单不存在"); } PointsExchangeOrder order = pointsExchangeOrderMapper.selectOne(new LambdaQueryWrapper() .eq(PointsExchangeOrder::getId, orderId) .eq(PointsExchangeOrder::getDeleted, 0) .last("limit 1")); if (order == null) { throw new IllegalArgumentException("兑换订单不存在"); } Long count = pointsExchangeOrderItemMapper.selectCount(new LambdaQueryWrapper() .eq(PointsExchangeOrderItem::getDeleted, 0) .eq(PointsExchangeOrderItem::getOrderId, orderId) .isNull(PointsExchangeOrderItem::getCancelledAt) .inSql(PointsExchangeOrderItem::getAppUserId, "select id from tcm_app_user where deleted = 0 and type = 0 and superior_clinic_user_id = " + doctorUserId)); if (count == null || count.longValue() <= 0L) { throw new IllegalArgumentException("兑换订单不存在"); } return order; } /** * 用昵称优先、手机号兜底的策略统一展示患者名称。 */ private String resolveNickname(String nickname, String phone) { String normalizedNickname = trimToEmpty(nickname); if (!normalizedNickname.isEmpty()) { return normalizedNickname; } return trimToEmpty(phone); } /** * 将空白文本统一转换为 null。 */ private String trimToNull(String value) { String text = trimToEmpty(value); return text.isEmpty() ? null : text; } /** * 将可空文本归一化为空字符串。 */ private String trimToEmpty(String value) { if (value == null) { return ""; } return value.trim(); } /** * 将分页页码归一化到合法范围。 */ private int normalizePage(int page) { return page <= 0 ? 1 : page; } /** * 将分页大小归一化到合法范围。 */ private int normalizePageSize(int pageSize) { return pageSize <= 0 ? 10 : pageSize; } /** * 将积分数值归一化,避免返回空值影响 APP 展示。 */ private Integer normalizeNumber(Integer value) { return value == null ? Integer.valueOf(0) : value; } }