Browse Source

新增
1、新增消费券功能

liangwenxuan 2 tháng trước cách đây
mục cha
commit
b9d4fff66e
17 tập tin đã thay đổi với 831 bổ sung97 xóa
  1. 1 0
      yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/change/BusinessConstants.java
  2. 2 2
      yudao-module-system/yudao-module-system-biz/pom.xml
  3. 81 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/biz/ElderlyConsumerVouchersController.java
  4. 33 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/biz/vo/ElderlyConsumerVouchersPageReqVO.java
  5. 60 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/biz/vo/ElderlyConsumerVouchersRespVO.java
  6. 41 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/biz/vo/ElderlyConsumerVouchersSaveReqVO.java
  7. 6 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/biz/vo/ElderlyWeeklyOperationSummaryRespVO.java
  8. 2 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dify/DifyWorkFlowRecordController.java
  9. 2 1
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dify/vo/DifyWorkFlowRunReqVO.java
  10. 76 94
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/pay/WeChatPayController.java
  11. 167 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/pay/vo/WechatUnifiedOrderRequest.java
  12. 109 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/biz/ElderlyConsumerVouchersDO.java
  13. 19 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/biz/ElderlyConsumerVouchersMapper.java
  14. 22 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/biz/ElderlyConsumerVouchersService.java
  15. 155 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/biz/ElderlyConsumerVouchersServiceImpl.java
  16. 3 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/biz/ElderlyInfoServiceImpl.java
  17. 52 0
      yudao-module-system/yudao-module-system-biz/src/main/resources/mapper/ElderlyConsumerVouchersMapper.xml

+ 1 - 0
yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/change/BusinessConstants.java

@@ -30,6 +30,7 @@ public interface BusinessConstants {
 
 
     String DISCOUNT_DEDUCTION = "discount_deduction"; //优惠规则折扣费用
+    String CONSUMER_VOUCHER = "consumer_voucher"; // 消费券抵扣
     String RESERVATION_DEPOSIT = "reservation_deposit"; // 预约床位定金
 
 }

+ 2 - 2
yudao-module-system/yudao-module-system-biz/pom.xml

@@ -20,8 +20,8 @@
     <dependencies>
         <dependency>
             <groupId>com.github.wechatpay-apiv3</groupId>
-            <artifactId>wechatpay-apache-httpclient</artifactId>
-            <version>0.4.7</version>
+            <artifactId>wechatpay-java</artifactId>
+            <version>0.2.15</version>
         </dependency>
         <dependency>
             <groupId>cn.iocoder.boot</groupId>

+ 81 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/biz/ElderlyConsumerVouchersController.java

@@ -0,0 +1,81 @@
+package cn.iocoder.yudao.module.system.controller.admin.biz;
+
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore;
+import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
+import cn.iocoder.yudao.module.system.controller.admin.biz.vo.ElderlyConsumerVouchersPageReqVO;
+import cn.iocoder.yudao.module.system.controller.admin.biz.vo.ElderlyConsumerVouchersRespVO;
+import cn.iocoder.yudao.module.system.controller.admin.biz.vo.ElderlyConsumerVouchersSaveReqVO;
+import cn.iocoder.yudao.module.system.dal.dataobject.biz.ElderlyConsumerVouchersDO;
+import cn.iocoder.yudao.module.system.service.biz.ElderlyConsumerVouchersService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.annotation.Resource;
+import javax.validation.Valid;
+import java.util.Objects;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+
+@Tag(name = "管理后台 - 长者消费券")
+@RestController
+@RequestMapping("/elderly/consumer-vouchers")
+@Validated
+public class ElderlyConsumerVouchersController {
+
+    @Resource
+    private ElderlyConsumerVouchersService elderlyConsumerVouchersService;
+
+    @PostMapping("/create")
+    @Operation(summary = "创建长者消费券")
+    @TenantIgnore
+    public CommonResult<Long> create(@Valid @RequestBody ElderlyConsumerVouchersDO createReqVO) {
+        createReqVO.setTenantId(createReqVO.getTenantId() == null ? TenantContextHolder.getTenantId() : createReqVO.getTenantId());
+        return success(elderlyConsumerVouchersService.create(createReqVO));
+    }
+
+    @PutMapping("/update")
+    @Operation(summary = "更新长者消费券")
+    public CommonResult<Boolean> update(@Valid @RequestBody ElderlyConsumerVouchersSaveReqVO updateReqVO) {
+        updateReqVO.setTenantId(updateReqVO.getTenantId() == null ? TenantContextHolder.getTenantId() : updateReqVO.getTenantId());
+        elderlyConsumerVouchersService.update(updateReqVO);
+        return success(true);
+    }
+
+    @DeleteMapping("/delete")
+    @Operation(summary = "删除长者消费券")
+    @Parameter(name = "id", description = "编号", required = true)
+    public CommonResult<Boolean> delete(@RequestParam("id") Long id) {
+        elderlyConsumerVouchersService.delete(id);
+        return success(true);
+    }
+
+    @GetMapping("/get")
+    @Operation(summary = "获得长者消费券")
+    @Parameter(name = "id", description = "编号", required = true, example = "1024")
+    public CommonResult<ElderlyConsumerVouchersRespVO> get(@RequestParam("id") Long id) {
+        ElderlyConsumerVouchersDO data = elderlyConsumerVouchersService.get(id);
+        return success(BeanUtils.toBean(data, ElderlyConsumerVouchersRespVO.class));
+    }
+
+    @GetMapping("/page")
+    @Operation(summary = "获得长者消费券分页")
+    @TenantIgnore
+    public CommonResult<PageResult<ElderlyConsumerVouchersRespVO>> getPage(@Valid ElderlyConsumerVouchersPageReqVO pageReqVO) {
+        pageReqVO.setTenantIds(Objects.isNull(pageReqVO.getTenantIds()) ? new Long[]{TenantContextHolder.getTenantId()} : pageReqVO.getTenantIds());
+        PageResult<ElderlyConsumerVouchersDO> pageResult = elderlyConsumerVouchersService.getPage(pageReqVO);
+        return success(BeanUtils.toBean(pageResult, ElderlyConsumerVouchersRespVO.class));
+    }
+}

