소스 검색

修改
1、账单推送服务号重构
2、健康档案导入调整
3、应收报表额外服务收费调整

liangwenxuan 1 개월 전
부모
커밋
4bcae0f3a8

+ 42 - 7
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/order/OrderApiImpl.java

@@ -64,7 +64,7 @@ public class OrderApiImpl implements OrderApi {
     ));
 
     private static final Set<String> REPORT_EXTRA_SERVICE_ITEMS = buildNormalizedSet(Arrays.asList(
-            "代开药", "陪同就医", "收视费", "紧急送医服务", "生活用品", "外出"
+            "代开药", "陪同就医", "收视费", "紧急送医服务", "生活用品", "外出","收视费(单人间)","收视费(两人间)","收视费(多人间)"
     ));
 
     private static final Set<String> REPORT_OTHER_INCOME_ITEMS = buildNormalizedSet(Arrays.asList(
@@ -232,8 +232,8 @@ public class OrderApiImpl implements OrderApi {
             // 月度费用类(expenseSource=EXPENSE_ITEM):按账单月区间计算“标准应收”和“实际应收”
             applyMonthlyItems(dto, expenseOrderItemDOS, expenseItemMap, billMonth);
 
-            // 3) 日常费用的外出/变更等调整:叠加到“实际应收”
-            applyDailyAdjustments(dto, expenseOrderItemDOS, dailyExpenseMap, remarkBuilder);
+            // 3) 日常费用的外出/变更等调整:叠加到“实际应收”;同时处理增值服务费类(含月度费用形态)归入“额外”
+            applyDailyAdjustments(dto, expenseOrderItemDOS, expenseItemMap, dailyExpenseMap, remarkBuilder);
             computeTotals(dto);
             applyRefundSettlementDeductionsIfRetreat(dto, retreatDeductByElderAndType);
             computeArrears(dto, expenseOrderItemDOS, dailyExpenseMap);
@@ -379,6 +379,12 @@ public class OrderApiImpl implements OrderApi {
 //                }
 //            }
 
+            // 增值服务费类:不在此处理,交由 applyDailyAdjustments 统一归入“额外”(extraServiceAmount),不计入服务费
+            if (isValueAddedServiceCategory(expenseItemDO.getItemCategoryName())
+                    || isValueAddedServiceCategory(expenseItemDO.getItemName())) {
+                continue;
+            }
+
             Integer type = expenseItemDO.getType();
             if ((type != null  && 1 == type) || expenseItemDO.getItemName().contains("床位费")) {
                 dto.setBedItemName(expenseItemDO.getItemName());
@@ -403,11 +409,26 @@ public class OrderApiImpl implements OrderApi {
      * - 其他类型:增值服务费、点餐、护理、服务等累加到对应字段
      */
     private void applyDailyAdjustments(OrderItemRespDTO dto, List<ExpenseOrderItemDO> items,
+                                       Map<Long, ExpenseItemDO> expenseItemMap,
                                        Map<Long, DailyExpensesDO> dailyExpenseMap, StringBuilder remarkBuilder) {
         for (ExpenseOrderItemDO expenseOrderItemDO : items) {
-            if(expenseOrderItemDO.getTotalAmount().compareTo(BigDecimal.ZERO) == 0){
+            if(expenseOrderItemDO.getTotalAmount() == null
+                    || expenseOrderItemDO.getTotalAmount().compareTo(BigDecimal.ZERO) == 0){
+                continue;
+            }
+
+            // 月度费用形态的“增值服务费”类:由 applyMonthlyItems 转交此处,走 applyDailyAdjustments 的完整分类逻辑(forceExtra=true 保证只进“额外”)
+            if (BusinessConstants.EXPENSE_ITEM.equals(expenseOrderItemDO.getExpenseSource())) {
+                ExpenseItemDO expenseItemDO = expenseItemMap.get(expenseOrderItemDO.getSourceExpenseItemId());
+                if (expenseItemDO != null
+                        && (isValueAddedServiceCategory(expenseItemDO.getItemCategoryName())
+                                || isValueAddedServiceCategory(expenseItemDO.getItemName()))) {
+                    handleOtherDailyExpenses(dto, expenseItemDO.getItemName(),
+                            expenseOrderItemDO.getTotalAmount(), null);
+                }
                 continue;
             }
+
             if (!BusinessConstants.DAILY_EXPENSES.equals(expenseOrderItemDO.getExpenseSource())) {
                 continue;
             }
@@ -492,8 +513,16 @@ public class OrderApiImpl implements OrderApi {
 
     /** 处理非外出/变更的日常费用(增值、点餐、护理、服务等) */
     private void handleOtherDailyExpenses(OrderItemRespDTO dto, DailyExpensesDO dailyExpensesDO) {
-        String itemName = dailyExpensesDO.getItemName();
-        BigDecimal extraAmount = dailyExpensesDO.getAmount() == null ? BigDecimal.ZERO : dailyExpensesDO.getAmount();
+        handleOtherDailyExpenses(dto, dailyExpensesDO.getItemName(), dailyExpensesDO.getAmount(),
+                dailyExpensesDO.getRemarks());
+    }
+
+    /**
+     * 处理非外出/变更的费用(增值、点餐、护理、服务等):
+     */
+    private void handleOtherDailyExpenses(OrderItemRespDTO dto, String itemName,
+                                          BigDecimal amount, String remarks) {
+        BigDecimal extraAmount = amount == null ? BigDecimal.ZERO : amount;
         String normalizedName = normalizeProjectKey(itemName);
         if (normalizedName != null && !normalizedName.isEmpty()) {
             if ("点餐".equals(normalizedName)) {
@@ -518,7 +547,7 @@ public class OrderApiImpl implements OrderApi {
         }
         dto.setAdjustAmount(dto.getAdjustAmount().add(extraAmount));
         dto.setRemark((dto.getRemark() == null ? "" : dto.getRemark())
-                + (dailyExpensesDO.getRemarks() == null ? "" : dailyExpensesDO.getRemarks())
+                + (remarks == null ? "" : remarks)
                 + (itemName == null ? "" : itemName)
                 + ":"
                 + extraAmount.setScale(2, RoundingMode.HALF_UP)
@@ -531,6 +560,12 @@ public class OrderApiImpl implements OrderApi {
                 && (itemName.contains("水费") || itemName.contains("电费") || itemName.contains("水电费"));
     }
 
+    /** 判断费用类别/名称是否为“增值服务费”类 */
+    private boolean isValueAddedServiceCategory(String value) {
+        return value != null && value.contains("增值服务费");
+    }
+
+
     /** 汇总标准/实际合计及占比 */
     private void computeTotals(OrderItemRespDTO dto) {
         BigDecimal standardTotal = dto.getBedAmount()

+ 83 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/miniapp/UserV2DO.java

@@ -0,0 +1,83 @@
+package cn.iocoder.yudao.module.system.dal.dataobject.miniapp;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+/**
+ * 用户信息 DO(yanglao-jiashu 数据源 user_v2 表)
+ *
+ * @author system
+ */
+@TableName("user_v2")
+@Data
+public class UserV2DO {
+
+    /**
+     * 用户ID(主键)
+     */
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    /**
+     * 微信小程序OpenID
+     */
+    private String openId;
+
+    /**
+     * 公众号OpenID(关注了公众号)
+     */
+    private String mpOpenId;
+
+    /**
+     * 微信开放平台UnionID
+     */
+    private String unionId;
+
+    /**
+     * 家属姓名
+     */
+    private String name;
+
+    /**
+     * 手机号码(带国际区号)
+     */
+    private String phone;
+
+    /**
+     * 用户角色:'RELATIVE','EMPLOYEE','VISITOR','BOSS','ORGADMIN','YKVERIFIER','NURSE','MEDICAL_ORDERLY','DOCTOR'
+     */
+    private String role;
+
+    /**
+     * 账号状态(1新用户未绑定机构 2已绑定机构 0冻结)
+     */
+    private Integer status;
+
+    /**
+     * employee身份证号
+     */
+    private String idCard;
+
+    /**
+     * employee所属机构id
+     */
+    private Integer orgId;
+
+    /**
+     * 创建时间
+     */
+    @TableField("created_at")
+    private LocalDateTime createdAt;
+
+    /**
+     * 更新时间
+     */
+    @TableField("updated_at")
+    private LocalDateTime updatedAt;
+
+}

+ 24 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/miniapp/UserV2Mapper.java

@@ -0,0 +1,24 @@
+package cn.iocoder.yudao.module.system.dal.mysql.miniapp;
+
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
+import cn.iocoder.yudao.module.system.dal.dataobject.miniapp.UserV2DO;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface UserV2Mapper extends BaseMapperX<UserV2DO> {
+
+    /**
+     * 根据手机号查询用户信息(取最近一条)
+     *
+     * @param phone 手机号码
+     * @return 用户信息,未找到返回 null
+     */
+    default UserV2DO selectByPhone(String phone) {
+        return selectOne(new LambdaQueryWrapperX<UserV2DO>()
+                .eq(UserV2DO::getPhone, phone)
+                .orderByDesc(UserV2DO::getId)
+                .last("LIMIT 1"));
+    }
+
+}

+ 5 - 1
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/biz/ElderlyHealthRecordServiceImpl.java

@@ -4,6 +4,7 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
 import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
+import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
 import cn.iocoder.yudao.module.system.controller.admin.biz.vo.ElderlyHealthRecordImportExcelVO;
 import cn.iocoder.yudao.module.system.controller.admin.biz.vo.ElderlyHealthRecordPageReqVO;
 import cn.iocoder.yudao.module.system.dal.dataobject.biz.ElderlyHealthRecordDO;
@@ -94,7 +95,10 @@ public class ElderlyHealthRecordServiceImpl implements ElderlyHealthRecordServic
         for (ElderlyHealthRecordImportExcelVO vo : dataList) {
             // 1. 以身份证匹配长者
             ElderlyInfoDO elderlyInfoDO = elderlyInfoMapper.selectOne(new LambdaQueryWrapperX<ElderlyInfoDO>()
-                    .eq(ElderlyInfoDO::getIdCard, vo.getIdCard()));
+                    .eq(ElderlyInfoDO::getElderName,vo.getElderName())
+                    .eq(ElderlyInfoDO::getInStatus,1)
+                    .eq(ElderlyInfoDO::getIdCard, vo.getIdCard())
+                    .eq(ElderlyInfoDO::getTenantId, TenantContextHolder.getTenantId()));
             if (elderlyInfoDO == null) {
                 vo.setErrorMsg("未找到身份证[" + vo.getIdCard() + "]对应的长者");
                 errorList.add(vo);

+ 1 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/biz/ElderlyInfoServiceImpl.java

@@ -364,6 +364,7 @@ public class ElderlyInfoServiceImpl implements ElderlyInfoService {
         relativesMapper.delete(new LambdaQueryWrapperX<RelativesDO>().eq(RelativesDO::getPeopleId,elderlyInfo.getId()));
         for (RelativesDO relativesDO : elderlyInfo.getRelativesList()) {
             relativesDO.setPeopleId(elderlyInfo.getId());
+            relativesDO.setPeopleName(elderlyInfo.getElderName());
             relativesDO.setCreateDate(LocalDateTime.now());
         }
         relativesMapper.insertBatch(elderlyInfo.getRelativesList());

+ 84 - 77
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/biz/ExpenseOrderServiceImpl.java

@@ -37,12 +37,12 @@ import cn.iocoder.yudao.module.system.enums.change.BusinessConstants;
 import cn.iocoder.yudao.module.system.enums.common.BooleanEnum;
 import cn.iocoder.yudao.module.system.framework.sms.util.AliyunSmsUtils;
 import cn.iocoder.yudao.module.system.service.discount.DiscountRuleService;
+import cn.iocoder.yudao.module.system.service.miniapp.JiashuUserService;
+import cn.iocoder.yudao.module.system.service.wx.WechatBusinessService;
 import com.alibaba.excel.EasyExcel;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
-import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.ObjectMapper;
 import com.mzt.logapi.context.LogRecordContext;
 import com.mzt.logapi.starter.annotation.LogRecord;
 import lombok.extern.slf4j.Slf4j;
@@ -51,7 +51,6 @@ import org.apache.commons.lang3.StringUtils;
 import org.springframework.beans.BeanUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.data.redis.core.RedisTemplate;
-import org.springframework.http.ResponseEntity;
 import org.springframework.stereotype.Service;
 
 import javax.annotation.Resource;
@@ -59,7 +58,6 @@ import javax.servlet.http.HttpServletResponse;
 
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.validation.annotation.Validated;
-import org.springframework.web.client.RestTemplate;
 
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exceptionCustomMsg;
@@ -158,7 +156,11 @@ public class ExpenseOrderServiceImpl implements ExpenseOrderService {
     private TaskStatusManager taskStatusManager;
 
     @Autowired
-    private RestTemplate restTemplate;
+    private WechatBusinessService wechatBusinessService;
+
+    @Autowired
+    private JiashuUserService jiashuUserService;
+
 
     @Autowired
     private ElderlyTempOutMapper elderlyTempOutMapper;
@@ -3439,6 +3441,47 @@ public class ExpenseOrderServiceImpl implements ExpenseOrderService {
         }
     }
 
+    /**
+     * 根据账单类型获取缴费类型描述
+     *
+     * @param type 账单类型(1入院账单 2月度账单 3试住账单)
+     * @return 缴费类型描述,未知返回空串
+     */
+    private String getBillTypeDesc(Integer type) {
+        if (type == null) {
+            return "";
+        }
+        switch (type) {
+            case 1:
+                return "入院账单";
+            case 2:
+                return "月度账单";
+            case 3:
+                return "试住账单";
+            default:
+                return "";
+        }
+    }
+
+    /**
+     * 将账单月份(yyyy-MM)格式化为微信模板 time 关键词所需的日期格式(yyyy-MM-dd)。
+     * 若格式异常则原样返回。
+     *
+     * @param billingMonth 账单月份,如 "2025-09"
+     * @return 格式化后的日期,如 "2025-09-01"
+     */
+    private String formatBillDate(String billingMonth) {
+        if (StringUtils.isBlank(billingMonth)) {
+            return "";
+        }
+        // 账单月份格式为 yyyy-MM,补齐为合法的日期(time 关键词要求日期类型)
+        if (billingMonth.length() == 7 && billingMonth.contains("-")) {
+            return billingMonth + "-01";
+        }
+        return billingMonth;
+    }
+
+
     private void addDiscountRecord(Long elderId, String billingMonth, ExpenseItemRespVO item) {
         DiscountRecordDO discountRecordDO = new DiscountRecordDO();
         discountRecordDO.setElderId(elderId);
@@ -3491,82 +3534,45 @@ public class ExpenseOrderServiceImpl implements ExpenseOrderService {
                 TenantDO tenantDO = tenantMapper.selectById(relativesDO.getTenantId());
                 String tenantName = tenantDO != null ? tenantDO.getName() : "";
 
-                // 组装推送参数
-                BillRequestDTO billRequestDTO = new BillRequestDTO();
-                billRequestDTO.setUserPhone(relativesDO.getPhone());
-                billRequestDTO.setElderId(expenseOrderDO.getElderId());
-                billRequestDTO.setElderName(relativesDO.getPeopleName());
-                billRequestDTO.setOrganizationId(relativesDO.getTenantId());
-                billRequestDTO.setOrganizationName(tenantName);
-                billRequestDTO.setRelationship(relativesDO.getRelation());
-
-                BillRequestDTO.BillMessage billMessage = new BillRequestDTO.BillMessage();
-                Integer type = expenseOrderDO.getType();
-                if (type == 1) {
-                    billMessage.setBillType("入院账单");
-                } else if (type == 2) {
-                    billMessage.setBillType("月度账单");
-                } else if (type == 3) {
-                    billMessage.setBillType("试住账单");
-                }
-                billMessage.setBillDate(expenseOrderDO.getBillingMonth());
-                billMessage.setAmount(String.valueOf(expenseOrderDO.getActualAmount().setScale(2,RoundingMode.HALF_UP)));
-                billMessage.setElderName(relativesDO.getPeopleName());
-                billMessage.setOrganizationName(tenantName);
-                billMessage.setOrderId(expenseOrderDO.getId());
-                billRequestDTO.setBillMessage(billMessage);
-
-                // 调用推送接口
-                List<BillRequestDTO> billList = Arrays.asList(billRequestDTO);
-                Map<String,Object> map = new HashMap<>();
-                map.put("billList", billList);
-
-                try {
-                    ResponseEntity<String> response = restTemplate.postForEntity(
-                            "http://home.ynims.com/api/wechat/pushBill", map, String.class);
-
-                    log.info("推送账单结果:{}", response.getBody());
-
-                    // 解析响应结果
-                    ObjectMapper objectMapper = new ObjectMapper();
-                    JsonNode rootNode = objectMapper.readTree(response.getBody());
-                    int code = rootNode.path("code").asInt();
-
-                    if (code == 200) {
-                        JsonNode dataNode = rootNode.path("data");
-                        successCount += dataNode.path("successCount").asInt(0);
-                        failCount += dataNode.path("failCount").asInt(0);
-
-                        // 根据results结果更新账单推送状态
-                        JsonNode resultsNode = dataNode.path("results");
-                        if (resultsNode != null && resultsNode.isArray() && resultsNode.size() > 0) {
-                            for (JsonNode result : resultsNode) {
-                                boolean success = result.path("success").asBoolean(false);
-                                if (success) {
-                                    // 将成功推送的账单ID加入集合
-                                    pushedOrderIds.add(expenseOrderDO.getId());
-                                }
-                            }
-                        }
+                // 查询该亲属是否已关注公众号(yanglao-jiashu.user_v2.mp_open_id)
+                String mpOpenId = jiashuUserService.getMpOpenIdByPhone(relativesDO.getPhone());
+                if (StringUtils.isBlank(mpOpenId)) {
+                    notFoundUsers.add(String.format("%s (%s): 未关注公众号或不存在",
+                            relativesDO.getPeopleName(), relativesDO.getPhone()));
+                    failCount++;
+                    continue;
+                }
 
+                // 组装账单模板消息参数(字段与微信公众号模板一致)
+                String billType = getBillTypeDesc(expenseOrderDO.getType());
+                BigDecimal actualAmount = expenseOrderDO.getActualAmount();
+                String amount = actualAmount == null ? "0.00"
+                        : actualAmount.setScale(2, RoundingMode.HALF_UP).toPlainString();
+                String billDate = formatBillDate(expenseOrderDO.getBillingMonth());
 
-                        // 收集未找到账单的用户信息
-                        JsonNode notFoundBillsNode = dataNode.path("notFoundBills");
-                        if (notFoundBillsNode != null && notFoundBillsNode.isArray() && notFoundBillsNode.size() > 0) {
-                            for (JsonNode notFound : notFoundBillsNode) {
-                                String userPhone = notFound.path("userPhone").asText();
-                                String reason = notFound.path("reason").asText();
-                                notFoundUsers.add(String.format("%s (%s): %s",
-                                        billRequestDTO.getElderName(), userPhone, reason));
-                            }
-                        }
-                    } else {
-                        failCount++;
-                    }
+                // 调用工程内实现:推送微信公众号账单模板消息
+                boolean sendSuccess;
+                try {
+                    sendSuccess = wechatBusinessService.pushBillMessage(
+                            mpOpenId,
+                            expenseOrderDO.getId(),
+                            billType,
+                            relativesDO.getPeopleName(),
+                            amount,
+                            billDate,
+                            tenantName);
                 } catch (Exception e) {
-                    log.error("推送账单失败,长者ID: {}", expenseOrderDO.getElderId(), e);
-                    failedElderNames.add(elderlyInfoDO.getElderName());
+                    log.error("推送账单失败,长者ID: {}, openId: {}", expenseOrderDO.getElderId(), mpOpenId, e);
+                    sendSuccess = false;
+                }
+
+                if (sendSuccess) {
+                    successCount++;
+                    // 将成功推送的账单ID加入集合(用于更新推送状态)
+                    pushedOrderIds.add(expenseOrderDO.getId());
+                } else {
                     failCount++;
+                    failedElderNames.add(elderlyInfoDO.getElderName());
                 }
             }
         }
@@ -3619,6 +3625,7 @@ public class ExpenseOrderServiceImpl implements ExpenseOrderService {
                 .eqIfPresent(ExpenseOrderDO::getPayStatus, pageReqVO.getPayStatus())
                 .orderByDesc(ExpenseOrderDO::getBillingMonth)
         );
+        log.info("小程序获取账单,长者id:{}",relativesDO.getPeopleId());
         expenseOrders.forEach(i -> i.setElderName(relativesDO.getPeopleName()));
         if (CollectionUtil.isEmpty(expenseOrders)) {
             throw exception(RELATIVES_NOT_HAVE_BILL);

+ 18 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/miniapp/JiashuUserService.java

@@ -0,0 +1,18 @@
+package cn.iocoder.yudao.module.system.service.miniapp;
+
+/**
+ * 家属端(yanglao-jiashu 数据源)用户 Service
+ *
+ * @author system
+ */
+public interface JiashuUserService {
+
+    /**
+     * 根据手机号查询用户关注的公众号 OpenID(mp_open_id)
+     *
+     * @param phone 手机号码(带国际区号)
+     * @return 公众号 OpenID,未关注或不存在返回 null
+     */
+    String getMpOpenIdByPhone(String phone);
+
+}

+ 42 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/miniapp/JiashuUserServiceImpl.java

@@ -0,0 +1,42 @@
+package cn.iocoder.yudao.module.system.service.miniapp;
+
+import cn.iocoder.yudao.module.system.dal.dataobject.miniapp.UserV2DO;
+import cn.iocoder.yudao.module.system.dal.mysql.miniapp.UserV2Mapper;
+import com.baomidou.dynamic.datasource.annotation.DS;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+
+/**
+ * 家属端(yanglao-jiashu 数据源)用户 Service 实现
+ *
+ * @author system
+ */
+@Service
+@DS("miniapp")
+@Slf4j
+public class JiashuUserServiceImpl implements JiashuUserService {
+
+    @Resource
+    private UserV2Mapper userV2Mapper;
+
+    @Override
+    public String getMpOpenIdByPhone(String phone) {
+        if (StringUtils.isBlank(phone)) {
+            return null;
+        }
+        UserV2DO userV2DO = userV2Mapper.selectByPhone(phone);
+        if (userV2DO == null) {
+            log.warn("未查询到家属端用户,手机号: {}", phone);
+            return null;
+        }
+        if (StringUtils.isBlank(userV2DO.getMpOpenId())) {
+            log.warn("家属端用户未关注公众号,手机号: {}", phone);
+            return null;
+        }
+        return userV2DO.getMpOpenId();
+    }
+
+}

+ 51 - 14
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/wx/WechatBusinessService.java

@@ -176,27 +176,41 @@ public class WechatBusinessService {
     }
 
 
-    private Map<String, Object> buildBillTemplateParam(String openid,Long orderId) {
+    /**
+     * 构建账单模板消息请求体
+     *
+     * <p>模板字段映射:
+     * <ul>
+     *     <li>thing2  - 缴费类型</li>
+     *     <li>thing18 - 客户名称</li>
+     *     <li>amount13 - 账单金额</li>
+     *     <li>time14  - 账单日期</li>
+     *     <li>thing12 - 账单所属</li>
+     * </ul>
+     */
+    private Map<String, Object> buildBillTemplateParam(String openid, Long orderId,
+                                                       String billType, String customerName,
+                                                       String amount, String billDate, String billOwner) {
         Map<String, Object> param = new HashMap<>();
         param.put("touser", openid); // 接收用户openid
         param.put("template_id", templateId); // 模板ID
         param.put("topcolor", "#FF0000"); // 消息顶部颜色(可选)
 
-        // ------------------------------ 核心修改:小程序跳转配置 ------------------------------
+        // ------------------------------ 小程序跳转配置 ------------------------------
         Map<String, String> miniprogram = new HashMap<>();
         miniprogram.put("appid", appId); // 关联的小程序AppID
         // 页面路径+携带订单ID参数(格式:页面路径?key=value,多参数用&分隔)
         miniprogram.put("pagepath", "pages/index/index?orderId=" + orderId.toString());
-        param.put("miniprogram", miniprogram); // 替换原url节点
-        // ----------------------------------------------------------------------------------
+        param.put("miniprogram", miniprogram);
+        // ---------------------------------------------------------------------------
 
         // 模板关键词参数(与模板一致)
         Map<String, Map<String, String>> data = new HashMap<>();
-        data.put("thing2", buildParamValue("月度账单"));
-        data.put("thing18", buildParamValue("张三"));
-        data.put("amount13", buildParamValue("100.00"));
-        data.put("time14", buildParamValue("2025-12-03"));
-        data.put("thing12", buildParamValue("李四"));
+        data.put("thing2", buildParamValue(billType));      // 缴费类型
+        data.put("thing18", buildParamValue(customerName)); // 客户名称
+        data.put("amount13", buildParamValue(amount));      // 账单金额
+        data.put("time14", buildParamValue(billDate));      // 账单日期
+        data.put("thing12", buildParamValue(billOwner));    // 账单所属
         param.put("data", data);
 
         return param;
@@ -214,16 +228,31 @@ public class WechatBusinessService {
 
 
     /**
-     * 推送账单模板消息
+     * 推送账单模板消息(使用真实账单数据)
      *
+     * @param openId       接收者公众号 OpenID
+     * @param orderId      账单订单ID(用于小程序跳转)
+     * @param billType     缴费类型(thing2)
+     * @param customerName 客户名称(thing18)
+     * @param amount       账单金额(amount13)
+     * @param billDate     账单日期(time14)
+     * @param billOwner    账单所属(thing12)
+     * @return 是否发送成功
      */
-    public boolean pushBillMessage(String openId) {
+    public boolean pushBillMessage(String openId, Long orderId,
+                                   String billType, String customerName,
+                                   String amount, String billDate, String billOwner) {
         try {
             // 1. 获取Access Token (需缓存,避免频繁调用)
             String accessToken = tokenService.getAccessToken();
+            if (accessToken == null) {
+                log.error("推送账单模板消息失败,获取 access_token 为空,openId: {}", openId);
+                return false;
+            }
 
-            // 2. 构建模板消息请求体 (关键!)
-            Map<String, Object> map = buildBillTemplateParam(openId,21897L);
+            // 2. 构建模板消息请求体
+            Map<String, Object> map = buildBillTemplateParam(openId, orderId,
+                    billType, customerName, amount, billDate, billOwner);
 
             // 3. 调用微信API发送
             String url = "https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=" + accessToken;
@@ -234,11 +263,19 @@ public class WechatBusinessService {
             log.info("发送模板消息结果:{}", result);
             return result.getInteger("errcode") == 0;
         } catch (Exception e) {
-            e.printStackTrace();
+            log.error("推送账单模板消息异常,openId: {}", openId, e);
             return false;
         }
     }
 
+    /**
+     * 推送账单模板消息(测试用,沿用默认示例数据)
+     */
+    public boolean pushBillMessage(String openId) {
+        return pushBillMessage(openId, 21897L,
+                "月度账单", "张三", "100.00", "2025-12-03", "李四");
+    }
+
 }