This commit is contained in:
墨大叔 2024-07-10 16:00:33 +08:00
parent c6edec8083
commit f75febd2b4
38 changed files with 1256 additions and 172 deletions

View File

@ -7,6 +7,7 @@ import com.wechat.pay.java.core.notification.NotificationParser;
import com.wechat.pay.java.service.payments.app.AppService;
import com.wechat.pay.java.service.payments.jsapi.JsapiService;
import com.wechat.pay.java.service.payments.jsapi.JsapiServiceExtension;
import com.wechat.pay.java.service.refund.RefundService;
import com.wechat.pay.java.service.transferbatch.TransferBatchService;
import lombok.Getter;
import org.springframework.beans.factory.annotation.Autowired;
@ -39,6 +40,10 @@ public class WxPayConfig {
@Value("${wx.pay.notifyUrl}")
private String notifyUrl;
// 退款通知回调地址
@Value("${wx.pay.refundNotifyUrl}")
private String refundNotifyUrl;
// 私钥证书路径
@Value("${wx.pay.privateKeyPath}")
private String privateKeyPath;
@ -93,6 +98,23 @@ public class WxPayConfig {
.build();
}
@Bean
public RefundService refundService() {
// 初始化商户配置
Config config = new RSAAutoCertificateConfig.Builder()
.merchantId(merchantId)
// 使用 com.wechat.pay.java.core.util 中的函数从本地文件中加载商户私钥商户私钥会用来生成请求的签名
.privateKeyFromPath(privateKeyPath)
.merchantSerialNumber(merchantSerialNumber)
.apiV3Key(apiV3Key)
.build();
// 初始化服务
return new RefundService
.Builder()
.config(config)
.build();
}
// 微信通知解析器
@Bean
public NotificationParser notificationParser() {

View File

@ -13,7 +13,8 @@ import lombok.Getter;
public enum NotifyEventType {
TRANSACTION_SUCCESS("TRANSACTION.SUCCESS", "支付成功"),
MCHTRANSFER_BATCH_FINISHED("MCHTRANSFER.BATCH.FINISHED", "商户转账批次完成通知");
MCHTRANSFER_BATCH_FINISHED("MCHTRANSFER.BATCH.FINISHED", "商户转账批次完成通知"),
REFUND_SUCCESS("REFUND.SUCCESS", "退款成功");
private final String value;
private final String name;

View File

@ -27,6 +27,10 @@ public class CollectionUtils extends org.springframework.util.CollectionUtils {
return org.springframework.util.CollectionUtils.isEmpty(collect);
}
public static boolean isNotEmptyElement(List<?> list) {
return !isEmptyElement(list);
}
/**
* 填充列表中的元素
* @param oldList 旧列表
@ -90,4 +94,8 @@ public class CollectionUtils extends org.springframework.util.CollectionUtils {
.map(date -> dateToObjectMap.getOrDefault(date, customFactory.apply(date)))
.collect(Collectors.toList());
}
public static boolean isNotEmpty(List<?> deviceList) {
return !isEmpty(deviceList);
}
}

View File

@ -252,8 +252,8 @@ public class SmDeviceServiceImpl implements ISmDeviceService
public int register(DeviceRegisterDTO dto) {
// 添加
SmDevice device = new SmDevice();
device.setMac(dto.getMac());
device.setDeviceName("未命名");
device.setMac(dto.getMac());
device.setDeviceNo(dto.getSn());
return this.addInitDevice(device);
}
@ -734,44 +734,15 @@ public class SmDeviceServiceImpl implements ISmDeviceService
device = selectByDeviceNo(smDevice.getDeviceNo());
ServiceUtil.assertion(device != null, "该设备SN已存在无需重复录入");
// 查询设备的型号
IotDeviceInfo deviceInfo = null;
try {
deviceInfo = iotService.getDeviceInfo(smDevice.getMac());
if (deviceInfo == null) {
log.info("iot device info is null");
throw new ServiceException("OneNet设备不存在");
}
} catch (Exception e) {
Integer result = transactionTemplate.execute(status -> {
int i = this.insertSmDevice(smDevice); // 添加设备
ServiceUtil.assertion(i != 1, "添加设备失败");
// 创建OneNet设备
boolean create = iotService.create(smDevice.getMac());
ServiceUtil.assertion(!create, "创建OneNet设备失败");
}
log.info("select model in:{}", deviceInfo.getModel());
SmModelVo model = smModelService.selectByModel(deviceInfo.getModel());
IotDeviceInfo finalDeviceInfo = deviceInfo;
Integer result = transactionTemplate.execute(status -> {
SmModelVo finalModel = model;
// 如果设备型号为空则添加新的设备型号
if (finalModel == null) {
log.info("model is null");
int addModel = smModelService.addInitModel(finalDeviceInfo);
ServiceUtil.assertion(addModel != 1, "添加设备型号失败");
finalModel = smModelService.selectByModel(finalDeviceInfo.getModel());
}
// 添加设备
SmDeviceVO byMac = selectByMac(smDevice.getMac());
if (byMac == null) {
smDevice.setModelId(finalModel != null ? finalModel.getModelId() : null);
smDevice.setCreateTime(DateUtils.getNowDate());
int i = smDeviceMapper.insertSmDevice(smDevice); // 添加设备
this.pullDeviceInfo(Collections.singletonList(smDevice.getDeviceId())); // 拉取设备信息
return i;
}
return 0;
return i;
});
return result == null ? 0 : result;

View File

@ -0,0 +1,51 @@
package com.ruoyi.ss.refund.domain;
import java.math.BigDecimal;
import com.ruoyi.ss.wxPay.domain.RefundAble;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import com.ruoyi.common.annotation.Excel;
import com.ruoyi.common.core.domain.BaseEntity;
/**
* 退款订单对象 ss_refund
*
* @author ruoyi
* @date 2024-07-09
*/
@Data
public class Refund extends BaseEntity
{
private static final long serialVersionUID = 1L;
/** 退款ID */
private Long refundId;
/** 退款订单编号 */
@Excel(name = "退款订单编号")
private String refundNo;
/** 原订单ID */
@Excel(name = "原订单ID")
private Long billId;
/** 退款金额(元) */
@Excel(name = "退款金额", readConverterExp = "元=")
private BigDecimal amount;
/** 退款订单状态 */
@Excel(name = "退款订单状态")
private String status;
@ApiModelProperty("退款原因")
private String reason;
@ApiModelProperty("商户退款金额")
private BigDecimal mchAmount;
@ApiModelProperty("手续费退款金额")
private BigDecimal serviceAmount;
}

View File

@ -0,0 +1,11 @@
package com.ruoyi.ss.refund.domain;
import lombok.Data;
/**
* @author wjh
* 2024/7/9
*/
@Data
public class RefundQuery extends Refund {
}

View File

@ -0,0 +1,47 @@
package com.ruoyi.ss.refund.domain;
import com.ruoyi.ss.wxPay.domain.RefundAble;
import com.wechat.pay.java.service.refund.model.AmountReq;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.math.BigDecimal;
/**
* @author wjh
* 2024/7/9
*/
@Data
public class RefundVO extends Refund implements RefundAble {
@ApiModelProperty("原订单编号")
private String billNo;
@ApiModelProperty("原订单交易总额")
private BigDecimal billAmount;
@Override
public String refundOutTradeNo() {
return this.billNo;
}
@Override
public String refundOutRefundNo() {
return this.getRefundNo();
}
@Override
public String refundReason() {
return this.getReason();
}
@Override
public AmountReq refundAmount() {
BigDecimal decimal100 = new BigDecimal(100);
AmountReq amount = new AmountReq();
amount.setRefund(this.getAmount().multiply(decimal100).longValue());
amount.setTotal(this.billAmount.multiply(decimal100).longValue());
amount.setCurrency("CNY");
return amount;
}
}

View File

@ -0,0 +1,21 @@
package com.ruoyi.ss.refund.domain.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* @author wjh
* 2024/7/9
*/
@Getter
@AllArgsConstructor
public enum RefundStatus {
REFUNDING("1", "退款中"),
REFUND_SUCCESS("2", "退款成功"),
REFUND_FAIL("3", "退款失败");
private final String status;
private final String desc;
}

View File

@ -0,0 +1,71 @@
package com.ruoyi.ss.refund.mapper;
import java.util.List;
import com.ruoyi.ss.refund.domain.Refund;
import com.ruoyi.ss.refund.domain.RefundQuery;
import com.ruoyi.ss.refund.domain.RefundVO;
import org.apache.ibatis.annotations.Param;
/**
* 退款订单Mapper接口
*
* @author ruoyi
* @date 2024-07-09
*/
public interface RefundMapper
{
/**
* 查询退款订单
*
* @param refundId 退款订单主键
* @return 退款订单
*/
public RefundVO selectRefundByRefundId(Long refundId);
/**
* 查询退款订单列表
*
* @param refund 退款订单
* @return 退款订单集合
*/
public List<RefundVO> selectRefundList(@Param("query") RefundQuery refund);
/**
* 新增退款订单
*
* @param refund 退款订单
* @return 结果
*/
public int insertRefund(Refund refund);
/**
* 修改退款订单
*
* @param refund 退款订单
* @return 结果
*/
public int updateRefund(@Param("data") Refund refund);
/**
* 删除退款订单
*
* @param refundId 退款订单主键
* @return 结果
*/
public int deleteRefundByRefundId(Long refundId);
/**
* 批量删除退款订单
*
* @param refundIds 需要删除的数据主键集合
* @return 结果
*/
public int deleteRefundByRefundIds(Long[] refundIds);
RefundVO selectOne(@Param("query") RefundQuery query);
/**
* 条件更新
*/
int updateByQuery(@Param("data") Refund data, @Param("query") RefundQuery query);
}

View File

@ -0,0 +1,117 @@
<?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.ss.refund.mapper.RefundMapper">
<resultMap type="RefundVO" id="RefundResult" autoMapping="true"/>
<sql id="selectRefundVo">
select
sr.refund_id,
sr.refund_no,
sr.bill_id,
sr.amount,
sr.create_time,
sr.status,
sr.reason,
sr.mch_amount,
sr.service_amount,
smb.money as bill_amount,
smb.bill_no as bill_no
from ss_refund sr
left join sm_transaction_bill smb on smb.bill_id = sr.bill_id
</sql>
<sql id="searchCondition">
<if test="query.refundId != null "> and sr.refund_id = #{query.refundId}</if>
<if test="query.refundNo != null and query.refundNo != ''"> and sr.refund_no = #{query.refundNo}</if>
<if test="query.billId != null "> and sr.bill_id = #{query.billId}</if>
<if test="query.status != null and query.status != ''"> and sr.status = #{query.status}</if>
</sql>
<select id="selectRefundList" parameterType="Refund" resultMap="RefundResult">
<include refid="selectRefundVo"/>
<where>
<include refid="searchCondition"/>
</where>
order by sr.create_time desc
</select>
<select id="selectRefundByRefundId" parameterType="Long" resultMap="RefundResult">
<include refid="selectRefundVo"/>
where refund_id = #{refundId}
</select>
<select id="selectOne" resultMap="RefundResult">
<include refid="selectRefundVo"/>
<where>
<include refid="searchCondition"/>
</where>
limit 1
</select>
<insert id="insertRefund" parameterType="Refund" useGeneratedKeys="true" keyProperty="refundId">
insert into ss_refund
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="refundNo != null and refundNo != ''">refund_no,</if>
<if test="billId != null">bill_id,</if>
<if test="amount != null">amount,</if>
<if test="createTime != null">create_time,</if>
<if test="status != null and status != ''">`status`,</if>
<if test="reason != null and reason != ''">`reason`,</if>
<if test="mchAmount != null">mch_amount,</if>
<if test="serviceAmount != null">service_amount,</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="refundNo != null and refundNo != ''">#{refundNo},</if>
<if test="billId != null">#{billId},</if>
<if test="amount != null">#{amount},</if>
<if test="createTime != null">#{createTime},</if>
<if test="status != null and status != ''">#{status},</if>
<if test="reason != null and reason != ''">#{reason},</if>
<if test="mchAmount != null">#{mchAmount},</if>
<if test="serviceAmount != null">#{serviceAmount},</if>
</trim>
</insert>
<update id="updateRefund" parameterType="Refund">
update ss_refund
<trim prefix="SET" suffixOverrides=",">
<include refid="updateColumns"/>
</trim>
where refund_id = #{refundId}
</update>
<sql id="updateColumns">
<if test="data.refundNo != null and data.refundNo != ''">refund_no = #{data.refundNo},</if>
<if test="data.billId != null">bill_id = #{data.billId},</if>
<if test="data.amount != null">amount = #{data.amount},</if>
<if test="data.mchAmount != null">mch_amount = #{data.mchAmount},</if>
<if test="data.serviceAmount != null">service_amount = #{data.serviceAmount},</if>
<if test="data.createTime != null">create_time = #{data.createTime},</if>
<if test="data.status != null and data.status != ''">`status` = #{data.status},</if>
<if test="data.reason != null and data.reason != ''">`reason` = #{data.reason},</if>
</sql>
<update id="updateByQuery">
update ss_refund sr
<trim prefix="SET" suffixOverrides=",">
<include refid="updateColumns"/>
</trim>
<where>
<include refid="searchCondition"/>
</where>
</update>
<delete id="deleteRefundByRefundId" parameterType="Long">
delete from ss_refund where refund_id = #{refundId}
</delete>
<delete id="deleteRefundByRefundIds" parameterType="String">
delete from ss_refund where refund_id in
<foreach item="refundId" collection="array" open="(" separator="," close=")">
#{refundId}
</foreach>
</delete>
</mapper>

View File

@ -0,0 +1,17 @@
package com.ruoyi.ss.refund.service;
import com.ruoyi.ss.refund.domain.Refund;
import com.ruoyi.ss.transactionBill.domain.dto.BillRefundDTO;
/**
* @author wjh
* 2024/7/9
*/
public interface RefundConverter {
/**
* 订单退款DTO -> 退款PO
*/
Refund toPo(BillRefundDTO dto);
}

View File

@ -0,0 +1,80 @@
package com.ruoyi.ss.refund.service;
import java.util.List;
import com.ruoyi.ss.refund.domain.Refund;
import com.ruoyi.ss.refund.domain.RefundQuery;
import com.ruoyi.ss.refund.domain.RefundVO;
/**
* 退款订单Service接口
*
* @author ruoyi
* @date 2024-07-09
*/
public interface RefundService
{
/**
* 查询退款订单
*
* @param refundId 退款订单主键
* @return 退款订单
*/
public RefundVO selectRefundByRefundId(Long refundId);
/**
* 查询退款订单列表
*
* @param refund 退款订单
* @return 退款订单集合
*/
public List<RefundVO> selectRefundList(RefundQuery refund);
/**
* 新增退款订单
*
* @param refund 退款订单
* @return 结果
*/
public int insertRefund(Refund refund);
/**
* 修改退款订单
*
* @param refund 退款订单
* @return 结果
*/
public int updateRefund(Refund refund);
/**
* 批量删除退款订单
*
* @param refundIds 需要删除的退款订单主键集合
* @return 结果
*/
public int deleteRefundByRefundIds(Long[] refundIds);
/**
* 删除退款订单信息
*
* @param refundId 退款订单主键
* @return 结果
*/
public int deleteRefundByRefundId(Long refundId);
/**
* 查询退款订单
*/
RefundVO selectRefundByRefundNo(String refundNo);
RefundVO selectOne(RefundQuery query);
/**
* 退款成功后操作
*/
void handleRefundSuccess(String refundNo);
/**
* 条件更新
*/
int updateByQuery(Refund data, RefundQuery query);
}

View File

@ -0,0 +1,51 @@
package com.ruoyi.ss.refund.service.impl;
import com.ruoyi.common.utils.ServiceUtil;
import com.ruoyi.common.utils.SnowFlakeUtil;
import com.ruoyi.ss.refund.domain.Refund;
import com.ruoyi.ss.refund.domain.enums.RefundStatus;
import com.ruoyi.ss.refund.service.RefundConverter;
import com.ruoyi.ss.transactionBill.domain.TransactionBill;
import com.ruoyi.ss.transactionBill.domain.TransactionBillVo;
import com.ruoyi.ss.transactionBill.domain.dto.BillRefundDTO;
import com.ruoyi.ss.transactionBill.service.TransactionBillService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.math.RoundingMode;
/**
* @author wjh
* 2024/7/9
*/
@Service
public class RefundConverterImpl implements RefundConverter {
@Autowired
private TransactionBillService transactionBillService;
@Override
public Refund toPo(BillRefundDTO dto) {
if (dto == null) {
return null;
}
TransactionBillVo bill = transactionBillService.selectSmTransactionBillByBillId(dto.getBillId());
ServiceUtil.assertion(bill == null, "待退款订单不存在");
// 按比例计算退款金额
BigDecimal refundAmount = dto.getRefundAmount();
BigDecimal refundRate = refundAmount.divide(bill.getMoney(), 2, RoundingMode.HALF_UP); // 退款金额比例
BigDecimal refundServiceAmount = bill.getServiceCharge().multiply(refundRate); // 退款的手续费
BigDecimal refundMchAmount = refundAmount.subtract(refundServiceAmount); // 退款的商户余额
Refund refund = new Refund();
refund.setBillId(dto.getBillId());
refund.setAmount(dto.getRefundAmount());
refund.setStatus(RefundStatus.REFUNDING.getStatus());
refund.setReason(String.format("充值订单%s退款", bill.getBillNo()));
refund.setMchAmount(refundMchAmount);
refund.setServiceAmount(refundServiceAmount);
return refund;
}
}

View File

@ -0,0 +1,174 @@
package com.ruoyi.ss.refund.service.impl;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.List;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.ServiceUtil;
import com.ruoyi.common.utils.SnowFlakeUtil;
import com.ruoyi.ss.refund.domain.RefundQuery;
import com.ruoyi.ss.refund.domain.RefundVO;
import com.ruoyi.ss.refund.domain.enums.RefundStatus;
import com.ruoyi.ss.transactionBill.domain.TransactionBill;
import com.ruoyi.ss.transactionBill.domain.TransactionBillQuery;
import com.ruoyi.ss.transactionBill.domain.TransactionBillVo;
import com.ruoyi.ss.transactionBill.domain.enums.TransactionBillStatus;
import com.ruoyi.ss.transactionBill.service.TransactionBillService;
import com.ruoyi.ss.user.service.ISmUserService;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.ruoyi.ss.refund.mapper.RefundMapper;
import com.ruoyi.ss.refund.domain.Refund;
import com.ruoyi.ss.refund.service.RefundService;
import org.springframework.transaction.support.TransactionTemplate;
/**
* 退款订单Service业务层处理
*
* @author ruoyi
* @date 2024-07-09
*/
@Service
public class RefundServiceImpl implements RefundService
{
@Autowired
private RefundMapper refundMapper;
@Autowired
private TransactionTemplate transactionTemplate;
@Autowired
private TransactionBillService transactionBillService;
@Autowired
private ISmUserService userService;
/**
* 查询退款订单
*
* @param refundId 退款订单主键
* @return 退款订单
*/
@Override
public RefundVO selectRefundByRefundId(Long refundId)
{
return refundMapper.selectRefundByRefundId(refundId);
}
/**
* 查询退款订单列表
*
* @param refund 退款订单
* @return 退款订单
*/
@Override
public List<RefundVO> selectRefundList(RefundQuery refund)
{
return refundMapper.selectRefundList(refund);
}
/**
* 新增退款订单
*
* @param refund 退款订单
* @return 结果
*/
@Override
public int insertRefund(Refund refund)
{
refund.setCreateTime(DateUtils.getNowDate());
refund.setRefundNo(String.valueOf(SnowFlakeUtil.newId()));
return refundMapper.insertRefund(refund);
}
/**
* 修改退款订单
*
* @param refund 退款订单
* @return 结果
*/
@Override
public int updateRefund(Refund refund)
{
return refundMapper.updateRefund(refund);
}
/**
* 批量删除退款订单
*
* @param refundIds 需要删除的退款订单主键
* @return 结果
*/
@Override
public int deleteRefundByRefundIds(Long[] refundIds)
{
return refundMapper.deleteRefundByRefundIds(refundIds);
}
/**
* 删除退款订单信息
*
* @param refundId 退款订单主键
* @return 结果
*/
@Override
public int deleteRefundByRefundId(Long refundId)
{
return refundMapper.deleteRefundByRefundId(refundId);
}
@Override
public RefundVO selectRefundByRefundNo(String refundNo) {
RefundQuery query = new RefundQuery();
query.setRefundNo(refundNo);
return this.selectOne(query);
}
@Override
public RefundVO selectOne(RefundQuery query) {
return refundMapper.selectOne(query);
}
@Override
public void handleRefundSuccess(String refundNo) {
ServiceUtil.assertion(StringUtils.isBlank(refundNo), "退款订单编号不允许为空");
RefundVO refund = this.selectRefundByRefundNo(refundNo);
ServiceUtil.assertion(refund == null, "退款订单不存在");
TransactionBillVo bill = transactionBillService.selectSmTransactionBillByBillId(refund.getBillId());
ServiceUtil.assertion(bill == null, "原订单不存在");
transactionTemplate.execute(status -> {
// 退款订单修改状态
Refund refundData = new Refund();
refundData.setStatus(RefundStatus.REFUND_SUCCESS.getStatus());
RefundQuery refundQuery = new RefundQuery();
refundQuery.setRefundId(refund.getRefundId());
refundQuery.setStatus(RefundStatus.REFUNDING.getStatus());
int updateRefund = this.updateByQuery(refundData, refundQuery);
ServiceUtil.assertion(updateRefund != 1, "修改退款订单状态失败");
// 修改原订单状态
TransactionBillQuery billQuery = new TransactionBillQuery();
billQuery.setBillId(refund.getBillId());
billQuery.setStatus(TransactionBillStatus.REFUNDING.getStatus());
TransactionBill billData = new TransactionBill();
billData.setStatus(TransactionBillStatus.REFUNDED.getStatus());
int updateBill = transactionBillService.updateByQuery(billData, billQuery);
ServiceUtil.assertion(updateBill != 1, "修改原订单状态失败");
return updateRefund;
});
}
@Override
public int updateByQuery(Refund data, RefundQuery query) {
if (query == null) {
return 0;
}
return refundMapper.updateByQuery(data, query);
}
}

View File

@ -4,22 +4,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.ss.suit.mapper.SuitMapper">
<resultMap type="SuitVo" id="SuitResult">
<result property="suitId" column="suit_id" />
<result property="deviceId" column="device_id" />
<result property="name" column="name" />
<result property="value" column="value" />
<result property="price" column="price" />
<result property="description" column="description" />
<result property="createTime" column="create_time" />
<result property="createBy" column="create_by" />
<result property="updateTime" column="update_time" />
<result property="updateBy" column="update_by" />
<result property="deleted" column="deleted" />
<result property="deviceName" column="device_name" />
<result property="storeName" column="store_name" />
<result property="userName" column="user_name" />
</resultMap>
<resultMap type="SuitVo" id="SuitResult" autoMapping="true"/>
<sql id="selectSuitVo">
select
@ -40,7 +25,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
from sm_suit ss
left join sm_device sd on sd.device_id = ss.device_id
left join sm_store store on store.store_id = sd.store_id
left join sm_user su on su.user_id = store.user_id
left join sm_user su on su.user_id = sd.user_id
</sql>
<sql id="searchCondition">

View File

@ -60,7 +60,7 @@ public class TransactionBill extends BaseEntity
/** 商户(到账用户) */
@Excel(name = "商户(到账用户)")
@ApiModelProperty("商户(到账用户)id")
@JsonView(JsonViewProfile.AppMch.class)
@JsonView(JsonViewProfile.App.class)
private Long mchId;
/** 交易金额 */

View File

@ -18,10 +18,13 @@ public class TransactionBillVo extends TransactionBill {
private String userName;
@ApiModelProperty("商户(到账用户)名称")
@JsonView(JsonViewProfile.AppMch.class)
@JsonView(JsonViewProfile.App.class)
private String mchName;
@ApiModelProperty("支付渠道名称")
@JsonView(JsonViewProfile.App.class)
private String channelName;
@ApiModelProperty("商户手机号")
private String mchMobile;
}

View File

@ -0,0 +1,27 @@
package com.ruoyi.ss.transactionBill.domain.dto;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import java.math.BigDecimal;
/**
* 订单退款DTO
* @author wjh
* 2024/7/9
*/
@Data
public class BillRefundDTO {
@ApiModelProperty("订单ID")
@NotNull(message = "订单ID不允许为空")
private Long billId;
@ApiModelProperty("退款金额")
@NotNull(message = "退款金额不允许为空")
@Min(value = 0, message = "退款金额不允许小于0")
private BigDecimal refundAmount;
}

View File

@ -22,6 +22,7 @@ public enum TransactionBillStatus {
CANCELED("4", "已取消(用户)"),
SYS_CANCELED("5", "已取消(系统)"),
PAYING("6", "支付中"),
REFUNDING("7", "退款中"),
WITHDRAW_APPROVING("11", "提现申请中"),
WITHDRAW_PASSED("12", "提现申请通过"),

View File

@ -6,6 +6,7 @@ import com.ruoyi.ss.transactionBill.domain.TransactionBillQuery;
import com.ruoyi.ss.transactionBill.domain.TransactionBillVo;
import org.apache.ibatis.annotations.Param;
import java.math.BigDecimal;
import java.util.Date;
import java.util.List;
@ -31,7 +32,7 @@ public interface TransactionBillMapper
* @param smTransactionBill 充值记录
* @return 充值记录集合
*/
public List<TransactionBillVo> selectSmTransactionBillList(TransactionBillQuery smTransactionBill);
public List<TransactionBillVo> selectSmTransactionBillList(@Param("query") TransactionBillQuery smTransactionBill);
/**
* 新增充值记录
@ -47,7 +48,7 @@ public interface TransactionBillMapper
* @param transactionBill 充值记录
* @return 结果
*/
public int updateSmTransactionBill(TransactionBill transactionBill);
public int updateSmTransactionBill(@Param("data")TransactionBill transactionBill);
/**
* 删除充值记录
@ -69,7 +70,7 @@ public interface TransactionBillMapper
* 获取账单统计数据
* @param dto 查询条件
*/
List<BillCountVo> selectCount(TransactionBillQuery dto);
List<BillCountVo> selectCount(@Param("query")TransactionBillQuery dto);
/**
* 充值成功
@ -82,7 +83,7 @@ public interface TransactionBillMapper
* @param billNo 订单编号
* @return
*/
TransactionBill selectSmTransactionBillByBillNo(String billNo);
TransactionBillVo selectSmTransactionBillByBillNo(String billNo);
/**
* 订单取消
@ -144,4 +145,22 @@ public interface TransactionBillMapper
* 蓝牙充值成功
*/
int bluetoothRechargeSuccess(String billNo);
/**
* 根据条件修改
*/
int updateByQuery(@Param("data") TransactionBill data, @Param("query") TransactionBillQuery query);
/**
* 添加退款金额
* @param billId 订单ID
* @param refundAmount 退款金额
* @param refundMchAmount 商户退款金额
* @param refundServiceAmount 服务费退款金额
*/
int addRefundAmount(@Param("billId") Long billId,
@Param("refundAmount") BigDecimal refundAmount,
@Param("refundMchAmount") BigDecimal refundMchAmount,
@Param("refundServiceAmount") BigDecimal refundServiceAmount
);
}

View File

@ -42,8 +42,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
stb.device_no,
stb.suit_name,
stb.device_mac,
su.user_name user_name,
su1.user_name mch_name
su.user_name as user_name,
su1.user_name as mch_name,
su1.phonenumber as mch_mobile
from sm_transaction_bill stb
left join sm_user su on su.user_id = stb.user_id
left join sm_user su1 on su1.user_id = stb.mch_id
@ -52,52 +53,53 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</sql>
<sql id="searchCondition">
<if test="userId != null "> and stb.user_id = #{userId}</if>
<if test="deviceId != null "> and stb.device_id = #{deviceId}</if>
<if test="type != null "> and stb.type = #{type} </if>
<if test="mchId != null "> and stb.mch_id = #{mchId}</if>
<if test="userName != null "> and su.user_name like concat('%', #{userName}, '%')</if>
<if test="mchName != null "> and su1.user_name like concat('%', #{mchName}, '%')</if>
<if test="deviceName != null "> and stb.device_name like concat('%', #{deviceName}, '%')</if>
<if test="createTime != null"> and stb.create_time = #{createTime}</if>
<if test="createDate != null"> and date(stb.create_time) = date(#{createDate})</if>
<if test="year != null "> and year(stb.create_time) = #{year}</if>
<if test="month != null"> and month(stb.create_time) = #{month}</if>
<if test="hour != null"> and hour(stb.create_time) = #{hour}</if>
<if test="status != null"> and stb.status = #{status}</if>
<if test="expire != null"> and NOW() >= stb.expire_time </if>
<if test="startDate != null"> and date(stb.create_time) >= date(#{startDate}) </if>
<if test="endDate != null"> and date(stb.create_time) &lt;= date(#{endDate}) </if>
<if test="deviceRechargeStatus != null"> and stb.device_recharge_status = #{deviceRechargeStatus} </if>
<if test="suitId != null"> and stb.suit_id = #{suitId} </if>
<if test="storeId != null"> and stb.store_id = #{storeId} </if>
<if test="billIds != null and billIds.size() > 0">
<if test="query.userId != null "> and stb.user_id = #{query.userId}</if>
<if test="query.billId != null "> and stb.bill_id = #{query.billId}</if>
<if test="query.deviceId != null "> and stb.device_id = #{query.deviceId}</if>
<if test="query.type != null "> and stb.type = #{query.type} </if>
<if test="query.mchId != null "> and stb.mch_id = #{query.mchId}</if>
<if test="query.userName != null "> and su.user_name like concat('%', #{query.userName}, '%')</if>
<if test="query.mchName != null "> and su1.user_name like concat('%', #{query.mchName}, '%')</if>
<if test="query.deviceName != null "> and stb.device_name like concat('%', #{query.deviceName}, '%')</if>
<if test="query.createTime != null"> and stb.create_time = #{query.createTime}</if>
<if test="query.createDate != null"> and date(stb.create_time) = date(#{query.createDate})</if>
<if test="query.year != null "> and year(stb.create_time) = #{query.year}</if>
<if test="query.month != null"> and month(stb.create_time) = #{query.month}</if>
<if test="query.hour != null"> and hour(stb.create_time) = #{query.hour}</if>
<if test="query.status != null"> and stb.status = #{query.status}</if>
<if test="query.expire != null"> and NOW() >= stb.expire_time </if>
<if test="query.startDate != null"> and date(stb.create_time) >= date(#{query.startDate}) </if>
<if test="query.endDate != null"> and date(stb.create_time) &lt;= date(#{query.endDate}) </if>
<if test="query.deviceRechargeStatus != null"> and stb.device_recharge_status = #{query.deviceRechargeStatus} </if>
<if test="query.suitId != null"> and stb.suit_id = #{query.suitId} </if>
<if test="query.storeId != null"> and stb.store_id = #{query.storeId} </if>
<if test="query.billIds != null and query.billIds.size() > 0">
and stb.bill_id in
<foreach item="item" collection="billIds" open="(" separator="," close=")">
<foreach item="item" collection="query.billIds" open="(" separator="," close=")">
#{item}
</foreach>
</if>
<if test="deviceRechargeStatusList != null and deviceRechargeStatusList.size() > 0">
<if test="query.deviceRechargeStatusList != null and query.deviceRechargeStatusList.size() > 0">
and stb.device_recharge_status in
<foreach item="item" collection="deviceRechargeStatusList" open="(" separator="," close=")">
<foreach item="item" collection="query.deviceRechargeStatusList" open="(" separator="," close=")">
#{item}
</foreach>
</if>
<if test="statusList != null and statusList.size() > 0">
<if test="query.statusList != null and query.statusList.size() > 0">
and stb.status in
<foreach item="item" collection="statusList" open="(" separator="," close=")">
<foreach item="item" collection="query.statusList" open="(" separator="," close=")">
#{item}
</foreach>
</if>
<if test="storeIds != null and storeIds.size() > 0">
<if test="query.storeIds != null and query.storeIds.size() > 0">
and stb.store_id in
<foreach item="item" collection="storeIds" open="(" separator="," close=")">
<foreach item="item" collection="query.storeIds" open="(" separator="," close=")">
#{item}
</foreach>
</if>
<if test="deviceIds != null and deviceIds.size() > 0">
<if test="query.deviceIds != null and query.deviceIds.size() > 0">
and stb.device_id in
<foreach item="item" collection="deviceIds" open="(" separator="," close=")">
<foreach item="item" collection="query.deviceIds" open="(" separator="," close=")">
#{item}
</foreach>
</if>
@ -133,28 +135,28 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<select id="selectCount" resultMap="BillCountVo">
SELECT
<if test="groupBy == 'create_date'">
<if test="query.groupBy == 'create_date'">
DATE( stb.create_time ) AS `create_date`,
</if>
<if test="groupBy == 'create_hour'">
<if test="query.groupBy == 'create_hour'">
HOUR( stb.create_time ) AS `create_hour`,
</if>
<if test="groupBy == 'create_year' or groupBy == 'create_year_month' or groupBy == 'create_date' or groupBy == 'create_month'">
<if test="query.groupBy == 'create_year' or query.groupBy == 'create_year_month' or query.groupBy == 'create_date' or query.groupBy == 'create_month'">
YEAR( stb.create_time ) AS `create_year`,
</if>
<if test="groupBy == 'create_month' or groupBy == 'create_year_month' or groupBy == 'create_date'">
<if test="query.groupBy == 'create_month' or query.groupBy == 'create_year_month' or query.groupBy == 'create_date'">
MONTH( stb.create_time ) AS `create_month`,
</if>
<if test="groupBy == 'create_day' or groupBy == 'create_date'">
<if test="query.groupBy == 'create_day' or query.groupBy == 'create_date'">
DAY( stb.create_time ) AS `create_day`,
</if>
<if test="groupBy == 'create_year_month' or groupBy == 'create_date' or groupBy == 'create_month'">
<if test="query.groupBy == 'create_year_month' or query.groupBy == 'create_date' or query.groupBy == 'create_month'">
DATE_FORMAT( stb.create_time, '%Y-%m' ) AS `create_year_month`,
</if>
<if test="groupBy == 'store_id'">
<if test="query.groupBy == 'store_id'">
stb.store_id as store_id,
</if>
<if test="groupBy == 'device_id'">
<if test="query.groupBy == 'device_id'">
stb.device_id as device_id,
</if>
SUM(IF( stb.type = '1' and stb.status = '2', stb.arrival_amount, 0 )) AS recharge,
@ -168,8 +170,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<where>
<include refid="searchCondition"/>
</where>
<if test="groupBy != null">
GROUP BY ${groupBy}
<if test="query.groupBy != null">
GROUP BY ${query.groupBy}
</if>
</select>
@ -251,43 +253,66 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</trim>
</insert>
<update id="addRefundAmount">
update sm_transaction_bill
set refund_amount = refund_amount + #{refundAmount},
refund_mch_amount = refund_mch_amount + #{refundMchAmount},
refund_service_amount = refund_service_amount + #{refundServiceAmount}
where bill_id = #{billId}
</update>
<update id="updateSmTransactionBill" parameterType="TransactionBill">
update sm_transaction_bill
<trim prefix="SET" suffixOverrides=",">
<if test="userId != null">user_id = #{userId},</if>
<if test="type != null">`type` = #{type},</if>
<if test="deviceId != null">device_id = #{deviceId},</if>
<if test="mchId != null">mch_id = #{mchId},</if>
<if test="money != null">money = #{money},</if>
<if test="arrivalAmount != null">arrival_amount = #{arrivalAmount},</if>
<if test="serviceCharge != null">service_charge = #{serviceCharge},</if>
<if test="createTime != null">create_time = #{createTime},</if>
<if test="remark != null">remark = #{remark},</if>
<if test="status != null">`status` = #{status},</if>
<if test="channelId != null">channel_id = #{channelId},</if>
<if test="afterBalance != null">after_balance = #{afterBalance},</if>
<if test="payTime != null">pay_time = #{payTime},</if>
<if test="expireTime != null">expire_time = #{expireTime},</if>
<if test="accountNo != null">account_no = #{accountNo},</if>
<if test="payedAmount != null">payed_amount = #{payedAmount},</if>
<if test="transferIds != null">transfer_ids = #{transferIds,typeHandler=com.ruoyi.system.mapper.typehandler.StringListTypeHandler},</if>
<if test="channelCost != null">channel_cost = #{channelCost},</if>
<if test="suitId != null">suit_id = #{suitId},</if>
<if test="suitTime != null">suit_time = #{suitTime},</if>
<if test="suitStartTime != null">suit_start_time = #{suitStartTime},</if>
<if test="suitEndTime != null">suit_end_time = #{suitEndTime},</if>
<if test="suitExpireTime != null">suit_expire_time = #{suitExpireTime},</if>
<if test="storeId != null ">store_id = #{storeId},</if>
<if test="storeName != null ">store_name = #{storeName},</if>
<if test="storeAddress != null ">store_address = #{storeAddress},</if>
<if test="deviceName != null ">device_name = #{deviceName},</if>
<if test="deviceNo != null ">device_no = #{deviceNo},</if>
<if test="suitName != null ">suit_name = #{suitName},</if>
<if test="deviceMac != null ">device_mac = #{deviceMac},</if>
<include refid="updateColumns"/>
</trim>
where bill_id = #{billId}
</update>
<sql id="updateColumns">
<if test="data.userId != null">user_id = #{data.userId},</if>
<if test="data.type != null">`type` = #{data.type},</if>
<if test="data.deviceId != null">device_id = #{data.deviceId},</if>
<if test="data.mchId != null">mch_id = #{data.mchId},</if>
<if test="data.money != null">money = #{data.money},</if>
<if test="data.arrivalAmount != null">arrival_amount = #{data.arrivalAmount},</if>
<if test="data.serviceCharge != null">service_charge = #{data.serviceCharge},</if>
<if test="data.createTime != null">create_time = #{data.createTime},</if>
<if test="data.remark != null">remark = #{data.remark},</if>
<if test="data.status != null">`status` = #{data.status},</if>
<if test="data.channelId != null">channel_id = #{data.channelId},</if>
<if test="data.afterBalance != null">after_balance = #{data.afterBalance},</if>
<if test="data.payTime != null">pay_time = #{data.payTime},</if>
<if test="data.expireTime != null">expire_time = #{data.expireTime},</if>
<if test="data.accountNo != null">account_no = #{data.accountNo},</if>
<if test="data.payedAmount != null">payed_amount = #{data.payedAmount},</if>
<if test="data.transferIds != null">transfer_ids = #{data.transferIds,typeHandler=com.ruoyi.system.mapper.typehandler.StringListTypeHandler},</if>
<if test="data.channelCost != null">channel_cost = #{data.channelCost},</if>
<if test="data.suitId != null">suit_id = #{data.suitId},</if>
<if test="data.suitTime != null">suit_time = #{data.suitTime},</if>
<if test="data.suitStartTime != null">suit_start_time = #{data.suitStartTime},</if>
<if test="data.suitEndTime != null">suit_end_time = #{data.suitEndTime},</if>
<if test="data.suitExpireTime != null">suit_expire_time = #{data.suitExpireTime},</if>
<if test="data.storeId != null ">store_id = #{data.storeId},</if>
<if test="data.storeName != null ">store_name = #{data.storeName},</if>
<if test="data.storeAddress != null ">store_address = #{data.storeAddress},</if>
<if test="data.deviceName != null ">device_name = #{data.deviceName},</if>
<if test="data.deviceNo != null ">device_no = #{data.deviceNo},</if>
<if test="data.suitName != null ">suit_name = #{data.suitName},</if>
<if test="data.deviceMac != null ">device_mac = #{data.deviceMac},</if>
</sql>
<update id="updateByQuery">
update sm_transaction_bill stb
<trim prefix="SET" suffixOverrides=",">
<include refid="updateColumns"/>
</trim>
<where>
<include refid="searchCondition"/>
</where>
</update>
<update id="updatechannelId">
update sm_transaction_bill
set channel_id = #{channelId},
@ -383,4 +408,5 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
set device_recharge_status = '1'
where bill_no = #{billNo} and type = '1' and device_recharge_status = '3' and status = '2'
</update>
</mapper>

View File

@ -5,6 +5,7 @@ import com.ruoyi.ss.transactionBill.domain.TransactionBill;
import com.ruoyi.ss.transactionBill.domain.TransactionBillQuery;
import com.ruoyi.ss.transactionBill.domain.TransactionBillVo;
import com.ruoyi.ss.transactionBill.domain.bo.TransactionBillBO;
import com.ruoyi.ss.transactionBill.domain.dto.BillRefundDTO;
import com.ruoyi.ss.transactionBill.domain.enums.TransactionBillStatus;
import java.math.BigDecimal;
@ -106,9 +107,10 @@ public interface TransactionBillService
/**
* 根据订单编号查询数据
*
* @param billNo 订单编号
*/
TransactionBill selectSmTransactionBillByBillNo(String billNo);
TransactionBillVo selectSmTransactionBillByBillNo(String billNo);
/**
* 获取未支付的充值订单
@ -169,9 +171,9 @@ public interface TransactionBillService
/**
* 刷新订单支付结果
* @param billIds
* @param billId
*/
void refreshPayResult(List<Long> billIds);
int refreshPayResult(Long billId);
/**
* 支付中
@ -223,4 +225,33 @@ public interface TransactionBillService
* @return
*/
boolean bluetoothRechargeSuccess(String billNo);
/**
* 订单退款
*/
int refund(BillRefundDTO dto);
/**
* 根据条件更新
* @param data
* @param query
*/
int updateByQuery(TransactionBill data, TransactionBillQuery query);
/**
* 刷新支付结果
*/
int refreshPayResult(String billNo);
int refreshPayResult(List<TransactionBillVo> billList);
/**
* 增加订单退款金额
*
* @param billId 订单ID
* @param refundAmount 退款金额
* @param refundMchAmount 商户退款金额
* @param refundServiceAmount 退款手续费
*/
int addRefundAmount(Long billId, BigDecimal refundAmount, BigDecimal refundMchAmount, BigDecimal refundServiceAmount);
}

View File

@ -16,6 +16,10 @@ import com.ruoyi.ss.device.domain.vo.SmDeviceVO;
import com.ruoyi.ss.device.service.ISmDeviceService;
import com.ruoyi.ss.record.time.service.IRecordTimeService;
import com.ruoyi.ss.record.time.service.RecordTimeConverter;
import com.ruoyi.ss.refund.domain.Refund;
import com.ruoyi.ss.refund.domain.RefundVO;
import com.ruoyi.ss.refund.service.RefundConverter;
import com.ruoyi.ss.refund.service.RefundService;
import com.ruoyi.ss.store.domain.StoreVo;
import com.ruoyi.ss.store.service.IStoreService;
import com.ruoyi.ss.suit.domain.SuitVo;
@ -24,6 +28,7 @@ import com.ruoyi.ss.transactionBill.domain.TransactionBill;
import com.ruoyi.ss.transactionBill.domain.TransactionBillQuery;
import com.ruoyi.ss.transactionBill.domain.TransactionBillVo;
import com.ruoyi.ss.transactionBill.domain.bo.TransactionBillBO;
import com.ruoyi.ss.transactionBill.domain.dto.BillRefundDTO;
import com.ruoyi.ss.transactionBill.domain.enums.*;
import com.ruoyi.ss.transactionBill.mapper.TransactionBillMapper;
import com.ruoyi.ss.transactionBill.service.TransactionBillService;
@ -117,6 +122,12 @@ public class TransactionBillServiceImpl implements TransactionBillService {
@Autowired
private RecordTimeConverter recordTimeConverter;
@Autowired
private RefundService refundService;
@Autowired
private RefundConverter refundConverter;
/**
* 查询充值记录
*
@ -510,28 +521,18 @@ public class TransactionBillServiceImpl implements TransactionBillService {
/**
* 刷新支付结果
* @param billIds
*
* @param billId
* @return
*/
@Override
@Transactional
public void refreshPayResult(List<Long> billIds) {
TransactionBillQuery dto = new TransactionBillQuery();
dto.setBillIds(billIds);
List<TransactionBillVo> billList = transactionBillMapper.selectSmTransactionBillList(dto);
if (CollectionUtils.isEmpty(billList)) {
return;
}
public int refreshPayResult(Long billId) {
return this.refreshPayResult(this.selectSmTransactionBillByBillId(billId));
}
// 获取支付结果并判断是否支付成功若成功则更新数据
Date now = new Date();
for (TransactionBillVo bill : billList) {
if (TransactionBillStatus.PAYING.getStatus().equals(bill.getStatus()) || TransactionBillStatus.UNPAID.getStatus().equals(bill.getStatus()) ) {
boolean payResult = getPayResult(bill.getBillNo());
if (payResult) {
this.rechargeSuccess(bill.getBillNo(), now);
}
}
}
@Transactional
public int refreshPayResult(TransactionBillVo bill) {
return this.refreshPayResult(Collections.singletonList(bill));
}
@Override
@ -677,7 +678,7 @@ public class TransactionBillServiceImpl implements TransactionBillService {
}
@Override
public TransactionBill selectSmTransactionBillByBillNo(String billNo) {
public TransactionBillVo selectSmTransactionBillByBillNo(String billNo) {
return transactionBillMapper.selectSmTransactionBillByBillNo(billNo);
}
@ -838,4 +839,91 @@ public class TransactionBillServiceImpl implements TransactionBillService {
return execute != null && execute;
}
@Override
public int refund(BillRefundDTO dto) {
// 校验订单
TransactionBillVo bill = this.selectSmTransactionBillByBillId(dto.getBillId());
if (bill == null) {
return 0;
}
ServiceUtil.assertion(dto.getRefundAmount().compareTo(bill.getMoney()) > 0, "退款金额不允许大于订单金额");
ServiceUtil.assertion(!TransactionBillStatus.SUCCESS.getStatus().equals(bill.getStatus()), "当前订单状态不允许退款");
Integer result = transactionTemplate.execute(status -> {
// 修改订单状态
TransactionBill data = new TransactionBill();
data.setStatus(TransactionBillStatus.REFUNDING.getStatus());
TransactionBillQuery billQuery = new TransactionBillQuery();
billQuery.setBillId(dto.getBillId());
billQuery.setStatus(TransactionBillStatus.SUCCESS.getStatus());
int updateBill = this.updateByQuery(data, billQuery);
ServiceUtil.assertion(updateBill != 1, "修改订单状态失败");
// 创建退款订单
Refund refund = refundConverter.toPo(dto);
int updateRefund = refundService.insertRefund(refund);
ServiceUtil.assertion(updateRefund != 1, "创建退款订单失败");
RefundVO refundVO = refundService.selectRefundByRefundNo(refund.getRefundNo());
// 商户余额按照比例扣减
userService.subtractBalance(bill.getMchId(), refund.getMchAmount());
// 修改原订单的退款金额和退款手续费
int updateRefundAmount = this.addRefundAmount(bill.getBillId(), refund.getAmount(), refund.getMchAmount(), refund.getServiceAmount());
ServiceUtil.assertion(updateRefundAmount != 1, "修改原订单的退款金额和退款手续费失败");
// 发起退款
if (TransactionBillPayType.WECHAT.getType().equals(bill.getChannelId())) {
wxPayService.refund(refundVO);
} else {
throw new ServiceException("当前支付方式不支持退款");
}
return 1;
});
return result == null ? 0 : result;
}
@Override
public int updateByQuery(TransactionBill data, TransactionBillQuery query) {
if (query == null) {
return 0;
}
return transactionBillMapper.updateByQuery(data, query);
}
@Override
@Transactional
public int refreshPayResult(String billNo) {
return this.refreshPayResult(this.selectSmTransactionBillByBillNo(billNo));
}
@Override
@Transactional
public int refreshPayResult(List<TransactionBillVo> billList) {
if (CollectionUtils.isEmptyElement(billList)) {
return 0;
}
// 获取支付结果并判断是否支付成功若成功则更新数据
Date now = new Date();
for (TransactionBillVo bill : billList) {
if (TransactionBillStatus.PAYING.getStatus().equals(bill.getStatus()) || TransactionBillStatus.UNPAID.getStatus().equals(bill.getStatus()) ) {
boolean payResult = getPayResult(bill.getBillNo());
if (payResult) {
this.rechargeSuccess(bill.getBillNo(), now);
}
}
}
return 1;
}
@Override
public int addRefundAmount(Long billId, BigDecimal refundAmount, BigDecimal refundMchAmount, BigDecimal refundServiceAmount) {
if (billId == null) {
return 0;
}
return transactionBillMapper.addRefundAmount(billId, refundAmount, refundMchAmount, refundServiceAmount);
}
}

View File

@ -5,7 +5,13 @@ import com.ruoyi.common.enums.UserType;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.ServiceUtil;
import com.ruoyi.common.utils.collection.CollectionUtils;
import com.ruoyi.ss.device.domain.SmDeviceQuery;
import com.ruoyi.ss.device.domain.vo.SmDeviceVO;
import com.ruoyi.ss.device.service.ISmDeviceService;
import com.ruoyi.ss.store.domain.StoreQuery;
import com.ruoyi.ss.store.domain.StoreVo;
import com.ruoyi.ss.store.service.IStoreService;
import com.ruoyi.ss.user.domain.SmUserQuery;
import com.ruoyi.ss.user.domain.SmUserVo;
import com.ruoyi.ss.user.domain.bo.UserUpdateServiceRateBO;
@ -15,7 +21,6 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.util.CollectionUtils;
import java.math.BigDecimal;
import java.util.Collections;
@ -42,6 +47,9 @@ public class SmUserServiceImpl implements ISmUserService
@Autowired
private ISmDeviceService deviceService;
@Autowired
private IStoreService storeService;
/**
* 查询普通用户信息
*
@ -244,8 +252,19 @@ public class SmUserServiceImpl implements ISmUserService
* @param userIds
*/
private void validatePreLogicDel(List<Long> userIds) {
ServiceUtil.assertion(!CollectionUtils.isEmpty(userIds), "用户id不能为空");
ServiceUtil.assertion(CollectionUtils.isEmpty(userIds), "用户id不能为空");
ServiceUtil.assertion(!isExistUsers(userIds), "用户不存在");
SmDeviceQuery deviceQuery = new SmDeviceQuery();
deviceQuery.setUserIds(userIds);
List<SmDeviceVO> deviceList = deviceService.selectSmDeviceList(deviceQuery);
ServiceUtil.assertion(CollectionUtils.isNotEmpty(deviceList), "用户存在设备,不允许删除");
StoreQuery storeQuery = new StoreQuery();
storeQuery.setUserIds(userIds);
List<StoreVo> storeList = storeService.selectSmStoreList(storeQuery);
ServiceUtil.assertion(CollectionUtils.isNotEmpty(storeList), "用户存在店铺,不允许删除");
}
/**

View File

@ -0,0 +1,23 @@
package com.ruoyi.ss.wxPay.domain;
import com.wechat.pay.java.service.refund.model.AmountReq;
/**
* @author wjh
* 2024/7/10
*/
public interface RefundAble {
// 原商户订单号
String refundOutTradeNo();
// 退款单号
String refundOutRefundNo();
// 退款原因
String refundReason();
// 退款金额
AmountReq refundAmount();
}

View File

@ -1,8 +1,11 @@
package com.ruoyi.ss.wxPay.service;
import com.ruoyi.ss.wxPay.domain.RefundAble;
import com.ruoyi.ss.wxPay.domain.enums.TransferScene;
import com.wechat.pay.java.service.payments.jsapi.model.PrepayWithRequestPaymentResponse;
import com.wechat.pay.java.service.payments.model.Transaction;
import com.wechat.pay.java.service.refund.model.Refund;
import com.wechat.pay.java.service.refund.model.RefundNotification;
import com.wechat.pay.java.service.transferbatch.model.InitiateBatchTransferResponse;
import com.wechat.pay.java.service.transferbatch.model.TransferDetailInput;
@ -70,4 +73,14 @@ public interface IWxPayService {
* @param transferScene 转账场景
*/
List<TransferDetailInput> buildTransferDetailList(BigDecimal totalAmount, String openId, TransferScene transferScene);
/**
* 发起退款
*/
Refund refund(RefundAble refund);
/**
* 验签并解析
*/
<T> T checkAndParse(HttpServletRequest request, String body, Class<T> clazz);
}

View File

@ -10,7 +10,6 @@ import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.ServiceUtil;
import com.ruoyi.common.utils.SnowFlakeUtil;
import com.ruoyi.common.utils.http.HttpUtils;
import com.ruoyi.ss.account.service.ISmAccountService;
import com.ruoyi.ss.transactionBill.domain.TransactionBill;
import com.ruoyi.ss.transactionBill.domain.enums.TransactionBillPayType;
import com.ruoyi.ss.transactionBill.domain.enums.TransactionBillStatus;
@ -18,6 +17,7 @@ import com.ruoyi.ss.transactionBill.domain.enums.TransactionBillType;
import com.ruoyi.ss.transactionBill.service.TransactionBillService;
import com.ruoyi.ss.user.domain.SmUserVo;
import com.ruoyi.ss.user.service.ISmUserService;
import com.ruoyi.ss.wxPay.domain.RefundAble;
import com.ruoyi.ss.wxPay.domain.enums.TransferScene;
import com.ruoyi.ss.wxPayNotify.domain.SmWxPayNotify;
import com.ruoyi.ss.wxPayNotify.mapper.SmWxPayNotifyMapper;
@ -28,6 +28,9 @@ import com.wechat.pay.java.service.payments.jsapi.JsapiService;
import com.wechat.pay.java.service.payments.jsapi.JsapiServiceExtension;
import com.wechat.pay.java.service.payments.jsapi.model.*;
import com.wechat.pay.java.service.payments.model.Transaction;
import com.wechat.pay.java.service.refund.RefundService;
import com.wechat.pay.java.service.refund.model.CreateRequest;
import com.wechat.pay.java.service.refund.model.Refund;
import com.wechat.pay.java.service.transferbatch.TransferBatchService;
import com.wechat.pay.java.service.transferbatch.model.InitiateBatchTransferRequest;
import com.wechat.pay.java.service.transferbatch.model.InitiateBatchTransferResponse;
@ -45,6 +48,7 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/**
@ -81,7 +85,7 @@ public class WxPayService implements IWxPayService {
private RedisLock redisLock;
@Autowired
private ISmAccountService accountService;
private RefundService refundService;
@Autowired
private TransferBatchService transferBatchService;
@ -125,9 +129,9 @@ public class WxPayService implements IWxPayService {
transactionBillService.paying(bill.getBillId());
// 每隔20秒查询支付结果直到过期首次10秒查询
// scheduledExecutorService.schedule(() -> {
// transactionBillService.refreshPayResultBeforeExpire(bill.getBillNo(), 20, TimeUnit.SECONDS);
// }, 10, TimeUnit.SECONDS);
scheduledExecutorService.schedule(() -> {
transactionBillService.refreshPayResultBeforeExpire(bill.getBillNo(), 20, TimeUnit.SECONDS);
}, 10, TimeUnit.SECONDS);
} catch (Exception e) {
this.closeOrder(bill.getBillNo());
@ -286,6 +290,25 @@ public class WxPayService implements IWxPayService {
return transferDetailList;
}
/**
* 发起退款
*
* @param refund
*/
@Override
public Refund refund(RefundAble refund) {
CreateRequest request = new CreateRequest();
request.setOutTradeNo(refund.refundOutTradeNo());
request.setOutRefundNo(refund.refundOutRefundNo());
request.setReason(refund.refundReason());
request.setAmount(refund.refundAmount());
request.setNotifyUrl(wxPayConfig.getRefundNotifyUrl());
log.info("【退款】请求微信参数: {}", JSON.toJSONString(request));
Refund res = refundService.create(request);
log.info("【退款】微信返回结果:【{}】",JSON.toJSONString(refund));
return res;
}
private Long getTransferAmount(BigDecimal money) {
return money.multiply(new BigDecimal(100)).longValue();
}
@ -322,7 +345,8 @@ public class WxPayService implements IWxPayService {
* @param body 请求体
* @param clazz 返回值类型
*/
private <T> T checkAndParse(HttpServletRequest request, String body, Class<T> clazz) {
@Override
public <T> T checkAndParse(HttpServletRequest request, String body, Class<T> clazz) {
// 构造 RequestParam
RequestParam requestParam = new RequestParam.Builder()
.serialNumber(request.getHeader("Wechatpay-Serial"))

View File

@ -49,7 +49,7 @@ public class PayTask implements ApplicationRunner {
return;
}
transactionBillService.refreshPayResult(billList.stream().map(TransactionBill::getBillId).collect(Collectors.toList()));
transactionBillService.refreshPayResult(billList);
}

View File

@ -1,10 +1,19 @@
package com.ruoyi.web.controller.app;
import com.alibaba.fastjson2.JSON;
import com.ruoyi.common.annotation.Anonymous;
import com.ruoyi.common.constant.Constants;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.pay.wx.domain.NotifyEventType;
import com.ruoyi.common.utils.http.HttpUtils;
import com.ruoyi.ss.refund.service.RefundService;
import com.ruoyi.ss.transactionBill.service.TransactionBillService;
import com.ruoyi.ss.wxPay.service.IWxPayService;
import com.wechat.pay.java.core.exception.ValidationException;
import com.wechat.pay.java.core.notification.Notification;
import com.wechat.pay.java.service.refund.model.RefundNotification;
import com.wechat.pay.java.service.refund.model.Status;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
@ -32,6 +41,9 @@ public class AppPayController {
@Autowired
private TransactionBillService transactionBillService;
@Autowired
private RefundService refundService;
@ApiOperation("微信支付")
@GetMapping("/wx/{billNo}")
public AjaxResult wxPay(@PathVariable @ApiParam("订单编号") String billNo) {
@ -54,6 +66,35 @@ public class AppPayController {
return ResponseEntity.status(HttpStatus.OK).body(null);
}
// 微信退款回调
@PostMapping("/notify/wx/refund")
@Anonymous
public ResponseEntity<Boolean> wxPayNotifyRefund(HttpServletRequest request) {
try {
String body = HttpUtils.getBody(request);
log.info("【微信退款回调】接收对象 : " + JSON.toJSONString(body));
// 解析通知数据
Notification notification = JSON.parseObject(body, Notification.class);
log.info("【微信退款回调】转换成notification: " + JSON.toJSONString(notification));
// 退款成功通知
if (NotifyEventType.REFUND_SUCCESS.getValue().equals(notification.getEventType())) {
// 验签解密并转换成 RefundNotification
RefundNotification refundNotification = wxPayService.checkAndParse(request, body, RefundNotification.class);
log.info("【微信退款回调】转换成RefundNotification: " + JSON.toJSONString(refundNotification));
if (Status.SUCCESS.equals(refundNotification.getRefundStatus())) {
// 退款成功操作
String outRefundNo = refundNotification.getOutRefundNo();
refundService.handleRefundSuccess(outRefundNo);
}
}
} catch (ValidationException e) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(null);
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(null);
}
return ResponseEntity.status(HttpStatus.OK).body(null);
}
@ApiOperation("查询支付结果")
@GetMapping("/result/{billNo}")
public AjaxResult payResult(@PathVariable @ApiParam("订单编号") String billNo) {

View File

@ -14,6 +14,7 @@ import com.ruoyi.ss.transactionBill.domain.TransactionBill;
import com.ruoyi.ss.transactionBill.domain.TransactionBillQuery;
import com.ruoyi.ss.transactionBill.domain.TransactionBillVo;
import com.ruoyi.ss.transactionBill.domain.bo.TransactionBillBO;
import com.ruoyi.ss.transactionBill.domain.dto.BillRefundDTO;
import com.ruoyi.ss.transactionBill.domain.enums.TransactionBillGroupBy;
import com.ruoyi.ss.transactionBill.domain.enums.TransactionBillStatus;
import com.ruoyi.ss.transactionBill.domain.enums.TransactionBillType;
@ -209,4 +210,23 @@ public class AppTransactionBillController extends BaseController
return success(smTransactionBillService.bluetoothRechargeSuccess(billNo));
}
@ApiOperation("订单退款")
@PutMapping("/refund")
public AjaxResult refund(@RequestBody @Validated BillRefundDTO dto) {
// 判断用户是否订单收款人
TransactionBillVo bill = smTransactionBillService.selectSmTransactionBillByBillId(dto.getBillId());
if (bill == null) {
return error("订单不存在");
}
if (!Objects.equals(bill.getMchId(), getUserId())) {
return error("您无权操作退款");
}
return toAjax(smTransactionBillService.refund(dto));
}
@ApiOperation("刷新支付结果")
@PutMapping("/{billNo}/refreshPayResult")
public AjaxResult refreshPayResult(@PathVariable String billNo ) {
return toAjax(smTransactionBillService.refreshPayResult(billNo));
}
}

View File

@ -0,0 +1,107 @@
package com.ruoyi.web.controller.ss;
import java.util.List;
import javax.servlet.http.HttpServletResponse;
import com.ruoyi.ss.refund.domain.RefundQuery;
import com.ruoyi.ss.refund.domain.RefundVO;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.ss.refund.domain.Refund;
import com.ruoyi.ss.refund.service.RefundService;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.common.core.page.TableDataInfo;
/**
* 退款订单Controller
*
* @author ruoyi
* @date 2024-07-09
*/
@RestController
@RequestMapping("/ss/refund")
public class RefundController extends BaseController
{
@Autowired
private RefundService refundService;
/**
* 查询退款订单列表
*/
@PreAuthorize("@ss.hasPermi('ss:refund:list')")
@GetMapping("/list")
public TableDataInfo list(RefundQuery refund)
{
startPage();
List<RefundVO> list = refundService.selectRefundList(refund);
return getDataTable(list);
}
/**
* 导出退款订单列表
*/
@PreAuthorize("@ss.hasPermi('ss:refund:export')")
@Log(title = "退款订单", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(HttpServletResponse response, RefundQuery refund)
{
List<RefundVO> list = refundService.selectRefundList(refund);
ExcelUtil<RefundVO> util = new ExcelUtil<RefundVO>(RefundVO.class);
util.exportExcel(response, list, "退款订单数据");
}
/**
* 获取退款订单详细信息
*/
@PreAuthorize("@ss.hasPermi('ss:refund:query')")
@GetMapping(value = "/{refundId}")
public AjaxResult getInfo(@PathVariable("refundId") Long refundId)
{
return success(refundService.selectRefundByRefundId(refundId));
}
/**
* 新增退款订单
*/
@PreAuthorize("@ss.hasPermi('ss:refund:add')")
@Log(title = "退款订单", businessType = BusinessType.INSERT)
@PostMapping
public AjaxResult add(@RequestBody Refund refund)
{
return toAjax(refundService.insertRefund(refund));
}
/**
* 修改退款订单
*/
@PreAuthorize("@ss.hasPermi('ss:refund:edit')")
@Log(title = "退款订单", businessType = BusinessType.UPDATE)
@PutMapping
public AjaxResult edit(@RequestBody Refund refund)
{
return toAjax(refundService.updateRefund(refund));
}
/**
* 删除退款订单
*/
@PreAuthorize("@ss.hasPermi('ss:refund:remove')")
@Log(title = "退款订单", businessType = BusinessType.DELETE)
@DeleteMapping("/{refundIds}")
public AjaxResult remove(@PathVariable Long[] refundIds)
{
return toAjax(refundService.deleteRefundByRefundIds(refundIds));
}
}

View File

@ -2,15 +2,18 @@ package com.ruoyi.web.controller.ss;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import javax.servlet.http.HttpServletResponse;
import com.ruoyi.ss.transactionBill.domain.bo.TransactionBillBO;
import com.ruoyi.ss.transactionBill.domain.TransactionBillQuery;
import com.ruoyi.ss.transactionBill.domain.TransactionBillVo;
import com.ruoyi.ss.transactionBill.domain.dto.BillRefundDTO;
import io.swagger.annotations.ApiModelProperty;
import io.swagger.annotations.ApiOperation;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
@ -145,11 +148,11 @@ public class SmTransactionBillController extends BaseController
@ApiModelProperty("刷新支付结果")
@PreAuthorize("@ss.hasPermi('system:bill:edit')")
@GetMapping("/refreshPayResult/{billIds}")
@GetMapping("/refreshPayResult/{billId}")
@Log(title = "刷新支付结果", businessType = BusinessType.UPDATE)
public AjaxResult refreshPayResult(@PathVariable Long[] billIds)
public AjaxResult refreshPayResult(@PathVariable Long billId)
{
smTransactionBillService.refreshPayResult(Arrays.asList(billIds));
smTransactionBillService.refreshPayResult(billId);
return success();
}
@ -162,4 +165,11 @@ public class SmTransactionBillController extends BaseController
return success();
}
// 订单退款
@PutMapping("/refund")
@PreAuthorize("@ss.hasPermi('system:bill:refund')")
public AjaxResult refund(@RequestBody @Validated BillRefundDTO dto) {
return toAjax(smTransactionBillService.refund(dto));
}
}

View File

@ -137,9 +137,9 @@ public class SmUserController extends BaseController
@PreAuthorize("@ss.hasPermi('system:smUser:remove')")
@Log(title = "普通用户信息", businessType = BusinessType.DELETE)
@DeleteMapping("/{userIds}")
public AjaxResult remove(@PathVariable Long[] userIds)
public AjaxResult remove(@PathVariable List<Long> userIds)
{
return toAjax(smUserService.deleteSmUserByUserIds(userIds));
return toAjax(smUserService.logicDel(userIds));
}
}

View File

@ -35,8 +35,7 @@ public class DeviceAdminRequiredAspect {
// 判断当前用户是否有权限访问
@Before("@annotation(required)")
public void doBefore(JoinPoint point, DeviceAdminRequired required) {
LoginUser loginUser = SecurityUtils.getLoginUser();
SmUser user = loginUser.getSmUser();
SmUserVo user = smUserService.selectSmUserByUserId(SecurityUtils.getUserId());
ServiceUtil.assertion(user == null || user.getDeviceAdmin() == null || !user.getDeviceAdmin(), "您无权操作" );
}

View File

@ -20,7 +20,9 @@ wx:
# apiV3密钥
apiV3Key: 49819e0f0abdb2df3246f7b27f264d75
# 通知回调地址
notifyUrl: http://124.221.246.124:2290/dev-api/app/pay/notify/wx # 内网穿透
notifyUrl: http://124.221.246.124:2290/app/pay/notify/wx # 内网穿透
# 退款通知回调地址
refundNotifyUrl: http://124.221.246.124:2290/app/pay/notify/wx/refund
# 密钥所在位置
privateKeyPath: D:/project/证书/wxpay/apiclient_key.pem
# 证书序列号

View File

@ -6,7 +6,7 @@ spring:
druid:
# 主库数据源
master:
url: jdbc:mysql://106.75.233.135:3306/smart-switch?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
url: jdbc:mysql://106.75.233.135:3306/smart-switch-test?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
username: root
password: 9671e015b05b3f11
# 从库数据源

View File

@ -21,6 +21,8 @@ wx:
apiV3Key: 49819e0f0abdb2df3246f7b27f264d75
# 通知回调地址
notifyUrl: https://kg.chuantewulian.cn/prod-api/app/pay/notify/wx # 正式环境
# 退款通知回调地址
refundNotifyUrl: https://kg.chuantewulian.cn/prod-api/app/pay/notify/wx/refund
# 密钥所在位置
privateKeyPath: /www/wwwroot/smart-switch/wxpay/apiclient_key.pem
# 证书序列号

View File

@ -21,6 +21,8 @@ wx:
apiV3Key: 49819e0f0abdb2df3246f7b27f264d75
# 通知回调地址
notifyUrl: https://kg.chuantewulian.cn/test-api/app/pay/notify/wx # 测试环境
# 退款通知回调地址
refundNotifyUrl: https://kg.chuantewulian.cn/test-api/app/pay/notify/wx/refund
# 密钥所在位置
privateKeyPath: /home/www/projects/smart-switch/wxpay/apiclient_key.pem
# 证书序列号