瀏覽代碼

Merge remote-tracking branch 'origin/master'

xiari 1 年之前
父節點
當前提交
1fb284aaa9
共有 41 個文件被更改,包括 2108 次插入90 次删除
  1. 9 0
      ruoyi-api/ruoyi-api-backstage/src/main/java/org/dromara/backstage/api/RemoteMealTypeService.java
  2. 19 0
      ruoyi-api/ruoyi-api-backstage/src/main/java/org/dromara/backstage/api/RemoteOperatorService.java
  3. 2 0
      ruoyi-api/ruoyi-api-backstage/src/main/java/org/dromara/backstage/api/RemotePtAccountService.java
  4. 3 0
      ruoyi-api/ruoyi-api-backstage/src/main/java/org/dromara/backstage/api/RemotePtRoomService.java
  5. 54 0
      ruoyi-api/ruoyi-api-backstage/src/main/java/org/dromara/backstage/api/domain/vo/RemoteMealTypeVo.java
  6. 67 0
      ruoyi-api/ruoyi-api-backstage/src/main/java/org/dromara/backstage/api/domain/vo/RemoteOperatorVo.java
  7. 72 0
      ruoyi-api/ruoyi-api-backstage/src/main/java/org/dromara/backstage/api/domain/vo/RemotePtAccountVo.java
  8. 79 0
      ruoyi-api/ruoyi-api-backstage/src/main/java/org/dromara/backstage/api/domain/vo/RemotePtRoomVo.java
  9. 30 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/ApiErrorTypeConstants.java
  10. 50 0
      ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/enums/TradeStatusEnum.java
  11. 1 0
      ruoyi-common/ruoyi-common-message/src/main/java/org/dromara/common/message/kafka/constant/MessageEventTypeConstants.java
  12. 19 0
      ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/basics/domain/convert/RemoteMealTypeVoConvert.java
  13. 19 0
      ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/basics/domain/convert/RemoteOperatorVoConvert.java
  14. 17 0
      ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/basics/domain/convert/RemotePtAccountVoConvert.java
  15. 17 0
      ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/basics/domain/convert/RemotePtRoomVoConvert.java
  16. 20 7
      ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/basics/dubbo/RemoteMealTypeServiceImpl.java
  17. 35 0
      ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/basics/dubbo/RemoteOperatorServiceImpl.java
  18. 9 0
      ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/basics/dubbo/RemotePtAccountServiceImpl.java
  19. 13 5
      ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/basics/dubbo/RemotePtRoomServiceImpl.java
  20. 0 1
      ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/basics/service/impl/PtRoomServiceImpl.java
  21. 1 0
      ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/business/payments/PayOrderBusiness.java
  22. 6 1
      ruoyi-server/ruoyi-server-common/src/main/java/org/dromara/server/common/constant/DefaultConstants.java
  23. 7 1
      ruoyi-server/ruoyi-server-common/src/main/java/org/dromara/server/common/domain/consume/bo/ConsumptionBo.java
  24. 26 0
      ruoyi-server/ruoyi-server-common/src/main/java/org/dromara/server/common/util/CardDateUtils.java
  25. 189 5
      ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/business/BaseBusiness.java
  26. 279 58
      ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/business/CheckBusiness.java
  27. 43 0
      ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/business/ConsumeBusiness.java
  28. 24 10
      ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/controller/v1/ConsumeController.java
  29. 2 1
      ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/convert/strategy/impl/YcRecordConvertStrategyImpl.java
  30. 80 0
      ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/domain/PtBag.java
  31. 92 0
      ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/domain/bo/PtBagBo.java
  32. 82 0
      ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/domain/vo/PtBagVo.java
  33. 169 0
      ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/domain/vo/XfTermVo.java
  34. 15 0
      ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/mapper/PtBagMapper.java
  35. 110 0
      ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/service/IPtBagService.java
  36. 4 0
      ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/service/IXfConsumeDetailService.java
  37. 380 0
      ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/service/impl/PtBagServiceImpl.java
  38. 18 0
      ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/service/impl/XfConsumeDetailServiceImpl.java
  39. 20 0
      ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/service/impl/XfTermServiceImpl.java
  40. 1 1
      ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/service/impl/XfUserTotalServiceImpl.java
  41. 25 0
      ruoyi-server/ruoyi-server-consume/src/main/resources/mapper/consume/PtBagMapper.xml

+ 9 - 0
ruoyi-api/ruoyi-api-backstage/src/main/java/org/dromara/backstage/api/RemoteMealTypeService.java

@@ -1,8 +1,11 @@
 package org.dromara.backstage.api;
 
 import org.dromara.backstage.api.domain.bo.RemotePtMealTypeBo;
+import org.dromara.backstage.api.domain.bo.RemotePtParameterBo;
+import org.dromara.backstage.api.domain.vo.RemoteMealTypeVo;
 
 import java.util.Collection;
+import java.util.Date;
 
 /**
  * 餐类服务
@@ -18,4 +21,10 @@ public interface RemoteMealTypeService {
 
     Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) throws Exception;
 
+    /**
+     * 根据就餐获取对应的餐类信息
+     * @param mealTime 就餐时间
+     * @return 餐类信息
+     */
+    RemoteMealTypeVo queryMealTypeVoByTime(Date mealTime);
 }

+ 19 - 0
ruoyi-api/ruoyi-api-backstage/src/main/java/org/dromara/backstage/api/RemoteOperatorService.java

@@ -0,0 +1,19 @@
+package org.dromara.backstage.api;
+
+import org.dromara.backstage.api.domain.vo.RemoteOperatorVo;
+
+/**
+ * 餐类服务
+ *
+ * @author bing
+ */
+public interface RemoteOperatorService {
+
+    /**
+     * 根据营业员Id获取营业员信息
+     *
+     * @param operatorId 营业员Id
+     * @return 餐类信息
+     */
+    RemoteOperatorVo getVoById(Long operatorId);
+}

+ 2 - 0
ruoyi-api/ruoyi-api-backstage/src/main/java/org/dromara/backstage/api/RemotePtAccountService.java

@@ -1,6 +1,7 @@
 package org.dromara.backstage.api;
 
 import org.dromara.backstage.api.domain.bo.RemotePtAccountBo;
+import org.dromara.backstage.api.domain.vo.RemotePtAccountVo;
 
 import java.util.Collection;
 
@@ -20,4 +21,5 @@ public interface RemotePtAccountService {
 
     Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) throws Exception;
 
+    RemotePtAccountVo selectVoById(Long id) ;
 }

+ 3 - 0
ruoyi-api/ruoyi-api-backstage/src/main/java/org/dromara/backstage/api/RemotePtRoomService.java

@@ -2,6 +2,7 @@ package org.dromara.backstage.api;
 
 import org.dromara.backstage.api.domain.bo.RemotePtRoomBatchSetBo;
 import org.dromara.backstage.api.domain.bo.RemotePtRoomBo;
+import org.dromara.backstage.api.domain.vo.RemotePtRoomVo;
 
 import java.util.Collection;
 
@@ -17,4 +18,6 @@ public interface RemotePtRoomService {
     Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) throws Exception;
 
     Boolean batchSet(RemotePtRoomBatchSetBo bo);
+
+    RemotePtRoomVo selectVoById(Long id) ;
 }

+ 54 - 0
ruoyi-api/ruoyi-api-backstage/src/main/java/org/dromara/backstage/api/domain/vo/RemoteMealTypeVo.java

@@ -0,0 +1,54 @@
+package org.dromara.backstage.api.domain.vo;
+
+import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
+import com.alibaba.excel.annotation.ExcelProperty;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+
+/**
+ * 营业时段视图对象 t_pt_mealType
+ *
+ * @author Yz
+ * @date 2024-08-05
+ */
+@Data
+@ExcelIgnoreUnannotated
+public class RemoteMealTypeVo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 营业时段id
+     */
+    private Long mealId;
+
+    /**
+     * 餐类Id
+     */
+    private String typeId;
+    /**
+     * 餐类名称
+     */
+    private String mealName;
+
+    /**
+     * 开始时间
+     */
+    private String beginTime;
+
+    /**
+     * 结束时间
+     */
+    private String endTime;
+
+    /**
+     * 类别说明
+     */
+    private String mealNotes;
+
+
+}

+ 67 - 0
ruoyi-api/ruoyi-api-backstage/src/main/java/org/dromara/backstage/api/domain/vo/RemoteOperatorVo.java

@@ -0,0 +1,67 @@
+package org.dromara.backstage.api.domain.vo;
+
+import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.Date;
+
+
+/**
+ * 营业员视图对象 t_pt_operator
+ *
+ * @author bing
+ * @date 2024-08-01
+ */
+@Data
+@ExcelIgnoreUnannotated
+public class RemoteOperatorVo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 营业员Id,主键
+     */
+    private Long operatorId;
+
+    /**
+     * 所属结算账户Id
+     */
+    private Long accountId;
+
+    private String accountName;
+
+    /**
+     * 营业员名称
+     */
+    private String operatorName;
+
+    /**
+     * 卡流水号,默认已有最大流水号+1
+     */
+    private Long cardNumber;
+
+    /**
+     * 物理卡号,卡片出厂的唯一id
+     */
+    private String factoryId;
+
+    /**
+     * 营业员卡密码,会和硬件对接
+     */
+    private String cardPwd;
+
+    /**
+     * 营业员卡有效期,会和硬件对接
+     */
+    private Date lifespan;
+
+    /**
+     * 备注
+     */
+    private String remark;
+
+
+}

+ 72 - 0
ruoyi-api/ruoyi-api-backstage/src/main/java/org/dromara/backstage/api/domain/vo/RemotePtAccountVo.java

@@ -0,0 +1,72 @@
+package org.dromara.backstage.api.domain.vo;
+
+import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.Date;
+
+
+/**
+ * 结算账户视图对象 t_pt_account
+ *
+ * @author bing
+ * @date 2024-07-31
+ */
+@Data
+@ExcelIgnoreUnannotated
+public class RemotePtAccountVo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    private Long accountId;
+
+    /**
+     * 结算账户名称
+     */
+    private String accountName;
+
+    /**
+     * 结算账户代码
+     */
+    private String accountCode;
+
+    /**
+     * 联系电话
+     */
+    private String telephone;
+
+    /**
+     * 银行卡号
+     */
+    private String bankNumber;
+
+    /**
+     * 账户状态,见sys_normal_disable字典类别
+     */
+    private String status;
+
+    /**
+     * 管理员姓名
+     */
+    private String adminName;
+
+    /**
+     * 证件编号
+     */
+    private String idNumber;
+
+    /**
+     * 备注
+     */
+    private String remark;
+
+    /**
+     * 创建时间
+     */
+    private Date createTime;
+
+
+}

+ 79 - 0
ruoyi-api/ruoyi-api-backstage/src/main/java/org/dromara/backstage/api/domain/vo/RemotePtRoomVo.java

@@ -0,0 +1,79 @@
+package org.dromara.backstage.api.domain.vo;
+
+import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+
+/**
+ * 房间定义视图对象 t_pt_room
+ *
+ * @author bing
+ * @date 2024-08-09
+ */
+@Data
+@ExcelIgnoreUnannotated
+public class RemotePtRoomVo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 房间Id,主键
+     */
+    private Long roomId;
+
+    /**
+     * 所属区域id
+     */
+//    @ExcelProperty(value = "所属区域id")
+    private Long areaId;
+
+    private String areaName;
+
+    /**
+     * 房间编码
+     */
+    private String roomCode;
+
+    /**
+     * 房间名称
+     */
+    private String roomName;
+
+    /**
+     * 房间类型,见FJLX字典类型
+     */
+    private String roomType;
+
+    /**
+     * 门牌号1
+     */
+    private String codeOne;
+
+    /**
+     * 门牌号2
+     */
+    private String codeTwo;
+
+    /**
+     * 门牌号3
+     */
+    private String codeThree;
+
+    /**
+     * 门牌号4
+     */
+    private String codeFour;
+
+    /**
+     * 门牌号5
+     */
+    private String codeFive;
+
+
+
+
+}

+ 30 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/constant/ApiErrorTypeConstants.java

@@ -0,0 +1,30 @@
+package org.dromara.common.core.constant;
+
+/**
+ * API接口调用错误类型枚举
+ */
+public interface ApiErrorTypeConstants {
+    String EXCEPTION = "EXCEPTION";
+     String BAD_REQUEST = "BAD_REQUEST";
+     String NOT_FOUND = "NOT_FOUND";
+     String PARAM_ERROR = "PARAM_ERROR";
+     String CONSUME_CHECK_FAIL = "CONSUME_CHECK_FAIL";
+     String CONSUME_FAIL = "CONSUME_FAIL";
+     String REFUND_FAIL = "REFUND_FAIL";
+     String CONDITION_DISSATISFY = "CONDITION_DISSATISFY";
+     String INVALID_SIGN = "INVALID_SIGN";
+     String INVALID_TOKEN = "INVALID_TOKEN";
+     String INVALID_MAC = "INVALID_MAC";
+     String AUTHENTICATION_FAIL = "AUTHENTICATION_FAIL";
+     String SQLDB_COMMIT_FAIL = "SQLDB_COMMIT_FAIL";
+     String REMOTE_OPEN_DOOR_FAIL = "REMOTE_OPEN_DOOR_FAIL";
+     String RECORD_IS_EXISTS = "RECORD_IS_EXISTS";
+     String RECORD_IS_DEALED = "RECORD_IS_DEALED";
+     String OBJECT_NOT_EXISTS = "OBJECT_NOT_EXISTS";
+     String CARD_NOT_EXISTS = "CARD_NOT_EXISTS";
+     String CARD_BAGS_NOT_EXISTS = "CARD_BAGS_NOT_EXISTS";
+     String CARD_STATUS_NOT_NORMAL = "CARD_STATUS_NOT_NORMAL";
+     String CREDIT_BACK_ID_NOT_EXISTS = "CREDIT_BACK_ID_NOT_EXISTS";
+     String DEAL_NOT_EXISTS = "DEAL_NOT_EXISTS";
+     String OPERATION_TOO_FREQUENT = "OPERATION_TOO_FREQUENT";
+}

