“mitao”
2026-04-26 6d0b7fdb91450b9939a0ead1b421910e0c8fee70
feat:管理后台用户管理、合作商管理、兑换码管理
28个文件已添加
15个文件已修改
1807 ■■■■■ 已修改文件
pom.xml 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/model/TAppUser.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-api/ruoyi-api-chargingPile/src/main/java/com/ruoyi/chargingPile/api/dto/PartnerListDTO.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-api/ruoyi-api-chargingPile/src/main/java/com/ruoyi/chargingPile/api/model/Partner.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-api/ruoyi-api-chargingPile/src/main/java/com/ruoyi/chargingPile/api/query/GetPartnerList.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-api/ruoyi-api-other/src/main/java/com/ruoyi/other/api/domain/TExchangeCodeDetail.java 76 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-api/ruoyi-api-other/src/main/java/com/ruoyi/other/api/domain/TExchangeCodePackage.java 91 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-api/ruoyi-api-other/src/main/java/com/ruoyi/other/api/domain/TExchangeCodePackageSpec.java 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-api/ruoyi-api-other/src/main/java/com/ruoyi/other/api/domain/TExchangeCodeUseRecord.java 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-api/ruoyi-api-other/src/main/java/com/ruoyi/other/api/dto/ExchangeCodeBindDTO.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-api/ruoyi-api-other/src/main/java/com/ruoyi/other/api/dto/ExchangeCodeConsumeDTO.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-api/ruoyi-api-other/src/main/java/com/ruoyi/other/api/dto/ExchangeCodePackageSaveDTO.java 61 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-api/ruoyi-api-other/src/main/java/com/ruoyi/other/api/dto/ExchangeCodePackageSpecDTO.java 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-api/ruoyi-api-other/src/main/java/com/ruoyi/other/api/factory/OtherFallbackFactory.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-api/ruoyi-api-other/src/main/java/com/ruoyi/other/api/feignClient/OtherClient.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-api/ruoyi-api-other/src/main/java/com/ruoyi/other/api/vo/ExchangeCodeBindResultVO.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-api/ruoyi-api-other/src/main/java/com/ruoyi/other/api/vo/ExchangeCodeConsumeResultVO.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-api/ruoyi-api-other/src/main/java/com/ruoyi/other/api/vo/ExchangeCodePackageDetailVO.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-api/ruoyi-api-other/src/main/java/com/ruoyi/other/api/vo/ExchangeCodePackageSpecVO.java 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-account/pom.xml 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/controller/TAppCouponController.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/controller/TAppUserController.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/service/impl/PartnerServiceImpl.java 67 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-chargingPile/src/main/resources/mapper/chargingPile/PartnerMapper.xml 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/TVipOrderController.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TChargingOrderServiceImpl.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-other/pom.xml 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/controller/TExchangeCodePackageController.java 190 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/mapper/TExchangeCodeDetailMapper.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/mapper/TExchangeCodePackageMapper.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/mapper/TExchangeCodePackageSpecMapper.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/mapper/TExchangeCodeUseRecordMapper.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/query/ExchangeCodeBatchInvalidDTO.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/query/ExchangeCodeDetailQuery.java 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/query/ExchangeCodePackageQuery.java 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/service/TExchangeCodePackageService.java 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/service/impl/TExchangeCodePackageServiceImpl.java 537 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/util/QRCodeUtils.java 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/vo/ExchangeCodeBatchInvalidResultVO.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/vo/ExchangeCodeDetailExportVO.java 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/vo/ExchangeCodeDetailVO.java 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-other/src/main/resources/mapper/other/TExchangeCodeDetailMapper.xml 60 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-service/ruoyi-other/src/main/resources/mapper/other/TExchangeCodePackageMapper.xml 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pom.xml
@@ -27,7 +27,7 @@
        <pagehelper.boot.version>1.4.6</pagehelper.boot.version>
        <druid.version>1.2.16</druid.version>
        <dynamic-ds.version>3.5.2</dynamic-ds.version>
        <commons.io.version>2.11.0</commons.io.version>
        <commons.io.version>2.15.1</commons.io.version>
        <velocity.version>2.3</velocity.version>
        <fastjson.version>2.0.25</fastjson.version>
        <jjwt.version>0.9.1</jjwt.version>
@@ -331,4 +331,4 @@
        </pluginRepository>
    </pluginRepositories>
</project>
</project>
ruoyi-api/ruoyi-api-account/src/main/java/com/ruoyi/account/api/model/TAppUser.java
@@ -141,6 +141,10 @@
    @TableField(exist = false)
    private Long orderCount;
    @ApiModelProperty("兑换码余额")
    @TableField(exist = false)
    private BigDecimal exchangeCodeBalance;
    @ApiModelProperty("剩余天数")
    @TableField(exist = false)
    private Long lastDays;
ruoyi-api/ruoyi-api-chargingPile/src/main/java/com/ruoyi/chargingPile/api/dto/PartnerListDTO.java
@@ -4,6 +4,8 @@
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.math.BigDecimal;
/**
 * @author zhibing.pu
 * @Date 2024/8/7 14:03
@@ -15,9 +17,6 @@
    private Integer id;
    @ApiModelProperty(value = "合作商名称")
    private String name;
    private Integer roleId;
    @ApiModelProperty(value = "角色名")
    private String roleName;
    @ApiModelProperty(value = "合作商电话1")
    private String phoneOne;
    @ApiModelProperty(value = "注册地址")
@@ -26,4 +25,8 @@
    private String account;
    @ApiModelProperty(value = "备注")
    private String remark;
    @ApiModelProperty(value = "电费分成比例")
    private BigDecimal electricityShareRatio;
    @ApiModelProperty(value = "服务费分成比例")
    private BigDecimal serviceShareRatio;
}
ruoyi-api/ruoyi-api-chargingPile/src/main/java/com/ruoyi/chargingPile/api/model/Partner.java
@@ -63,6 +63,24 @@
    @ApiModelProperty(value = "费率")
    private BigDecimal rate;
    /**
     * 工行合作商ID
     */
    @TableField("icbc_partner_id")
    @ApiModelProperty(value = "工行合作商ID")
    private String icbcPartnerId;
    /**
     * 电费分成比例
     */
    @TableField("electricity_share_ratio")
    @ApiModelProperty(value = "电费分成比例")
    private BigDecimal electricityShareRatio;
    /**
     * 服务费分成比例
     */
    @TableField("service_share_ratio")
    @ApiModelProperty(value = "服务费分成比例")
    private BigDecimal serviceShareRatio;
    /**
     * 注册地址
     */
    @TableField("address")