+ 33 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/biz/vo/ElderlyConsumerVouchersPageReqVO.java

@@ -0,0 +1,33 @@
+package cn.iocoder.yudao.module.system.controller.admin.biz.vo;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+@Schema(description = "管理后台 - 长者消费券分页 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class ElderlyConsumerVouchersPageReqVO extends PageParam {
+
+    @Schema(description = "长者id", example = "1")
+    private Long elderId;
+
+    @Schema(description = "账单归属月", example = "2025-01")
+    private String billingMonth;
+
+    @Schema(description = "使用状态,0:未抵扣 1:已抵扣", example = "0")
+    private Integer status;
+
+    @Schema(description = "返还状态", example = "0")
+    private Integer returnStatus;
+
+    @Schema(description = "缴费单号")
+    private String orderNumber;
+
+    @Schema(description = "租户ID数组")
+    private Long[] tenantIds;
+}
+

+ 60 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/biz/vo/ElderlyConsumerVouchersRespVO.java

@@ -0,0 +1,60 @@
+package cn.iocoder.yudao.module.system.controller.admin.biz.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.util.Date;
+
+@Schema(description = "管理后台 - 长者消费券 Response VO")
+@Data
+public class ElderlyConsumerVouchersRespVO {
+
+    @Schema(description = "id", example = "1")
+    private Long id;
+
+    @Schema(description = "长者id", example = "1")
+    private Long elderId;
+
+    @Schema(description = "长者姓名", example = "张三")
+    private String elderName;
+
+    @Schema(description = "身份证号")
+    private String idCard;
+
+    @Schema(description = "床位号")
+    private String bedName;
+
+    @Schema(description = "账单归属月", example = "2025-01")
+    private String billingMonth;
+
+    @Schema(description = "金额", example = "100.00")
+    private BigDecimal amount;
+
+    @Schema(description = "备注")
+    private String remarks;
+
+    @Schema(description = "使用状态,0:未抵扣 1:已抵扣", example = "0")
+    private Integer status;
+
+    @Schema(description = "返还状态", example = "0")
+    private Integer returnStatus;
+
+    @Schema(description = "缴费单号")
+    private String orderNumber;
+
+    @Schema(description = "租户ID", example = "1")
+    private Long tenantId;
+
+    @Schema(description = "创建时间")
+    private Date createdTime;
+
+    @Schema(description = "更新时间")
+    private Date updateTime;
+
+    @Schema(description = "创建人")
+    private String creator;
+
+    @Schema(description = "更新人")
+    private String updater;
+}

+ 41 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/biz/vo/ElderlyConsumerVouchersSaveReqVO.java