+ 50 - 0
ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/enums/TradeStatusEnum.java

@@ -0,0 +1,50 @@
+package org.dromara.common.core.enums;
+
+import lombok.Getter;
+
+/**
+ * @ClassName TradeStatusEnum
+ * @Description 消费交易状态枚举
+ * @Author luoyibo
+ * @Date 2024-11-06 10:55
+ * @Version 1.0
+ * @since jdk17
+ */
+@Getter
+public enum TradeStatusEnum {
+    ReqSuccess("成功", (byte)0x00),
+    InitCardFail("初始化卡片失败", (byte)0x04),
+    ReqTrade("应答", (byte)0x51),
+    Blacklist("黑名单卡", (byte)0x52),
+    NOMoney("余额不足", (byte)0x53),
+    DayLimitTimes("日限次", (byte)0x54),
+    DayLimitMoney("日限额", (byte)0x55),
+    TimeInterval("消费间隔时间", (byte)0x56),
+    OnceBigMoney("单次消费限额", (byte)0x57),
+    InvalidCard("非本系统卡", (byte)0x58),
+    MealLimitTimes("餐限次", (byte)0x59),
+    CardValidDate("超过卡片有效期", (byte)0x5A),
+    CardTypeLimit("卡类限制", (byte)0x5B),
+    NoByCardDeal("副卡不允许在线交易", (byte)0x5C),
+    MealLimitMoney("餐限额", (byte)0x60),
+    NotInTradeTime("不在交易时间段", (byte)0x61),
+    NonScanCode("没有扫码数据", (byte)0x62),
+    SysError("系统错误", (byte)0x63),
+    MidasApiError("米大师接口调用失败", (byte)0x64),
+    NoDish("无订餐数据", (byte)0x70);
+
+    private String name; // 名称
+    private Byte value; // 值
+
+    /**
+     * 私有构造,防止被外部调用
+     *
+     */
+    TradeStatusEnum(){}
+
+    TradeStatusEnum(String name, Byte value) {
+        this.name = name;
+        this.value = value;
+    }
+
+}

+ 1 - 0
ruoyi-common/ruoyi-common-message/src/main/java/org/dromara/common/message/kafka/constant/MessageEventTypeConstants.java

@@ -137,6 +137,7 @@ public class MessageEventTypeConstants {
     public static final String	ptCard_CARD_UNLOCK = "YKT_109_UNLOCK";
     /** 补助设置新增 */
     public static final String	SUBSIDY_ADD = "YKT_102_ADD";
+
     /** 补助设置修改 */
     public static final String	SUBSIDY_EDIT = "YKT_102_EDIT";
     /** 补助设置删除 */

+ 19 - 0
ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/basics/domain/convert/RemoteMealTypeVoConvert.java

@@ -0,0 +1,19 @@
+package org.dromara.backstage.basics.domain.convert;
+
+import io.github.linpeilie.BaseMapper;
+import org.dromara.backstage.api.domain.bo.RemotePtSchoolBo;
+import org.dromara.backstage.api.domain.vo.RemoteMealTypeVo;
+import org.dromara.backstage.basics.domain.bo.PtSchoolBo;
+import org.dromara.backstage.basics.domain.vo.PtMealTypeVo;
+import org.mapstruct.Mapper;
+import org.mapstruct.MappingConstants;
+import org.mapstruct.ReportingPolicy;
+
+/**
+ * 餐类信息转换器
+ * @author bing
+ */
+@Mapper(componentModel = MappingConstants.ComponentModel.SPRING, unmappedTargetPolicy = ReportingPolicy.IGNORE)
+public interface RemoteMealTypeVoConvert extends BaseMapper<PtMealTypeVo, RemoteMealTypeVo> {
+
+}

+ 19 - 0
ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/basics/domain/convert/RemoteOperatorVoConvert.java

@@ -0,0 +1,19 @@
+package org.dromara.backstage.basics.domain.convert;
+
+import io.github.linpeilie.BaseMapper;
+import org.dromara.backstage.api.domain.vo.RemoteMealTypeVo;
+import org.dromara.backstage.api.domain.vo.RemoteOperatorVo;
+import org.dromara.backstage.basics.domain.vo.PtMealTypeVo;
+import org.dromara.backstage.basics.domain.vo.PtOperatorVo;
+import org.mapstruct.Mapper;
+import org.mapstruct.MappingConstants;
+import org.mapstruct.ReportingPolicy;
+
+/**
+ * 餐类信息转换器
+ * @author bing
+ */
+@Mapper(componentModel = MappingConstants.ComponentModel.SPRING, unmappedTargetPolicy = ReportingPolicy.IGNORE)
+public interface RemoteOperatorVoConvert extends BaseMapper<PtOperatorVo, RemoteOperatorVo> {
+
+}

+ 17 - 0
ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/basics/domain/convert/RemotePtAccountVoConvert.java

@@ -0,0 +1,17 @@
+package org.dromara.backstage.basics.domain.convert;
+
+import io.github.linpeilie.BaseMapper;
+import org.dromara.backstage.api.domain.vo.RemotePtAccountVo;
+import org.dromara.backstage.basics.domain.vo.PtAccountVo;
+import org.mapstruct.Mapper;
+import org.mapstruct.MappingConstants;
+import org.mapstruct.ReportingPolicy;
+
+/**
+ * 餐类信息转换器
+ * @author bing
+ */
+@Mapper(componentModel = MappingConstants.ComponentModel.SPRING, unmappedTargetPolicy = ReportingPolicy.IGNORE)
+public interface RemotePtAccountVoConvert extends BaseMapper<PtAccountVo, RemotePtAccountVo> {
+
+}

+ 17 - 0
ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/basics/domain/convert/RemotePtRoomVoConvert.java

@@ -0,0 +1,17 @@
+package org.dromara.backstage.basics.domain.convert;
+
+import io.github.linpeilie.BaseMapper;
+import org.dromara.backstage.api.domain.vo.RemotePtRoomVo;
+import org.dromara.backstage.basics.domain.vo.PtRoomVo;
+import org.mapstruct.Mapper;
+import org.mapstruct.MappingConstants;
+import org.mapstruct.ReportingPolicy;
+
+/**
+ * 餐类信息转换器
+ * @author bing
+ */
+@Mapper(componentModel = MappingConstants.ComponentModel.SPRING, unmappedTargetPolicy = ReportingPolicy.IGNORE)
+public interface RemotePtRoomVoConvert extends BaseMapper<PtRoomVo, RemotePtRoomVo> {
+
+}

+ 20 - 7
ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/basics/dubbo/RemoteMealTypeServiceImpl.java

@@ -2,41 +2,54 @@ package org.dromara.backstage.basics.dubbo;
 
 
 import cn.hutool.core.bean.BeanUtil;
+import cn.hutool.core.date.DateUtil;
 import lombok.RequiredArgsConstructor;
 import org.apache.dubbo.config.annotation.DubboService;
 import org.dromara.backstage.api.RemoteMealTypeService;
 import org.dromara.backstage.api.domain.bo.RemotePtMealTypeBo;
+import org.dromara.backstage.api.domain.vo.RemoteMealTypeVo;
 import org.dromara.backstage.basics.domain.bo.PtMealTypeBo;
+import org.dromara.backstage.basics.domain.vo.PtMealTypeVo;
 import org.dromara.backstage.basics.service.IPtMealtypeService;
+import org.dromara.common.core.utils.MapstructUtils;
 import org.springframework.stereotype.Service;
 
 import java.util.Collection;
+import java.util.Date;
 
 @RequiredArgsConstructor
 @Service
 @DubboService
 public class RemoteMealTypeServiceImpl implements RemoteMealTypeService {
 
-    private final IPtMealtypeService mealtypeService;
+    private final IPtMealtypeService mealTypeService;
     @Override
     public String selectMealTypeNameByIds(String ids) {
-        return mealtypeService.selectMealTypeNameByIds(ids);
+        return mealTypeService.selectMealTypeNameByIds(ids);
     }
 
     @Override
     public Boolean insertByBo(RemotePtMealTypeBo bo) throws Exception {
-        return mealtypeService.insertByBo(BeanUtil.toBean(bo, PtMealTypeBo.class));
+        return mealTypeService.insertByBo(BeanUtil.toBean(bo, PtMealTypeBo.class));
     }
 
     @Override
     public Boolean updateByBo(RemotePtMealTypeBo bo) throws Exception {
-        return mealtypeService.updateByBo(BeanUtil.toBean(bo, PtMealTypeBo.class));
+        return mealTypeService.updateByBo(BeanUtil.toBean(bo, PtMealTypeBo.class));
     }
 
     @Override
     public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) throws Exception {
-        return mealtypeService.deleteWithValidByIds(ids,isValid);
+        return mealTypeService.deleteWithValidByIds(ids,isValid);
+    }
+    /**
+     * 根据就餐获取对应的餐类信息
+     * @param mealTime 就餐时间
+     * @return 餐类信息
+     */
+    @Override
+    public RemoteMealTypeVo queryMealTypeVoByTime(Date mealTime) {
+        PtMealTypeVo vo = mealTypeService.queryVoByTime(DateUtil.format(mealTime, "HH:mm:ss"));
+        return MapstructUtils.convert(vo, RemoteMealTypeVo.class);
     }
-
-
 }

+ 35 - 0
ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/basics/dubbo/RemoteOperatorServiceImpl.java

@@ -0,0 +1,35 @@
+package org.dromara.backstage.basics.dubbo;
+
+
+import cn.hutool.core.bean.BeanUtil;
+import cn.hutool.core.date.DateUtil;
+import lombok.RequiredArgsConstructor;
+import org.apache.dubbo.config.annotation.DubboService;
+import org.dromara.backstage.api.RemoteMealTypeService;
+import org.dromara.backstage.api.RemoteOperatorService;
+import org.dromara.backstage.api.domain.bo.RemotePtMealTypeBo;
+import org.dromara.backstage.api.domain.vo.RemoteMealTypeVo;
+import org.dromara.backstage.api.domain.vo.RemoteOperatorVo;
+import org.dromara.backstage.basics.domain.bo.PtMealTypeBo;
+import org.dromara.backstage.basics.domain.vo.PtMealTypeVo;
+import org.dromara.backstage.basics.domain.vo.PtOperatorVo;
+import org.dromara.backstage.basics.service.IPtMealtypeService;
+import org.dromara.backstage.basics.service.IPtOperatorService;
+import org.dromara.common.core.utils.MapstructUtils;
+import org.springframework.stereotype.Service;
+
+import java.util.Collection;
+import java.util.Date;
+
+@RequiredArgsConstructor
+@Service
+@DubboService
+public class RemoteOperatorServiceImpl implements RemoteOperatorService {
+    private final IPtOperatorService ptOperatorService;
+
+    @Override
+    public RemoteOperatorVo getVoById(Long operatorId) {
+        PtOperatorVo vo = ptOperatorService.queryById(operatorId);
+        return MapstructUtils.convert(vo, RemoteOperatorVo.class);
+    }
+}

+ 9 - 0
ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/basics/dubbo/RemotePtAccountServiceImpl.java

@@ -5,8 +5,11 @@ import lombok.RequiredArgsConstructor;
 import org.apache.dubbo.config.annotation.DubboService;
 import org.dromara.backstage.api.RemotePtAccountService;
 import org.dromara.backstage.api.domain.bo.RemotePtAccountBo;
+import org.dromara.backstage.api.domain.vo.RemotePtAccountVo;
 import org.dromara.backstage.basics.domain.bo.PtAccountBo;
+import org.dromara.backstage.basics.domain.vo.PtAccountVo;
 import org.dromara.backstage.basics.service.IPtAccountService;
+import org.dromara.common.core.utils.MapstructUtils;
 import org.springframework.stereotype.Service;
 
 import java.util.Collection;
@@ -37,4 +40,10 @@ public class RemotePtAccountServiceImpl implements RemotePtAccountService {
     public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) throws Exception {
         return accountService.deleteWithValidByIds(ids, isValid);
     }
+
+    @Override
+    public RemotePtAccountVo selectVoById(Long id) {
+        PtAccountVo vo = accountService.queryById(id);
+        return MapstructUtils.convert(vo,RemotePtAccountVo.class);
+    }
 }

+ 13 - 5
ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/basics/dubbo/RemotePtRoomServiceImpl.java

@@ -6,9 +6,12 @@ import org.apache.dubbo.config.annotation.DubboService;
 import org.dromara.backstage.api.RemotePtRoomService;
 import org.dromara.backstage.api.domain.bo.RemotePtRoomBatchSetBo;
 import org.dromara.backstage.api.domain.bo.RemotePtRoomBo;
+import org.dromara.backstage.api.domain.vo.RemotePtRoomVo;
 import org.dromara.backstage.basics.domain.bo.PtRoomBatchSetBo;
 import org.dromara.backstage.basics.domain.bo.PtRoomBo;
+import org.dromara.backstage.basics.domain.vo.PtRoomVo;
 import org.dromara.backstage.basics.service.IPtRoomService;
+import org.dromara.common.core.utils.MapstructUtils;
 import org.springframework.stereotype.Service;
 
 import java.util.Collection;
@@ -21,25 +24,30 @@ import java.util.Collection;
 @DubboService
 public class RemotePtRoomServiceImpl implements RemotePtRoomService {
 
-    private final  IPtRoomService iPtRoomService;
+    private final  IPtRoomService roomService;
     @Override
     public Boolean insertByBo(RemotePtRoomBo bo) throws Exception {
-        return iPtRoomService.insertByBo(BeanUtil.toBean(bo, PtRoomBo.class));
+        return roomService.insertByBo(BeanUtil.toBean(bo, PtRoomBo.class));
     }
 
     @Override
     public Boolean updateByBo(RemotePtRoomBo bo) throws Exception {
-        return iPtRoomService.updateByBo(BeanUtil.toBean(bo, PtRoomBo.class));
+        return roomService.updateByBo(BeanUtil.toBean(bo, PtRoomBo.class));
     }
 
     @Override
     public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) throws Exception {
-        return iPtRoomService.deleteWithValidByIds(ids,isValid);
+        return roomService.deleteWithValidByIds(ids,isValid);
     }
 
     @Override
     public Boolean batchSet(RemotePtRoomBatchSetBo bo) {
-        return iPtRoomService.batchSet(BeanUtil.toBean(bo, PtRoomBatchSetBo.class));
+        return roomService.batchSet(BeanUtil.toBean(bo, PtRoomBatchSetBo.class));
     }
 