ruoyi-api/ruoyi-api-chargingPile/src/main/java/com/ruoyi/chargingPile/api/query/GetPartnerList.java
@@ -14,6 +14,4 @@
public class GetPartnerList extends BasePage {
    @ApiModelProperty(value = "合作商名称")
    private String name;
    @ApiModelProperty(value = "角色id")
    private Integer roleId;
}
ruoyi-api/ruoyi-api-other/src/main/java/com/ruoyi/other/api/domain/TExchangeCodeDetail.java
New file
@@ -0,0 +1,76 @@
package com.ruoyi.other.api.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.ruoyi.common.core.web.domain.BasePojo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("t_exchange_code_detail")
@ApiModel(value = "TExchangeCodeDetail对象", description = "兑换码明细表")
public class TExchangeCodeDetail extends BasePojo {
    @TableId(value = "id", type = IdType.AUTO)
    @ApiModelProperty("主键")
    private Long id;
    @TableField("package_id")
    @ApiModelProperty("卡包ID")
    private Long packageId;
    @TableField("package_spec_id")
    @ApiModelProperty("规格ID")
    private Long packageSpecId;
    @TableField("code")
    @ApiModelProperty("兑换码")
    private String code;
    @TableField("password")
    @ApiModelProperty("密码")
    private String password;
    @TableField("status")
    @ApiModelProperty("状态 1未绑定 2已绑定 3已失效")
    private Integer status;
    @TableField("used_amount")
    @ApiModelProperty("已使用金额")
    private BigDecimal usedAmount;
    @TableField("bind_user_id")
    @ApiModelProperty("绑定用户ID")
    private Long bindUserId;
    @TableField("bind_phone")
    @ApiModelProperty("绑定手机号")
    private String bindPhone;
    @TableField("bind_time")
    @ApiModelProperty("绑定时间")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private LocalDateTime bindTime;
    @TableField("invalid_time")
    @ApiModelProperty("失效时间")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private LocalDateTime invalidTime;
    @TableField("invalid_reason")
    @ApiModelProperty("失效原因")
    private String invalidReason;
    @TableField("batch_no")
    @ApiModelProperty("批次号")
    private String batchNo;
}
ruoyi-api/ruoyi-api-other/src/main/java/com/ruoyi/other/api/domain/TExchangeCodePackage.java
New file
@@ -0,0 +1,91 @@
package com.ruoyi.other.api.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.ruoyi.common.core.web.domain.BasePojo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("t_exchange_code_package")
@ApiModel(value = "TExchangeCodePackage对象", description = "兑换码卡包主表")
public class TExchangeCodePackage extends BasePojo {
    @TableId(value = "id", type = IdType.AUTO)
    @ApiModelProperty("主键")
    private Long id;
    @TableField("package_name")
    @ApiModelProperty("卡包名称")
    private String packageName;
    @TableField("cover_picture")
    @ApiModelProperty("封面图")
    private String coverPicture;
    @TableField("details_picture")
    @ApiModelProperty("详情图,逗号分隔")
    private String detailsPicture;
    @TableField("exchange_description")
    @ApiModelProperty("兑换说明")
    private String exchangeDescription;
    @TableField("remark")
    @ApiModelProperty("备注")
    private String remark;
    @TableField("inventory")
    @ApiModelProperty("库存总数")
    private Integer inventory;
    @TableField("bound_count")
    @ApiModelProperty("已绑定数量")
    private Integer boundCount;
    @TableField("invalid_count")
    @ApiModelProperty("已失效数量")
    private Integer invalidCount;
    @TableField("unlimited_receive_limit")
    @ApiModelProperty("是否不限领取 0否 1是")
    private Integer unlimitedReceiveLimit;
    @TableField("receive_limit")
    @ApiModelProperty("单手机号领取限制次数")
    private Integer receiveLimit;
    @TableField("finance_received_amount")
    @ApiModelProperty("财务实收")
    private BigDecimal financeReceivedAmount;
    @TableField("gift_amount")
    @ApiModelProperty("赠送金额")
    private BigDecimal giftAmount;
    @TableField("total_amount")
    @ApiModelProperty("总金额")
    private BigDecimal totalAmount;
    @TableField("expire_time")
    @ApiModelProperty("失效时间")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private LocalDateTime expireTime;
    @TableField("status")
    @ApiModelProperty("状态 1启用 2禁用")
    private Integer status;
    @TableField(exist = false)
    @ApiModelProperty("未绑定数量")
    private Integer unboundCount;
}
ruoyi-api/ruoyi-api-other/src/main/java/com/ruoyi/other/api/domain/TExchangeCodePackageSpec.java
New file
@@ -0,0 +1,48 @@
package com.ruoyi.other.api.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.ruoyi.common.core.web.domain.BasePojo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("t_exchange_code_package_spec")
@ApiModel(value = "TExchangeCodePackageSpec对象", description = "兑换码卡包规格表")
public class TExchangeCodePackageSpec extends BasePojo {
    @TableId(value = "id", type = IdType.AUTO)
    @ApiModelProperty("主键")
    private Long id;
    @TableField("package_id")
    @ApiModelProperty("卡包ID")
    private Long packageId;
    @TableField("amount")
    @ApiModelProperty("规格金额")
    private BigDecimal amount;
    @TableField("quantity")
    @ApiModelProperty("规格数量")
    private Integer quantity;
    @TableField("subtotal_amount")
    @ApiModelProperty("规格总金额")
    private BigDecimal subtotalAmount;
    @TableField("discount_amount")
    @ApiModelProperty("规格分摊优惠金额")
    private BigDecimal discountAmount;
    @TableField("sort")
    @ApiModelProperty("排序")
    private Integer sort;
}
ruoyi-api/ruoyi-api-other/src/main/java/com/ruoyi/other/api/domain/TExchangeCodeUseRecord.java
New file
@@ -0,0 +1,40 @@
package com.ruoyi.other.api.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.ruoyi.common.core.web.domain.BasePojo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("t_exchange_code_use_record")
@ApiModel(value = "TExchangeCodeUseRecord对象", description = "兑换码使用流水表")
public class TExchangeCodeUseRecord extends BasePojo {
    @TableId(value = "id", type = IdType.AUTO)
    @ApiModelProperty("主键")
    private Long id;
    @TableField("detail_id")
    @ApiModelProperty("兑换码明细ID")
    private Long detailId;
    @TableField("order_id")
    @ApiModelProperty("订单ID")
    private Long orderId;
    @TableField("user_id")
    @ApiModelProperty("用户ID")
    private Long userId;
    @TableField("used_amount")
    @ApiModelProperty("本次使用金额")
    private BigDecimal usedAmount;
}
ruoyi-api/ruoyi-api-other/src/main/java/com/ruoyi/other/api/dto/ExchangeCodeBindDTO.java
New file
@@ -0,0 +1,19 @@
package com.ruoyi.other.api.dto;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@Data
@ApiModel("兑换码绑定DTO")
public class ExchangeCodeBindDTO {
    @ApiModelProperty("兑换码")
    private String code;
    @ApiModelProperty("密码")
    private String password;
    @ApiModelProperty("手机号,默认取登录用户手机号")
    private String phone;
}
ruoyi-api/ruoyi-api-other/src/main/java/com/ruoyi/other/api/dto/ExchangeCodeConsumeDTO.java
New file
@@ -0,0 +1,21 @@
package com.ruoyi.other.api.dto;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.math.BigDecimal;
@Data
@ApiModel("兑换码余额扣减DTO")
public class ExchangeCodeConsumeDTO {
    @ApiModelProperty("用户ID")
    private Long userId;
    @ApiModelProperty("订单ID")
    private Long orderId;
    @ApiModelProperty("扣减金额")
    private BigDecimal amount;
}
ruoyi-api/ruoyi-api-other/src/main/java/com/ruoyi/other/api/dto/ExchangeCodePackageSaveDTO.java
New file
@@ -0,0 +1,61 @@
package com.ruoyi.other.api.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
@Data
@ApiModel("兑换码卡包新增编辑DTO")
public class ExchangeCodePackageSaveDTO {
    @ApiModelProperty("卡包ID,编辑时必填")
    private Long id;
    @ApiModelProperty("卡包名称")
    private String packageName;
    @ApiModelProperty("封面图")
    private String coverPicture;
    @ApiModelProperty("详情图,逗号分隔")
    private String detailsPicture;
    @ApiModelProperty("兑换说明")
    private String exchangeDescription;
    @ApiModelProperty("备注")
    private String remark;
    @ApiModelProperty("库存")
    private Integer inventory;
    @ApiModelProperty("是否不限领 0否 1是")
    private Integer unlimitedReceiveLimit;
    @ApiModelProperty("领取限制次数")
    private Integer receiveLimit;
    @ApiModelProperty("财务实收")
    private BigDecimal financeReceivedAmount;
    @ApiModelProperty("赠送金额")
    private BigDecimal giftAmount;
    @ApiModelProperty("总金额")
    private BigDecimal totalAmount;
    @ApiModelProperty("失效时间")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private LocalDateTime expireTime;
    @ApiModelProperty("状态 1启用 2禁用")
    private Integer status;
    @ApiModelProperty("规格列表")
    private List<ExchangeCodePackageSpecDTO> specList;
}
ruoyi-api/ruoyi-api-other/src/main/java/com/ruoyi/other/api/dto/ExchangeCodePackageSpecDTO.java
New file
@@ -0,0 +1,30 @@
package com.ruoyi.other.api.dto;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.math.BigDecimal;
@Data
@ApiModel("兑换码卡包规格DTO")
public class ExchangeCodePackageSpecDTO {
    @ApiModelProperty("规格ID")
    private Long id;
    @ApiModelProperty("金额")
    private BigDecimal amount;
    @ApiModelProperty("数量")
    private Integer quantity;
    @ApiModelProperty("规格总金额")
    private BigDecimal subtotalAmount;
    @ApiModelProperty("优惠金额")
    private BigDecimal discountAmount;
    @ApiModelProperty("排序")
    private Integer sort;
}
ruoyi-api/ruoyi-api-other/src/main/java/com/ruoyi/other/api/factory/OtherFallbackFactory.java
@@ -3,10 +3,14 @@
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.other.api.domain.*;
import com.ruoyi.other.api.dto.ExchangeCodeBindDTO;
import com.ruoyi.other.api.dto.ExchangeCodeConsumeDTO;
import com.ruoyi.other.api.dto.TagListQueryDto;
import com.ruoyi.other.api.dto.UnitListQueryDto;
import com.ruoyi.other.api.feignClient.OtherClient;
import com.ruoyi.other.api.vo.CouponListVOVO;
import com.ruoyi.other.api.vo.ExchangeCodeBindResultVO;
import com.ruoyi.other.api.vo.ExchangeCodeConsumeResultVO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.openfeign.FallbackFactory;
@@ -141,6 +145,21 @@
            public R reset(String electricity) {
                return R.fail("远程调用获取配置失败:"+throwable.getMessage());
            }
            @Override
            public R<ExchangeCodeBindResultVO> bindExchangeCode(ExchangeCodeBindDTO dto, Long userId, String phone) {
                return R.fail("兑换码绑定失败:" + throwable.getMessage());
            }
            @Override
            public R<Map<Long, BigDecimal>> getExchangeCodeBalanceMap(List<Long> userIds) {
                return R.fail("查询兑换码余额失败:" + throwable.getMessage());
            }
            @Override
            public R<ExchangeCodeConsumeResultVO> consumeExchangeCodeBalance(ExchangeCodeConsumeDTO dto) {
                return R.fail("扣减兑换码余额失败:" + throwable.getMessage());
            }
        };
    }
}
ruoyi-api/ruoyi-api-other/src/main/java/com/ruoyi/other/api/feignClient/OtherClient.java
@@ -4,10 +4,14 @@
import com.ruoyi.common.core.constant.ServiceNameConstants;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.other.api.domain.*;
import com.ruoyi.other.api.dto.ExchangeCodeBindDTO;
import com.ruoyi.other.api.dto.ExchangeCodeConsumeDTO;
import com.ruoyi.other.api.dto.TagListQueryDto;
import com.ruoyi.other.api.dto.UnitListQueryDto;
import com.ruoyi.other.api.factory.OtherFallbackFactory;
import com.ruoyi.other.api.vo.CouponListVOVO;
import com.ruoyi.other.api.vo.ExchangeCodeBindResultVO;
import com.ruoyi.other.api.vo.ExchangeCodeConsumeResultVO;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.*;
@@ -96,4 +100,15 @@
    @GetMapping(value = "/region/reset")
    R reset(@RequestParam("electricity") String electricity);
    @PostMapping(value = "/exchange-code-package/bind")
    R<ExchangeCodeBindResultVO> bindExchangeCode(@RequestBody ExchangeCodeBindDTO dto,
                                                 @RequestParam("userId") Long userId,
                                                 @RequestParam("phone") String phone);
    @PostMapping(value = "/exchange-code-package/balanceMap")
    R<Map<Long, BigDecimal>> getExchangeCodeBalanceMap(@RequestBody List<Long> userIds);
    @PostMapping(value = "/exchange-code-package/consumeBalance")
    R<ExchangeCodeConsumeResultVO> consumeExchangeCodeBalance(@RequestBody ExchangeCodeConsumeDTO dto);
}
ruoyi-api/ruoyi-api-other/src/main/java/com/ruoyi/other/api/vo/ExchangeCodeBindResultVO.java
New file
@@ -0,0 +1,22 @@
package com.ruoyi.other.api.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@Data
@ApiModel("兑换码绑定结果VO")
public class ExchangeCodeBindResultVO {
    @ApiModelProperty("明细ID")
    private Long detailId;
    @ApiModelProperty("卡包ID")
    private Long packageId;
    @ApiModelProperty("卡包名称")
    private String packageName;
    @ApiModelProperty("绑定手机号")
    private String bindPhone;
}
ruoyi-api/ruoyi-api-other/src/main/java/com/ruoyi/other/api/vo/ExchangeCodeConsumeResultVO.java
New file
@@ -0,0 +1,15 @@
package com.ruoyi.other.api.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.math.BigDecimal;
@Data
@ApiModel("兑换码余额扣减结果VO")
public class ExchangeCodeConsumeResultVO {
    @ApiModelProperty("实际扣减金额")
    private BigDecimal deductedAmount;
}
ruoyi-api/ruoyi-api-other/src/main/java/com/ruoyi/other/api/vo/ExchangeCodePackageDetailVO.java
New file
@@ -0,0 +1,18 @@
package com.ruoyi.other.api.vo;
import com.ruoyi.other.api.domain.TExchangeCodePackage;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.List;
@Data
@EqualsAndHashCode(callSuper = true)
@ApiModel("兑换码卡包详情VO")
public class ExchangeCodePackageDetailVO extends TExchangeCodePackage {
    @ApiModelProperty("规格列表")
    private List<ExchangeCodePackageSpecVO> specList;
}
ruoyi-api/ruoyi-api-other/src/main/java/com/ruoyi/other/api/vo/ExchangeCodePackageSpecVO.java
New file
@@ -0,0 +1,36 @@
package com.ruoyi.other.api.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.math.BigDecimal;
@Data
@ApiModel("兑换码卡包规格VO")
public class ExchangeCodePackageSpecVO {
    @ApiModelProperty("规格ID")
    private Long id;
    @ApiModelProperty("金额")
    private BigDecimal amount;
    @ApiModelProperty("数量")
    private Integer quantity;
    @ApiModelProperty("已绑定数量")
    private Integer boundCount;
    @ApiModelProperty("剩余数量")
    private Integer unboundCount;
    @ApiModelProperty("小计金额")
    private BigDecimal subtotalAmount;
    @ApiModelProperty("优惠金额")
    private BigDecimal discountAmount;
    @ApiModelProperty("排序")
    private Integer sort;
}
ruoyi-service/ruoyi-account/pom.xml
@@ -176,6 +176,9 @@
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <includeSystemScope>true</includeSystemScope>
                </configuration>
                <executions>
                    <execution>
                        <goals>