@@ -0,0 +1,41 @@
+package cn.iocoder.yudao.module.system.controller.admin.biz.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+import java.math.BigDecimal;
+
+@Schema(description = "管理后台 - 长者消费券新增/修改 Request VO")
+@Data
+public class ElderlyConsumerVouchersSaveReqVO {
+
+    @Schema(description = "id", example = "1")
+    private Long id;
+
+    @Schema(description = "长者id", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    @NotNull(message = "长者id不能为空")
+    private Long elderId;
+
+    @Schema(description = "账单归属月", example = "2025-01")
+    private String billingMonth;
+
+    @Schema(description = "金额", example = "100.00")
+    private BigDecimal amount;
+
+    @Schema(description = "备注")
+    private String remarks;
+
+    @Schema(description = "使用状态,0:未抵扣 1:已抵扣", example = "0")
+    private Integer status;
+
+    @Schema(description = "返还状态", example = "0")
+    private Integer returnStatus;
+
+    @Schema(description = "缴费单号")
+    private String orderNumber;
+
+    @Schema(description = "租户ID", example = "1")
+    private Long tenantId;
+}
+

+ 6 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/biz/vo/ElderlyWeeklyOperationSummaryRespVO.java

@@ -25,6 +25,12 @@ public class ElderlyWeeklyOperationSummaryRespVO {
     @Schema(description = "本周净增长长者数", example = "1")
     private Long weeklyNetGrowthCount;
 
+    @Schema(description = "本月入住数", example = "26")
+    private Long monthlyCheckInCount;
+
+    @Schema(description = "本月退住数", example = "1")
+    private Long monthlyCheckOutCount;
+
     @Schema(description = "本月净增长长者数合计", example = "25")
     private Long monthlyNetGrowthCount;
 

+ 2 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dify/DifyWorkFlowRecordController.java

@@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.system.controller.admin.dify;
 
 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore;
 import cn.iocoder.yudao.module.system.controller.admin.dify.vo.DifyWorkFlowRecordPageReqVO;
 import cn.iocoder.yudao.module.system.controller.admin.dify.vo.DifyWorkFlowRecordRespVO;
 import cn.iocoder.yudao.module.system.controller.admin.dify.vo.DifyWorkFlowRecordSaveReqVO;
@@ -64,6 +65,7 @@ public class DifyWorkFlowRecordController {
 
     @PostMapping("/workflow-run")
     @Operation(summary = "执行 Dify 工作流")
+    @TenantIgnore
     public CommonResult<JSONObject> workflowRun(@Valid @RequestBody DifyWorkFlowRunReqVO reqVO) {
         return success(difyWorkFlowRecordService.workflowRun(reqVO));
     }

+ 2 - 1
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dify/vo/DifyWorkFlowRunReqVO.java

@@ -4,13 +4,14 @@ import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.Data;
 
 import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
 
 @Schema(description = "管理后台 - Dify 工作流运行 Request VO")
 @Data
 public class DifyWorkFlowRunReqVO {
 
     @Schema(description = "租户id", requiredMode = Schema.RequiredMode.REQUIRED)
-    @NotBlank(message = "tenantId 不能为空")
+    @NotNull(message = "tenantId 不能为空")
     private Long tenantId;
 
     @Schema(description = "工作流编号", requiredMode = Schema.RequiredMode.REQUIRED)

+ 76 - 94
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/pay/WeChatPayController.java

@@ -1,31 +1,29 @@
 package cn.iocoder.yudao.module.system.controller.admin.pay;
 
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore;
+import cn.iocoder.yudao.module.system.controller.admin.pay.vo.WechatUnifiedOrderRequest;
 import cn.iocoder.yudao.module.system.util.pay.CertUtils;
-import com.wechat.pay.contrib.apache.httpclient.WechatPayHttpClientBuilder;
-import com.wechat.pay.contrib.apache.httpclient.auth.PrivateKeySigner;
-import com.wechat.pay.contrib.apache.httpclient.auth.Verifier;
-import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Credentials;
-import com.wechat.pay.contrib.apache.httpclient.auth.WechatPay2Validator;
-import com.wechat.pay.contrib.apache.httpclient.cert.CertificatesManager;
-import com.wechat.pay.contrib.apache.httpclient.exception.HttpCodeException;
-import com.wechat.pay.contrib.apache.httpclient.exception.NotFoundException;
-import com.wechat.pay.contrib.apache.httpclient.util.PemUtil;
-import com.wechat.pay.contrib.apache.httpclient.util.RsaCryptoUtil;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.wechat.pay.java.core.Config;
+import com.wechat.pay.java.core.RSAPublicKeyConfig;
+import com.wechat.pay.java.service.partnerpayments.jsapi.JsapiServiceExtension;
+import com.wechat.pay.java.service.partnerpayments.jsapi.model.Amount;
+import com.wechat.pay.java.service.partnerpayments.jsapi.model.Payer;
+import com.wechat.pay.java.service.partnerpayments.jsapi.model.PrepayRequest;
+import com.wechat.pay.java.service.partnerpayments.jsapi.model.PrepayWithRequestPaymentResponse;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.http.client.methods.CloseableHttpResponse;
 import org.apache.http.client.methods.HttpPost;
 import org.apache.http.entity.StringEntity;
-import org.apache.http.impl.client.CloseableHttpClient;
-import org.aspectj.lang.annotation.Before;
+import org.apache.http.util.EntityUtils;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
 
-import javax.crypto.BadPaddingException;
-import javax.crypto.IllegalBlockSizeException;
-import java.io.IOException;
-import java.nio.charset.StandardCharsets;
-import java.security.GeneralSecurityException;
-import java.security.PrivateKey;
+import javax.annotation.security.PermitAll;
+import java.util.Arrays;
 
 /**
  * 微信小程序统一下单控制器
@@ -35,10 +33,6 @@ import java.security.PrivateKey;
 @RequestMapping("/pay/wx")
 public class WeChatPayController {
 
-    private CloseableHttpClient httpClient;
-    private CertificatesManager certificatesManager;
-    private Verifier verifier;
-
     private static final String privateKey = "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCzbDDaK6pFz8fEBIM/pFlIV4uApuhr/TDm8mlK0+Cw1+om1p2s2RFItgBw7Gk9qTN72L68utj5bEDJRQgdr1ixAjc4AH1AOYeV4YZnWTQkcjsRiEimqo+bkIm+osTpFEO9kdvfFMILOl1MDipIAcUrNq9CMs6s6nFDLxLlwy8Ap4VrP4N+FF04efG+8dj35z8dUdi/8WrAe4p7GYGojHHoZ/mIqEDnLdt+PM5pRizCfUCtmSF5UxnmNUlUAutIedvtOAT9rs9E71aO+ldhXNMfA+jLxuBmyp+oy3hWS4Vqp4RbC1a2xRSULvEgSUwrdNq/rytpK66/XY7Vk2f/onCdAgMBAAECggEAXtI/ox1OeGSN01b7MggeMzApBo2u6Vs+m97irGv7JBqHoYzseWuiScX9x6/5EmA4WCw86Srp/i8qsfOsjVucyLc+DXecufNtZ1VvnXC0MMq50tMuaf5btMAXO8tzuyY9gpLyGxwyTJLEuawGxlVBKUxWJOsK9LFVuEbJeunDDlmWsn+euRw3F7jxtNnjVjfqXsY9O+TtB8p0bOduWZc7OvDHoIZQL4NnDivnAR5q1dSLt77pzdinvGiXNYNuTTULstR+KVhJy5Kmsldtj3nrP/fcy/N+JsXm9+3xgaJ2edKwmfnwvQjJAWOtltOSoHx4RK3kzbC8UhKs/E3iaUEUgQKBgQDp0qoNKHbTCiwDhmrnnCyaoW16qEOj0MqHOy5mKYmJkcSDGgNQM/44gN7vOu5BBJHXdkizxaoX3w/ufW2ahhJ/LcjITjFIq8gpiFKbLNfhkCw47ZDtmhB9jAnaB+9+rLeAFXkfXKi+PYJGZIIzbgn0u5LADzRjvzaxe/Z+Ed5ooQKBgQDEcKkk0SmcsHdTkLs2kf6fu8z7MAg9l42fCO18TPt3PeoRcvf8AKw+WDjARSo0ojqL23T9U0IydKdsbcxrYsHETUBS+MAP09Ckpd8p7F5evJghXwhhQRx0gzVuNMkbeP1a3xnC3yL7V5fluGQqz7Xiv3sg9dxYDh8cVQBu6ckafQKBgQCcoF0Ay2YtH9cz1UqvMtI+Enw/eY81oJrJ0z7VeGWFHXvBRh+KDgnw14J+Rb9rFiCLb9Rrd7DkpKsLWkGdDMo/HvAsHRSuVUOTbpnHEFbb5bN5vskiH92D+9Ztkns/I3sX9UpZU7xFEva9KH5+7OsGYM+Aj67MUj3UzfDjqhyNgQKBgFcNojeZpbo1jbvvsLd/PXqmLDHI2G4LIoyu1Se3qdzvCDLRY0o/NhWu3P9/5zNKDW37RD4bToOzpJptkiCotDv9DBt49wxMjvLYOyyF/lA3faeUSM9onmaX2u7K37CYDpbdtbnhTsxZxgvcii9auz0QJE24BvzSzUCt/rIoUqG1AoGAX9sTEp77kdVrM8ECEKgLsOtRWEAgeNkWLgHZKYcEYyu3D4XEVScYNtTBLBHXs3n1GIYdLjWEAWWb55sqKSlVzVWfOhpnOqoFq51xi2TGr3pkp9cqZf0lxYKs7P4tlgeL0icXfb+lFTPmgfgE5vjHBvYQcj7o9lO5hRe46fVoxT8=";
 
     private static final String merchantId = "1105835366";
@@ -68,82 +62,70 @@ public class WeChatPayController {
             "rXbHeFgiQcZb/sR8sTBV0KXR";
 
     private static final String apiV3Key = "W2sE4rF6tY8uI0oP1aQ2wS3dF4gH5jK6";
-    public void setup() throws Exception {
+
+    private static final String wechatpayPublicKeyPem = "-----BEGIN PUBLIC KEY-----\n" +
+            "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuh82JjfQ/r0+mz8W9ZAr\n" +
+            "Uw5nLSSzUKxvepSnWny0a7ccZFB+j8q9pZWq7gmfyOmsC6uq8gmzPDnOD+xrBfj7\n" +
+            "t8R3x/VU7tpmGtdNecQFTaJ6NrausM82sMVdf9XOpQvERgVtfIO5U1q3jqSfTyNk\n" +
+            "MchXhQMoWa3RXDl7EO0okLIKoNgA1o4AWZZaVyiJ0BjF/URwk6O0eg+g2PqDldFr\n" +
+            "RxQIHLsaFnFsDJx5bNia9Jb48g+nNpoKtW1njsPDn/ckTOvnZxdkbkwIaQ2NDli1\n" +
+            "ehsaDK6ACReD/A6nIlDQB50duKhVNzx10MZwikD2OWAPUnY6rIE9cqWwKUH4okWJ\n" +
+            "BwIDAQAB\n" +
+            "-----END PUBLIC KEY-----\n";
+
+    //Call Unified Order API
+    @PostMapping(value = "/unifiedOrder", consumes = {MediaType.APPLICATION_FORM_URLENCODED_VALUE, MediaType.APPLICATION_JSON_VALUE})
+    @PermitAll
+    @TenantIgnore
+    public CommonResult<String> unifiedOrder() throws Exception {
         String merchantSerialNumber = CertUtils.getMerchantSerialNumberFromPem(certPem);
-        PrivateKey merchantPrivateKey = PemUtil.loadPrivateKey(privateKey);
-        certificatesManager = CertificatesManager.getInstance();
-        certificatesManager.putMerchant(merchantId, new WechatPay2Credentials(merchantId,
-                        new PrivateKeySigner(merchantSerialNumber, merchantPrivateKey)),
-                apiV3Key.getBytes(StandardCharsets.UTF_8));
-        verifier = certificatesManager.getVerifier(merchantId);
-        WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create()
-                .withMerchant(merchantId, merchantSerialNumber, merchantPrivateKey)
-                .withValidator(new WechatPay2Validator(verifier));
-        httpClient = builder.build();
-    }
+        // 1. 构建配置类(就是你之前写的那部分)
+        Config config =
+                new RSAPublicKeyConfig.Builder()
+                        .merchantId(merchantId)
+                        .privateKey(privateKey)          // 商户私钥字符串
+                        .publicKey(wechatpayPublicKeyPem)   // 微信支付公钥字符串
+                        .publicKeyId("PUB_KEY_ID_0111058353662026030200381966000601")  // 公钥ID(必须!)
+                        .merchantSerialNumber(merchantSerialNumber)
+                        .apiV3Key(apiV3Key)
+                        .build();
 
-    public void encryptedTest() throws IllegalBlockSizeException {
-        String text = "helloWorld";
-        String transformation = "RSA/ECB/PKCS1Padding";
-        String encryptedText = RsaCryptoUtil.encrypt(text, verifier.getValidCertificate(), transformation);
-        System.out.println(encryptedText);
-    }
+        // 2. 创建 JSAPI 扩展服务
+        JsapiServiceExtension service = new JsapiServiceExtension.Builder().config(config).build();
 
-    public void decryptedTest() throws BadPaddingException {
-        String encryptedText = "";
-        String transformation = "RSA/ECB/PKCS1Padding";
-        String decryptedText = RsaCryptoUtil.decrypt(encryptedText, PemUtil.loadPrivateKey(privateKey), transformation);
-        assert("helloWorld".equals(decryptedText));
-    }
+        // 3. 构造统一下单请求
+        PrepayRequest request = new PrepayRequest();
+        Amount amount = new Amount();
+        amount.setTotal(1); // 金额,单位:分
+        request.setAmount(amount);
+        request.setSpAppid("wx0fe6b44ff9e5d4d6");
+        request.setSpMchid(merchantId);
+        request.setSubAppid("wx0fe6b44ff9e5d4d6");
+        request.setSpMchid("1739815694");
+        request.setDescription("测试商品");
+        request.setNotifyUrl("https://47.112.126.153:48080//pay/wx/callback");
+        request.setOutTradeNo("OUT_TRADE_NO_20250303_001");
 
+        Payer payer = new Payer();
+        payer.setSpOpenid("omRZq10rC8JR5SysFqVj82CJDqVk"); // 必须传入用户的openid
+        request.setPayer(payer);
+        String requestPaymentAppid = request.getSubAppid() != null ? request.getSubAppid() : request.getSpAppid();
+        // 4. 调用扩展方法,直接得到前端调起支付所需的参数
+        PrepayWithRequestPaymentResponse response = service.prepayWithRequestPayment(request,requestPaymentAppid);
 
-    //Call Unified Order API
-    public void unifiedOrderTest() throws IOException {
-        String unifiedOrderBody = String.join("\n" ,
-                "{" ,
-                "'sp_appid': 'wx2421b1c4370ec43b'," ,
-                "'sp_mchid': '10000100'," ,
-                "'sub_mchid': '20000100'," ,
-                "'sub_appid': 'wx352671b037b437ec'," ,
-                "'out_trade_no': '20150806125346'," ,
-                "'merchant_category_code': '1011'," ,
-                "'payer': {" ,
-                "'sub_openid': 'oUpF8uMuAJO_M2pxb1Q9zNjWeS6o'" ,
-                "}," ,
-                "'notify_url': 'https://wxpay.wxutil.com/pub_v2/pay/notify.v2.php'," ,
-                "'trade_type': 'JSAPI'," ,
-                "'amount': {" ,
-                "'total': 10000," ,
-                "'currency': 'HKD'" ,
-                "}," ,
-                "'attach': 'Payment test'," ,
-                "'description': 'Miniprogramm Pay test'," ,
-                "'detail': {" ,
-                "'cost_price': 10000," ,
-                "'receipt_id': '1234'," ,
-                "'goods_detail': [{" ,
-                "'goods_id': 'iphone6s_16G'," ,
-                "'wxpay_goods_id': '1001'," ,
-                "'goods_name': 'iPhone6s 16G'," ,
-                "'quantity': 1," ,
-                "'price': 528800" ,
-                "}]" ,
-                "}," ,
-                "'scene_info': {" ,
-                "'payer_client_ip': '14.23.150.211'," ,
-                "'device_ip': '59.37.125.32'," ,
-                "'device_id': '013467007045764'," ,
-                "'operator_id': 'P001'," ,
-                "'store_info': {" ,
-                "'id': 'SZTX001'" ,
-                "}" ,
-                "}" ,
-                "}").replace("'","\"");
-        HttpPost httpPost = new HttpPost("https://apihk.mch.weixin.qq.com/v3/global/transactions/jsapi");
-        httpPost.addHeader("Accept", "application/json");
-        httpPost.addHeader("Content-type", "application/json; charset=utf-8");
-        httpPost.setEntity(new StringEntity(unifiedOrderBody));
-        CloseableHttpResponse response = httpClient.execute(httpPost);
-        //Process the response
+        // 5. 输出或返回给前端
+        System.out.println("package: " + response.getPackageVal());
+        System.out.println("nonceStr: " + response.getNonceStr());
+        System.out.println("timeStamp: " + response.getTimeStamp());
+        System.out.println("signType: " + response.getSignType());
+        System.out.println("paySign: " + response.getPaySign());
+        return CommonResult.success("ok");
+    }
+    @PostMapping(value = "/callback", consumes = {MediaType.APPLICATION_FORM_URLENCODED_VALUE, MediaType.APPLICATION_JSON_VALUE})
+    @PermitAll
+    @TenantIgnore
+    public CommonResult<String> weChatPayCallBack(){
+        System.out.println("wechat callback");
+        return CommonResult.success("callBackOk");
     }
 }

+ 167 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/pay/vo/WechatUnifiedOrderRequest.java

@@ -0,0 +1,167 @@
+package cn.iocoder.yudao.module.system.controller.admin.pay.vo;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 微信支付统一下单请求体(APIv3 全球接口)
+ * 对应接口:POST /v3/global/transactions/jsapi
+ * 文档参考:https://pay.weixin.qq.com/doc/global/v3/zh/api/jsapi/orders.html
+ */
+@Data
+public class WechatUnifiedOrderRequest {
+
+    /** 服务商应用ID(必填) */
+    @JsonProperty("sp_appid")
+    private String spAppid;
+
+    /** 服务商商户号(必填) */
+    @JsonProperty("sp_mchid")
+    private String spMchid;
+
+    /** 子商户号(必填) */
+    @JsonProperty("sub_mchid")
+    private String subMchid;
+
+    /** 子商户应用ID(若传入sub_mchid,该字段必填) */
+    @JsonProperty("sub_appid")
+    private String subAppid;
+
+    /** 商户订单号(必填,商户系统内部唯一) */
+    @JsonProperty("out_trade_no")
+    private String outTradeNo;
+
+    /** 商户类别码(必填,如:1011 代表餐饮) */
+    @JsonProperty("merchant_category_code")
+    private String merchantCategoryCode;
+
+    /** 支付者信息(必填) */
+    private Payer payer;
+
+    /** 通知地址(必填,用于接收支付结果回调) */
+    @JsonProperty("notify_url")
+    private String notifyUrl;
+
+    /** 交易类型(必填,JSAPI 代表公众号/小程序支付) */
+    @JsonProperty("trade_type")
+    private String tradeType;
+
+    /** 订单金额信息(必填) */
+    private Amount amount;
+
+    /** 附加数据(选填,在查询API和支付通知中原样返回) */
+    private String attach;
+
+    /** 商品描述(必填) */
+    private String description;
+
+    /** 商品详情(选填) */
+    private Detail detail;
+
+    /** 场景信息(选填) */
+    @JsonProperty("scene_info")
+    private SceneInfo sceneInfo;
+
+    // ---------- 内部类 ----------
+
+    /**
+     * 支付者信息
+     */
+    @Data
+    public static class Payer {
+        /** 用户在子商户appid下的唯一标识(必填) */
+        @JsonProperty("sub_openid")
+        private String subOpenid;
+    }
+
+    /**
+     * 订单金额
+     */
+    @Data
+    public static class Amount {
+        /** 总金额(单位:分,必填) */
+        private int total;
+
+        /** 货币类型(符合ISO 4217的三位字母代码,如CNY、HKD,必填) */
+        private String currency;
+    }
+
+    /**
+     * 商品详情
+     */
+    @Data
+    public static class Detail {
+        /** 订单原价(单位:分,选填) */
+        @JsonProperty("cost_price")
+        private int costPrice;
+
+        /** 商品小票ID(选填) */
+        @JsonProperty("receipt_id")
+        private String receiptId;
+
+        /** 商品列表(选填) */
+        @JsonProperty("goods_detail")
+        private List<GoodsDetail> goodsDetail;
+    }
+
+    /**
+     * 单品信息
+     */
+    @Data
+    public static class GoodsDetail {
+        /** 商品编码(必填) */
+        @JsonProperty("goods_id")
+        private String goodsId;
+
+        /** 微信支付商品编码(选填) */
+        @JsonProperty("wxpay_goods_id")
+        private String wxpayGoodsId;
+
+        /** 商品名称(选填) */
+        @JsonProperty("goods_name")
+        private String goodsName;
+
+        /** 商品数量(必填) */
+        private int quantity;
+
+        /** 商品单价(单位:分,必填) */
+        private int price;
+    }
+
+    /**
+     * 场景信息
+     */
+    @Data
+    public static class SceneInfo {
+        /** 用户终端IP(必填) */
+        @JsonProperty("payer_client_ip")
+        private String payerClientIp;
+
+        /** 商户端设备IP(选填) */
+        @JsonProperty("device_ip")
+        private String deviceIp;
+
+        /** 商户端设备号(选填) */
+        @JsonProperty("device_id")
+        private String deviceId;
+
+        /** 操作员ID(选填) */
+        @JsonProperty("operator_id")
+        private String operatorId;
+
+        /** 门店信息(选填) */
+        @JsonProperty("store_info")
+        private StoreInfo storeInfo;
+    }
+
+    /**
+     * 门店信息
+     */
+    @Data
+    public static class StoreInfo {
+        /** 门店ID(选填) */
+        private String id;
+    }
+}

+ 109 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/biz/ElderlyConsumerVouchersDO.java

@@ -0,0 +1,109 @@
+package cn.iocoder.yudao.module.system.dal.dataobject.biz;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.KeySequence;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.ToString;
+
+import java.math.BigDecimal;
+import java.util.Date;
+
+/**
+ * 长者消费券 DO
+ */
+@TableName("elderly_consumer_vouchers")
+@KeySequence("elderly_consumer_vouchers_seq")
+@Data
+@ToString
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class ElderlyConsumerVouchersDO {
+
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    /**
+     * 长者id
+     */
+    private Long elderId;
+
+    /**
+     * 长者姓名(联表字段)
+     */
+    @TableField(exist = false)
+    private String elderName;
+
+    /**
+     * 身份证号(联表字段)
+     */
+    @TableField(exist = false)
+    private String idCard;
+
+    /**
+     * 床位号(联表字段)
+     */
+    @TableField(exist = false)
+    private String bedName;
+
+    /**
+     * 账单归属月
+     */
+    private String billingMonth;
+
+    /**
+     * 金额
+     */
+    private BigDecimal amount;
+
+    /**
+     * 备注
+     */
+    private String remarks;
+
+    /**
+     * 使用状态,0:未抵扣 1:已抵扣
+     */
+    private Integer status;
+
+    /**
+     * 返还状态
+     */
+    private Integer returnStatus;
+
+    /**
+     * 缴费单号
+     */
+    private String orderNumber;
+
+    /**
+     * 租户ID
+     */
+    private Long tenantId;
+
+    /**
+     * 创建时间
+     */
+    private Date createdTime;
+
+    /**
+     * 更新时间
+     */
+    private Date updateTime;
+
+    /**
+     * 创建人
+     */
+    private String creator;
+
+    /**
+     * 更新人
+     */
+    private String updater;
+}

+ 19 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/biz/ElderlyConsumerVouchersMapper.java

@@ -0,0 +1,19 @@
+package cn.iocoder.yudao.module.system.dal.mysql.biz;
+
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.module.system.controller.admin.biz.vo.ElderlyConsumerVouchersPageReqVO;
+import cn.iocoder.yudao.module.system.dal.dataobject.biz.ElderlyConsumerVouchersDO;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+@Mapper
+public interface ElderlyConsumerVouchersMapper extends BaseMapperX<ElderlyConsumerVouchersDO> {
+
+    List<ElderlyConsumerVouchersDO> selectPageWithElderInfo(Page<ElderlyConsumerVouchersDO> page,
+                                                            @Param("reqVO") ElderlyConsumerVouchersPageReqVO reqVO);
+
+    ElderlyConsumerVouchersDO selectByIdWithElderInfo(@Param("id") Long id);
+}

+ 22 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/biz/ElderlyConsumerVouchersService.java

@@ -0,0 +1,22 @@
+package cn.iocoder.yudao.module.system.service.biz;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.system.controller.admin.biz.vo.ElderlyConsumerVouchersPageReqVO;
+import cn.iocoder.yudao.module.system.controller.admin.biz.vo.ElderlyConsumerVouchersSaveReqVO;
+import cn.iocoder.yudao.module.system.dal.dataobject.biz.ElderlyConsumerVouchersDO;
+
+import javax.validation.Valid;
+
+public interface ElderlyConsumerVouchersService {
+
+    Long create(@Valid ElderlyConsumerVouchersDO createReqVO);
+
+    void update(@Valid ElderlyConsumerVouchersSaveReqVO updateReqVO);
+
+    void delete(Long id);
+
+    ElderlyConsumerVouchersDO get(Long id);
+
+    PageResult<ElderlyConsumerVouchersDO> getPage(ElderlyConsumerVouchersPageReqVO pageReqVO);
+}
+

+ 155 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/biz/ElderlyConsumerVouchersServiceImpl.java

@@ -0,0 +1,155 @@
+package cn.iocoder.yudao.module.system.service.biz;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
+import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
+import cn.iocoder.yudao.module.system.controller.admin.biz.vo.ElderlyConsumerVouchersPageReqVO;
+import cn.iocoder.yudao.module.system.controller.admin.biz.vo.ElderlyConsumerVouchersSaveReqVO;
+import cn.iocoder.yudao.module.system.dal.dataobject.biz.ElderlyConsumerVouchersDO;
+import cn.iocoder.yudao.module.system.dal.dataobject.biz.ExpenseOrderDO;
+import cn.iocoder.yudao.module.system.dal.dataobject.biz.ExpenseOrderItemDO;
+import cn.iocoder.yudao.module.system.dal.mysql.biz.ElderlyConsumerVouchersMapper;
+import cn.iocoder.yudao.module.system.dal.mysql.biz.ExpenseOrderItemMapper;
+import cn.iocoder.yudao.module.system.dal.mysql.biz.ExpenseOrderMapper;
+import cn.iocoder.yudao.module.system.enums.change.BusinessConstants;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.stereotype.Service;
+import org.springframework.validation.annotation.Validated;
+
+import javax.annotation.Resource;
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
+import java.util.Date;
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.COMMON_ERROR;
+import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.COMMON_NOT_FOUND;
+import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.EXPENSE_ORDER_ALREADY_CONFIRMED;
+import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.EXPENSE_ORDER_ALREADY_LOCK;
+
+@Service
+@Validated
+public class ElderlyConsumerVouchersServiceImpl implements ElderlyConsumerVouchersService {
+
+    @Resource
+    private ElderlyConsumerVouchersMapper elderlyConsumerVouchersMapper;
+
+    @Resource
+    private ExpenseOrderMapper expenseOrderMapper;
+
+    @Resource
+    private ExpenseOrderItemMapper expenseOrderItemMapper;
+
+    @Override
+    public Long create(ElderlyConsumerVouchersDO createReqVO) {
+        validateBillingMonth(createReqVO.getBillingMonth());
+        createReqVO.setCreatedTime(new Date());
+        elderlyConsumerVouchersMapper.insert(createReqVO);
+        updateExpenseOrder(createReqVO);
+        return createReqVO.getId();
+    }
+
+    @Override
+    public void update(ElderlyConsumerVouchersSaveReqVO updateReqVO) {
+        validateExists(updateReqVO.getId());
+        validateBillingMonth(updateReqVO.getBillingMonth());
+        ElderlyConsumerVouchersDO data = BeanUtils.toBean(updateReqVO, ElderlyConsumerVouchersDO.class);
+        elderlyConsumerVouchersMapper.updateById(data);
+    }
+
+    @Override
+    public void delete(Long id) {
+        validateExists(id);
+        elderlyConsumerVouchersMapper.deleteById(id);
+    }
+
+    @Override
+    public ElderlyConsumerVouchersDO get(Long id) {
+        return elderlyConsumerVouchersMapper.selectByIdWithElderInfo(id);
+    }
+
+    @Override
+    public PageResult<ElderlyConsumerVouchersDO> getPage(ElderlyConsumerVouchersPageReqVO pageReqVO) {
+        validateBillingMonth(pageReqVO.getBillingMonth());
+        Page<ElderlyConsumerVouchersDO> page = new Page<>(pageReqVO.getPageNo(), pageReqVO.getPageSize());
+        List<ElderlyConsumerVouchersDO> list = elderlyConsumerVouchersMapper.selectPageWithElderInfo(page, pageReqVO);
+        return new PageResult<>(list, page.getTotal());
+    }
+
+    /**
+     * 消费券录入对应未缴费的账单需要更新
+     */
+    private void updateExpenseOrder(ElderlyConsumerVouchersDO createReqVO) {
+        ExpenseOrderDO expenseOrder = expenseOrderMapper.selectOne(new LambdaQueryWrapperX<ExpenseOrderDO>()
+                .eq(ExpenseOrderDO::getElderId, createReqVO.getElderId())
+                .eq(ExpenseOrderDO::getIsShow, true)
+                .eq(ExpenseOrderDO::getBillingMonth, createReqVO.getBillingMonth())
+                .eq(ExpenseOrderDO::getType, 2));
+
+        if (expenseOrder == null) {
+            return;
+        }
+
+        validateExpenseOrderNotConfirmedAndNotLock(expenseOrder);
+
+        ExpenseOrderItemDO orderItem = new ExpenseOrderItemDO();
+        orderItem.setExpenseOrderId(expenseOrder.getId());
+        orderItem.setSourceExpenseItemId(createReqVO.getId());
+        orderItem.setExpenseSource(BusinessConstants.CONSUMER_VOUCHER);
+        orderItem.setCount(1);
+        orderItem.setItemName("消费券抵扣");
+        orderItem.setPrice(createReqVO.getAmount().negate());
+        orderItem.setActualPrice(createReqVO.getAmount().negate());
+        orderItem.setTotalAmount(createReqVO.getAmount().negate());
+        orderItem.setRoundAmount(orderItem.getTotalAmount().setScale(0, RoundingMode.HALF_UP));
+        orderItem.setRoundTwoDecimalAmount(orderItem.getTotalAmount().setScale(2, RoundingMode.HALF_UP));
+        orderItem.setCreatedTime(new Date());
+        orderItem.setCreatedBy(SecurityFrameworkUtils.getLoginUserNickname());
+        orderItem.setTenantId(createReqVO.getTenantId());
+        orderItem.setPayStatus(0);
+        expenseOrderItemMapper.insert(orderItem);
+
+        BigDecimal currentAmount = expenseOrder.getActualAmount() != null ? expenseOrder.getActualAmount() : BigDecimal.ZERO;
+        expenseOrder.setActualAmount(currentAmount.add(orderItem.getTotalAmount()));
+        expenseOrderMapper.updateById(expenseOrder);
+
+        createReqVO.setStatus(1);
+        createReqVO.setOrderNumber(expenseOrder.getBillOrderNumber());
+        elderlyConsumerVouchersMapper.updateById(createReqVO);
+    }
+
+    private void validateExpenseOrderNotConfirmedAndNotLock(ExpenseOrderDO expenseOrder) {
+        if (Boolean.TRUE.equals(expenseOrder.getConfirmStatus())) {
+            throw exception(EXPENSE_ORDER_ALREADY_CONFIRMED);
+        }
+        if (Boolean.TRUE.equals(expenseOrder.getIsLock())) {
+            throw exception(EXPENSE_ORDER_ALREADY_LOCK);
+        }
+    }
+
+    private void validateExists(Long id) {
+        if (id == null || elderlyConsumerVouchersMapper.selectById(id) == null) {
+            throw exception(COMMON_NOT_FOUND);
+        }
+    }
+
+    /**
+     * 校验账单月格式 yyyy-MM
+     */
+    private void validateBillingMonth(String billingMonth) {
+        if (StringUtils.isBlank(billingMonth)) {
+            return;
+        }
+        try {
+            LocalDate.parse(billingMonth + "-01", DateTimeFormatter.ofPattern("yyyy-MM-dd"));
+        } catch (DateTimeParseException ex) {
+            throw exception(COMMON_ERROR, "账单归属月格式需为 yyyy-MM,例如 2025-01");
+        }
+    }
+}

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

@@ -1522,6 +1522,9 @@ public class ElderlyInfoServiceImpl implements ElderlyInfoService {
         long safeMonthIn = monthlyCheckInCount == null ? 0L : monthlyCheckInCount;
         long safeMonthOut = monthlyCheckOutCount == null ? 0L : monthlyCheckOutCount;
 
+        respVO.setMonthlyCheckInCount(safeMonthIn);
+        respVO.setMonthlyCheckOutCount(safeMonthOut);
+
         long weeklyNetGrowth = safeWeekIn - safeWeekOut;
         respVO.setWeeklyNetGrowthCount(weeklyNetGrowth);
         respVO.setPeriodBeginCount(Math.max(0L, respVO.getPeriodEndCount() - weeklyNetGrowth));

+ 52 - 0
yudao-module-system/yudao-module-system-biz/src/main/resources/mapper/ElderlyConsumerVouchersMapper.xml

@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="cn.iocoder.yudao.module.system.dal.mysql.biz.ElderlyConsumerVouchersMapper">
+
+    <select id="selectPageWithElderInfo"
+            resultType="cn.iocoder.yudao.module.system.dal.dataobject.biz.ElderlyConsumerVouchersDO">
+        select ecv.*,
+               ei.elder_name as elderName,
+               ei.id_card as idCard,
+               ei.bed_name as bedName
+        from elderly_consumer_vouchers ecv
+                 left join elderly_info ei on ei.id = ecv.elder_id
+        <where>
+            1 = 1
+            <if test="reqVO.elderId != null">
+                and ecv.elder_id = #{reqVO.elderId}
+            </if>
+            <if test="reqVO.billingMonth != null and reqVO.billingMonth != ''">
+                and ecv.billing_month = #{reqVO.billingMonth}
+            </if>
+            <if test="reqVO.status != null">
+                and ecv.status = #{reqVO.status}
+            </if>
+            <if test="reqVO.returnStatus != null">
+                and ecv.return_status = #{reqVO.returnStatus}
+            </if>
+            <if test="reqVO.orderNumber != null and reqVO.orderNumber != ''">
+                and ecv.order_number like concat('%', #{reqVO.orderNumber}, '%')
+            </if>
+            <if test="reqVO.tenantIds != null and reqVO.tenantIds.length > 0">
+                and ecv.tenant_id in
+                <foreach collection="reqVO.tenantIds" item="tenantId" open="(" separator="," close=")">
+                    #{tenantId}
+                </foreach>
+            </if>
+        </where>
+        order by ecv.id desc
+    </select>
+
+    <select id="selectByIdWithElderInfo"
+            resultType="cn.iocoder.yudao.module.system.dal.dataobject.biz.ElderlyConsumerVouchersDO">
+        select ecv.*,
+               ei.elder_name as elderName,
+               ei.id_card as idCard,
+               ei.bed_name as bedName
+        from elderly_consumer_vouchers ecv
+                 left join elderly_info ei on ei.id = ecv.elder_id
+        where ecv.id = #{id}
+        limit 1
+    </select>
+
+</mapper>