+    @Override
+    public RemotePtRoomVo selectVoById(Long id) {
+        PtRoomVo vo = roomService.queryById(id);
+        return MapstructUtils.convert(vo,RemotePtRoomVo.class);
+    }
 }

+ 0 - 1
ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/basics/service/impl/PtRoomServiceImpl.java

@@ -1,6 +1,5 @@
 package org.dromara.backstage.basics.service.impl;
 
-import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.collection.CollectionUtil;
 import cn.hutool.core.map.MapUtil;
 import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;

+ 1 - 0
ruoyi-modules/ruoyi-backstage/src/main/java/org/dromara/backstage/business/payments/PayOrderBusiness.java

@@ -235,6 +235,7 @@ public class PayOrderBusiness {
             return R.fail(e.getMessage(),boCopy);
         }
     }
+
     /**
      * 将要入账的补助信息转成收支订单业务对象
      * @param item 补助信息

+ 6 - 1
ruoyi-server/ruoyi-server-common/src/main/java/org/dromara/server/common/constant/DefaultConstants.java

@@ -43,9 +43,14 @@ public interface DefaultConstants {
     String DATE_FORMAT = "yyyy-MM-dd";
 
     /**
-     * 时间格式
+     * 日期时间格式
      */
     String DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
+    /**
+     * 时间格式
+     */
+    String TIME_FORMAT = "HH:mm:ss";
+
     /**
      * 消费机接收的时间格式
      */

+ 7 - 1
ruoyi-server/ruoyi-server-common/src/main/java/org/dromara/server/common/domain/consume/bo/ConsumptionBo.java

@@ -81,8 +81,9 @@ public class ConsumptionBo {
      * 餐类Id
      */
     private Long mealType;
+
     /**
-     * 原始记录Id
+     * 消费明细记录Id
      */
     private String consumeId;
 
@@ -126,6 +127,11 @@ public class ConsumptionBo {
      * 防伪验证码(记录消费模式的中文信息)
      */
     private String digitalSign;
+
+    /**
+     * 原始消费记录Id
+     */
+    private String originalId;
     //endregion
 
     //region 错扣补款属性

+ 26 - 0
ruoyi-server/ruoyi-server-common/src/main/java/org/dromara/server/common/util/CardDateUtils.java

@@ -0,0 +1,26 @@
+package org.dromara.server.common.util;
+
+import java.text.DateFormat;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+
+public class CardDateUtils {
+    static ThreadLocal<DateFormat> YYYYMMDD_SHORT = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyyMMdd", Locale.CHINA));
+    static ThreadLocal<DateFormat> YYYYMMDD_LONG = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd", Locale.CHINA));
+    static ThreadLocal<DateFormat> YYYYMMDDHHMMSS_SHORT = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyyMMddHHmmss", Locale.CHINA));
+    static ThreadLocal<DateFormat> YYYYMMDDHHMMSS_LONG = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.CHINA));
+    static ThreadLocal<DateFormat> YYYYMMDDHHMMSS_LONG_T = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.CHINA));
+
+    public static Date parseDateTime(String str) {
+        try {
+            if (str.contains("T")) {
+                return YYYYMMDDHHMMSS_LONG_T.get().parse(str);
+            }
+            return YYYYMMDDHHMMSS_LONG.get().parse(str);
+        } catch (ParseException e) {
+            return null;
+        }
+    }
+}

+ 189 - 5
ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/business/BaseBusiness.java

@@ -1,20 +1,31 @@
 package org.dromara.server.consume.business;
 
 import cn.hutool.core.bean.BeanUtil;
+import cn.hutool.core.date.DateUtil;
 import cn.hutool.core.util.ObjUtil;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
+import org.dromara.backstage.api.domain.vo.RemoteCardVo;
+import org.dromara.backstage.api.domain.vo.RemoteMealTypeVo;
 import org.dromara.backstage.api.domain.vo.RemoteUserAccountVo;
+import org.dromara.common.core.constant.ApiErrorTypeConstants;
 import org.dromara.common.core.domain.R;
 import org.dromara.common.core.domain.model.ErrorInfo;
+import org.dromara.common.core.enums.BalanceUpdateEnum;
+import org.dromara.common.core.exception.consume.ConsumeException;
 import org.dromara.common.core.utils.RecordIdUtils;
+import org.dromara.server.common.constant.DefaultConstants;
 import org.dromara.server.common.domain.consume.bo.ConsumptionBo;
-import org.dromara.server.consume.domain.bo.XfConsumeDetailOriginalBo;
-import org.dromara.server.consume.domain.vo.XfConsumeDetailOriginalVo;
-import org.dromara.server.consume.service.IXfConsumeDetailOriginalService;
+import org.dromara.server.consume.domain.bo.*;
+import org.dromara.server.consume.domain.vo.*;
+import org.dromara.server.consume.service.*;
 import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
 
+import java.util.ArrayList;
 import java.util.Date;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
 
 /**
  * name: BaseBusiness
@@ -32,6 +43,10 @@ import java.util.Date;
 @RequiredArgsConstructor
 public class BaseBusiness {
     private final IXfConsumeDetailOriginalService originalService;
+    private final IXfUserTotalService userTotalService;
+    private final IXfTermTotalService termTotalService;
+    private final IXfConsumeDetailService consumeDetailService;
+    private final IPtBagService bagService;
 
     /**
      * 生成原始消费记录
@@ -41,8 +56,11 @@ public class BaseBusiness {
      * @param originalVo 原始记录
      * @return 生成结果
      */
-    public R<ErrorInfo> createOriginalOrder(ConsumptionBo consumeBo, RemoteUserAccountVo accountVo, XfConsumeDetailOriginalVo originalVo) {
-        String originalId = RecordIdUtils.getRecordId(new Date(), consumeBo.getTermNo().shortValue(), consumeBo.getTermRecordId().shortValue(), accountVo.getUserNo().intValue(), 0);
+    public R<ErrorInfo> createOriginalOrder(ConsumptionBo consumeBo, RemoteUserAccountVo accountVo,
+                                            XfConsumeDetailOriginalVo originalVo) {
+        String originalId = RecordIdUtils.getRecordId(consumeBo.getConsumeDate(), consumeBo.getTermNo().shortValue(),
+                                                      consumeBo.getTermRecordId().shortValue(),
+                                                      accountVo.getUserNo().intValue(), 0);
 
         XfConsumeDetailOriginalBo originalBo = new XfConsumeDetailOriginalBo();
         BeanUtil.copyProperties(consumeBo, originalBo);
@@ -61,4 +79,170 @@ public class BaseBusiness {
         }
         return R.fail();
     }
+
+    @Transactional(rollbackFor = ConsumeException.class)
+    public R<ErrorInfo> postConsumeRecord(ConsumptionBo bo, RemoteUserAccountVo userAccountVo, RemoteCardVo cardVo,
+                                          List<PtBagVo> bagVos, XfTermVo termVo, RemoteMealTypeVo mealTypeVo,
+                                          String remark) {
+        ErrorInfo errorInfo;
+        // 1.入消费明细表,根据消费金额与扣款方式及扣款钱包的余额,可能会从多个钱包扣钱,则对应的有多笔消费明细记录
+        AtomicReference<Boolean> result = new AtomicReference<>();
+        List<XfConsumeDetailVo> detailVos = new ArrayList<>();
+        for (PtBagVo bagVo : bagVos) {
+            XfConsumeDetailVo vo = createConsumeRecord(bo, userAccountVo, cardVo, bagVo, termVo, mealTypeVo, remark);
+            // 多钱包扣费时,只要有一个钱包入消费明细表失败,则都失败
+            if (ObjUtil.isEmpty(vo)) {
+                result.set(false);
+                break;
+            }
+            result.set(true);
+            detailVos.add(vo);
+        }
+        if (!result.get()) {
+            errorInfo = new ErrorInfo(400, ApiErrorTypeConstants.EXCEPTION, "入消费明细表失败", "");
+            return R.fail(errorInfo);
+        }
+        // 2.更新人员日统计表
+        if (!createOrUpdateUserTotal(bo, userAccountVo, cardVo)) {
+            return R.fail(new ErrorInfo(400, ApiErrorTypeConstants.EXCEPTION, "更新个人日统计表失败", ""));
+        }
+        // 3.更新设备日统计表
+        if (!createOrUpdateTermTotal(bo, termVo, mealTypeVo)) {
+            return R.fail(new ErrorInfo(400, ApiErrorTypeConstants.EXCEPTION, "更新设备日统计表失败", ""));
+        }
+        // 4.更新钱包余额
+        if (!updateBagBalance(bagVos)) {
+            return R.fail(new ErrorInfo(400, ApiErrorTypeConstants.EXCEPTION, "更新钱包余额表失败", ""));
+        }
+        // 5.更新记录入库成功状态
+        return R.ok();
+    }
+
+    /**
+     * 创建或更新人员日统计表
+     *
+     * @param bo        消费业务对象
+     * @param accountVo 人员账户视图
+     * @param cardVo    账户卡片视图
+     * @return 更新后的人员日统计表
+     */
+    public boolean createOrUpdateUserTotal(ConsumptionBo bo, RemoteUserAccountVo accountVo, RemoteCardVo cardVo) {
+        XfUserTotalBo xfUserTotalBo = new XfUserTotalBo();
+        xfUserTotalBo.setUserId(accountVo.getUserId());
+        xfUserTotalBo.setUserNumb(accountVo.getUserNumb());
+        xfUserTotalBo.setRealName(accountVo.getRealName());
+        xfUserTotalBo.setDeptName(accountVo.getDeptName());
+        xfUserTotalBo.setCardNo(cardVo.getCardNo());
+        xfUserTotalBo.setDateDay(DateUtil.format(new Date(), DefaultConstants.DATE_FORMAT));
+        xfUserTotalBo.setUseType(bo.getUseType().code());
+        xfUserTotalBo.setConsumeMoney(bo.getConsumeMoney());
+
+        XfUserTotalVo vo = userTotalService.createOrUpdateUserTotal(xfUserTotalBo);
+        return !ObjUtil.isEmpty(vo);
+    }
+
+    /**
+     * 创建或更新设备日统计
+     *
+     * @param bo         消费业务对象
+     * @param termVo     设备视图
+     * @param mealTypeVo 餐类视图
+     * @return 日统计视图
+     */
+    public boolean createOrUpdateTermTotal(ConsumptionBo bo, XfTermVo termVo, RemoteMealTypeVo mealTypeVo) {
+        XfTermTotalBo termTotalBo = new XfTermTotalBo();
+        BeanUtil.copyProperties(termVo, termTotalBo);
+        termTotalBo.setDateDay(DateUtil.format(new Date(), "yyyy-MM-dd"));
+        termTotalBo.setMealType(Long.valueOf(mealTypeVo.getTypeId()));
+        termTotalBo.setUseType(bo.getUseType().code());
+        switch (bo.getCreditType()) {
+            case TERM_CONSUME:
+                termTotalBo.setMealCount(1L);
+                termTotalBo.setMealAmount(bo.getConsumeMoney());
+                break;
+            case ERROR_FILL:
+                termTotalBo.setErrFillCount(1L);
+                termTotalBo.setErrFillMoney(bo.getOperatorMoney());
+                break;
+        }
+        XfTermTotalVo vo = termTotalService.createOrUpdateTermTotal(termTotalBo);
+        return !ObjUtil.isEmpty(vo);
+    }
+
+    /**
+     * 更新账户钱包余额
+     *
+     * @param bagVos 账户钱包视图
+     * @return 更新后的账户钱包
+     */
+    public boolean updateBagBalance(List<PtBagVo> bagVos) {
+        AtomicReference<Boolean> result = new AtomicReference<>();
+        bagVos.parallelStream().forEach(bagVo -> {
+            PtBagBo bagBo = new PtBagBo();
+            bagBo.setUserId(bagVo.getUserId());
+            bagBo.setBagId(bagVo.getBagId());
+            bagBo.setBagCode(bagVo.getBagCode());
+            bagBo.setReceiptMoney(bagVo.getReceiptMoney());
+            bagBo.setOperationMode(BalanceUpdateEnum.CONSUME);
+            PtBagVo vo = bagService.updateBalanceByBo(bagBo);
+            // 多钱包更新余额时,只要有一个钱包更新余额失败,则都失败
+            if (ObjUtil.isEmpty(vo)) {
+                result.set(false);
+            }
+            result.set(true);
+        });
+        return result.get();
+    }
+
+    /**
+     * 创建一条消费明细记录
+     *
+     * @param bo            消费业务对象
+     * @param userAccountVo 账户对象视图
+     * @param cardVo        账户卡片视图
+     * @param bagVo         账户钱包视图
+     * @param termVo        消费设备视图
+     * @param mealTypeVo    消费餐类视图
+     * @return 消费明细视图
+     */
+    private XfConsumeDetailVo createConsumeRecord(ConsumptionBo bo, RemoteUserAccountVo userAccountVo,
+                                                  RemoteCardVo cardVo, PtBagVo bagVo, XfTermVo termVo,
+                                                  RemoteMealTypeVo mealTypeVo, String remark) {
+        String recordId = RecordIdUtils.getRecordId(new Date(), Short.parseShort(bo.getTermNo().toString()),
+                                                    bo.getTermRecordId().intValue(),
+                                                    userAccountVo.getUserNo().intValue(),
+                                                    Integer.parseInt(bagVo.getBagCode()));
+
+        XfConsumeDetailBo consumeDetailBo = new XfConsumeDetailBo();
+        BeanUtil.copyProperties(bo, consumeDetailBo);
+        consumeDetailBo.setConsumeId(recordId);
+        // 设置消费账户信息
+        BeanUtil.copyProperties(userAccountVo, consumeDetailBo);
+        // 设置消费信息
+        consumeDetailBo.setConsumeDate(bo.getConsumeDate());
+        consumeDetailBo.setConsumeMoney(bo.getConsumeMoney());
+        consumeDetailBo.setConsumeBalance(bagVo.getBalance());
+        consumeDetailBo.setCardValue(bagVo.getBalance());
+        // 设置卡片信息
+        consumeDetailBo.setCardNo(cardVo.getCardNo());
+        consumeDetailBo.setFactoryId(cardVo.getFactoryId());
+        consumeDetailBo.setCardValue(bo.getBalance());
+        // 设置设备信息
+        BeanUtil.copyProperties(termVo, consumeDetailBo);
+        // 设置操作员信息
+        consumeDetailBo.setOperatorId(bo.getOperatorId());
+        consumeDetailBo.setOperatorName(bo.getOperatorName());
+        // 设置餐类信息
+        consumeDetailBo.setMealType(Long.valueOf(mealTypeVo.getTypeId()));
+        consumeDetailBo.setMealName(mealTypeVo.getMealName());
+        // 设置钱包信息
+        consumeDetailBo.setBagType(bagVo.getBagCode());
+        consumeDetailBo.setStatusFlag(bo.getStatusFlag().longValue());
+        // 该字段为补款记录对应的消费明细Id,以便追查消费记录是否有补扣以及对应的补扣记录
+        consumeDetailBo.setDetailId(bo.getConsumeId());
+        consumeDetailBo.setRemark(remark);
+
+        return consumeDetailService.createConsumeDetailRecord(consumeDetailBo);
+    }
+
 }