@@ -188,13 +191,6 @@
        <resources>
            <resource>
                <directory>src/main/resources</directory>
            </resource>
            <resource>
                <directory>lib</directory>
                <targetPath>/BOOT-INF/lib/</targetPath>
                <includes>
                    <include>**/*.jar</include>
                </includes>
            </resource>
            <resource>
                <directory>src/main/java</directory>
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/controller/TAppCouponController.java
@@ -35,7 +35,9 @@
import com.ruoyi.common.security.service.TokenService;
import com.ruoyi.order.api.feignClient.ChargingOrderClient;
import com.ruoyi.other.api.domain.TCoupon;
import com.ruoyi.other.api.dto.ExchangeCodeBindDTO;
import com.ruoyi.other.api.feignClient.OtherClient;
import com.ruoyi.other.api.vo.ExchangeCodeBindResultVO;
import io.swagger.annotations.ApiModelProperty;
import io.swagger.annotations.ApiOperation;
import org.redisson.api.RLock;
@@ -249,6 +251,15 @@
        return R.ok(count);
    }
    @ApiOperation(value = "兑换码绑定", tags = {"小程序-兑换码"})
    @PostMapping(value = "/exchangeCode/bind")
    public AjaxResult<ExchangeCodeBindResultVO> bindExchangeCode(@RequestBody ExchangeCodeBindDTO dto) {
        Long userId = tokenService.getLoginUserApplet().getUserId();
        TAppUser user = appUserService.getById(userId);
        ExchangeCodeBindResultVO data = otherClient.bindExchangeCode(dto, userId, user == null ? "" : user.getPhone()).getData();
        return AjaxResult.ok(data);
    }
    
    /**
@@ -351,4 +362,3 @@
        return R.ok();
    }
}
ruoyi-service/ruoyi-account/src/main/java/com/ruoyi/account/controller/TAppUserController.java
@@ -368,12 +368,18 @@
        if (page.getRecords().isEmpty()){
            return R.ok(page);
        }
        List<Long> pageUserIds = page.getRecords().stream().map(TAppUser::getId).collect(Collectors.toList());
        Map<Long, BigDecimal> exchangeCodeBalanceMap = otherClient.getExchangeCodeBalanceMap(pageUserIds).getData();
        if (exchangeCodeBalanceMap == null) {
            exchangeCodeBalanceMap = new HashMap<>();
        }
        List<Integer> vipIds = new ArrayList<>();
//              vipIds     = page.getRecords().stream().map(TAppUser::getVipId).collect(Collectors.toList());
        //获取会员map
        R<Map<Integer, String>> vipMap = otherClient.getVipMap(vipIds);
        //循环处理
        for (TAppUser appUser : page.getRecords()) {
            appUser.setExchangeCodeBalance(exchangeCodeBalanceMap.getOrDefault(appUser.getId(), BigDecimal.ZERO));
            //拿到最新的tagId
            TAppUserTag one = appUserTagService.lambdaQuery().eq(TAppUserTag::getAppUserId, appUser.getId()).orderByDesc(TAppUserTag::getCreateTime).last("limit 1").one();
            if (one!=null) {
@@ -1475,4 +1481,3 @@
    }
    
}
ruoyi-service/ruoyi-chargingPile/src/main/java/com/ruoyi/chargingPile/service/impl/PartnerServiceImpl.java
@@ -17,12 +17,9 @@
import com.ruoyi.common.core.web.page.PageInfo;
import com.ruoyi.other.api.feignClient.RoleSiteClient;
import com.ruoyi.other.api.feignClient.UserSiteClient;
import com.ruoyi.system.api.domain.SysRole;
import com.ruoyi.system.api.domain.SysUser;
import com.ruoyi.system.api.feignClient.SysRoleClient;
import com.ruoyi.system.api.feignClient.SysUserClient;
import com.ruoyi.system.api.feignClient.SysUserRoleClient;
import com.ruoyi.system.api.model.GetSysRoleByIds;
import com.ruoyi.system.api.model.SysUserRoleVo;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -37,9 +34,11 @@
 */
@Service
public class PartnerServiceImpl extends ServiceImpl<PartnerMapper, Partner> implements IPartnerService {
    @Resource
    private SysRoleClient sysRoleClient;
    /**
     * TODO 确认合作商默认角色ID,当前先使用通用角色占位。
     */
    private static final Long PARTNER_DEFAULT_ROLE_ID = 2L;
    