+ 279 - 58
ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/business/CheckBusiness.java

@@ -3,22 +3,40 @@ package org.dromara.server.consume.business;
 import cn.hutool.core.bean.BeanUtil;
 import cn.hutool.core.util.ObjectUtil;
 import cn.hutool.core.util.StrUtil;
+import cn.hutool.json.JSONUtil;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.dubbo.config.annotation.DubboReference;
 import org.dromara.backstage.api.RemoteCardService;
+import org.dromara.backstage.api.RemoteMealTypeService;
+import org.dromara.backstage.api.RemoteOperatorService;
 import org.dromara.backstage.api.RemoteUserAccountService;
 import org.dromara.backstage.api.domain.vo.RemoteCardVo;
+import org.dromara.backstage.api.domain.vo.RemoteMealTypeVo;
+import org.dromara.backstage.api.domain.vo.RemoteOperatorVo;
 import org.dromara.backstage.api.domain.vo.RemoteUserAccountVo;
+import org.dromara.common.core.constant.ApiErrorTypeConstants;
 import org.dromara.common.core.domain.R;
 import org.dromara.common.core.domain.model.ErrorInfo;
+import org.dromara.common.core.enums.TradeStatusEnum;
+import org.dromara.common.core.enums.UserAccountStatusEnum;
+import org.dromara.common.core.utils.RecordIdUtils;
 import org.dromara.server.common.domain.consume.bo.ConsumptionBo;
+import org.dromara.server.consume.domain.vo.PtBagVo;
+import org.dromara.server.consume.domain.vo.XfConsumeDetailOriginalVo;
+import org.dromara.server.consume.domain.vo.XfConsumeDetailVo;
 import org.dromara.server.consume.domain.vo.XfTermVo;
+import org.dromara.server.consume.service.IPtBagService;
+import org.dromara.server.consume.service.IXfConsumeDetailOriginalService;
+import org.dromara.server.consume.service.IXfConsumeDetailService;
 import org.dromara.server.consume.service.IXfTermService;
 import org.jetbrains.annotations.NotNull;
 import org.springframework.stereotype.Service;
 
+import java.math.BigDecimal;
 import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.List;
 import java.util.Objects;
 
 /**
@@ -39,8 +57,15 @@ public class CheckBusiness {
     private final RemoteCardService remoteCardService;
     @DubboReference
     private final RemoteUserAccountService remoteUserAccountService;
+    @DubboReference
+    private final RemoteMealTypeService remoteMealTypeService;
+    @DubboReference
+    private final RemoteOperatorService remoteOperatorService;
 
     private final IXfTermService termService;
+    private final IXfConsumeDetailOriginalService consumeDetailOriginalService;
+    private final IXfConsumeDetailService consumeDetailService;
+    private final IPtBagService bagService;
 
     /**
      * 消费记录参数检检查
@@ -52,82 +77,156 @@ public class CheckBusiness {
      */
     public R<ErrorInfo> checkParam(ConsumptionBo bo) {
         ErrorInfo errorInfo;
-        //检查设备机号
+        // 检查设备机号
         if (ObjectUtil.isEmpty(bo.getTermNo()) || bo.getTermNo() == 0) {
             errorInfo = new ErrorInfo(1, "", "设备机号不正确", "设备机号必须大于零!");
             return R.fail(errorInfo);
         }
-        //检查交易人员标识
+        // 检查交易人员标识
         if (bo.getCardNo() <= 0 && bo.getFactoryId() == 0 && bo.getUserNo() <= 0 && StrUtil.isEmpty(bo.getUserNumb())) {
-            errorInfo = new ErrorInfo(1, "", "交易人员标识不满足", "必须提供 [CardNo | FactoryId | userNo | userNumb] 中至少1项来标识交易用户");
+            errorInfo = new ErrorInfo(1, "", "交易人员标识不满足",
+                                      "必须提供 [CardNo | FactoryId | userNo | userNumb] 中至少1项来标识交易用户");
             return R.fail(errorInfo);
         }
         return R.ok();
     }
 
     /**
-     * 消费记录人员、卡片检查,一切通过后获取到消费账户、卡片及设备的视图信息
+     * 消费记录人员、卡片检查,全部通过后获取到消费账户、卡片及设备的视图信息
      *
      * @param bo            消费记录
      * @param userAccountVo 消费账户
-     * @param userCardVo  消费卡片
-     * @param useTermVo 消费设备
+     * @param userCardVo    消费卡片
+     * @param useTermVo     消费设备
      * @return 验证结果
      */
-    public R<ErrorInfo> checkUser(ConsumptionBo bo, RemoteUserAccountVo userAccountVo, RemoteCardVo userCardVo, XfTermVo useTermVo) {
-        long termNo = ObjectUtil.isEmpty(bo.getTermNo())? 0 : bo.getTermNo();
+    public R<ErrorInfo> checkUser(ConsumptionBo bo, RemoteUserAccountVo userAccountVo, RemoteCardVo userCardVo,
+                                  XfTermVo useTermVo) {
+        long termNo = ObjectUtil.isEmpty(bo.getTermNo()) ? 0 : bo.getTermNo();
         long cardNo = ObjectUtil.isEmpty(bo.getCardNo()) ? 0 : bo.getCardNo();
         long factoryId = ObjectUtil.isEmpty(bo.getFactoryId()) ? 0 : bo.getFactoryId();
         long userNo = ObjectUtil.isEmpty(bo.getUserNo()) ? 0 : bo.getUserNo();
         String userNumb = bo.getUserNumb() == null ? null : bo.getUserNumb();
-        XfTermVo termVo = termService.queryVoOneByNo(termNo);
-        if(ObjectUtil.isEmpty(termVo)){
-            ErrorInfo errorInfo = new ErrorInfo(400, "", "设备不存在", MessageFormat.format("设备机号为[{0}]的设备不存在,不允许交易", termNo));
-
-            return R.fail(errorInfo);
+        // 获取设备信息
+        if (termNo > 0) {
+            R<ErrorInfo> result = checkTerm(bo, useTermVo);
+            if (R.isError(result)) {
+                return result;
+            }
         }
-       BeanUtil.copyProperties(termVo,useTermVo);
-        //卡流水号检查
+        // 卡流水号检查
         if (cardNo > 0) {
-            return checkCardNo(bo,userAccountVo ,userCardVo );
+            return checkCardNo(bo, userAccountVo, userCardVo);
         }
-        //物理卡号检查
+        // 物理卡号检查
         if (factoryId > 0) {
-            return checkFactoryId(bo,userAccountVo ,userCardVo);
+            return checkFactoryId(bo, userAccountVo, userCardVo);
         }
-        //用户流水号检查
+        // 用户流水号检查
         if (userNo > 0) {
-            return checkUserNo(bo,userAccountVo ,userCardVo);
+            return checkUserNo(bo, userAccountVo, userCardVo);
         }
-        //人员编号检查
+        // 人员编号检查
         if (StrUtil.isNotEmpty(userNumb)) {
-            return checkUserNumb(bo,userAccountVo ,userCardVo);
+            return checkUserNumb(bo, userAccountVo, userCardVo);
         }
         return R.ok();
     }
 
     /**
-     * 消费逻辑检查,检查是否能消费
-     * @param bo 消费记录
-     * @param userAccountVo 消费账户
-     * @param userCardVo 消费卡片
+     * 消费设备检查
+     *
+     * @param bo        消费记录
      * @param useTermVo 消费设备
-     * @return 验证结果
+     * @return 检查结果
      */
-    public R<ErrorInfo> checkConsume(ConsumptionBo bo, RemoteUserAccountVo userAccountVo, RemoteCardVo userCardVo, XfTermVo useTermVo){
-        // TODO 2024-11-03 14:36:42 luoyibo 1.获取传入的参数
-        // TODO 2024-11-03 14:36:42 luoyibo 2.卡有效性验证
-        // TODO 2024-11-03 14:39:21 luoyibo 3.消费账户状态验证
-        // TODO 2024-11-03 14:39:49 luoyibo 4.餐类验证
-        // TODO 2024-11-03 14:41:28 luoyibo 5.折扣\限额\限次验证
-        // TODO 2024-11-03 14:45:18 luoyibo 6.根据消费机的消费模式验证余额
+    public R<ErrorInfo> checkTerm(ConsumptionBo bo, XfTermVo useTermVo) {
+        long termNo = bo.getTermNo();
+        XfTermVo termVo = termService.queryVoOneByNo(termNo);
+        if (ObjectUtil.isEmpty(termVo)) {
+            ErrorInfo errorInfo = new ErrorInfo(400, "", "设备不存在",
+                                                MessageFormat.format("机号为[{0}]的设备不存在,不允许交易", termNo));
+
+            return R.fail(errorInfo);
+        }
+        BeanUtil.copyProperties(termVo, useTermVo);
         return R.ok();
     }
+
     /**
-     * 验证后设置人员信息
-     * @param bo 消费记录
-     * @param accountVo 人员账户信息
+     * 消费逻辑检查,检查是否能消费
+     *
+     * @param bo            消费记录
+     * @param userAccountVo 消费账户
+     * @param userCardVo    消费卡片
+     * @param useTermVo     消费设备
+     * @return 验证结果
      */
+    public R<ErrorInfo> checkConsume(ConsumptionBo bo, RemoteUserAccountVo userAccountVo, RemoteCardVo userCardVo, XfTermVo useTermVo) {
+        R<ErrorInfo> result;
+        // 1.消费账户状态验证,验证账户是否已开户、是否冻结、状态是否正常、是否过有效期
+        if ("Y".equals(userAccountVo.getFreezeStatus())) {
+            return R.fail(new ErrorInfo(400, ApiErrorTypeConstants.NOT_FOUND, "账户已被冻结", "账户已被冻结"));
+        }
+        if ("N".equals(userAccountVo.getStatus())) {
+            return R.fail(new ErrorInfo(400, ApiErrorTypeConstants.NOT_FOUND, "账户状态不正常", "账户状态不正常"));
+        }
+        if (!ObjectUtil.equal(userAccountVo.getAccountStatus(), UserAccountStatusEnum.IS_OPEN.code().toString())) {
+            return R.fail(new ErrorInfo(400, ApiErrorTypeConstants.NOT_FOUND, "账户尚未开户", "账户尚未开户"));
+        }
+        if (userAccountVo.getLifespan() != null && bo.getConsumeDate().getTime() > userAccountVo.getLifespan().getTime()) {
+            return R.fail(new ErrorInfo(400, ApiErrorTypeConstants.NOT_FOUND, "账户已过有效期", "账户已过有效期"));
+        }
+        // 2.餐类验证
+        RemoteMealTypeVo mealType = remoteMealTypeService.queryMealTypeVoByTime(bo.getConsumeDate());
+        if (ObjectUtil.isEmpty(mealType)) {
+            return R.fail(new ErrorInfo(400, ApiErrorTypeConstants.NOT_FOUND, "不在交易时段", "不在交易时段"));
+        }
+        // 3.折扣\限额\限次验证
+        result = checkLimitDeal(bo, userAccountVo, useTermVo, userCardVo, mealType);
+        if (R.isError(result)) {
+            return R.fail(new ErrorInfo(400, ApiErrorTypeConstants.NOT_FOUND, "消费限制判断存在问题", JSONUtil.toJsonStr(result.getData())));
+        }
+        // 4.根据消费机的消费模式验证余额,如果余额不足则返回
+        List<PtBagVo> bagVos = new ArrayList<>();
+        result = checkDeductionBag(bo, userAccountVo, useTermVo, bagVos);
+        if (R.isError(result)) {
+            return result;
+        }
+        return R.ok();
+    }
+
+    public R<ErrorInfo> checkBill(ConsumptionBo bo, RemoteUserAccountVo userAccountVo, RemoteCardVo userCardVo,
+                                  XfTermVo useTermVo, List<PtBagVo> bagVoList, RemoteMealTypeVo mealTypeVo,
+                                  RemoteOperatorVo operatorVo) {
+        R<ErrorInfo> result = checkCardNo(bo, userAccountVo, userCardVo);
+        if (R.isError(result)) {
+            return result;
+        }
+        result = checkOriginalRecord(bo);
+        if (R.isError(result)) {
+            return result;
+        }
+        // 获取餐类信息
+        RemoteMealTypeVo mealType = remoteMealTypeService.queryMealTypeVoByTime(bo.getConsumeDate());
+        if (ObjectUtil.isEmpty(mealType)) {
+            mealType.setTypeId("0");
+            mealType.setMealName("未知");
+        }
+        BeanUtil.copyProperties(mealType, mealTypeVo);
+        // 获取扣费钱包
+        List<PtBagVo> bagVos = new ArrayList<>();
+        result = checkDeductionBag(bo, userAccountVo, useTermVo, bagVos);
+        if (R.isError(result)) {
+            return result;
+        }
+        bagVoList.addAll(bagVos);
+        // 获取营业员信息
+        BeanUtil.copyProperties(getOperatorVo(bo), operatorVo);
+
+        return R.ok();
+    }
+
     private void setUserInfo(ConsumptionBo bo, RemoteUserAccountVo accountVo) {
         bo.setRealName(StrUtil.isEmpty(accountVo.getRealName()) ? "----" : accountVo.getRealName());
         bo.setUserNo(accountVo.getUserNo());
@@ -140,25 +239,29 @@ public class CheckBusiness {
         ErrorInfo errorInfo;
         RemoteCardVo cardVo = remoteCardService.queryCardByCardNo(cardNo);
         if (ObjectUtil.isEmpty(cardVo)) {
-            errorInfo = new ErrorInfo(400, "", "卡片不存在", MessageFormat.format("流水号为[{0}]的卡片不存在,不允许交易", cardNo));
+            errorInfo = new ErrorInfo(400, ApiErrorTypeConstants.PARAM_ERROR, "卡片不存在",
+                                      MessageFormat.format("流水号为[{0}]的卡片不存在,不允许交易", cardNo));
 
             return R.fail(errorInfo);
         }
         if (!"1".equals(cardVo.getStatus())) {
-            errorInfo = new ErrorInfo(400, "", "卡片状态不正确", MessageFormat.format("流水号为[{0}]的卡片状态不正确,不允许交易", cardNo));
+            errorInfo = new ErrorInfo(400, ApiErrorTypeConstants.PARAM_ERROR, "卡片状态不正确",
+                                      MessageFormat.format("流水号为[{0}]的卡片状态不正确,不允许交易", cardNo));
 
             return R.fail(errorInfo);
         }
-        if (!Objects.equals(cardVo.getFactoryId(), bo.getFactoryId())) {
-            errorInfo = new ErrorInfo(400, "", "卡片不正确", "物理卡不一致,不允许交易");
+        if (bo.getFactoryId() > 0) {
+            if (!Objects.equals(cardVo.getFactoryId(), bo.getFactoryId())) {
+                errorInfo = new ErrorInfo(400, ApiErrorTypeConstants.PARAM_ERROR, "卡片不正确", "物理卡号不一致,不允许交易");
 
-            return R.fail(errorInfo);
+                return R.fail(errorInfo);
+            }
         }
         RemoteUserAccountVo accountVo = remoteUserAccountService.getUserAccountVoById(cardVo.getUserId());
         setUserInfo(bo, accountVo);
         bo.setFactoryId(cardVo.getFactoryId());
-        BeanUtil.copyProperties(accountVo,userAccountVo);
-        BeanUtil.copyProperties(cardVo,userCardVo);
+        BeanUtil.copyProperties(accountVo, userAccountVo);
+        BeanUtil.copyProperties(cardVo, userCardVo);
         return R.ok();
     }
 
@@ -168,20 +271,23 @@ public class CheckBusiness {
         Long factoryId = bo.getFactoryId();
         RemoteCardVo cardVo = remoteCardService.queryCardByFactoryId(factoryId);
         if (ObjectUtil.isEmpty(cardVo)) {
-            errorInfo = new ErrorInfo(400, "", "卡片不存在", MessageFormat.format("物理卡号为[{0}]的卡片不存在,不允许交易", factoryId));
+            errorInfo = new ErrorInfo(400, ApiErrorTypeConstants.PARAM_ERROR, "卡片不存在",
+                                      MessageFormat.format("物理卡号为[{0}]的卡片不存在,不允许交易", factoryId));
 
             return R.fail(errorInfo);
         }
         if (!"1".equals(cardVo.getStatus())) {
-            errorInfo = new ErrorInfo(400, "", "卡片状态不正确", MessageFormat.format("物理卡号为[{0}]的卡片卡片状态不正确,不允许交易", factoryId));
+            errorInfo = new ErrorInfo(400, ApiErrorTypeConstants.PARAM_ERROR, "卡片状态不正确",
+                                      MessageFormat.format("物理卡号为[{0}]的卡片卡片状态不正确,不允许交易",
+                                                           factoryId));
 
             return R.fail(errorInfo);
         }
         RemoteUserAccountVo accountVo = remoteUserAccountService.getUserAccountVoById(cardVo.getUserId());
         setUserInfo(bo, accountVo);
         bo.setCardNo(cardVo.getCardNo());
-        BeanUtil.copyProperties(accountVo,userAccountVo);
-        BeanUtil.copyProperties(cardVo,userCardVo);
+        BeanUtil.copyProperties(accountVo, userAccountVo);
+        BeanUtil.copyProperties(cardVo, userCardVo);
         return R.ok();
     }
 
@@ -191,26 +297,29 @@ public class CheckBusiness {
         Long userNo = bo.getUserNo();
         RemoteUserAccountVo accountVo = remoteUserAccountService.getUserAccountVoByUserNo(userNo);
         if (ObjectUtil.isEmpty(accountVo)) {
-            errorInfo = new ErrorInfo(400, "", "人员不存在", MessageFormat.format("流水号为[{0}]的人员不存在,不允许交易", userNo));
+            errorInfo = new ErrorInfo(400, ApiErrorTypeConstants.PARAM_ERROR, "人员不存在",
+                                      MessageFormat.format("流水号为[{0}]的人员不存在,不允许交易", userNo));
 
             return R.fail(errorInfo);
         }
         RemoteCardVo cardVo = remoteCardService.queryMainCardByUserId(accountVo.getUserId());
         if (ObjectUtil.isEmpty(cardVo)) {
-            errorInfo = new ErrorInfo(400, "", "卡片不存在", MessageFormat.format("流水号为[{0}]的人员卡片不存在,不允许交易", userNo));
+            errorInfo = new ErrorInfo(400, ApiErrorTypeConstants.PARAM_ERROR, "卡片不存在",
+                                      MessageFormat.format("流水号为[{0}]的人员卡片不存在,不允许交易", userNo));
 
             return R.fail(errorInfo);
         }
         if (!"1".equals(cardVo.getStatus())) {
-            errorInfo = new ErrorInfo(400, "", "卡片状态不正确", MessageFormat.format("流水号为[{0}]的人员卡片状态不正确,不允许交易", userNo));
+            errorInfo = new ErrorInfo(400, ApiErrorTypeConstants.PARAM_ERROR, "卡片状态不正确",
+                                      MessageFormat.format("流水号为[{0}]的人员卡片状态不正确,不允许交易", userNo));
 
             return R.fail(errorInfo);
         }
         setUserInfo(bo, accountVo);
         bo.setCardNo(cardVo.getCardNo());
         bo.setFactoryId(cardVo.getFactoryId());
-        BeanUtil.copyProperties(accountVo,userAccountVo);
-        BeanUtil.copyProperties(cardVo,userCardVo);
+        BeanUtil.copyProperties(accountVo, userAccountVo);
+        BeanUtil.copyProperties(cardVo, userCardVo);
         return R.ok();
     }
 
@@ -221,18 +330,21 @@ public class CheckBusiness {
         RemoteUserAccountVo accountVo = remoteUserAccountService.getUserAccountVoByUserNumb(userNumb);
 
         if (ObjectUtil.isEmpty(accountVo)) {
-            errorInfo = new ErrorInfo(400, "", "人员不存在", MessageFormat.format("编号为[{0}]的人员不存在,不允许交易", userNumb));
+            errorInfo = new ErrorInfo(400, ApiErrorTypeConstants.PARAM_ERROR, "人员不存在",
+                                      MessageFormat.format("编号为[{0}]的人员不存在,不允许交易", userNumb));
 
             return R.fail(errorInfo);
         }
         RemoteCardVo cardVo = remoteCardService.queryMainCardByUserId(accountVo.getUserId());
         if (ObjectUtil.isEmpty(cardVo)) {
-            errorInfo = new ErrorInfo(400, "", "卡片不存在", MessageFormat.format("编号为[{0}]的人员卡片不存在,不允许交易", userNumb));
+            errorInfo = new ErrorInfo(400, ApiErrorTypeConstants.PARAM_ERROR, "卡片不存在",
+                                      MessageFormat.format("编号为[{0}]的人员卡片不存在,不允许交易", userNumb));
 
             return R.fail(errorInfo);
         }
         if (!"1".equals(cardVo.getStatus())) {
-            errorInfo = new ErrorInfo(400, "", "卡片状态不正确", MessageFormat.format("编号为[{0}]的人员卡片状态不正确,不允许交易", userNumb));
+            errorInfo = new ErrorInfo(400, ApiErrorTypeConstants.PARAM_ERROR, "卡片状态不正确",
+                                      MessageFormat.format("编号为[{0}]的人员卡片状态不正确,不允许交易", userNumb));
 
             return R.fail(errorInfo);
         }
@@ -240,9 +352,118 @@ public class CheckBusiness {
         setUserInfo(bo, accountVo);
         bo.setCardNo(cardVo.getCardNo());
         bo.setFactoryId(cardVo.getFactoryId());
-        BeanUtil.copyProperties(accountVo,userAccountVo);
-        BeanUtil.copyProperties(cardVo,userCardVo);
+        BeanUtil.copyProperties(accountVo, userAccountVo);
+        BeanUtil.copyProperties(cardVo, userCardVo);
 
         return R.ok();
     }
+
+    private R<ErrorInfo> checkOriginalRecord(ConsumptionBo bo) {
+        String originalId = RecordIdUtils.getRecordId(bo.getConsumeDate(), bo.getTermNo().shortValue(),
+                                                      bo.getTermRecordId().shortValue(), bo.getUserNo().intValue(), 0);
+        XfConsumeDetailOriginalVo consumeDetailOriginalVo = consumeDetailOriginalService.queryById(originalId);
+        if (ObjectUtil.isEmpty(consumeDetailOriginalVo)) {
+            // TODO 正常应该是进行记录类型和脱机消费的检查,此处先按错误处理
+            return R.fail(new ErrorInfo(400, ApiErrorTypeConstants.NOT_FOUND, "原始消费记录不存在",
+                                        MessageFormat.format("标识为[{0}]的原始消费记录不存在", bo.getRecordId())));
+        }
+        XfConsumeDetailVo consumeDetailVo = consumeDetailService.queryVoByOriginalId(originalId);
+        if (ObjectUtil.isNotEmpty(consumeDetailVo)) {
+            // 认为是重复上传,不再入账
+            return R.fail(new ErrorInfo(400, ApiErrorTypeConstants.RECORD_IS_EXISTS, "原始消费记录已处理",
+                                        MessageFormat.format("标识为[{0}]的原始消费记录已处理", bo.getRecordId())));
+        }
+        bo.setOriginalId(originalId);
+        return R.ok();
+    }
+
+    public R<ErrorInfo> checkDeductionBag(ConsumptionBo bo, RemoteUserAccountVo userAccountVo, XfTermVo termVo, List<PtBagVo> bagVos) {
+        // 设备的扣费钱包字符串
+        String consumeType = termVo.getConsumeType();
+        // 分解扣费钱包
+        List<String> bagCodes = StrUtil.split(consumeType, ",");
+        Long userId = userAccountVo.getUserId();
+        // 可能会在处理过程中更改实际的消费金额,因此先取出来
+        BigDecimal consumeMoney = bo.getConsumeMoney();
+        BigDecimal doMoney = bo.getConsumeMoney();
+        // 计算后实际需要扣费的钱包,会小于或等于指定的扣费钱包数
+        List<PtBagVo> doBagVos = new ArrayList<>();
+        BigDecimal totalBalance = BigDecimal.ZERO;
+        for (String bagCode : bagCodes) {
+            // 1.查询对应的钱包
+            PtBagVo bagVo = bagService.queryByUserBagCode(userId, bagCode);
+            if (ObjectUtil.isEmpty(bagVo)) {
+                log.warn("人员Id:[{}]没有代码:[{}]的钱包,无法从此钱包扣费", userId, bagCode);
+                continue;
+            }
+            // 2.比较扣费金额
+            BigDecimal balance = bagVo.getBalance();
+            if (balance.compareTo(BigDecimal.ZERO) > 0) {
+                totalBalance = totalBalance.add(balance);
+                if (balance.compareTo(doMoney) >= 0) {
+                    // 如果钱包金额>扣费金额,设置扣费结果并中断循环
+                    bagVo.setReceiptMoney(doMoney);
+                    bagVo.setBalance(balance.subtract(doMoney));
+                    doMoney = BigDecimal.ZERO;
+                    doBagVos.add(bagVo);
+                    break;
+                } else {
+                    // 将钱包扣费为0,剩余待扣金额=消费金额-原钱包余额
+                    bagVo.setReceiptMoney(balance);
+                    bagVo.setBalance(BigDecimal.ZERO);
+                    doMoney = doMoney.subtract(balance);
+                    doBagVos.add(bagVo);
+                }
+            }
+        }
+        if (doMoney.compareTo(BigDecimal.ZERO) > 0) {
+            // 循环扣费后,如果还有未扣的金额,表示钱包余额不足,不允许交易
+            return R.fail(new ErrorInfo(400, ApiErrorTypeConstants.CONSUME_CHECK_FAIL, "钱包余额不足",
+                                        MessageFormat.format("余额不足,余额[{0}],消费金额[{1}]", totalBalance,
+                                                             consumeMoney)));
+        }
+        bagVos.addAll(doBagVos);
+        return R.ok();
+    }
+
+    public R<ErrorInfo> checkLimitDeal(ConsumptionBo bo, RemoteUserAccountVo userAccountVo, XfTermVo termVo, RemoteCardVo userCardVo,
+                                       RemoteMealTypeVo mealTypeVo) {
+        Long termNo = termVo.getTermNo();
+        String mealTypeId = mealTypeVo.getTypeId();
+        if (termNo == 0 && ObjectUtil.equal("0", mealTypeId)) {
+            return R.ok();
+        }
+        // 当前卡片类型
+        int cardTypeId = userCardVo.getCardType().intValue();
+        // 设备允许卡类
+        int termCardTypeId = termVo.getCardType() == null ? 0 : termVo.getCardType();
+        // 设备每天最大消费次数
+        Integer termDayCount = termVo.getDayCount() == null ? 0 : termVo.getDayCount();
+        // 每天最大消费金额
+        BigDecimal termDayMoney = termVo.getDayMoney() == null ? BigDecimal.ZERO : termVo.getDayMoney();
+        // 每餐最大消费次数
+        Integer termmealCount = termVo.getMealCount() == null ? 0 : termVo.getMealCount();
+        // 单次最大消费金额
+        BigDecimal termSignleMoney = termVo.getSingleMoney() == null ? BigDecimal.ZERO : termVo.getSingleMoney();
+        // 二次可用最大时间间隔
+        Integer termSwipeInterval = termVo.getSwipeInterval() == null ? 0 : termVo.getSwipeInterval();
+        // 设备是否启用了卡有效
+        Boolean termUseValidity = Objects.equals(termVo.getTermValidity(), "0") ? Boolean.FALSE : Boolean.TRUE;
+
+        int offsetTypeId = (int) Math.pow(2, (cardTypeId - 1));
+        if ((offsetTypeId & termCardTypeId) != offsetTypeId) {
+            return R.fail(new ErrorInfo(400, ApiErrorTypeConstants.NOT_FOUND, "卡类限制", TradeStatusEnum.CardTypeLimit.getName()));
+        }
+        return R.fail();
+    }
+
+    private RemoteOperatorVo getOperatorVo(ConsumptionBo bo) {
+        RemoteOperatorVo operatorVo = remoteOperatorService.getVoById(bo.getOperatorId());
+        if (ObjectUtil.isEmpty(operatorVo)) {
+            operatorVo = new RemoteOperatorVo();
+            operatorVo.setOperatorId(bo.getOperatorId());
+            operatorVo.setOperatorName(bo.getOperatorName());
+        }
+        return operatorVo;
+    }
 }

+ 43 - 0
ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/business/ConsumeBusiness.java

@@ -4,15 +4,21 @@ import cn.hutool.json.JSONUtil;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.dromara.backstage.api.domain.vo.RemoteCardVo;
+import org.dromara.backstage.api.domain.vo.RemoteMealTypeVo;
+import org.dromara.backstage.api.domain.vo.RemoteOperatorVo;
 import org.dromara.backstage.api.domain.vo.RemoteUserAccountVo;
 import org.dromara.common.core.domain.R;
 import org.dromara.common.core.domain.model.ErrorInfo;
 import org.dromara.server.common.domain.consume.bo.ConsumptionBo;
+import org.dromara.server.consume.convert.strategy.RecordConvertStrategyContent;
+import org.dromara.server.consume.domain.vo.PtBagVo;
 import org.dromara.server.consume.domain.vo.XfConsumeDetailOriginalVo;
 import org.dromara.server.consume.domain.vo.XfTermVo;
 import org.springframework.stereotype.Service;
 
 import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.List;
 
 /**
  * name: ConsumeBusiness
@@ -30,6 +36,7 @@ import java.text.MessageFormat;
 public class ConsumeBusiness {
     private final CheckBusiness checkBusiness;
     private final BaseBusiness baseBusiness;
+    private final RecordConvertStrategyContent recordConvertStrategy;
 
     /**
      * 请求消费
@@ -82,6 +89,42 @@ public class ConsumeBusiness {
             log.error("[上传交易]-[参数验证失败]-[{}]", JSONUtil.toJsonStr(result.getData()));
             return result;
         }
+        XfTermVo termVo = new XfTermVo();
+        result = checkBusiness.checkTerm(bo, termVo);
+        if (R.isError(result)) {
+            log.error("[上传交易]-[交易验证失败]-[{}]", result.getData());
+            return result;
+        }
+
+        RemoteUserAccountVo userAccountVo = new RemoteUserAccountVo();
+        RemoteCardVo userCardVo = new RemoteCardVo();
+        List<PtBagVo> bagVos = new ArrayList<>();
+        RemoteMealTypeVo mealTypeVo = new RemoteMealTypeVo();
+        RemoteOperatorVo operatorVo = new RemoteOperatorVo();
+        result = checkBusiness.checkBill(bo, userAccountVo, userCardVo, termVo, bagVos, mealTypeVo,operatorVo);
+        if (R.isError(result)) {
+            log.error("[上传交易]-[交易账单处理失败]-[{}]", result.getData());
+            return result;
+        }
+        result = baseBusiness.postConsumeRecord(bo, userAccountVo, userCardVo, bagVos, termVo, mealTypeVo, "");
+        if (R.isError(result)) {
+            log.error("[上传交易]-[交易入库失败]-[{}]", result.getData());
+            return result;
+        }
+        return R.ok();
+    }
+
+    public R<ErrorInfo> fullOrder(ConsumptionBo bo) {
+        R<ErrorInfo> result = this.createOrder(bo);
+        if(!R.isSuccess(result)) {
+            log.error("[请求交易]-[请求交易处理失败]-[{}]", JSONUtil.toJsonStr(result.getData()));
+            return result;
+        }
+        result = this.postOrder(bo);
+        if(!R.isSuccess(result)) {
+            log.error("[交易上传]-[交易上传处理失败]-[{}]", JSONUtil.toJsonStr(result.getData()));
+            return result;
+        }
         return R.ok();
     }
 }

+ 24 - 10
ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/controller/v1/ConsumeController.java

@@ -5,6 +5,8 @@ import lombok.RequiredArgsConstructor;
 import org.dromara.common.core.domain.R;
 import org.dromara.common.core.domain.model.ErrorInfo;
 import org.dromara.common.core.domain.model.ErrorResult;
+import org.dromara.common.core.enums.CreditTypeEnum;
+import org.dromara.common.core.enums.SystemUseTypeEnum;
 import org.dromara.server.common.domain.consume.bo.ConsumptionBo;
 import org.dromara.server.consume.business.ConsumeBusiness;
 import org.dromara.server.consume.convert.strategy.RecordConvertStrategyContent;
@@ -39,34 +41,46 @@ public class ConsumeController {
 
     @PostMapping("/ConsumeOriginal")
     public Object consumeOriginal(@RequestBody Object record) {
-        return doRecordData(record,"requestConsume");
+        return doRecordData(record, "requestConsume");
     }
+
     @PostMapping("/Consume")
     public Object uploadRecord(@RequestBody Object record) {
-        return doRecordData(record,"uploadRecord");
+        return doRecordData(record, "uploadRecord");
+    }
+
+    @PostMapping("/Consume/all")
+    public Object consumeAll(@RequestBody Object record) {
+        return doRecordData(record, "fullRecord");
     }
 
-    private Object doRecordData(Object record,String type){
-        if(ObjectUtil.length(record)==0){
+    private Object doRecordData(Object record, String type) {
+        if (ObjectUtil.length(record) == 0) {
             ErrorResult result = ErrorResult.instance(HttpStatus.FORBIDDEN, "处理失败", 1, "PARAM_ERROR", "参数错误",
-                "json字符串无法还原为对象,可能原因是格式不正确。");
+                                                      "json字符串无法还原为对象,可能原因是格式不正确。");
             return new ResponseEntity<Object>(result, null, HttpStatus.FORBIDDEN);
         }
         ConsumptionBo bo = recordConvertStrategy.convert(record, "YC");
+        bo.setStatusFlag(4);
+        bo.setUseType(SystemUseTypeEnum.CONSUME);
+        bo.setCreditType(CreditTypeEnum.TERM_CONSUME);
+
         R<ErrorInfo> errorInfo;
-        if(Objects.equals(type, "requestConsume")) {
-             errorInfo = consumeBusiness.createOrder(bo);
-        } else {
+        if (Objects.equals(type, "requestConsume")) {
+            errorInfo = consumeBusiness.createOrder(bo);
+        } else if (Objects.equals(type, "uploadRecord")) {
             errorInfo = consumeBusiness.postOrder(bo);
+        } else {
+            errorInfo = consumeBusiness.fullOrder(bo);
         }
-        if(!R.isSuccess(errorInfo)){
+        if (!R.isSuccess(errorInfo)) {
             ErrorResult result = new ErrorResult();
             result.setStatusCode(HttpStatus.NOT_FOUND.value());
             result.setMessage(errorInfo.getMsg());
             result.getErrors().add(errorInfo.getData());
             return new ResponseEntity<Object>(result, null, HttpStatus.NOT_FOUND);
         }
-        return recordConvertStrategy.reConvert(bo,"YC");
+        return recordConvertStrategy.reConvert(bo, "YC");
     }
 
 }

+ 2 - 1
ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/convert/strategy/impl/YcRecordConvertStrategyImpl.java

@@ -7,6 +7,7 @@ import cn.hutool.core.util.StrUtil;
 import cn.hutool.json.JSONObject;
 import org.dromara.server.common.constant.DefaultConstants;
 import org.dromara.server.common.domain.consume.bo.ConsumptionBo;
+import org.dromara.server.common.util.CardDateUtils;
 import org.dromara.server.consume.constant.TermSupplierConstants;
 import org.dromara.server.consume.convert.strategy.IRecordConvertStrategy;
 import org.dromara.server.consume.domain.YcConsumeInfo;
@@ -41,7 +42,7 @@ public class YcRecordConvertStrategyImpl implements IRecordConvertStrategy {
             consumptionBo.setConsumeMoney(ObjectUtil.isEmpty(yc.getConsumeValue()) ? BigDecimal.ZERO : yc.getConsumeValue().setScale(2, RoundingMode.HALF_UP));
             String tempDate = yc.getConsumeDate();
             if(StrUtil.isNotEmpty(tempDate)){
-                consumptionBo.setConsumeDate(DateUtil.parse(tempDate, DefaultConstants.DATE_TIME_FORMAT));
+                consumptionBo.setConsumeDate(CardDateUtils.parseDateTime(tempDate));
             }
             consumptionBo.setTermNo(ObjectUtil.isEmpty(yc.getTermID()) ? 0 : yc.getTermID());
             consumptionBo.setTermRecordId(ObjectUtil.isEmpty(yc.getTermRecordID()) ? 0 : yc.getTermRecordID());

+ 80 - 0
ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/domain/PtBag.java

@@ -0,0 +1,80 @@
+package org.dromara.server.consume.domain;
+
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableLogic;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.dromara.common.tenant.core.TenantEntity;
+
+import java.io.Serial;
+import java.math.BigDecimal;
+
+/**
+ * 账户钱包对象 t_pt_bag
+ *
+ * @author LionLi
+ * @date 2024-08-06
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName("t_pt_bag")
+public class PtBag extends TenantEntity {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 钱包Id,主键
+     */
+    @TableId(value = "bag_id")
+    private Long bagId;
+
+    /**
+     * 所属账户Id
+     */
+    private Long userId;
+
+    /**
+     * 钱包代码,见CARDBAGTYPE数据字典类别
+     */
+    private String bagCode;
+
+    /**
+     * 钱包余额
+     */
+    private BigDecimal balance;
+
+    /**
+     * 钱包加密余额
+     */
+    private String encryptBalance;
+
+    /**
+     * 消费总金额
+     */
+    private BigDecimal consumeTotal;
+
+    /**
+     * 消费总次数
+     */
+    private Long consumeCount;
+
+    /**
+     * 充值总金额
+     */
+    private BigDecimal rechargeTotal;
+
+    /**
+     * 充值总次数
+     */
+    private Long rechargeCount;
+
+    /**
+     * 删除标志(0-未删除 2-已删除)
+     */
+    @TableLogic
+    private String delFlag;
+
+
+}

+ 92 - 0
ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/domain/bo/PtBagBo.java

@@ -0,0 +1,92 @@
+package org.dromara.server.consume.domain.bo;
+
+import io.github.linpeilie.annotations.AutoMapper;
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.dromara.common.core.enums.BalanceUpdateEnum;
+import org.dromara.common.core.enums.CreditTypeEnum;
+import org.dromara.common.core.validate.AddGroup;
+import org.dromara.common.core.validate.EditGroup;
+import org.dromara.common.mybatis.core.domain.BaseEntity;
+import org.dromara.server.consume.domain.PtBag;
+
+import java.math.BigDecimal;
+
+/**
+ * 账户钱包业务对象 t_pt_bag
+ *
+ * @author LionLi
+ * @date 2024-08-06
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@AutoMapper(target = PtBag.class, reverseConvertGenerate = false)
+public class PtBagBo extends BaseEntity {
+
+    //region 这是钱包数据库对应的数据
+    /**
+     * 钱包Id,主键
+     */
+    private Long bagId;
+
+    /**
+     * 所属账户Id
+     */
+    private Long userId;
+
+    /**
+     * 钱包代码,见CARDBAGTYPE数据字典类别
+     */
+    @NotBlank(message = "钱包代码", groups = { AddGroup.class, EditGroup.class })
+    private String bagCode;
+
+    /**
+     * 钱包余额
+     */
+    private BigDecimal balance;
+
+    /**
+     * 钱包加密余额
+     */
+    private String encryptBalance;
+
+    /**
+     * 消费总金额
+     */
+    private BigDecimal consumeTotal;
+
+    /**
+     * 消费总次数
+     */
+    private Long consumeCount;
+
+    /**
+     * 充值总金额
+     */
+    private BigDecimal rechargeTotal;
+
+
+    /**
+     * 充值总次数
+     */
+    private Long rechargeCount;
+    //endregion
+
+    //region 以下属性是钱包余额变更业务所需要的
+    /**
+     * 充值/退款/重置余额时的操作金额
+     */
+   private BigDecimal receiptMoney;
+
+    /**
+     * 卡余的操作类型
+     */
+   private BalanceUpdateEnum operationMode;
+
+    /**
+     * 交易类型
+     */
+   private CreditTypeEnum creditType;
+    //endregion
+}

+ 82 - 0
ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/domain/vo/PtBagVo.java

@@ -0,0 +1,82 @@
+package org.dromara.server.consume.domain.vo;
+
+import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
+import io.github.linpeilie.annotations.AutoMapper;
+import lombok.Data;
+import org.dromara.server.consume.domain.PtBag;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.math.BigDecimal;
+
+
+/**
+ * 账户钱包视图对象 t_pt_bag
+ *
+ * @author LionLi
+ * @date 2024-08-06
+ */
+@Data
+@ExcelIgnoreUnannotated
+@AutoMapper(target = PtBag.class)
+public class PtBagVo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 钱包Id,主键
+     */
+    private Long bagId;
+
+    /**
+     * 所属账户Id
+     */
+    private Long userId;
+
+    /**
+     * 钱包代码,见CARDBAGTYPE数据字典类别
+     */
+    private String bagCode;
+
+    /**
+     * 钱包余额
+     */
+    private BigDecimal balance;
+
+    /**
+     * 钱包加密余额
+     */
+    private String encryptBalance;
+
+    /**
+     * 消费总金额
+     */
+    private BigDecimal consumeTotal;
+
+    /**
+     * 消费总次数
+     */
+    private Long consumeCount;
+
+    /**
+     * 充值总金额
+     */
+    private BigDecimal rechargeTotal;
+
+    /**
+     * 充值总次数
+     */
+    private Long rechargeCount;
+
+    /**
+     * 钱包名称
+     */
+    private String bagName;
+
+    /**
+     * 充值/退款/消费/错扣补款/重置余额等的操作金额
+     */
+    private BigDecimal receiptMoney;
+
+}

+ 169 - 0
ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/domain/vo/XfTermVo.java

@@ -7,6 +7,7 @@ import org.dromara.server.consume.domain.XfTerm;
 
 import java.io.Serial;
 import java.io.Serializable;
+import java.math.BigDecimal;
 
 
 /**
@@ -100,4 +101,172 @@ public class XfTermVo implements Serializable {
      */
     private String tenantId;
 
+    //region 以下为消费机的参数属性
+    private Integer cardType;
+
+    /**
+     * 消费工作模式,见XF_WorkMode字典类别
+     */
+    private String workMode;
+
+    /**
+     * 开机模式,0-不需要营业员开机,1-需要营业员开机
+     */
+    private String openMode;
+
+    /**
+     * 卡上最大金额
+     */
+    private BigDecimal maxCardMoney;
+
+    /**
+     * 定值消费金额
+     */
+    private BigDecimal constantValue;
+
+    /**
+     * 编号0代表的金额
+     */
+    private BigDecimal rationZero;
+
+    /**
+     * 编号1代表的金额
+     */
+    private BigDecimal rationOne;
+
+    /**
+     * 编号2代表的金额
+     */
+    private BigDecimal rationTwo;
+
+    /**
+     * 编号3代表的金额
+     */
+    private BigDecimal rationThree;
+
+    /**
+     * 编号4代表的金额
+     */
+    private BigDecimal rationFour;
+
+    /**
+     * 编号5代表的金额
+     */
+    private BigDecimal rationFive;
+
+    /**
+     * 编号6代表的金额
+     */
+    private BigDecimal rationSix;
+
+    /**
+     * 编号7代表的金额
+     */
+    private BigDecimal rationSeven;
+
+    /**
+     * 编号8代表的金额
+     */
+    private BigDecimal rationEight;
+
+    /**
+     * 编号9代表的金额
+     */
+    private BigDecimal rationNine;
+
+    /**
+     * 每天最大消费次数,0-不限
+     */
+    private Integer dayCount;
+
+    /**
+     * 每天最大消费金额
+     */
+    private BigDecimal dayMoney;
+
+    /**
+     * 每餐最大消费次数,0-不限
+     */
+    private Integer mealCount;
+
+    /**
+     * 单次最大消费金额
+     */
+    private BigDecimal singleMoney;
+
+    /**
+     * 早餐消费金额
+     */
+    private BigDecimal breakfastMoney;
+
+    /**
+     * 午餐消费金额
+     */
+    private BigDecimal lunchMoney;
+
+    /**
+     * 晚餐消费金额
+     */
+    private BigDecimal supperMoney;
+
+    /**
+     * 夜宵消费金额
+     */
+    private BigDecimal nightMoney;
+
+    /**
+     * 早餐开始时间
+     */
+    private Long breakfastBegin;
+
+    /**
+     * 早餐结束时间
+     */
+    private Long breakfastEnd;
+
+    /**
+     * 午餐开始时间
+     */
+    private Long lunchBegin;
+
+    /**
+     * 午餐结束时间
+     */
+    private Long lunchEnd;
+
+    /**
+     * 晚餐开始时间
+     */
+    private Long supperBegin;
+
+    /**
+     * 晚餐结束时间
+     */
+    private Long supperEnd;
+
+    /**
+     * 宵夜开始时间
+     */
+    private Long nightBegin;
+
+    /**
+     * 宵夜结束时间
+     */
+    private Long nightEnd;
+
+    /**
+     * 两次刷卡间隔,0-不限制
+     */
+    private Integer swipeInterval;
+
+    /**
+     * 是否启用卡片有效限制,0-禁用 1-启用
+     */
+    private String termValidity;
+
+    /**
+     * 重启时间
+     */
+    private String rebootTime;
+    //endregion
 }

+ 15 - 0
ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/mapper/PtBagMapper.java

@@ -0,0 +1,15 @@
+package org.dromara.server.consume.mapper;
+
+import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
+import org.dromara.server.consume.domain.PtBag;
+import org.dromara.server.consume.domain.vo.PtBagVo;
+
+/**
+ * 账户钱包Mapper接口
+ *
+ * @author LionLi
+ * @date 2024-08-06
+ */
+public interface PtBagMapper extends BaseMapperPlus<PtBag, PtBagVo> {
+
+}

+ 110 - 0
ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/service/IPtBagService.java

@@ -0,0 +1,110 @@
+package org.dromara.server.consume.service;
+
+import org.dromara.common.mybatis.core.page.PageQuery;
+import org.dromara.common.mybatis.core.page.TableDataInfo;
+import org.dromara.server.consume.domain.PtBag;
+import org.dromara.server.consume.domain.bo.PtBagBo;
+import org.dromara.server.consume.domain.vo.PtBagVo;
+
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * 账户钱包Service接口
+ *
+ * @author LionLi
+ * @date 2024-08-06
+ */
+public interface IPtBagService {
+
+    /**
+     * 查询账户钱包
+     *
+     * @param bagId 主键
+     * @return 账户钱包
+     */
+    PtBagVo queryById(Long bagId);
+
+    /**
+     * 分页查询账户钱包列表
+     *
+     * @param bo        查询条件
+     * @param pageQuery 分页参数
+     * @return 账户钱包分页列表
+     */
+    TableDataInfo<PtBagVo> queryPageList(PtBagBo bo, PageQuery pageQuery);
+
+    /**
+     * 查询符合条件的账户钱包列表
+     *
+     * @param bo 查询条件
+     * @return 账户钱包列表
+     */
+    List<PtBagVo> queryList(PtBagBo bo);
+
+    /**
+     * 新增账户钱包
+     *
+     * @param bo 账户钱包
+     * @return 是否新增成功
+     */
+    Boolean insertByBo(PtBagBo bo);
+
+    /**
+     * 修改账户钱包
+     *
+     * @param bo 账户钱包
+     * @return 是否修改成功
+     */
+    Boolean updateByBo(PtBagBo bo);
+
+    /**
+     * 校验并批量删除账户钱包信息
+     *
+     * @param ids     待删除的主键集合
+     * @param isValid 是否进行有效性校验
+     * @return 是否删除成功
+     */
+    Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
+
+    /**
+     * 根据Id获取账户钱包余额
+     *
+     * @param userIds  人员Id串,英文逗号分隔
+     * @return 余额串,英文逗号分隔
+     */
+    String selectAccountBalanceByIds(String userIds);
+
+    /**
+     * 初始化账户钱包
+     *
+     * @param userId 账户Id
+     * @return 是否初始化成功
+     */
+    Boolean initAccountBag(Long userId);
+
+    /**
+     * 更新钱包
+     *
+     * @param bo 账户钱包
+     * @return 更新后的钱包
+     */
+    PtBagVo updateBalanceByBo(PtBagBo bo);
+    /**
+     * 钱包更新前的验证
+     *
+     * @param bo 账户钱包
+     * @return 验证通过后的账户钱包
+     */
+    PtBag verification(PtBagBo bo);
+    /**
+     * 查询账户钱包
+     *
+     * @param userId 用户Id
+     * @param bagCode 敖包代码
+     * @return 账户钱包
+     */
+    PtBagVo queryByUserBagCode(Long userId,String bagCode);
+
+
+}

+ 4 - 0
ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/service/IXfConsumeDetailService.java

@@ -21,6 +21,7 @@ public interface IXfConsumeDetailService {
      * @return 消费明细
      */
     XfConsumeDetailVo queryById(String consumeId);
+
     /**
      * 查询符合条件的消费明细列表
      *
@@ -61,4 +62,7 @@ public interface IXfConsumeDetailService {
      */
     Boolean updateByBoId(XfConsumeDetailBo bo);
 
+    XfConsumeDetailVo queryVoByBo(XfConsumeDetailBo bo);
+
+    XfConsumeDetailVo queryVoByOriginalId(String originalId);
 }

+ 380 - 0
ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/service/impl/PtBagServiceImpl.java

@@ -0,0 +1,380 @@
+package org.dromara.server.consume.service.impl;
+
+import cn.hutool.core.convert.Convert;
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.json.JSONUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import lombok.RequiredArgsConstructor;
+import org.dromara.common.core.exception.consume.BagException;
+import org.dromara.common.core.utils.MapstructUtils;
+import org.dromara.common.core.utils.SpringUtils;
+import org.dromara.common.core.utils.StringUtils;
+import org.dromara.common.encrypt.utils.YcEncryptUtil;
+import org.dromara.common.mybatis.core.page.PageQuery;
+import org.dromara.common.mybatis.core.page.TableDataInfo;
+import org.dromara.server.consume.domain.PtBag;
+import org.dromara.server.consume.domain.bo.PtBagBo;
+import org.dromara.server.consume.domain.vo.PtBagVo;
+import org.dromara.server.consume.mapper.PtBagMapper;
+import org.dromara.server.consume.service.IPtBagService;
+import org.springframework.stereotype.Service;
+
+import java.math.BigDecimal;
+import java.util.*;
+
+/**
+ * 账户钱包Service业务层处理
+ *
+ * @author LionLi
+ * @date 2024-08-06
+ */
+@RequiredArgsConstructor
+@Service
+public class PtBagServiceImpl implements IPtBagService {
+
+    private final PtBagMapper baseMapper;
+
+    /**
+     * 查询账户钱包
+     *
+     * @param bagId 主键
+     * @return 账户钱包
+     */
+    @Override
+    public PtBagVo queryById(Long bagId) {
+        return baseMapper.selectVoById(bagId);
+    }
+    /**
+     * 查询账户钱包
+     *
+     * @param userId 用户Id
+     * @param bagCode 敖包代码
+     * @return 账户钱包
+     */
+    @Override
+    public PtBagVo queryByUserBagCode(Long userId, String bagCode) {
+        PtBagVo vo = baseMapper.selectVoOne(Wrappers.<PtBag>lambdaQuery()
+            .eq(PtBag::getUserId, userId)
+            .eq(PtBag::getBagCode,bagCode),PtBagVo.class);
+        if (ObjectUtil.isNull(vo)) {
+            return null;
+        }
+        return vo;
+    }
+
+    /**
+     * 分页查询账户钱包列表
+     *
+     * @param bo        查询条件
+     * @param pageQuery 分页参数
+     * @return 账户钱包分页列表
+     */
+    @Override
+    public TableDataInfo<PtBagVo> queryPageList(PtBagBo bo, PageQuery pageQuery) {
+        LambdaQueryWrapper<PtBag> lqw = buildQueryWrapper(bo);
+        Page<PtBagVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
+        return TableDataInfo.build(result);
+    }
+
+    /**
+     * 查询符合条件的账户钱包列表
+     *
+     * @param bo 查询条件
+     * @return 账户钱包列表
+     */
+    @Override
+    public List<PtBagVo> queryList(PtBagBo bo) {
+        LambdaQueryWrapper<PtBag> lqw = buildQueryWrapper(bo);
+        return baseMapper.selectVoList(lqw);
+    }
+
+    private LambdaQueryWrapper<PtBag> buildQueryWrapper(PtBagBo bo) {
+        Map<String, Object> params = bo.getParams();
+        LambdaQueryWrapper<PtBag> lqw = Wrappers.lambdaQuery();
+        lqw.eq(bo.getUserId() != null, PtBag::getUserId, bo.getUserId());
+        lqw.eq(StringUtils.isNotBlank(bo.getBagCode()), PtBag::getBagCode, bo.getBagCode());
+        lqw.eq(bo.getBalance() != null, PtBag::getBalance, bo.getBalance());
+        lqw.eq(bo.getConsumeTotal() != null, PtBag::getConsumeTotal, bo.getConsumeTotal());
+        lqw.eq(bo.getConsumeCount() != null, PtBag::getConsumeCount, bo.getConsumeCount());
+        lqw.eq(bo.getRechargeTotal() != null, PtBag::getRechargeTotal, bo.getRechargeTotal());
+        lqw.eq(bo.getRechargeCount() != null, PtBag::getRechargeCount, bo.getRechargeCount());
+        return lqw;
+    }
+
+    /**
+     * 新增账户钱包
+     *
+     * @param bo 账户钱包
+     * @return 是否新增成功
+     */
+    @Override
+    public Boolean insertByBo(PtBagBo bo) {
+        PtBag add = MapstructUtils.convert(bo, PtBag.class);
+        validEntityBeforeSave(add);
+        boolean flag = baseMapper.insert(add) > 0;
+        if (flag) {
+            if (add != null) {
+                bo.setBagId(add.getBagId());
+            }
+        }
+        return flag;
+    }
+
+    /**
+     * 修改账户钱包
+     *
+     * @param bo 账户钱包
+     * @return 是否修改成功
+     */
+    @Override
+    public Boolean updateByBo(PtBagBo bo) {
+        PtBag update = MapstructUtils.convert(bo, PtBag.class);
+        if (update != null) {
+            verification(bo);
+            setEncryptBalance(update);
+            return baseMapper.updateById(update) > 0;
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * 保存前的数据校验
+     */
+    private void validEntityBeforeSave(PtBag entity) {
+        //TODO 做一些数据校验,如唯一约束
+    }
+
+    /**
+     * 校验并批量删除账户钱包信息
+     *
+     * @param ids     待删除的主键集合
+     * @param isValid 是否进行有效性校验
+     * @return 是否删除成功
+     */
+    @Override
+    public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
+        if (isValid) {
+            //TODO 做一些业务上的校验,判断是否需要校验
+        }
+        return baseMapper.deleteByIds(ids) > 0;
+    }
+
+    /**
+     * 根据Id获取账户钱包余额
+     *
+     * @param userIds 人员Id串,英文逗号分隔
+     * @return 余额串,英文逗号分隔
+     */
+    @Override
+    public String selectAccountBalanceByIds(String userIds) {
+        List<String> list = new ArrayList<>();
+        for (Long id : StringUtils.splitTo(userIds, Convert::toLong)) {
+            PtBagBo bo = new PtBagBo();
+            bo.setUserId(id);
+            List<PtBagVo> listVo = SpringUtils.getAopProxy(this).queryList(bo);
+            BigDecimal balance1 = BigDecimal.ZERO;
+            BigDecimal balance2 = BigDecimal.ZERO;
+            Optional<PtBagVo> vo = listVo.stream().filter(p -> "1".equals(p.getBagCode()))
+                .findFirst();
+            if (vo.isPresent()) {
+                balance1 = vo.get().getBalance();
+            }
+            vo = listVo.stream().filter(p -> "3".equals(p.getBagCode()))
+                .findFirst();
+            if (vo.isPresent()) {
+                balance2 = vo.get().getBalance();
+            }
+            BigDecimal total = balance1.add(balance2);
+
+            list.add(total.toString());
+        }
+        return String.join(StringUtils.SEPARATOR, list);
+    }
+
+    /**
+     * 初始化账户钱包
+     *
+     * @param userId 账户Id
+     * @return 是否初始化成功
+     */
+    @Override
+    public Boolean initAccountBag(Long userId) {
+        PtBagBo bo = new PtBagBo();
+        bo.setUserId(userId);
+        List<PtBagVo> listVo = SpringUtils.getAopProxy(this).queryList(bo);
+        if (listVo.isEmpty()) {
+            //没有钱包,初始化
+            for (int i = 1; i < 7; i++) {
+                bo = new PtBagBo();
+                bo.setUserId(userId);
+                bo.setBagCode(String.valueOf(i));
+                insertByBo(bo);
+            }
+        }
+        return true;
+    }
+
+    /**
+     * 更新钱包
+     *
+     * @param bo 账户钱包
+     * @return 更新后的账户钱包
+     */
+    @Override
+    public PtBagVo updateBalanceByBo(PtBagBo bo) {
+        //校验入库数据
+        PtBag entity = verification(bo);
+        //根据不同的操作组装入库数据
+        switch (bo.getOperationMode()) {
+            case REFUND:
+                refundBag(entity, bo);
+                break;
+            case CONSUME:
+                consumeBag(entity, bo);
+                break;
+            case RECOVER:
+                recoverBag(entity, bo);
+                break;
+            case COMPENSATE:
+                compensateBag(entity, bo);
+                break;
+            case RECHARGE:
+            default:
+                rechargeBag(entity, bo);
+        }
+        //数据入库
+        if (baseMapper.updateById(entity) > 0) {
+            return queryById(entity.getBagId());
+        } else {
+            return null;
+        }
+    }
+    /**
+     * 钱包更新前的验证
+     *
+     * @param bo 账户钱包
+     * @return 验证通过后的账户钱包
+     */
+    @Override
+    public PtBag verification(PtBagBo bo) {
+        //获取数据库中对应的钱包
+        PtBag entity = baseMapper.selectOne(Wrappers.<PtBag>lambdaQuery()
+            .eq(PtBag::getUserId, bo.getUserId())
+            .eq(PtBag::getBagCode,bo.getBagCode()));
+
+        //解密余额密文,得到加密的余额
+        //加密余额默认=明文余额
+        BigDecimal entryptValue = entity.getBalance();
+        if (StrUtil.isNotBlank(entity.getEncryptBalance())) {
+            String decryptValue = YcEncryptUtil.decryptBagBalanceByPublicKey(entity.getEncryptBalance(), entity.getUserId().toString());
+            entryptValue = new BigDecimal(decryptValue);
+        }
+        //如果明文余额与解密后余额不一致则验证不通过
+        if (entryptValue.compareTo(entity.getBalance()) != 0) {
+            throw new BagException("bag.balance.valid", JSONUtil.parse(bo));
+        }
+        return entity;
+    }
+    /**
+     * 组装充值钱包数据
+     * 1.设置充值后余额=账户原余额+充值金额
+     * 2.设置充值次数+1
+     * 3.设置充值总金额额=原总金额+充值金额
+     * 4.对充值后余额进行加密处理
+     *
+     * @param bag 账户钱包(数据库)
+     * @param bo 账户钱包(业务)
+     */
+    private void rechargeBag(PtBag bag, PtBagBo bo) {
+        bag.setBalance(bag.getBalance().add(bo.getReceiptMoney()));
+        bag.setRechargeCount(bag.getRechargeCount() + 1);
+        bag.setRechargeTotal(bag.getRechargeTotal().add(bo.getReceiptMoney()));
+        setEncryptBalance(bag);
+    }
+    /**
+     * 组装退款钱包数据
+     * 1.如果原账户余额>退款金额,则设置退款后余额=账户原余额-退款金额,否则为0
+     * 2.对退款后余额进行加密处理
+     *
+     * @param bag 账户钱包(数据库)
+     * @param bo 账户钱包(业务)
+     */
+    private void refundBag(PtBag bag, PtBagBo bo) {
+        //退款后的余额
+        BigDecimal after = BigDecimal.ZERO;
+        //账户现有余额
+        BigDecimal balance  = bag.getBalance();
+        //退款金额,传入的金额为负数,转换成正数进行余额处理
+        BigDecimal doValue = bo.getReceiptMoney().negate();
+        //如果账户现有余额比退款金额大,则最后余额为账户余额-退款金额,否则为0
+        if(balance.compareTo(doValue)>0){
+            after = balance.subtract(doValue);
+        }
+        bag.setBalance(after);
+        setEncryptBalance(bag);
+    }
+    /**
+     * 组装重置钱包数据
+     * 1.设置重置后余额=操作金额
+     * 2.对退款后余额进行加密处理
+     *
+     * @param bag 账户钱包(数据库)
+     * @param bo 账户钱包(业务)
+     */
+    private void recoverBag(PtBag bag, PtBagBo bo) {
+        if(bag.getBalance().compareTo(bo.getReceiptMoney())>0){
+            //如果钱包余额比设置的金额大,则最后余额=设置的金额
+            bag.setBalance(bo.getReceiptMoney());
+        }
+        setEncryptBalance(bag);
+    }
+    /**
+     * 组装消费钱包数据
+     * 1.设置消费后余额=账户原余额-消费金额
+     * 2.设置消费次数+1
+     * 3.设置消费总金额额=原总金额+消费金额
+     * 4.对消费后余额进行加密处理
+     *
+     * @param bag 账户钱包(数据库)
+     * @param bo 账户钱包(业务)
+     */
+    private void consumeBag(PtBag bag, PtBagBo bo) {
+        bag.setBalance(bag.getBalance().subtract(bo.getReceiptMoney()));
+        bag.setConsumeCount(bag.getConsumeCount() + 1);
+        bag.setConsumeTotal(bag.getConsumeTotal().add(bo.getReceiptMoney()));
+        setEncryptBalance(bag);
+    }
+    /**
+     * 组装错扣补款钱包数据
+     * 1.设置补款后余额=账户原余额+补款金额
+     * 2.设置消费次数+1
+     * 3.设置消费总金额额=原总金额-补款金额
+     * 4.对补款后余额进行加密处理
+     *
+     * @param bag 账户钱包(数据库)
+     * @param bo 账户钱包(业务)
+     */
+    private void compensateBag(PtBag bag, PtBagBo bo) {
+        //余额=原余额+补款金额
+        bag.setBalance(bag.getBalance().add(bo.getReceiptMoney()));
+        //消费次数+1
+        bag.setConsumeCount(bag.getConsumeCount() + 1);
+        //因为是多消费了才补款,所以总消费金额=原总消费金额-补款的金额
+        bag.setConsumeTotal(bag.getConsumeTotal().subtract(bo.getReceiptMoney()));
+        setEncryptBalance(bag);
+    }
+    /**
+     * 设置加密卡余
+     *
+     * @param bag 账户钱包(数据库)
+     */
+    private void setEncryptBalance(PtBag bag) {
+        String encryptValue = YcEncryptUtil.encryptBagBalanceByPublicKey(bag.getBalance().toString(), bag.getUserId().toString());
+        bag.setEncryptBalance(encryptValue);
+    }
+
+}

+ 18 - 0
ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/service/impl/XfConsumeDetailServiceImpl.java

@@ -1,5 +1,6 @@
 package org.dromara.server.consume.service.impl;
 
+import cn.hutool.core.collection.CollectionUtil;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
 import com.baomidou.mybatisplus.core.toolkit.Wrappers;
@@ -161,4 +162,21 @@ public class XfConsumeDetailServiceImpl implements IXfConsumeDetailService {
         updateWrapper.eq(XfConsumeDetail::getConsumeId, bo.getConsumeId());
         return baseMapper.update(null, updateWrapper)>0;
     }
+
+    @Override
+    public XfConsumeDetailVo queryVoByBo(XfConsumeDetailBo bo) {
+        List<XfConsumeDetailVo> list = this.queryList(bo);
+        if(CollectionUtil.isNotEmpty(list)){
+            return list.get(0);
+        }
+        return null;
+    }
+
+    @Override
+    public XfConsumeDetailVo queryVoByOriginalId(String originalId) {
+        XfConsumeDetailBo bo = new XfConsumeDetailBo();
+        bo.setOriginalId(originalId);
+
+        return this.queryVoByBo(bo);
+    }
 }

+ 20 - 0
ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/service/impl/XfTermServiceImpl.java

@@ -5,6 +5,11 @@ import cn.hutool.core.util.ObjUtil;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.toolkit.Wrappers;
 import lombok.RequiredArgsConstructor;
+import org.apache.dubbo.config.annotation.DubboReference;
+import org.dromara.backstage.api.RemotePtAccountService;
+import org.dromara.backstage.api.RemotePtRoomService;
+import org.dromara.backstage.api.domain.vo.RemotePtAccountVo;
+import org.dromara.backstage.api.domain.vo.RemotePtRoomVo;
 import org.dromara.common.core.constant.CacheNames;
 import org.dromara.common.core.utils.StringUtils;
 import org.dromara.common.redis.utils.RedisUtils;
@@ -29,8 +34,15 @@ import java.util.Objects;
 @Service
 public class XfTermServiceImpl implements IXfTermService {
 
+    @DubboReference
+    RemotePtRoomService remotePtRoomService;
+
+    @DubboReference
+    RemotePtAccountService remotePtAccountService;
+
     private final XfTermMapper baseMapper;
 
+
     /**
      * 查询消费设备列表
      *
@@ -66,6 +78,14 @@ public class XfTermServiceImpl implements IXfTermService {
         LambdaQueryWrapper<XfTerm> lqw = this.buildQueryWrapper(bo);
         XfTermVo vo =  baseMapper.selectVoOne(lqw);
         if(ObjUtil.isNotEmpty(vo)){
+            RemotePtAccountVo accountVo = remotePtAccountService.selectVoById(vo.getAccountId());
+            if(ObjUtil.isNotEmpty(accountVo)){
+                vo.setAccountName(accountVo.getAccountName());
+            }
+            RemotePtRoomVo roomVo = remotePtRoomService.selectVoById(vo.getRoomId());
+            if(ObjUtil.isNotEmpty(roomVo)){
+                vo.setRoomName(roomVo.getRoomName());
+            }
             List<XfTermVo> redisList = RedisUtils.getCacheList(CacheNames.PT_TERM);
             if (CollectionUtil.isNotEmpty(redisList)) {
                 RedisUtils.addCacheList(CacheNames.PT_TERM, vo);

+ 1 - 1
ruoyi-server/ruoyi-server-consume/src/main/java/org/dromara/server/consume/service/impl/XfUserTotalServiceImpl.java

@@ -24,8 +24,8 @@ import java.math.BigDecimal;
 @RequiredArgsConstructor
 @Service
 public class XfUserTotalServiceImpl implements IXfUserTotalService {
-
     private final XfUserTotalMapper baseMapper;
+
     /**
      * 新增个人与消费与充值统计
      *

+ 25 - 0
ruoyi-server/ruoyi-server-consume/src/main/resources/mapper/consume/PtBagMapper.xml

@@ -0,0 +1,25 @@
+<?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="org.dromara.server.consume.mapper.PtBagMapper">
+
+    <resultMap type="org.dromara.server.consume.domain.PtBag" id="PtBagResult">
+            <result property="bagId"    column="bag_id"    />
+            <result property="tenantId"    column="tenant_id"    />
+            <result property="userId"    column="user_id"    />
+            <result property="bagCode"    column="bag_code"    />
+            <result property="balance"    column="balance"    />
+            <result property="encryptBalance"    column="encrypt_balance"    />
+            <result property="consumeTotal"    column="consume_total"    />
+            <result property="consumeCount"    column="consume_count"    />
+            <result property="rechargeTotal"    column="recharge_total"    />
+            <result property="rechargeCount"    column="recharge_count"    />
+            <result property="delFlag"    column="del_flag"    />
+            <result property="createDept"    column="create_dept"    />
+            <result property="createBy"    column="create_by"    />
+            <result property="createTime"    column="create_time"    />
+            <result property="updateBy"    column="update_by"    />
+            <result property="updateTime"    column="update_time"    />
+    </resultMap>
+</mapper>