    @Resource
    private SysUserClient sysUserClient;
@@ -76,25 +75,6 @@
    public PageInfo<PartnerListDTO> getPartnerList(GetPartnerList partnerList) {
        PageInfo<PartnerListDTO> pageInfo = new PageInfo(partnerList.getPageCurr(), partnerList.getPageSize());
        List<PartnerListDTO> list = this.baseMapper.getPartnerList(pageInfo, partnerList);
        List<Integer> roleIds = list.stream().map(PartnerListDTO::getRoleId).collect(Collectors.toList());
        GetSysRoleByIds ids = new GetSysRoleByIds();
        ids.setIds(roleIds);
        //获取相关的角色id
        R<List<SysRole>> r = sysRoleClient.getSysRoleByIds(ids);
        if(r.getCode() != 200){
            throw new RuntimeException(r.getMsg());
        }
        List<SysRole> data = r.getData();
        for (PartnerListDTO partnerListDTO : list) {
            Integer roleId = partnerListDTO.getRoleId();
            if(null == roleId){
                continue;
            }
            SysRole sysRole = data.stream().filter(s -> roleId.compareTo(s.getRoleId().intValue()) == 0).findFirst().get();
            if(null != sysRole){
                partnerListDTO.setRoleName(sysRole.getRoleName());
            }
        }
        pageInfo.setRecords(list);
        return pageInfo;
    }
@@ -112,6 +92,7 @@
        if(ajaxResult.isError()){
            return ajaxResult;
        }
        partner.setRoleId(PARTNER_DEFAULT_ROLE_ID.intValue());
        Partner one = this.getOne(new LambdaQueryWrapper<Partner>()
                .eq(Partner::getCode, partner.getCode())
                .eq(Partner::getDelFlag, 0)
@@ -135,7 +116,7 @@
        user.setPassword(partner.getPassword());
        user.setStatus("0");
        user.setDelFlag("0");
        user.setRoleId(partner.getRoleId().longValue());
        user.setRoleId(PARTNER_DEFAULT_ROLE_ID);
        user.setRoleType(2);
        user.setPassWordUpdate(new Date());
        R<Long> r = sysUserClient.addSysUser(user);
@@ -163,9 +144,6 @@
        if(StringUtils.isEmpty(partner.getName())){
            return AjaxResult.error("合作商名称不能为空");
        }
        if(null == partner.getRoleId()){
            return AjaxResult.error("合作商角色不能为空");
        }
        if(StringUtils.isEmpty(partner.getPhoneOne())){
            return AjaxResult.error("合作商电话1不能为空");
        }
@@ -186,22 +164,7 @@
     */
    @Override
    public Partner getPartner(Integer id) {
        Partner partner = this.getById(id);
        if(null == partner){
            return partner;
        }
        GetSysRoleByIds ids = new GetSysRoleByIds();
        ids.setIds(Arrays.asList(partner.getRoleId()));
        //获取相关的角色id
        R<List<SysRole>> r = sysRoleClient.getSysRoleByIds(ids);
        if(r.getCode() != 200){
            throw new RuntimeException(r.getMsg());
        }
        List<SysRole> data = r.getData();
        if(data.size() > 0){
            partner.setRoleName(data.get(0).getRoleName());
        }
        return partner;
        return this.getById(id);
    }
    
    
@@ -217,6 +180,7 @@
        if(ajaxResult.isError()){
            return ajaxResult;
        }
        partner.setRoleId(PARTNER_DEFAULT_ROLE_ID.intValue());
        Partner one = this.getOne(new LambdaQueryWrapper<Partner>()
                .eq(Partner::getCode, partner.getCode())
                .eq(Partner::getDelFlag, 0)
@@ -233,6 +197,9 @@
        }
        Partner oldPartner = this.getById(partner.getId());
        SysUser user = sysUserClient.queryUserByUserName(oldPartner.getAccount()).getData();
        if(StringUtils.isEmpty(partner.getPassword())){
            partner.setPassword(oldPartner.getPassword());
        }
        //修改登录账户
        if(null == user){
            user = new SysUser();
@@ -243,7 +210,7 @@
            user.setNickName(partner.getName());
            user.setAvatar(partner.getLogoUrl());
            user.setPassword(partner.getPassword());
            user.setRoleId(partner.getRoleId().longValue());
            user.setRoleId(PARTNER_DEFAULT_ROLE_ID);
            user.setRoleType(2);
            user.setObjectId(partner.getId());
            user.setPassWordUpdate(new Date());
@@ -256,9 +223,11 @@
            user.setUserName(partner.getAccount());
            user.setNickName(partner.getName());
            user.setAvatar(partner.getLogoUrl());
            user.setPassword(partner.getPassword());
            user.setRoleId(partner.getRoleId().longValue());
            user.setPassWordUpdate(new Date());
            if(StringUtils.isNotEmpty(partner.getPassword())){
                user.setPassword(partner.getPassword());
                user.setPassWordUpdate(new Date());
            }
            user.setRoleId(PARTNER_DEFAULT_ROLE_ID);
            R r = sysUserClient.updateSysUser(user);
            if(200 != r.getCode()){
                throw new RuntimeException(r.getMsg());
ruoyi-service/ruoyi-chargingPile/src/main/resources/mapper/chargingPile/PartnerMapper.xml
@@ -10,6 +10,9 @@
        <result column="role_id" property="roleId" />
        <result column="logo_url" property="logoUrl" />
        <result column="rate" property="rate" />
        <result column="icbc_partner_id" property="icbcPartnerId" />
        <result column="electricity_share_ratio" property="electricityShareRatio" />
        <result column="service_share_ratio" property="serviceShareRatio" />
        <result column="address" property="address" />
        <result column="phone_one" property="phoneOne" />
        <result column="phone_two" property="phoneTwo" />
@@ -26,18 +29,16 @@
        select
        id,
        name,
        role_id as roleId,
        phone_one as phoneOne,
        address,
        account,
        remark
        remark,
        electricity_share_ratio as electricityShareRatio,
        service_share_ratio as serviceShareRatio
        from t_partner where del_flag = 0
        <if test="null != item.name and '' != item.name">
            and `name` like CONCAT('%', #{item.name}, '%')
        </if>
        <if test="null != item.roleId and '' != item.name">
            and role_id =  #{item.roleId}
        </if>
        order by create_time desc
    </select>
</mapper>
</mapper>
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/controller/TVipOrderController.java
@@ -30,7 +30,6 @@
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.apache.poi.ss.usermodel.Workbook;
import org.omg.CORBA.PRIVATE_MEMBER;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;
@@ -210,4 +209,3 @@
    }
ruoyi-service/ruoyi-order/src/main/java/com/ruoyi/order/service/impl/TChargingOrderServiceImpl.java
@@ -63,7 +63,9 @@
import com.ruoyi.other.api.domain.*;
import com.ruoyi.order.vo.ChargingOrderListInfoVO;
import com.ruoyi.other.api.domain.TVip;
import com.ruoyi.other.api.dto.ExchangeCodeConsumeDTO;
import com.ruoyi.other.api.feignClient.*;
import com.ruoyi.other.api.vo.ExchangeCodeConsumeResultVO;
import com.ruoyi.payment.api.feignClient.AliPaymentClient;
import com.ruoyi.payment.api.feignClient.H5AliPaymentClient;
import com.ruoyi.payment.api.feignClient.WxPaymentClient;
@@ -201,6 +203,8 @@
    
    @Resource
    private VipClient vipClient;
    @Resource
    private OtherClient otherClient;
    
    @Resource
    private UserTagClient userTagClient;
@@ -2672,6 +2676,22 @@
                }
                chargingOrder.setCouponDiscountAmount(couponDiscount.setScale(2, RoundingMode.HALF_EVEN));
                chargingOrder.setServiceCharge(serviceCharge_total.setScale(2, RoundingMode.HALF_EVEN));
                BigDecimal exchangeCodeDeductAmount = BigDecimal.ZERO;
                if (chargingOrder.getAppUserId() != null && payAmount.compareTo(BigDecimal.ZERO) > 0) {
                    ExchangeCodeConsumeDTO exchangeCodeConsumeDTO = new ExchangeCodeConsumeDTO();
                    exchangeCodeConsumeDTO.setUserId(chargingOrder.getAppUserId());
                    exchangeCodeConsumeDTO.setOrderId(chargingOrder.getId());
                    exchangeCodeConsumeDTO.setAmount(payAmount.setScale(2, RoundingMode.HALF_EVEN));
                    R<ExchangeCodeConsumeResultVO> consumeResult = otherClient.consumeExchangeCodeBalance(exchangeCodeConsumeDTO);
                    if (consumeResult != null && consumeResult.getData() != null
                            && consumeResult.getData().getDeductedAmount() != null) {
                        exchangeCodeDeductAmount = consumeResult.getData().getDeductedAmount();
                    }
                }
                payAmount = payAmount.subtract(exchangeCodeDeductAmount);
                if (payAmount.compareTo(BigDecimal.ZERO) < 0) {
                    payAmount = BigDecimal.ZERO;
                }
                chargingOrder.setPaymentAmount(payAmount.setScale(2, RoundingMode.HALF_EVEN));
                chargingOrder.setRefundAmount(refundAmount.setScale(2, RoundingMode.HALF_EVEN));
                chargingOrder.setRefundStatus(1);
ruoyi-service/ruoyi-other/pom.xml
@@ -132,9 +132,19 @@
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>easyexcel-core</artifactId>
            <version>RELEASE</version>
            <version>3.3.4</version>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>com.google.zxing</groupId>
            <artifactId>core</artifactId>
            <version>3.3.0</version>
        </dependency>
        <dependency>
            <groupId>com.google.zxing</groupId>
            <artifactId>javase</artifactId>
            <version>3.3.0</version>
        </dependency>
    </dependencies>
    <build>
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/controller/TExchangeCodePackageController.java
New file
@@ -0,0 +1,190 @@
package com.ruoyi.other.controller;
import cn.afterturn.easypoi.excel.ExcelExportUtil;
import cn.afterturn.easypoi.excel.entity.ExportParams;
import com.ruoyi.common.core.utils.WebUtils;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.other.util.QRCodeUtils;
import com.ruoyi.common.core.web.domain.AjaxResult;
import com.ruoyi.common.core.web.page.PageInfo;
import com.ruoyi.common.log.annotation.Log;
import com.ruoyi.common.log.enums.BusinessType;
import com.ruoyi.common.log.enums.OperatorType;
import com.ruoyi.common.security.annotation.Logical;
import com.ruoyi.common.security.annotation.RequiresPermissions;
import com.ruoyi.other.api.dto.ExchangeCodeBindDTO;
import com.ruoyi.other.api.dto.ExchangeCodeConsumeDTO;
import com.ruoyi.other.api.dto.ExchangeCodePackageSaveDTO;
import com.ruoyi.other.api.vo.ExchangeCodeBindResultVO;
import com.ruoyi.other.api.vo.ExchangeCodeConsumeResultVO;
import com.ruoyi.other.api.vo.ExchangeCodePackageDetailVO;
import com.ruoyi.other.query.ExchangeCodeBatchInvalidDTO;
import com.ruoyi.other.query.ExchangeCodeDetailQuery;
import com.ruoyi.other.query.ExchangeCodePackageQuery;
import com.ruoyi.other.service.TExchangeCodePackageService;
import com.ruoyi.other.vo.ExchangeCodeBatchInvalidResultVO;
import com.ruoyi.other.vo.ExchangeCodeDetailExportVO;
import com.ruoyi.other.vo.ExchangeCodeDetailVO;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.BeanUtils;
import org.apache.poi.ss.usermodel.Workbook;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.URLEncoder;
import java.util.List;
import java.math.BigDecimal;
import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
@RestController
@RequestMapping("/exchange-code-package")
public class TExchangeCodePackageController {
    @Resource
    private TExchangeCodePackageService exchangeCodePackageService;
    @RequiresPermissions(value = {"/exchangeCode"}, logical = Logical.OR)
    @PostMapping("/pageList")
    @ApiOperation(value = "兑换码卡包分页", tags = {"管理后台-兑换码管理"})
    public AjaxResult<PageInfo<?>> pageList(@RequestBody ExchangeCodePackageQuery query) {
        return AjaxResult.ok(exchangeCodePackageService.pageList(query));
    }
    @RequiresPermissions(value = {"/exchangeCode/add"}, logical = Logical.OR)
    @PostMapping("/save")
    @ApiOperation(value = "新增兑换码卡包", tags = {"管理后台-兑换码管理"})
    @Log(title = "【兑换码管理】新增卡包", businessType = BusinessType.INSERT, operatorType = OperatorType.MANAGE)
    public AjaxResult<Void> save(@RequestBody ExchangeCodePackageSaveDTO dto) {
        exchangeCodePackageService.savePackage(dto);
        return AjaxResult.success();
    }
    @RequiresPermissions(value = {"/exchangeCode/update"}, logical = Logical.OR)
    @PostMapping("/update")
    @ApiOperation(value = "编辑兑换码卡包", tags = {"管理后台-兑换码管理"})
    @Log(title = "【兑换码管理】编辑卡包", businessType = BusinessType.UPDATE, operatorType = OperatorType.MANAGE)
    public AjaxResult<Void> update(@RequestBody ExchangeCodePackageSaveDTO dto) {
        exchangeCodePackageService.updatePackage(dto);
        return AjaxResult.success();
    }
    @RequiresPermissions(value = {"/exchangeCode/del"}, logical = Logical.OR)
    @GetMapping("/delete")
    @ApiOperation(value = "删除兑换码卡包", tags = {"管理后台-兑换码管理"})
    @Log(title = "【兑换码管理】删除卡包", businessType = BusinessType.DELETE, operatorType = OperatorType.MANAGE)
    public AjaxResult<Void> delete(String ids) {
        exchangeCodePackageService.deletePackage(ids);
        return AjaxResult.success();
    }
    @RequiresPermissions(value = {"/exchangeCode/select"}, logical = Logical.OR)
    @GetMapping("/detail")
    @ApiOperation(value = "兑换码卡包详情", tags = {"管理后台-兑换码管理"})
    public AjaxResult<ExchangeCodePackageDetailVO> detail(@RequestParam("id") Long id) {
        return AjaxResult.ok(exchangeCodePackageService.getDetail(id));
    }
    @RequiresPermissions(value = {"/exchangeCode/select"}, logical = Logical.OR)
    @PostMapping("/codePage")
    @ApiOperation(value = "兑换码明细分页", tags = {"管理后台-兑换码管理"})
    public AjaxResult<PageInfo<?>> codePage(@RequestBody ExchangeCodeDetailQuery query) {
        return AjaxResult.ok(exchangeCodePackageService.codePage(query));
    }
    @RequiresPermissions(value = {"/exchangeCode/del"}, logical = Logical.OR)
    @PostMapping("/batchInvalidate")
    @ApiOperation(value = "批量失效兑换码", tags = {"管理后台-兑换码管理"})
    @Log(title = "【兑换码管理】批量失效", businessType = BusinessType.UPDATE, operatorType = OperatorType.MANAGE)
    public AjaxResult<ExchangeCodeBatchInvalidResultVO> batchInvalidate(@RequestBody ExchangeCodeBatchInvalidDTO dto) {
        ExchangeCodeBatchInvalidResultVO result = exchangeCodePackageService.batchInvalidate(dto);
        return AjaxResult.ok(result).put("msg", "已失效" + result.getSuccessCount() + "条,跳过" + result.getSkipCount() + "条");
    }
    @RequiresPermissions(value = {"/exchangeCode/select"}, logical = Logical.OR)
    @PutMapping("/export")
    @ApiOperation(value = "导出兑换码明细", tags = {"管理后台-兑换码管理"})
    @Log(title = "【兑换码管理】导出明细", businessType = BusinessType.EXPORT, operatorType = OperatorType.MANAGE)
    public void export(@RequestBody ExchangeCodeDetailQuery query) throws IOException {
        List<ExchangeCodeDetailVO> list = exchangeCodePackageService.exportCodes(query);
        List<ExchangeCodeDetailExportVO> exportList = new java.util.ArrayList<>();
        for (ExchangeCodeDetailVO detailVO : list) {
            ExchangeCodeDetailExportVO exportVO = new ExchangeCodeDetailExportVO();
            BeanUtils.copyProperties(detailVO, exportVO);
            exportList.add(exportVO);
        }
        Workbook workbook = ExcelExportUtil.exportExcel(new ExportParams(), ExchangeCodeDetailExportVO.class, exportList);
        HttpServletResponse response = WebUtils.response();
        response.setContentType("application/vnd.ms-excel;charset=UTF-8");
        response.setCharacterEncoding("utf-8");
        String fileName = URLEncoder.encode("兑换码明细.xls", "utf-8");
        response.setHeader("Content-Disposition", "attachment;filename=" + fileName);
        ServletOutputStream outputStream = response.getOutputStream();
        workbook.write(outputStream);
        outputStream.flush();
        outputStream.close();
    }
    @RequiresPermissions(value = {"/exchangeCode/select"}, logical = Logical.OR)
    @GetMapping("/downloadQRCode/{id}")
    @ApiOperation(value = "下载单个兑换码二维码", tags = {"管理后台-兑换码管理"})
    @Log(title = "【兑换码管理】下载二维码", businessType = BusinessType.OTHER, operatorType = OperatorType.MANAGE)
    public void downloadQRCode(@PathVariable("id") Long id) throws Exception {
        ExchangeCodeDetailVO detail = exchangeCodePackageService.getCodeDetail(id);
        HttpServletResponse response = WebUtils.response();
        response.setContentType("image/jpeg");
        response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(detail.getCode() + ".jpg", "UTF-8"));
        QRCodeUtils.encode(buildQrPayload(detail), response.getOutputStream());
    }
    @RequiresPermissions(value = {"/exchangeCode/select"}, logical = Logical.OR)
    @PutMapping("/downloadUnboundQRCodes")
    @ApiOperation(value = "批量下载未绑定二维码", tags = {"管理后台-兑换码管理"})
    @Log(title = "【兑换码管理】批量下载未绑定二维码", businessType = BusinessType.EXPORT, operatorType = OperatorType.MANAGE)
    public void downloadUnboundQRCodes(@RequestBody ExchangeCodeDetailQuery query) throws Exception {
        List<ExchangeCodeDetailVO> list = exchangeCodePackageService.listDownloadableCodes(query.getPackageId(), true);
        HttpServletResponse response = WebUtils.response();
        response.setContentType("application/zip");
        response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode("未绑定二维码.zip", "UTF-8"));
        ZipOutputStream zipOutputStream = new ZipOutputStream(response.getOutputStream());
        for (ExchangeCodeDetailVO detail : list) {
            zipOutputStream.putNextEntry(new ZipEntry(detail.getCode() + ".jpg"));
            ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
            QRCodeUtils.encode(buildQrPayload(detail), outputStream);
            zipOutputStream.write(outputStream.toByteArray());
            zipOutputStream.closeEntry();
        }
        zipOutputStream.finish();
        zipOutputStream.flush();
        zipOutputStream.close();
    }
    @PostMapping("/bind")
    @ApiOperation(value = "小程序兑换码绑定", tags = {"小程序-兑换码"})
    public R<ExchangeCodeBindResultVO> bind(@RequestBody ExchangeCodeBindDTO dto,
                                            @RequestParam("userId") Long userId,
                                            @RequestParam("phone") String phone) {
        return R.ok(exchangeCodePackageService.bindExchangeCode(dto, userId, phone));
    }
    @PostMapping("/balanceMap")
    @ApiOperation(value = "查询用户兑换码余额Map", tags = {"内部-兑换码"})
    public R<Map<Long, BigDecimal>> balanceMap(@RequestBody List<Long> userIds) {
        return R.ok(exchangeCodePackageService.getUserExchangeCodeBalanceMap(userIds));
    }
    @PostMapping("/consumeBalance")
    @ApiOperation(value = "扣减用户兑换码余额", tags = {"内部-兑换码"})
    public R<ExchangeCodeConsumeResultVO> consumeBalance(@RequestBody ExchangeCodeConsumeDTO dto) {
        return R.ok(exchangeCodePackageService.consumeBalance(dto));
    }
    private String buildQrPayload(ExchangeCodeDetailVO detail) {
        return "code=" + detail.getCode() + "&password=" + detail.getPassword();
    }
}
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/mapper/TExchangeCodeDetailMapper.java
New file
@@ -0,0 +1,18 @@
package com.ruoyi.other.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.common.core.web.page.PageInfo;
import com.ruoyi.other.api.domain.TExchangeCodeDetail;
import com.ruoyi.other.vo.ExchangeCodeDetailVO;
import com.ruoyi.other.query.ExchangeCodeDetailQuery;
import org.apache.ibatis.annotations.Param;
import java.util.List;
public interface TExchangeCodeDetailMapper extends BaseMapper<TExchangeCodeDetail> {
    List<ExchangeCodeDetailVO> pageList(@Param("pageInfo") PageInfo<ExchangeCodeDetailVO> pageInfo,
                                        @Param("req") ExchangeCodeDetailQuery query);
    List<ExchangeCodeDetailVO> exportList(@Param("req") ExchangeCodeDetailQuery query);
}
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/mapper/TExchangeCodePackageMapper.java
New file
@@ -0,0 +1,15 @@
package com.ruoyi.other.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.common.core.web.page.PageInfo;
import com.ruoyi.other.api.domain.TExchangeCodePackage;
import com.ruoyi.other.query.ExchangeCodePackageQuery;
import org.apache.ibatis.annotations.Param;
import java.util.List;
public interface TExchangeCodePackageMapper extends BaseMapper<TExchangeCodePackage> {
    List<TExchangeCodePackage> pageList(@Param("pageInfo") PageInfo<TExchangeCodePackage> pageInfo,
                                        @Param("req") ExchangeCodePackageQuery query);
}
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/mapper/TExchangeCodePackageSpecMapper.java
New file
@@ -0,0 +1,7 @@
package com.ruoyi.other.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.other.api.domain.TExchangeCodePackageSpec;
public interface TExchangeCodePackageSpecMapper extends BaseMapper<TExchangeCodePackageSpec> {
}
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/mapper/TExchangeCodeUseRecordMapper.java
New file
@@ -0,0 +1,7 @@
package com.ruoyi.other.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.other.api.domain.TExchangeCodeUseRecord;
public interface TExchangeCodeUseRecordMapper extends BaseMapper<TExchangeCodeUseRecord> {
}
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/query/ExchangeCodeBatchInvalidDTO.java
New file
@@ -0,0 +1,18 @@
package com.ruoyi.other.query;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.List;
@Data
@ApiModel("兑换码批量失效DTO")
public class ExchangeCodeBatchInvalidDTO {
    @ApiModelProperty("卡包ID")
    private Long packageId;
    @ApiModelProperty("明细ID列表")
    private List<Long> detailIds;
}
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/query/ExchangeCodeDetailQuery.java
New file
@@ -0,0 +1,23 @@
package com.ruoyi.other.query;
import com.ruoyi.common.core.web.page.BasePage;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@Data
@ApiModel("兑换码明细分页查询DTO")
public class ExchangeCodeDetailQuery extends BasePage {
    @ApiModelProperty("卡包ID")
    private Long packageId;
    @ApiModelProperty("兑换码/卡号")
    private String code;
    @ApiModelProperty("绑定手机号")
    private String bindPhone;
    @ApiModelProperty("状态 1未绑定 2已绑定 3已失效")
    private Integer status;
}
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/query/ExchangeCodePackageQuery.java
New file
@@ -0,0 +1,17 @@
package com.ruoyi.other.query;
import com.ruoyi.common.core.web.page.BasePage;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
@Data
@ApiModel("兑换码卡包分页查询DTO")
public class ExchangeCodePackageQuery extends BasePage {
    @ApiModelProperty("卡包名称")
    private String packageName;
    @ApiModelProperty("状态 1启用 2禁用")
    private Integer status;
}
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/service/TExchangeCodePackageService.java
New file
@@ -0,0 +1,48 @@
package com.ruoyi.other.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.ruoyi.common.core.web.page.PageInfo;
import com.ruoyi.other.api.domain.TExchangeCodePackage;
import com.ruoyi.other.api.dto.ExchangeCodeBindDTO;
import com.ruoyi.other.api.dto.ExchangeCodeConsumeDTO;
import com.ruoyi.other.api.dto.ExchangeCodePackageSaveDTO;
import com.ruoyi.other.api.vo.ExchangeCodeBindResultVO;
import com.ruoyi.other.api.vo.ExchangeCodeConsumeResultVO;
import com.ruoyi.other.api.vo.ExchangeCodePackageDetailVO;
import com.ruoyi.other.query.ExchangeCodeBatchInvalidDTO;
import com.ruoyi.other.query.ExchangeCodeDetailQuery;
import com.ruoyi.other.query.ExchangeCodePackageQuery;
import com.ruoyi.other.vo.ExchangeCodeBatchInvalidResultVO;
import com.ruoyi.other.vo.ExchangeCodeDetailVO;
import java.util.List;
import java.util.Map;
public interface TExchangeCodePackageService extends IService<TExchangeCodePackage> {
    PageInfo<TExchangeCodePackage> pageList(ExchangeCodePackageQuery query);
    void savePackage(ExchangeCodePackageSaveDTO dto);
    void updatePackage(ExchangeCodePackageSaveDTO dto);
    void deletePackage(String ids);
    ExchangeCodePackageDetailVO getDetail(Long id);
    PageInfo<ExchangeCodeDetailVO> codePage(ExchangeCodeDetailQuery query);
    List<ExchangeCodeDetailVO> exportCodes(ExchangeCodeDetailQuery query);
    ExchangeCodeBatchInvalidResultVO batchInvalidate(ExchangeCodeBatchInvalidDTO dto);
    List<ExchangeCodeDetailVO> listDownloadableCodes(Long packageId, boolean onlyUnbound);
    ExchangeCodeDetailVO getCodeDetail(Long id);
    ExchangeCodeBindResultVO bindExchangeCode(ExchangeCodeBindDTO dto, Long userId, String phone);
    Map<Long, java.math.BigDecimal> getUserExchangeCodeBalanceMap(List<Long> userIds);
    ExchangeCodeConsumeResultVO consumeBalance(ExchangeCodeConsumeDTO dto);
}
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/service/impl/TExchangeCodePackageServiceImpl.java
New file
@@ -0,0 +1,537 @@
package com.ruoyi.other.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.common.core.exception.ServiceException;
import com.ruoyi.common.core.utils.StringUtils;
import com.ruoyi.common.core.web.page.PageInfo;
import com.ruoyi.other.api.domain.TExchangeCodeDetail;
import com.ruoyi.other.api.domain.TExchangeCodePackage;
import com.ruoyi.other.api.domain.TExchangeCodePackageSpec;
import com.ruoyi.other.api.domain.TExchangeCodeUseRecord;
import com.ruoyi.other.api.dto.ExchangeCodeBindDTO;
import com.ruoyi.other.api.dto.ExchangeCodeConsumeDTO;
import com.ruoyi.other.api.dto.ExchangeCodePackageSaveDTO;
import com.ruoyi.other.api.dto.ExchangeCodePackageSpecDTO;
import com.ruoyi.other.api.vo.ExchangeCodeBindResultVO;
import com.ruoyi.other.api.vo.ExchangeCodeConsumeResultVO;
import com.ruoyi.other.api.vo.ExchangeCodePackageDetailVO;
import com.ruoyi.other.api.vo.ExchangeCodePackageSpecVO;
import com.ruoyi.other.mapper.TExchangeCodeDetailMapper;
import com.ruoyi.other.mapper.TExchangeCodePackageMapper;
import com.ruoyi.other.mapper.TExchangeCodePackageSpecMapper;
import com.ruoyi.other.mapper.TExchangeCodeUseRecordMapper;
import com.ruoyi.other.query.ExchangeCodeBatchInvalidDTO;
import com.ruoyi.other.query.ExchangeCodeDetailQuery;
import com.ruoyi.other.query.ExchangeCodePackageQuery;
import com.ruoyi.other.service.TExchangeCodePackageService;
import com.ruoyi.other.vo.ExchangeCodeBatchInvalidResultVO;
import com.ruoyi.other.vo.ExchangeCodeDetailVO;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.stream.Collectors;
@Service
public class TExchangeCodePackageServiceImpl extends ServiceImpl<TExchangeCodePackageMapper, TExchangeCodePackage>
        implements TExchangeCodePackageService {
    private static final Integer STATUS_ENABLE = 1;
    private static final Integer STATUS_DISABLE = 2;
    private static final Integer CODE_STATUS_UNBOUND = 1;
    private static final Integer CODE_STATUS_BOUND = 2;
    private static final Integer CODE_STATUS_INVALID = 3;
    private static final DateTimeFormatter BATCH_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
    @Resource
    private TExchangeCodePackageSpecMapper packageSpecMapper;
    @Resource
    private TExchangeCodeDetailMapper codeDetailMapper;
    @Resource
    private TExchangeCodeUseRecordMapper useRecordMapper;
    @Override
    public PageInfo<TExchangeCodePackage> pageList(ExchangeCodePackageQuery query) {
        PageInfo<TExchangeCodePackage> pageInfo = new PageInfo<>(query.getPageCurr(), query.getPageSize());
        List<TExchangeCodePackage> records = this.baseMapper.pageList(pageInfo, query);
        pageInfo.setRecords(records);
        return pageInfo;
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void savePackage(ExchangeCodePackageSaveDTO dto) {
        validateSaveDTO(dto, false);
        TExchangeCodePackage entity = buildPackageEntity(dto, new TExchangeCodePackage());
        this.save(entity);
        saveSpecsAndCodes(entity.getId(), dto.getSpecList(), dto.getInventory(), null);
        refreshPackageStats(entity.getId());
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void updatePackage(ExchangeCodePackageSaveDTO dto) {
        if (dto.getId() == null) {
            throw new ServiceException("卡包ID不能为空");
        }
        validateSaveDTO(dto, true);
        TExchangeCodePackage old = this.getById(dto.getId());
        if (old == null) {
            throw new ServiceException("卡包不存在");
        }
        if (dto.getInventory() < old.getInventory()) {
            throw new ServiceException("编辑时库存只能增加");
        }
        TExchangeCodePackage entity = buildPackageEntity(dto, old);
        this.updateById(entity);
        packageSpecMapper.delete(new QueryWrapper<TExchangeCodePackageSpec>().eq("package_id", dto.getId()));
        saveSpecsAndCodes(dto.getId(), dto.getSpecList(), dto.getInventory(), old.getInventory());
        refreshPackageStats(dto.getId());
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void deletePackage(String ids) {
        if (!StringUtils.hasText(ids)) {
            throw new ServiceException("卡包ID不能为空");
        }
        List<Long> idList = Arrays.stream(ids.split(","))
                .filter(StringUtils::hasText)
                .map(Long::valueOf)
                .collect(Collectors.toList());
        if (idList.isEmpty()) {
            throw new ServiceException("卡包ID不能为空");
        }
        Long boundCount = codeDetailMapper.selectCount(new QueryWrapper<TExchangeCodeDetail>()
                .in("package_id", idList)
                .eq("status", CODE_STATUS_BOUND)
                .eq("del_flag", 0));
        if (boundCount != null && boundCount > 0) {
            throw new ServiceException("存在已绑定兑换码,不允许删除");
        }
        this.removeByIds(idList);
        packageSpecMapper.delete(new QueryWrapper<TExchangeCodePackageSpec>().in("package_id", idList));
        codeDetailMapper.delete(new QueryWrapper<TExchangeCodeDetail>().in("package_id", idList));
    }
    @Override
    public ExchangeCodePackageDetailVO getDetail(Long id) {
        TExchangeCodePackage entity = this.getById(id);
        if (entity == null) {
            throw new ServiceException("卡包不存在");
        }
        refreshPackageStats(id);
        entity = this.getById(id);
        ExchangeCodePackageDetailVO detailVO = new ExchangeCodePackageDetailVO();
        BeanUtils.copyProperties(entity, detailVO);
        List<TExchangeCodePackageSpec> specs = packageSpecMapper.selectList(new QueryWrapper<TExchangeCodePackageSpec>()
                .eq("package_id", id)
                .orderByAsc("sort", "id"));
        List<ExchangeCodePackageSpecVO> specVOList = new ArrayList<>();
        for (TExchangeCodePackageSpec spec : specs) {
            ExchangeCodePackageSpecVO vo = new ExchangeCodePackageSpecVO();
            BeanUtils.copyProperties(spec, vo);
            Long boundCount = codeDetailMapper.selectCount(new QueryWrapper<TExchangeCodeDetail>()
                    .eq("package_spec_id", spec.getId())
                    .eq("status", CODE_STATUS_BOUND)
                    .eq("del_flag", 0));
            Long unboundCount = codeDetailMapper.selectCount(new QueryWrapper<TExchangeCodeDetail>()
                    .eq("package_spec_id", spec.getId())
                    .eq("status", CODE_STATUS_UNBOUND)
                    .eq("del_flag", 0));
            vo.setBoundCount(boundCount == null ? 0 : boundCount.intValue());
            vo.setUnboundCount(unboundCount == null ? 0 : unboundCount.intValue());
            specVOList.add(vo);
        }
        detailVO.setSpecList(specVOList);
        return detailVO;
    }
    @Override
    public PageInfo<ExchangeCodeDetailVO> codePage(ExchangeCodeDetailQuery query) {
        PageInfo<ExchangeCodeDetailVO> pageInfo = new PageInfo<>(query.getPageCurr(), query.getPageSize());
        List<ExchangeCodeDetailVO> records = codeDetailMapper.pageList(pageInfo, query);
        pageInfo.setRecords(records);
        return pageInfo;
    }
    @Override
    public List<ExchangeCodeDetailVO> exportCodes(ExchangeCodeDetailQuery query) {
        return codeDetailMapper.exportList(query);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public ExchangeCodeBatchInvalidResultVO batchInvalidate(ExchangeCodeBatchInvalidDTO dto) {
        if (dto.getPackageId() == null || dto.getDetailIds() == null || dto.getDetailIds().isEmpty()) {
            throw new ServiceException("请选择需要失效的兑换码");
        }
        int success = 0;
        int skip = 0;
        List<TExchangeCodeDetail> details = codeDetailMapper.selectBatchIds(dto.getDetailIds());
        for (TExchangeCodeDetail detail : details) {
            if (!Objects.equals(detail.getPackageId(), dto.getPackageId())) {
                continue;
            }
            if (Objects.equals(detail.getStatus(), CODE_STATUS_UNBOUND)) {
                detail.setStatus(CODE_STATUS_INVALID);
                detail.setInvalidTime(LocalDateTime.now());
                detail.setInvalidReason("后台批量失效");
                codeDetailMapper.updateById(detail);
                success++;
            } else {
                skip++;
            }
        }
        refreshPackageStats(dto.getPackageId());
        return new ExchangeCodeBatchInvalidResultVO(success, skip);
    }
    @Override
    public List<ExchangeCodeDetailVO> listDownloadableCodes(Long packageId, boolean onlyUnbound) {
        ExchangeCodeDetailQuery query = new ExchangeCodeDetailQuery();
        query.setPackageId(packageId);
        if (onlyUnbound) {
            query.setStatus(CODE_STATUS_UNBOUND);
        }
        return exportCodes(query);
    }
    @Override
    public ExchangeCodeDetailVO getCodeDetail(Long id) {
        TExchangeCodeDetail detail = codeDetailMapper.selectById(id);
        if (detail == null || Boolean.TRUE.equals(detail.getDelFlag())) {
            throw new ServiceException("兑换码不存在");
        }
        ExchangeCodeDetailVO vo = new ExchangeCodeDetailVO();
        BeanUtils.copyProperties(detail, vo);
        if (detail.getPackageSpecId() != null) {
            TExchangeCodePackageSpec spec = packageSpecMapper.selectById(detail.getPackageSpecId());
            if (spec != null) {
                vo.setAmount(spec.getAmount());
                vo.setQuantity(spec.getQuantity());
            }
        }
        return vo;
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public ExchangeCodeBindResultVO bindExchangeCode(ExchangeCodeBindDTO dto, Long userId, String phone) {
        if (!StringUtils.hasText(dto.getCode()) || !StringUtils.hasText(dto.getPassword())) {
            throw new ServiceException("兑换码或密码不能为空");
        }
        TExchangeCodeDetail detail = codeDetailMapper.selectOne(new QueryWrapper<TExchangeCodeDetail>()
                .eq("code", dto.getCode())
                .eq("password", dto.getPassword())
                .last("limit 1"));
        if (detail == null) {
            throw new ServiceException("兑换码或密码错误");
        }
        TExchangeCodePackage exchangePackage = this.getById(detail.getPackageId());
        if (exchangePackage == null || !Objects.equals(exchangePackage.getStatus(), STATUS_ENABLE)) {
            throw new ServiceException("卡包已下架");
        }
        if (exchangePackage.getExpireTime() != null && exchangePackage.getExpireTime().isBefore(LocalDateTime.now())) {
            throw new ServiceException("已失效");
        }
        if (Objects.equals(detail.getStatus(), CODE_STATUS_INVALID)) {
            throw new ServiceException("卡号失效");
        }
        if (Objects.equals(detail.getStatus(), CODE_STATUS_BOUND)) {
            throw new ServiceException("兑换码已绑定");
        }
        String bindPhone = StringUtils.hasText(dto.getPhone()) ? dto.getPhone() : phone;
        if (!StringUtils.hasText(bindPhone)) {
            throw new ServiceException("绑定手机号不能为空");
        }
        if (Objects.equals(exchangePackage.getUnlimitedReceiveLimit(), 0) && exchangePackage.getReceiveLimit() != null) {
            Long bindCount = codeDetailMapper.selectCount(new QueryWrapper<TExchangeCodeDetail>()
                    .eq("package_id", exchangePackage.getId())
                    .eq("bind_phone", bindPhone)
                    .eq("status", CODE_STATUS_BOUND)
                    .eq("del_flag", 0));
            if (bindCount >= exchangePackage.getReceiveLimit()) {
                throw new ServiceException("该手机号领取次数已达上限");
            }
        }
        detail.setStatus(CODE_STATUS_BOUND);
        detail.setBindUserId(userId);
        detail.setBindPhone(bindPhone);
        detail.setBindTime(LocalDateTime.now());
        codeDetailMapper.updateById(detail);
        refreshPackageStats(exchangePackage.getId());
        ExchangeCodeBindResultVO resultVO = new ExchangeCodeBindResultVO();
        resultVO.setDetailId(detail.getId());
        resultVO.setPackageId(exchangePackage.getId());
        resultVO.setPackageName(exchangePackage.getPackageName());
        resultVO.setBindPhone(bindPhone);
        return resultVO;
    }
    @Override
    public Map<Long, BigDecimal> getUserExchangeCodeBalanceMap(List<Long> userIds) {
        Map<Long, BigDecimal> result = new HashMap<>();
        if (userIds == null || userIds.isEmpty()) {
            return result;
        }
        List<TExchangeCodeDetail> details = codeDetailMapper.selectList(new QueryWrapper<TExchangeCodeDetail>()
                .in("bind_user_id", userIds)
                .eq("status", CODE_STATUS_BOUND)
                .eq("del_flag", 0));
        for (TExchangeCodeDetail detail : details) {
            BigDecimal remainAmount = getDetailRemainAmount(detail);
            if (remainAmount.compareTo(BigDecimal.ZERO) > 0) {
                result.merge(detail.getBindUserId(), remainAmount, BigDecimal::add);
            }
        }
        return result;
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public ExchangeCodeConsumeResultVO consumeBalance(ExchangeCodeConsumeDTO dto) {
        if (dto == null || dto.getUserId() == null || dto.getOrderId() == null || dto.getAmount() == null) {
            throw new ServiceException("扣减参数不能为空");
        }
        ExchangeCodeConsumeResultVO resultVO = new ExchangeCodeConsumeResultVO();
        BigDecimal requestedAmount = dto.getAmount().setScale(2, RoundingMode.HALF_UP);
        if (requestedAmount.compareTo(BigDecimal.ZERO) <= 0) {
            resultVO.setDeductedAmount(BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP));
            return resultVO;
        }
        List<TExchangeCodeUseRecord> existingRecords = useRecordMapper.selectList(new QueryWrapper<TExchangeCodeUseRecord>()
                .eq("order_id", dto.getOrderId())
                .eq("del_flag", 0));
        if (!existingRecords.isEmpty()) {
            BigDecimal deducted = existingRecords.stream()
                    .map(TExchangeCodeUseRecord::getUsedAmount)
                    .filter(Objects::nonNull)
                    .reduce(BigDecimal.ZERO, BigDecimal::add)
                    .setScale(2, RoundingMode.HALF_UP);
            resultVO.setDeductedAmount(deducted);
            return resultVO;
        }
        BigDecimal remainingNeed = requestedAmount;
        BigDecimal deductedAmount = BigDecimal.ZERO;
        List<TExchangeCodeDetail> details = codeDetailMapper.selectList(new QueryWrapper<TExchangeCodeDetail>()
                .eq("bind_user_id", dto.getUserId())
                .eq("status", CODE_STATUS_BOUND)
                .eq("del_flag", 0)
                .orderByAsc("bind_time", "id"));
        for (TExchangeCodeDetail detail : details) {
            if (remainingNeed.compareTo(BigDecimal.ZERO) <= 0) {
                break;
            }
            BigDecimal remainAmount = getDetailRemainAmount(detail);
            if (remainAmount.compareTo(BigDecimal.ZERO) <= 0) {
                continue;
            }
            BigDecimal useAmount = remainAmount.min(remainingNeed).setScale(2, RoundingMode.HALF_UP);
            detail.setUsedAmount(defaultMoney(detail.getUsedAmount()).add(useAmount).setScale(2, RoundingMode.HALF_UP));
            codeDetailMapper.updateById(detail);
            TExchangeCodeUseRecord record = new TExchangeCodeUseRecord();
            record.setDetailId(detail.getId());
            record.setOrderId(dto.getOrderId());
            record.setUserId(dto.getUserId());
            record.setUsedAmount(useAmount);
            useRecordMapper.insert(record);
            remainingNeed = remainingNeed.subtract(useAmount);
            deductedAmount = deductedAmount.add(useAmount);
        }
        resultVO.setDeductedAmount(deductedAmount.setScale(2, RoundingMode.HALF_UP));
        return resultVO;
    }
    private BigDecimal getDetailRemainAmount(TExchangeCodeDetail detail) {
        if (detail == null || detail.getPackageSpecId() == null) {
            return BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
        }
        TExchangeCodePackageSpec spec = packageSpecMapper.selectById(detail.getPackageSpecId());
        if (spec == null || spec.getAmount() == null) {
            return BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
        }
        BigDecimal remainAmount = spec.getAmount().subtract(defaultMoney(detail.getUsedAmount()));
        return remainAmount.compareTo(BigDecimal.ZERO) < 0
                ? BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP)
                : remainAmount.setScale(2, RoundingMode.HALF_UP);
    }
    private TExchangeCodePackage buildPackageEntity(ExchangeCodePackageSaveDTO dto, TExchangeCodePackage entity) {
        entity.setPackageName(dto.getPackageName());
        entity.setCoverPicture(dto.getCoverPicture());
        entity.setDetailsPicture(dto.getDetailsPicture());
        entity.setExchangeDescription(dto.getExchangeDescription());
        entity.setRemark(dto.getRemark());
        entity.setInventory(dto.getInventory());
        entity.setUnlimitedReceiveLimit(dto.getUnlimitedReceiveLimit());
        entity.setReceiveLimit(Objects.equals(dto.getUnlimitedReceiveLimit(), 1) ? null : dto.getReceiveLimit());
        entity.setFinanceReceivedAmount(defaultMoney(dto.getFinanceReceivedAmount()));
        entity.setGiftAmount(defaultMoney(dto.getGiftAmount()));
        entity.setTotalAmount(defaultMoney(dto.getTotalAmount()));
        entity.setExpireTime(dto.getExpireTime());
        entity.setStatus(dto.getStatus());
        return entity;
    }
    private void validateSaveDTO(ExchangeCodePackageSaveDTO dto, boolean editing) {
        if (!StringUtils.hasText(dto.getPackageName())) {
            throw new ServiceException("请输入卡包名称");
        }
        if (dto.getInventory() == null || dto.getInventory() <= 0) {
            throw new ServiceException("库存必须大于0");
        }
        if (dto.getExpireTime() != null && dto.getExpireTime().isBefore(LocalDateTime.now())) {
            throw new ServiceException("截止领取时间不能早于当前时间");
        }
        if (!Objects.equals(dto.getUnlimitedReceiveLimit(), 1)
                && (dto.getReceiveLimit() == null || dto.getReceiveLimit() <= 0)) {
            throw new ServiceException("请输入领取限制次数");
        }
        if (dto.getSpecList() == null || dto.getSpecList().isEmpty()) {
            throw new ServiceException("请至少添加一个规格");
        }
        if (dto.getStatus() == null) {
            dto.setStatus(STATUS_DISABLE);
        }
        BigDecimal finance = defaultMoney(dto.getFinanceReceivedAmount());
        BigDecimal gift = defaultMoney(dto.getGiftAmount());
        BigDecimal total = defaultMoney(dto.getTotalAmount());
        if (finance.add(gift).compareTo(total) != 0) {
            throw new ServiceException("财务实收 + 赠送金额 必须等于规格总金额");
        }
        BigDecimal specTotal = BigDecimal.ZERO;
        int quantityTotal = 0;
        for (ExchangeCodePackageSpecDTO spec : dto.getSpecList()) {
            if (spec.getAmount() == null || spec.getAmount().compareTo(BigDecimal.ZERO) < 0) {
                throw new ServiceException("规格金额不正确");
            }
            if (spec.getQuantity() == null || spec.getQuantity() <= 0) {
                throw new ServiceException("规格数量必须大于0");
            }
            BigDecimal subtotal = spec.getAmount().multiply(BigDecimal.valueOf(spec.getQuantity()))
                    .setScale(2, RoundingMode.HALF_UP);
            spec.setSubtotalAmount(subtotal);
            specTotal = specTotal.add(subtotal);
            quantityTotal += spec.getQuantity();
        }
        if (specTotal.compareTo(total) != 0) {
            throw new ServiceException("总额必须等于所有规格金额乘数量之和");
        }
        if (quantityTotal != dto.getInventory()) {
            throw new ServiceException(editing ? "编辑后的库存必须等于规格数量总和" : "库存必须等于规格数量总和");
        }
        allocateDiscount(dto.getSpecList(), gift, total);
    }
    private void allocateDiscount(List<ExchangeCodePackageSpecDTO> specList, BigDecimal giftAmount, BigDecimal totalAmount) {
        if (giftAmount.compareTo(BigDecimal.ZERO) == 0 || totalAmount.compareTo(BigDecimal.ZERO) == 0) {
            for (ExchangeCodePackageSpecDTO spec : specList) {
                spec.setDiscountAmount(BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP));
            }
            return;
        }
        BigDecimal allocated = BigDecimal.ZERO;
        for (int i = 0; i < specList.size(); i++) {
            ExchangeCodePackageSpecDTO spec = specList.get(i);
            BigDecimal discount;
            if (i == specList.size() - 1) {
                discount = giftAmount.subtract(allocated).setScale(2, RoundingMode.HALF_UP);
            } else {
                discount = spec.getSubtotalAmount().multiply(giftAmount)
                        .divide(totalAmount, 2, RoundingMode.HALF_UP);
                allocated = allocated.add(discount);
            }
            spec.setDiscountAmount(discount);
        }
    }
    private void saveSpecsAndCodes(Long packageId, List<ExchangeCodePackageSpecDTO> specList, Integer inventory, Integer oldInventory) {
        List<TExchangeCodePackageSpec> specEntities = new ArrayList<>();
        for (int i = 0; i < specList.size(); i++) {
            ExchangeCodePackageSpecDTO spec = specList.get(i);
            TExchangeCodePackageSpec entity = new TExchangeCodePackageSpec();
            entity.setPackageId(packageId);
            entity.setAmount(spec.getAmount());
            entity.setQuantity(spec.getQuantity());
            entity.setSubtotalAmount(spec.getSubtotalAmount());
            entity.setDiscountAmount(spec.getDiscountAmount());
            entity.setSort(spec.getSort() == null ? i + 1 : spec.getSort());
            packageSpecMapper.insert(entity);
            specEntities.add(entity);
        }
        int needCreate = oldInventory == null ? inventory : inventory - oldInventory;
        if (needCreate <= 0) {
            return;
        }
        String batchNo = "PKG" + LocalDateTime.now().format(BATCH_FORMATTER);
        List<TExchangeCodePackageSpec> expandList = new ArrayList<>();
        for (TExchangeCodePackageSpec spec : specEntities) {
            for (int i = 0; i < spec.getQuantity(); i++) {
                expandList.add(spec);
            }
        }
        if (expandList.isEmpty()) {
            expandList = Collections.nCopies(needCreate, null);
        }
        for (int i = 0; i < needCreate; i++) {
            TExchangeCodePackageSpec spec = expandList.get(i % expandList.size());
            TExchangeCodeDetail detail = new TExchangeCodeDetail();
            detail.setPackageId(packageId);
            detail.setPackageSpecId(spec == null ? null : spec.getId());
            detail.setCode(generateCode(packageId, i));
            detail.setPassword(generatePassword());
            detail.setStatus(CODE_STATUS_UNBOUND);
            detail.setBatchNo(batchNo);
            codeDetailMapper.insert(detail);
        }
    }
    private void refreshPackageStats(Long packageId) {
        TExchangeCodePackage entity = this.getById(packageId);
        if (entity == null) {
            return;
        }
        Long boundCount = codeDetailMapper.selectCount(new QueryWrapper<TExchangeCodeDetail>()
                .eq("package_id", packageId)
                .eq("status", CODE_STATUS_BOUND)
                .eq("del_flag", 0));
        Long invalidCount = codeDetailMapper.selectCount(new QueryWrapper<TExchangeCodeDetail>()
                .eq("package_id", packageId)
                .eq("status", CODE_STATUS_INVALID)
                .eq("del_flag", 0));
        entity.setBoundCount(boundCount.intValue());
        entity.setInvalidCount(invalidCount.intValue());
        entity.setUnboundCount(entity.getInventory() - entity.getBoundCount() - entity.getInvalidCount());
        this.baseMapper.updateById(entity);
    }
    private String generateCode(Long packageId, int seq) {
        return "CD" + packageId + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyMMddHHmmss")) + String.format("%03d", seq);
    }
    private String generatePassword() {
        return UUID.randomUUID().toString().replace("-", "").substring(0, 8).toUpperCase();
    }
    private BigDecimal defaultMoney(BigDecimal amount) {
        return amount == null ? BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP)
                : amount.setScale(2, RoundingMode.HALF_UP);
    }
}
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/util/QRCodeUtils.java
New file
@@ -0,0 +1,43 @@
package com.ruoyi.other.util;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.EncodeHintType;
import com.google.zxing.MultiFormatWriter;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.OutputStream;
import java.util.Hashtable;
public class QRCodeUtils {
    private static final String CHARSET = "utf-8";
    private static final String FORMAT_NAME = "JPG";
    private static final int QRCODE_SIZE = 300;
    private QRCodeUtils() {
    }
    public static void encode(String content, OutputStream output) throws Exception {
        Hashtable<EncodeHintType, Object> hints = new Hashtable<>();
        hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H);
        hints.put(EncodeHintType.CHARACTER_SET, CHARSET);
        hints.put(EncodeHintType.MARGIN, 1);
        BitMatrix bitMatrix = new MultiFormatWriter().encode(
                content,
                BarcodeFormat.QR_CODE,
                QRCODE_SIZE,
                QRCODE_SIZE,
                hints
        );
        BufferedImage image = new BufferedImage(QRCODE_SIZE, QRCODE_SIZE, BufferedImage.TYPE_INT_RGB);
        for (int x = 0; x < QRCODE_SIZE; x++) {
            for (int y = 0; y < QRCODE_SIZE; y++) {
                image.setRGB(x, y, bitMatrix.get(x, y) ? 0xFF000000 : 0xFFFFFFFF);
            }
        }
        ImageIO.write(image, FORMAT_NAME, output);
    }
}
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/vo/ExchangeCodeBatchInvalidResultVO.java
New file
@@ -0,0 +1,20 @@
package com.ruoyi.other.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
@ApiModel("兑换码批量失效结果VO")
public class ExchangeCodeBatchInvalidResultVO {
    @ApiModelProperty("成功失效数量")
    private Integer successCount;
    @ApiModelProperty("跳过数量")
    private Integer skipCount;
}
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/vo/ExchangeCodeDetailExportVO.java
New file
@@ -0,0 +1,41 @@
package com.ruoyi.other.vo;
import cn.afterturn.easypoi.excel.annotation.Excel;
import io.swagger.annotations.ApiModel;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Data
@ApiModel("兑换码导出VO")
public class ExchangeCodeDetailExportVO implements Serializable {
    @Excel(name = "兑换码", width = 30)
    private String code;
    @Excel(name = "密码", width = 20)
    private String password;
    @Excel(name = "金额", width = 20)
    private BigDecimal amount;
    @Excel(name = "数量", width = 15)
    private Integer quantity;
    @Excel(name = "状态", replace = {"未绑定_1", "已绑定_2", "已失效_3"}, width = 20)
    private Integer status;
    @Excel(name = "绑定手机号", width = 20)
    private String bindPhone;
    @Excel(name = "绑定时间", exportFormat = "yyyy-MM-dd HH:mm:ss", width = 25)
    private LocalDateTime bindTime;
    @Excel(name = "失效时间", exportFormat = "yyyy-MM-dd HH:mm:ss", width = 25)
    private LocalDateTime invalidTime;
    @Excel(name = "失效原因", width = 25)
    private String invalidReason;
}
ruoyi-service/ruoyi-other/src/main/java/com/ruoyi/other/vo/ExchangeCodeDetailVO.java
New file
@@ -0,0 +1,48 @@
package com.ruoyi.other.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@ApiModel("兑换码明细VO")
public class ExchangeCodeDetailVO {
    @ApiModelProperty("明细ID")
    private Long id;
    @ApiModelProperty("卡包ID")
    private Long packageId;
    @ApiModelProperty("规格ID")
    private Long packageSpecId;
    @ApiModelProperty("金额")
    private java.math.BigDecimal amount;
    @ApiModelProperty("数量")
    private Integer quantity;
    @ApiModelProperty("兑换码")
    private String code;
    @ApiModelProperty("密码")
    private String password;
    @ApiModelProperty("状态 1未绑定 2已绑定 3已失效")
    private Integer status;
    @ApiModelProperty("绑定手机号")
    private String bindPhone;
    @ApiModelProperty("绑定时间")
    private LocalDateTime bindTime;
    @ApiModelProperty("失效时间")
    private LocalDateTime invalidTime;
    @ApiModelProperty("失效原因")
    private String invalidReason;
}
ruoyi-service/ruoyi-other/src/main/resources/mapper/other/TExchangeCodeDetailMapper.xml
New file
@@ -0,0 +1,60 @@
<?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="com.ruoyi.other.mapper.TExchangeCodeDetailMapper">
    <sql id="BaseQuery">
        from t_exchange_code_detail d
        left join t_exchange_code_package_spec s on s.id = d.package_spec_id and s.del_flag = 0
        <where>
            and d.del_flag = 0
            <if test="req.packageId != null">
                and d.package_id = #{req.packageId}
            </if>
            <if test="req.code != null and req.code != ''">
                and d.code like concat('%', #{req.code}, '%')
            </if>
            <if test="req.bindPhone != null and req.bindPhone != ''">
                and d.bind_phone like concat('%', #{req.bindPhone}, '%')
            </if>
            <if test="req.status != null">
                and d.status = #{req.status}
            </if>
        </where>
    </sql>
    <select id="pageList" resultType="com.ruoyi.other.vo.ExchangeCodeDetailVO">
        select
            d.id,
            d.package_id,
            d.package_spec_id,
            s.amount,
            s.quantity,
            d.code,
            d.password,
            d.status,
            d.bind_phone,
            d.bind_time,
            d.invalid_time,
            d.invalid_reason
        <include refid="BaseQuery"/>
        order by d.create_time desc, d.id desc
    </select>
    <select id="exportList" resultType="com.ruoyi.other.vo.ExchangeCodeDetailVO">
        select
            d.id,
            d.package_id,
            d.package_spec_id,
            s.amount,
            s.quantity,
            d.code,
            d.password,
            d.status,
            d.bind_phone,
            d.bind_time,
            d.invalid_time,
            d.invalid_reason
        <include refid="BaseQuery"/>
        order by d.create_time desc, d.id desc
    </select>
</mapper>
ruoyi-service/ruoyi-other/src/main/resources/mapper/other/TExchangeCodePackageMapper.xml
New file
@@ -0,0 +1,24 @@
<?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="com.ruoyi.other.mapper.TExchangeCodePackageMapper">
    <select id="pageList" resultType="com.ruoyi.other.api.domain.TExchangeCodePackage">
        select
            p.*,
            (
                select count(1) from t_exchange_code_detail d
                where d.package_id = p.id and d.status = 1 and d.del_flag = 0
            ) as unbound_count
        from t_exchange_code_package p
        <where>
            <if test="req.packageName != null and req.packageName != ''">
                and p.package_name like concat('%', #{req.packageName}, '%')
            </if>
            <if test="req.status != null">
                and p.status = #{req.status}
            </if>
            and p.del_flag = 0
        </where>
        order by p.create_time desc
    </select>
</mapper>