feat: 新增虚拟支付订单与网盘链接检测功能
This commit is contained in:
@@ -40,6 +40,9 @@ public class AppBlogArticle extends BaseEntity
|
||||
@Excel(name = "封面图url")
|
||||
private String showImg;
|
||||
|
||||
/** List-card thumbnail URL (not persisted). */
|
||||
private String showImgThumb;
|
||||
|
||||
/** 关联已有资源(关联app_resource id) */
|
||||
@Excel(name = "关联已有资源", readConverterExp = "关=联app_resource,i=d")
|
||||
private Long appResourceId;
|
||||
@@ -159,6 +162,15 @@ public class AppBlogArticle extends BaseEntity
|
||||
{
|
||||
return showImg;
|
||||
}
|
||||
public void setShowImgThumb(String showImgThumb)
|
||||
{
|
||||
this.showImgThumb = showImgThumb;
|
||||
}
|
||||
|
||||
public String getShowImgThumb()
|
||||
{
|
||||
return showImgThumb;
|
||||
}
|
||||
public void setAppResourceId(Long appResourceId)
|
||||
{
|
||||
this.appResourceId = appResourceId;
|
||||
|
||||
@@ -27,6 +27,9 @@ public class AppResource extends BaseEntity
|
||||
@Excel(name = "封面图")
|
||||
private String showImg;
|
||||
|
||||
/** List-card thumbnail URL (not persisted). */
|
||||
private String showImgThumb;
|
||||
|
||||
/** 说明 */
|
||||
@Excel(name = "说明")
|
||||
private String explain;
|
||||
@@ -51,6 +54,13 @@ public class AppResource extends BaseEntity
|
||||
@Excel(name = "需要观看几次广告解锁")
|
||||
private Long adNumber;
|
||||
|
||||
/** 虚拟支付价格,单位分 */
|
||||
@Excel(name = "付费价格(分)")
|
||||
private Integer priceFen;
|
||||
|
||||
/** 同价格档位对应的微信虚拟道具ID(非 app_resource 表字段) */
|
||||
private String virtualProductId;
|
||||
|
||||
/** 下载次数 */
|
||||
@Excel(name = "下载次数")
|
||||
private Long downNum;
|
||||
@@ -100,6 +110,15 @@ public class AppResource extends BaseEntity
|
||||
{
|
||||
return showImg;
|
||||
}
|
||||
public void setShowImgThumb(String showImgThumb)
|
||||
{
|
||||
this.showImgThumb = showImgThumb;
|
||||
}
|
||||
|
||||
public String getShowImgThumb()
|
||||
{
|
||||
return showImgThumb;
|
||||
}
|
||||
public void setExplain(String explain)
|
||||
{
|
||||
this.explain = explain;
|
||||
@@ -145,6 +164,26 @@ public class AppResource extends BaseEntity
|
||||
{
|
||||
return adNumber;
|
||||
}
|
||||
|
||||
public Integer getPriceFen()
|
||||
{
|
||||
return priceFen;
|
||||
}
|
||||
|
||||
public void setPriceFen(Integer priceFen)
|
||||
{
|
||||
this.priceFen = priceFen;
|
||||
}
|
||||
|
||||
public String getVirtualProductId()
|
||||
{
|
||||
return virtualProductId;
|
||||
}
|
||||
|
||||
public void setVirtualProductId(String virtualProductId)
|
||||
{
|
||||
this.virtualProductId = virtualProductId;
|
||||
}
|
||||
public void setDownNum(Long downNum)
|
||||
{
|
||||
this.downNum = downNum;
|
||||
@@ -194,6 +233,8 @@ public class AppResource extends BaseEntity
|
||||
.append("isShow", getIsShow())
|
||||
.append("isAd", getIsAd())
|
||||
.append("adNumber", getAdNumber())
|
||||
.append("priceFen", getPriceFen())
|
||||
.append("virtualProductId", getVirtualProductId())
|
||||
.append("downNum", getDownNum())
|
||||
.append("weight", getWeight())
|
||||
.append("delFlag", getDelFlag())
|
||||
|
||||
@@ -0,0 +1,239 @@
|
||||
package com.ruoyi.app.domain;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import com.ruoyi.common.annotation.Excel;
|
||||
import com.ruoyi.common.core.domain.BaseEntity;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 资源虚拟支付订单。
|
||||
*/
|
||||
public class AppVirtualOrder extends BaseEntity
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private Long id;
|
||||
|
||||
@Excel(name = "业务订单号")
|
||||
private String orderNo;
|
||||
|
||||
@Excel(name = "用户ID")
|
||||
private Long userId;
|
||||
|
||||
@Excel(name = "用户账号")
|
||||
private String userName;
|
||||
|
||||
@Excel(name = "用户昵称")
|
||||
private String nickName;
|
||||
|
||||
@Excel(name = "资源ID")
|
||||
private Long resourceId;
|
||||
|
||||
@Excel(name = "资源标题")
|
||||
private String resourceTitle;
|
||||
|
||||
@Excel(name = "微信道具ID")
|
||||
private String productId;
|
||||
|
||||
@Excel(name = "支付金额(分)")
|
||||
private Integer priceFen;
|
||||
|
||||
@Excel(name = "OpenID")
|
||||
private String openId;
|
||||
|
||||
/** 0-待支付,1-已支付并发货,2-已退款,3-已关闭。 */
|
||||
@Excel(name = "订单状态", readConverterExp = "0=待支付,1=已支付并发货,2=已退款,3=已关闭")
|
||||
private Integer status;
|
||||
|
||||
@Excel(name = "微信内部订单号")
|
||||
private String wxOrderNo;
|
||||
|
||||
@Excel(name = "微信支付交易单号")
|
||||
private String transactionId;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@Excel(name = "支付时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date payTime;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@Excel(name = "发货时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date provideTime;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@Excel(name = "退款时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date refundTime;
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private Date lastQueryTime;
|
||||
|
||||
public Long getId()
|
||||
{
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id)
|
||||
{
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getOrderNo()
|
||||
{
|
||||
return orderNo;
|
||||
}
|
||||
|
||||
public void setOrderNo(String orderNo)
|
||||
{
|
||||
this.orderNo = orderNo;
|
||||
}
|
||||
|
||||
public Long getUserId()
|
||||
{
|
||||
return userId;
|
||||
}
|
||||
|
||||
public void setUserId(Long userId)
|
||||
{
|
||||
this.userId = userId;
|
||||
}
|
||||
|
||||
public String getUserName()
|
||||
{
|
||||
return userName;
|
||||
}
|
||||
|
||||
public void setUserName(String userName)
|
||||
{
|
||||
this.userName = userName;
|
||||
}
|
||||
|
||||
public String getNickName()
|
||||
{
|
||||
return nickName;
|
||||
}
|
||||
|
||||
public void setNickName(String nickName)
|
||||
{
|
||||
this.nickName = nickName;
|
||||
}
|
||||
|
||||
public Long getResourceId()
|
||||
{
|
||||
return resourceId;
|
||||
}
|
||||
|
||||
public void setResourceId(Long resourceId)
|
||||
{
|
||||
this.resourceId = resourceId;
|
||||
}
|
||||
|
||||
public String getResourceTitle()
|
||||
{
|
||||
return resourceTitle;
|
||||
}
|
||||
|
||||
public void setResourceTitle(String resourceTitle)
|
||||
{
|
||||
this.resourceTitle = resourceTitle;
|
||||
}
|
||||
|
||||
public String getProductId()
|
||||
{
|
||||
return productId;
|
||||
}
|
||||
|
||||
public void setProductId(String productId)
|
||||
{
|
||||
this.productId = productId;
|
||||
}
|
||||
|
||||
public Integer getPriceFen()
|
||||
{
|
||||
return priceFen;
|
||||
}
|
||||
|
||||
public void setPriceFen(Integer priceFen)
|
||||
{
|
||||
this.priceFen = priceFen;
|
||||
}
|
||||
|
||||
public String getOpenId()
|
||||
{
|
||||
return openId;
|
||||
}
|
||||
|
||||
public void setOpenId(String openId)
|
||||
{
|
||||
this.openId = openId;
|
||||
}
|
||||
|
||||
public Integer getStatus()
|
||||
{
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(Integer status)
|
||||
{
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public String getWxOrderNo()
|
||||
{
|
||||
return wxOrderNo;
|
||||
}
|
||||
|
||||
public void setWxOrderNo(String wxOrderNo)
|
||||
{
|
||||
this.wxOrderNo = wxOrderNo;
|
||||
}
|
||||
|
||||
public String getTransactionId()
|
||||
{
|
||||
return transactionId;
|
||||
}
|
||||
|
||||
public void setTransactionId(String transactionId)
|
||||
{
|
||||
this.transactionId = transactionId;
|
||||
}
|
||||
|
||||
public Date getPayTime()
|
||||
{
|
||||
return payTime;
|
||||
}
|
||||
|
||||
public void setPayTime(Date payTime)
|
||||
{
|
||||
this.payTime = payTime;
|
||||
}
|
||||
|
||||
public Date getProvideTime()
|
||||
{
|
||||
return provideTime;
|
||||
}
|
||||
|
||||
public void setProvideTime(Date provideTime)
|
||||
{
|
||||
this.provideTime = provideTime;
|
||||
}
|
||||
|
||||
public Date getRefundTime()
|
||||
{
|
||||
return refundTime;
|
||||
}
|
||||
|
||||
public void setRefundTime(Date refundTime)
|
||||
{
|
||||
this.refundTime = refundTime;
|
||||
}
|
||||
|
||||
public Date getLastQueryTime()
|
||||
{
|
||||
return lastQueryTime;
|
||||
}
|
||||
|
||||
public void setLastQueryTime(Date lastQueryTime)
|
||||
{
|
||||
this.lastQueryTime = lastQueryTime;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package com.ruoyi.app.domain;
|
||||
|
||||
import com.ruoyi.common.core.domain.BaseEntity;
|
||||
|
||||
/**
|
||||
* 虚拟支付价格档位与微信道具的映射。
|
||||
*/
|
||||
public class AppVirtualProduct extends BaseEntity
|
||||
{
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private Long id;
|
||||
private String productId;
|
||||
private Integer priceFen;
|
||||
private String productName;
|
||||
private Integer status;
|
||||
|
||||
public Long getId()
|
||||
{
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id)
|
||||
{
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getProductId()
|
||||
{
|
||||
return productId;
|
||||
}
|
||||
|
||||
public void setProductId(String productId)
|
||||
{
|
||||
this.productId = productId;
|
||||
}
|
||||
|
||||
public Integer getPriceFen()
|
||||
{
|
||||
return priceFen;
|
||||
}
|
||||
|
||||
public void setPriceFen(Integer priceFen)
|
||||
{
|
||||
this.priceFen = priceFen;
|
||||
}
|
||||
|
||||
public String getProductName()
|
||||
{
|
||||
return productName;
|
||||
}
|
||||
|
||||
public void setProductName(String productName)
|
||||
{
|
||||
this.productName = productName;
|
||||
}
|
||||
|
||||
public Integer getStatus()
|
||||
{
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(Integer status)
|
||||
{
|
||||
this.status = status;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.ruoyi.app.domain.request;
|
||||
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
public class CreateVirtualOrderRequest
|
||||
{
|
||||
@NotNull(message = "资源ID不能为空")
|
||||
private Long resourceId;
|
||||
|
||||
@NotBlank(message = "微信登录凭证不能为空")
|
||||
private String code;
|
||||
|
||||
public Long getResourceId()
|
||||
{
|
||||
return resourceId;
|
||||
}
|
||||
|
||||
public void setResourceId(Long resourceId)
|
||||
{
|
||||
this.resourceId = resourceId;
|
||||
}
|
||||
|
||||
public String getCode()
|
||||
{
|
||||
return code;
|
||||
}
|
||||
|
||||
public void setCode(String code)
|
||||
{
|
||||
this.code = code;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.ruoyi.app.mapper;
|
||||
|
||||
import com.ruoyi.app.domain.AppVirtualOrder;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
public interface AppVirtualOrderMapper
|
||||
{
|
||||
int insertAppVirtualOrder(AppVirtualOrder order);
|
||||
|
||||
AppVirtualOrder selectByOrderNo(String orderNo);
|
||||
|
||||
AppVirtualOrder selectAppVirtualOrderById(Long id);
|
||||
|
||||
List<AppVirtualOrder> selectAppVirtualOrderList(AppVirtualOrder order);
|
||||
|
||||
int countAnyEntitlement(@Param("userId") Long userId, @Param("resourceId") Long resourceId);
|
||||
|
||||
int markPaid(@Param("orderNo") String orderNo,
|
||||
@Param("wxOrderNo") String wxOrderNo,
|
||||
@Param("transactionId") String transactionId,
|
||||
@Param("payTime") Date payTime);
|
||||
|
||||
int insertEntitlement(AppVirtualOrder order);
|
||||
|
||||
int markRefunded(@Param("orderNo") String orderNo, @Param("refundTime") Date refundTime);
|
||||
|
||||
int deleteEntitlementByOrderNo(String orderNo);
|
||||
|
||||
int markClosed(String orderNo);
|
||||
|
||||
int markQuerying(String orderNo);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.ruoyi.app.mapper;
|
||||
|
||||
import com.ruoyi.app.domain.AppVirtualProduct;
|
||||
|
||||
public interface AppVirtualProductMapper
|
||||
{
|
||||
AppVirtualProduct selectActiveByPrice(Integer priceFen);
|
||||
|
||||
AppVirtualProduct selectByProductId(String productId);
|
||||
|
||||
int insertAppVirtualProduct(AppVirtualProduct product);
|
||||
|
||||
int updateProductIdByPrice(AppVirtualProduct product);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.ruoyi.app.service;
|
||||
|
||||
import com.ruoyi.app.domain.AppVirtualOrder;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 虚拟支付订单管理服务。
|
||||
*/
|
||||
public interface IAppVirtualOrderService
|
||||
{
|
||||
AppVirtualOrder selectAppVirtualOrderById(Long id);
|
||||
|
||||
List<AppVirtualOrder> selectAppVirtualOrderList(AppVirtualOrder order);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.ruoyi.app.service;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.ruoyi.app.domain.request.CreateVirtualOrderRequest;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public interface IAppVirtualPayService
|
||||
{
|
||||
Map<String, Object> createOrder(CreateVirtualOrderRequest request);
|
||||
|
||||
Map<String, Object> queryOrder(String orderNo, boolean sync);
|
||||
|
||||
boolean verifyCallbackSignature(String signature, String timestamp, String nonce);
|
||||
|
||||
void handleCallback(JsonNode body);
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
package com.ruoyi.app.service;
|
||||
|
||||
import com.ruoyi.app.domain.AppBlogArticle;
|
||||
import com.ruoyi.app.domain.AppResource;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Builds CDN image variants without persisting derived URLs in the database.
|
||||
*/
|
||||
@Component
|
||||
public class ImageUrlService
|
||||
{
|
||||
@Value("${app.image.cdn-domain:https://img.yidaima.cn}")
|
||||
private String cdnDomain;
|
||||
|
||||
@Value("${app.image.article-card-operation:imageView2/1/w/600/h/375/format/webp/q/75/ignore-error/1}")
|
||||
private String articleCardOperation;
|
||||
|
||||
@Value("${app.image.resource-card-operation:imageView2/1/w/500/h/400/format/webp/q/75/ignore-error/1}")
|
||||
private String resourceCardOperation;
|
||||
|
||||
public void decorateArticles(List<AppBlogArticle> articles)
|
||||
{
|
||||
if (articles == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
for (AppBlogArticle article : articles)
|
||||
{
|
||||
decorateArticle(article);
|
||||
}
|
||||
}
|
||||
|
||||
public AppBlogArticle decorateArticle(AppBlogArticle article)
|
||||
{
|
||||
if (article == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
String originalUrl = normalizeOriginalUrl(article.getShowImg());
|
||||
article.setShowImg(originalUrl);
|
||||
article.setShowImgThumb(buildThumbnailUrl(originalUrl, articleCardOperation));
|
||||
decorateResource(article.getAppResource());
|
||||
return article;
|
||||
}
|
||||
|
||||
public void decorateResources(List<AppResource> resources)
|
||||
{
|
||||
if (resources == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
for (AppResource resource : resources)
|
||||
{
|
||||
decorateResource(resource);
|
||||
}
|
||||
}
|
||||
|
||||
public AppResource decorateResource(AppResource resource)
|
||||
{
|
||||
if (resource == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
String originalUrl = normalizeOriginalUrl(resource.getShowImg());
|
||||
resource.setShowImg(originalUrl);
|
||||
resource.setShowImgThumb(buildThumbnailUrl(originalUrl, resourceCardOperation));
|
||||
return resource;
|
||||
}
|
||||
|
||||
public String normalizeOriginalUrl(String originalUrl)
|
||||
{
|
||||
if (originalUrl == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
String url = originalUrl.trim();
|
||||
String host = cdnHost();
|
||||
String httpPrefix = "http://" + host + "/";
|
||||
if (url.startsWith(httpPrefix))
|
||||
{
|
||||
return "https://" + url.substring("http://".length());
|
||||
}
|
||||
String protocolRelativePrefix = "//" + host + "/";
|
||||
if (url.startsWith(protocolRelativePrefix))
|
||||
{
|
||||
return "https:" + url;
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
private String buildThumbnailUrl(String originalUrl, String operation)
|
||||
{
|
||||
if (originalUrl == null || originalUrl.isEmpty())
|
||||
{
|
||||
return originalUrl;
|
||||
}
|
||||
String cdnPrefix = "https://" + cdnHost() + "/";
|
||||
if (!originalUrl.startsWith(cdnPrefix))
|
||||
{
|
||||
return originalUrl;
|
||||
}
|
||||
// Avoid corrupting signed or already processed URLs.
|
||||
if (originalUrl.indexOf('?') >= 0)
|
||||
{
|
||||
return originalUrl;
|
||||
}
|
||||
return originalUrl + "?" + operation;
|
||||
}
|
||||
|
||||
private String cdnHost()
|
||||
{
|
||||
String value = cdnDomain == null ? "img.yidaima.cn" : cdnDomain.trim();
|
||||
if (value.startsWith("https://"))
|
||||
{
|
||||
value = value.substring("https://".length());
|
||||
}
|
||||
else if (value.startsWith("http://"))
|
||||
{
|
||||
value = value.substring("http://".length());
|
||||
}
|
||||
while (value.endsWith("/"))
|
||||
{
|
||||
value = value.substring(0, value.length() - 1);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,9 @@ import com.ruoyi.app.domain.AppResourceList;
|
||||
import com.ruoyi.app.mapper.AppResourceMapper;
|
||||
import com.ruoyi.app.domain.AppResource;
|
||||
import com.ruoyi.app.service.IAppResourceService;
|
||||
import com.ruoyi.app.domain.AppVirtualProduct;
|
||||
import com.ruoyi.app.mapper.AppVirtualProductMapper;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
|
||||
/**
|
||||
* 资源Service业务层处理
|
||||
@@ -24,6 +27,9 @@ public class AppResourceServiceImpl implements IAppResourceService
|
||||
@Autowired
|
||||
private AppResourceMapper appResourceMapper;
|
||||
|
||||
@Autowired
|
||||
private AppVirtualProductMapper virtualProductMapper;
|
||||
|
||||
/**
|
||||
* 查询资源
|
||||
*
|
||||
@@ -58,6 +64,7 @@ public class AppResourceServiceImpl implements IAppResourceService
|
||||
@Override
|
||||
public int insertAppResource(AppResource appResource)
|
||||
{
|
||||
configureVirtualProduct(appResource);
|
||||
appResource.setCreateTime(DateUtils.getNowDate());
|
||||
int rows = appResourceMapper.insertAppResource(appResource);
|
||||
insertAppResourceList(appResource);
|
||||
@@ -74,6 +81,7 @@ public class AppResourceServiceImpl implements IAppResourceService
|
||||
@Override
|
||||
public int updateAppResource(AppResource appResource)
|
||||
{
|
||||
configureVirtualProduct(appResource);
|
||||
appResourceMapper.deleteAppResourceListByAppResourceId(appResource.getId());
|
||||
insertAppResourceList(appResource);
|
||||
return appResourceMapper.updateAppResource(appResource);
|
||||
@@ -130,4 +138,26 @@ public class AppResourceServiceImpl implements IAppResourceService
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 付费资源按价格档位自动匹配已配置的微信道具。
|
||||
*/
|
||||
private void configureVirtualProduct(AppResource resource)
|
||||
{
|
||||
if (resource.getIsAd() == null || resource.getIsAd() != 3L)
|
||||
{
|
||||
resource.setPriceFen(0);
|
||||
return;
|
||||
}
|
||||
if (resource.getPriceFen() == null || resource.getPriceFen() <= 0)
|
||||
{
|
||||
throw new ServiceException("付费资源价格必须大于0分");
|
||||
}
|
||||
AppVirtualProduct priceProduct = virtualProductMapper.selectActiveByPrice(resource.getPriceFen());
|
||||
if (priceProduct == null || StringUtils.isBlank(priceProduct.getProductId()))
|
||||
{
|
||||
throw new ServiceException("当前价格档位未配置微信道具,请先配置价格与道具映射");
|
||||
}
|
||||
resource.setVirtualProductId(priceProduct.getProductId());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.ruoyi.app.service.impl;
|
||||
|
||||
import com.ruoyi.app.domain.AppVirtualOrder;
|
||||
import com.ruoyi.app.mapper.AppVirtualOrderMapper;
|
||||
import com.ruoyi.app.service.IAppVirtualOrderService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 虚拟支付订单管理服务实现。
|
||||
*/
|
||||
@Service
|
||||
public class AppVirtualOrderServiceImpl implements IAppVirtualOrderService
|
||||
{
|
||||
@Autowired
|
||||
private AppVirtualOrderMapper appVirtualOrderMapper;
|
||||
|
||||
@Override
|
||||
public AppVirtualOrder selectAppVirtualOrderById(Long id)
|
||||
{
|
||||
return appVirtualOrderMapper.selectAppVirtualOrderById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AppVirtualOrder> selectAppVirtualOrderList(AppVirtualOrder order)
|
||||
{
|
||||
return appVirtualOrderMapper.selectAppVirtualOrderList(order);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,529 @@
|
||||
package com.ruoyi.app.service.impl;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.ruoyi.app.domain.AppIntegralRecord;
|
||||
import com.ruoyi.app.domain.AppResource;
|
||||
import com.ruoyi.app.domain.AppVirtualOrder;
|
||||
import com.ruoyi.app.domain.AppVirtualProduct;
|
||||
import com.ruoyi.app.domain.request.CreateVirtualOrderRequest;
|
||||
import com.ruoyi.app.mapper.AppIntegralRecordMapper;
|
||||
import com.ruoyi.app.mapper.AppResourceMapper;
|
||||
import com.ruoyi.app.mapper.AppVirtualOrderMapper;
|
||||
import com.ruoyi.app.mapper.AppVirtualProductMapper;
|
||||
import com.ruoyi.app.service.IAppVirtualPayService;
|
||||
import com.ruoyi.common.core.domain.entity.SysUser;
|
||||
import com.ruoyi.common.exception.ServiceException;
|
||||
import com.ruoyi.common.utils.DateUtils;
|
||||
import com.ruoyi.common.utils.SecurityUtils;
|
||||
import com.ruoyi.common.wx.VirtualPayConfig;
|
||||
import com.ruoyi.common.wx.WxCodeSession;
|
||||
import com.ruoyi.common.wx.WxCodeSessionService;
|
||||
import com.ruoyi.common.wx.WxPayConfig;
|
||||
import com.ruoyi.system.mapper.SysUserMapper;
|
||||
import okhttp3.HttpUrl;
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.Response;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Service
|
||||
public class AppVirtualPayServiceImpl implements IAppVirtualPayService
|
||||
{
|
||||
private static final Logger log = LoggerFactory.getLogger(AppVirtualPayServiceImpl.class);
|
||||
private static final MediaType JSON_MEDIA_TYPE = MediaType.parse("application/json; charset=utf-8");
|
||||
private static final String ACCESS_TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token";
|
||||
private static final String QUERY_ORDER_URI = "/xpay/query_order";
|
||||
private static final String QUERY_ORDER_URL = "https://api.weixin.qq.com/xpay/query_order";
|
||||
private static final String NOTIFY_GOODS_URI = "/xpay/notify_provide_goods";
|
||||
private static final String NOTIFY_GOODS_URL = "https://api.weixin.qq.com/xpay/notify_provide_goods";
|
||||
|
||||
private final VirtualPayConfig virtualPayConfig;
|
||||
private final WxPayConfig wxPayConfig;
|
||||
private final WxCodeSessionService wxCodeSessionService;
|
||||
private final AppResourceMapper appResourceMapper;
|
||||
private final AppIntegralRecordMapper integralRecordMapper;
|
||||
private final AppVirtualProductMapper virtualProductMapper;
|
||||
private final AppVirtualOrderMapper virtualOrderMapper;
|
||||
private final SysUserMapper sysUserMapper;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final OkHttpClient httpClient;
|
||||
|
||||
private volatile String cachedAccessToken;
|
||||
private volatile long accessTokenExpiresAt;
|
||||
|
||||
public AppVirtualPayServiceImpl(VirtualPayConfig virtualPayConfig,
|
||||
WxPayConfig wxPayConfig,
|
||||
WxCodeSessionService wxCodeSessionService,
|
||||
AppResourceMapper appResourceMapper,
|
||||
AppIntegralRecordMapper integralRecordMapper,
|
||||
AppVirtualProductMapper virtualProductMapper,
|
||||
AppVirtualOrderMapper virtualOrderMapper,
|
||||
SysUserMapper sysUserMapper,
|
||||
ObjectMapper objectMapper)
|
||||
{
|
||||
this.virtualPayConfig = virtualPayConfig;
|
||||
this.wxPayConfig = wxPayConfig;
|
||||
this.wxCodeSessionService = wxCodeSessionService;
|
||||
this.appResourceMapper = appResourceMapper;
|
||||
this.integralRecordMapper = integralRecordMapper;
|
||||
this.virtualProductMapper = virtualProductMapper;
|
||||
this.virtualOrderMapper = virtualOrderMapper;
|
||||
this.sysUserMapper = sysUserMapper;
|
||||
this.objectMapper = objectMapper;
|
||||
this.httpClient = new OkHttpClient.Builder()
|
||||
.connectTimeout(5, TimeUnit.SECONDS)
|
||||
.readTimeout(10, TimeUnit.SECONDS)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public Map<String, Object> createOrder(CreateVirtualOrderRequest request)
|
||||
{
|
||||
checkEnabled();
|
||||
Long userId = SecurityUtils.getUserId();
|
||||
SysUser user = sysUserMapper.selectUserById(userId);
|
||||
if (user == null || StringUtils.isBlank(user.getOpenId()))
|
||||
{
|
||||
throw new ServiceException("当前账号未绑定微信");
|
||||
}
|
||||
|
||||
WxCodeSession codeSession = wxCodeSessionService.exchange(request.getCode());
|
||||
if (!MessageDigest.isEqual(user.getOpenId().getBytes(StandardCharsets.UTF_8),
|
||||
codeSession.getOpenId().getBytes(StandardCharsets.UTF_8)))
|
||||
{
|
||||
throw new ServiceException("微信身份与当前登录账号不一致");
|
||||
}
|
||||
|
||||
AppResource resource = appResourceMapper.selectAppResourceById(request.getResourceId());
|
||||
if (resource == null || resource.getIsAd() == null || resource.getIsAd() != 3L)
|
||||
{
|
||||
throw new ServiceException("该资源不支持虚拟支付");
|
||||
}
|
||||
if (resource.getPriceFen() == null || resource.getPriceFen() <= 0)
|
||||
{
|
||||
throw new ServiceException("资源价格未配置");
|
||||
}
|
||||
if (virtualOrderMapper.countAnyEntitlement(userId, resource.getId()) > 0)
|
||||
{
|
||||
throw new ServiceException("该资源已经解锁");
|
||||
}
|
||||
|
||||
AppVirtualProduct product = virtualProductMapper.selectActiveByPrice(resource.getPriceFen());
|
||||
if (product == null || StringUtils.isBlank(product.getProductId()))
|
||||
{
|
||||
throw new ServiceException("当前价格档位尚未配置微信道具");
|
||||
}
|
||||
|
||||
AppVirtualOrder order = new AppVirtualOrder();
|
||||
order.setOrderNo(generateOrderNo());
|
||||
order.setUserId(userId);
|
||||
order.setResourceId(resource.getId());
|
||||
order.setProductId(product.getProductId());
|
||||
order.setPriceFen(resource.getPriceFen());
|
||||
order.setOpenId(codeSession.getOpenId());
|
||||
order.setStatus(0);
|
||||
order.setCreateTime(DateUtils.getNowDate());
|
||||
virtualOrderMapper.insertAppVirtualOrder(order);
|
||||
|
||||
LinkedHashMap<String, Object> signDataMap = new LinkedHashMap<>();
|
||||
signDataMap.put("offerId", virtualPayConfig.getOfferId());
|
||||
signDataMap.put("buyQuantity", 1);
|
||||
signDataMap.put("env", virtualPayConfig.getEnv());
|
||||
signDataMap.put("currencyType", "CNY");
|
||||
signDataMap.put("productId", product.getProductId());
|
||||
signDataMap.put("goodsPrice", resource.getPriceFen());
|
||||
signDataMap.put("outTradeNo", order.getOrderNo());
|
||||
signDataMap.put("attach", order.getOrderNo());
|
||||
|
||||
try
|
||||
{
|
||||
String signData = objectMapper.writeValueAsString(signDataMap);
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
result.put("orderNo", order.getOrderNo());
|
||||
result.put("signData", signData);
|
||||
result.put("paySig", hmacSha256(virtualPayConfig.getAppKey(),
|
||||
"requestVirtualPayment&" + signData));
|
||||
result.put("signature", hmacSha256(codeSession.getSessionKey(), signData));
|
||||
result.put("mode", "short_series_goods");
|
||||
return result;
|
||||
}
|
||||
catch (JsonProcessingException e)
|
||||
{
|
||||
throw new ServiceException("生成虚拟支付参数失败");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public Map<String, Object> queryOrder(String orderNo, boolean sync)
|
||||
{
|
||||
Long userId = SecurityUtils.getUserId();
|
||||
AppVirtualOrder order = virtualOrderMapper.selectByOrderNo(orderNo);
|
||||
if (order == null || !userId.equals(order.getUserId()))
|
||||
{
|
||||
throw new ServiceException("订单不存在");
|
||||
}
|
||||
|
||||
if (sync && order.getStatus() == 0 && virtualOrderMapper.markQuerying(orderNo) == 1)
|
||||
{
|
||||
syncOrderFromWechat(order);
|
||||
order = virtualOrderMapper.selectByOrderNo(orderNo);
|
||||
}
|
||||
|
||||
Map<String, Object> result = new LinkedHashMap<>();
|
||||
result.put("orderNo", order.getOrderNo());
|
||||
result.put("resourceId", order.getResourceId());
|
||||
result.put("status", order.getStatus());
|
||||
result.put("unlocked", order.getStatus() == 1);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean verifyCallbackSignature(String signature, String timestamp, String nonce)
|
||||
{
|
||||
if (StringUtils.isAnyBlank(signature, timestamp, nonce, virtualPayConfig.getCallbackToken()))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
String[] values = {virtualPayConfig.getCallbackToken(), timestamp, nonce};
|
||||
Arrays.sort(values);
|
||||
String actual = sha1(values[0] + values[1] + values[2]);
|
||||
return MessageDigest.isEqual(actual.getBytes(StandardCharsets.UTF_8),
|
||||
signature.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void handleCallback(JsonNode body)
|
||||
{
|
||||
String event = body.path("Event").asText();
|
||||
String orderNo = body.path("OutTradeNo").asText();
|
||||
log.info("收到微信虚拟支付回调: event={}, orderNo={}", event, orderNo);
|
||||
if ("xpay_goods_deliver_notify".equals(event))
|
||||
{
|
||||
handleGoodsDeliver(body);
|
||||
}
|
||||
else if ("xpay_refund_notify".equals(event))
|
||||
{
|
||||
handleRefund(body);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 消息推送 URL 可能同时接收其他小程序事件,未知事件不应触发微信反复重试。
|
||||
log.debug("忽略非资源支付回调事件: {}", event);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleGoodsDeliver(JsonNode body)
|
||||
{
|
||||
String orderNo = body.path("OutTradeNo").asText();
|
||||
AppVirtualOrder order = virtualOrderMapper.selectByOrderNo(orderNo);
|
||||
if (order == null)
|
||||
{
|
||||
throw new ServiceException("虚拟支付订单不存在");
|
||||
}
|
||||
|
||||
JsonNode goods = body.path("GoodsInfo");
|
||||
if (!order.getOpenId().equals(text(body, "OpenId", "openid"))
|
||||
|| virtualPayConfig.getEnv() != integer(body, "Env", "env")
|
||||
|| !order.getProductId().equals(goods.path("ProductId").asText())
|
||||
|| order.getPriceFen() != goods.path("ActualPrice").asInt(-1)
|
||||
|| goods.path("Quantity").asInt(0) != 1
|
||||
|| !orderNo.equals(goods.path("Attach").asText()))
|
||||
{
|
||||
throw new ServiceException("虚拟支付通知与本地订单不一致");
|
||||
}
|
||||
|
||||
JsonNode wxPayInfo = body.path("WeChatPayInfo");
|
||||
Date paidTime = fromUnixSeconds(wxPayInfo.path("PaidTime").asLong(0));
|
||||
grantOrder(order,
|
||||
wxPayInfo.path("MchOrderNo").asText(null),
|
||||
wxPayInfo.path("TransactionId").asText(null),
|
||||
paidTime);
|
||||
}
|
||||
|
||||
private void handleRefund(JsonNode body)
|
||||
{
|
||||
if (body.path("RetCode").asInt(-1) != 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
String orderNo = text(body, "MchOrderId", "MchOrderNo", "OutTradeNo");
|
||||
AppVirtualOrder order = virtualOrderMapper.selectByOrderNo(orderNo);
|
||||
if (order == null)
|
||||
{
|
||||
throw new ServiceException("退款对应的虚拟支付订单不存在");
|
||||
}
|
||||
int refundFee = body.path("RefundFee").asInt(-1);
|
||||
if (!order.getOpenId().equals(text(body, "OpenId", "openid"))
|
||||
|| refundFee <= 0
|
||||
|| refundFee > order.getPriceFen())
|
||||
{
|
||||
throw new ServiceException("虚拟支付退款通知与本地订单不一致");
|
||||
}
|
||||
revokeOrder(order, fromUnixSeconds(body.path("RefundSuccTimestamp").asLong(0)));
|
||||
}
|
||||
|
||||
private void syncOrderFromWechat(AppVirtualOrder order)
|
||||
{
|
||||
try
|
||||
{
|
||||
String accessToken = getAccessToken();
|
||||
LinkedHashMap<String, Object> payload = new LinkedHashMap<>();
|
||||
payload.put("openid", order.getOpenId());
|
||||
payload.put("env", virtualPayConfig.getEnv());
|
||||
payload.put("order_id", order.getOrderNo());
|
||||
String body = objectMapper.writeValueAsString(payload);
|
||||
String paySig = hmacSha256(virtualPayConfig.getAppKey(), QUERY_ORDER_URI + "&" + body);
|
||||
|
||||
HttpUrl url = HttpUrl.parse(QUERY_ORDER_URL).newBuilder()
|
||||
.addQueryParameter("access_token", accessToken)
|
||||
.addQueryParameter("pay_sig", paySig)
|
||||
.build();
|
||||
Request httpRequest = new Request.Builder()
|
||||
.url(url)
|
||||
.post(RequestBody.create(JSON_MEDIA_TYPE, body))
|
||||
.build();
|
||||
try (Response response = httpClient.newCall(httpRequest).execute())
|
||||
{
|
||||
if (!response.isSuccessful() || response.body() == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
JsonNode result = objectMapper.readTree(response.body().string());
|
||||
if (result.path("errcode").asInt(-1) != 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
JsonNode wxOrder = result.path("order");
|
||||
if (!order.getOrderNo().equals(wxOrder.path("order_id").asText())
|
||||
|| wxOrder.path("order_fee").asInt(-1) != order.getPriceFen())
|
||||
{
|
||||
return;
|
||||
}
|
||||
int status = wxOrder.path("status").asInt(-1);
|
||||
if (status >= 2 && status <= 4
|
||||
&& wxOrder.path("paid_fee").asInt(-1) == order.getPriceFen())
|
||||
{
|
||||
grantOrder(order,
|
||||
wxOrder.path("wx_order_id").asText(null),
|
||||
wxOrder.path("wxpay_order_id").asText(null),
|
||||
fromUnixSeconds(wxOrder.path("paid_time").asLong(0)));
|
||||
if (status == 2)
|
||||
{
|
||||
notifyProvideGoods(accessToken, order.getOrderNo());
|
||||
}
|
||||
}
|
||||
else if (status == 5 || status == 8)
|
||||
{
|
||||
revokeOrder(order, fromUnixSeconds(wxOrder.path("paid_time").asLong(0)));
|
||||
}
|
||||
else if (status == 6)
|
||||
{
|
||||
virtualOrderMapper.markClosed(order.getOrderNo());
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (IOException ignored)
|
||||
{
|
||||
// 查询仅作为回调丢失时的兜底,不影响客户端继续轮询本地状态。
|
||||
}
|
||||
}
|
||||
|
||||
private void notifyProvideGoods(String accessToken, String orderNo)
|
||||
{
|
||||
try
|
||||
{
|
||||
LinkedHashMap<String, Object> payload = new LinkedHashMap<>();
|
||||
payload.put("order_id", orderNo);
|
||||
payload.put("env", virtualPayConfig.getEnv());
|
||||
String body = objectMapper.writeValueAsString(payload);
|
||||
String paySig = hmacSha256(virtualPayConfig.getAppKey(), NOTIFY_GOODS_URI + "&" + body);
|
||||
HttpUrl url = HttpUrl.parse(NOTIFY_GOODS_URL).newBuilder()
|
||||
.addQueryParameter("access_token", accessToken)
|
||||
.addQueryParameter("pay_sig", paySig)
|
||||
.build();
|
||||
Request request = new Request.Builder()
|
||||
.url(url)
|
||||
.post(RequestBody.create(JSON_MEDIA_TYPE, body))
|
||||
.build();
|
||||
try (Response ignored = httpClient.newCall(request).execute())
|
||||
{
|
||||
// 微信侧失败时仍会继续推送发货通知,保持本地发货幂等即可。
|
||||
}
|
||||
}
|
||||
catch (Exception ignored)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private void grantOrder(AppVirtualOrder order, String wxOrderNo, String transactionId, Date paidTime)
|
||||
{
|
||||
int changed = virtualOrderMapper.markPaid(order.getOrderNo(), wxOrderNo, transactionId, paidTime);
|
||||
if (changed == 1)
|
||||
{
|
||||
order.setStatus(1);
|
||||
virtualOrderMapper.insertEntitlement(order);
|
||||
|
||||
AppIntegralRecord purchaseRecord = new AppIntegralRecord();
|
||||
purchaseRecord.setSource("资源购买");
|
||||
purchaseRecord.setIsAdd(3L);
|
||||
// 现金消费使用分作为最小单位,避免小数金额精度丢失。
|
||||
purchaseRecord.setIntegralNumber(order.getPriceFen().longValue());
|
||||
purchaseRecord.setUserId(order.getUserId());
|
||||
purchaseRecord.setResourceId(order.getResourceId());
|
||||
purchaseRecord.setIntegralTime(DateUtils.getNowDate());
|
||||
if (integralRecordMapper.insertAppIntegralRecord(purchaseRecord) != 1)
|
||||
{
|
||||
throw new ServiceException("生成资源购买记录失败");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void revokeOrder(AppVirtualOrder order, Date refundTime)
|
||||
{
|
||||
if (virtualOrderMapper.markRefunded(order.getOrderNo(), refundTime) == 1)
|
||||
{
|
||||
virtualOrderMapper.deleteEntitlementByOrderNo(order.getOrderNo());
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized String getAccessToken() throws IOException
|
||||
{
|
||||
long now = System.currentTimeMillis();
|
||||
if (StringUtils.isNotBlank(cachedAccessToken) && now < accessTokenExpiresAt)
|
||||
{
|
||||
return cachedAccessToken;
|
||||
}
|
||||
HttpUrl url = HttpUrl.parse(ACCESS_TOKEN_URL).newBuilder()
|
||||
.addQueryParameter("grant_type", "client_credential")
|
||||
.addQueryParameter("appid", wxPayConfig.getAppid())
|
||||
.addQueryParameter("secret", wxPayConfig.getSecret())
|
||||
.build();
|
||||
Request request = new Request.Builder().url(url).get().build();
|
||||
try (Response response = httpClient.newCall(request).execute())
|
||||
{
|
||||
if (!response.isSuccessful() || response.body() == null)
|
||||
{
|
||||
throw new IOException("get access token failed");
|
||||
}
|
||||
JsonNode result = objectMapper.readTree(response.body().string());
|
||||
String token = result.path("access_token").asText();
|
||||
if (StringUtils.isBlank(token))
|
||||
{
|
||||
throw new IOException("empty access token");
|
||||
}
|
||||
int expiresIn = result.path("expires_in").asInt(7200);
|
||||
cachedAccessToken = token;
|
||||
accessTokenExpiresAt = now + Math.max(60, expiresIn - 300) * 1000L;
|
||||
return token;
|
||||
}
|
||||
}
|
||||
|
||||
private void checkEnabled()
|
||||
{
|
||||
if (!virtualPayConfig.isEnabled())
|
||||
{
|
||||
throw new ServiceException("虚拟支付暂未启用");
|
||||
}
|
||||
if (StringUtils.isAnyBlank(virtualPayConfig.getOfferId(), virtualPayConfig.getAppKey()))
|
||||
{
|
||||
throw new ServiceException("虚拟支付参数未配置完整");
|
||||
}
|
||||
}
|
||||
|
||||
private static String generateOrderNo()
|
||||
{
|
||||
String time = new SimpleDateFormat("yyyyMMddHHmmss").format(new Date());
|
||||
return "VP" + time + UUID.randomUUID().toString().replace("-", "").substring(0, 12);
|
||||
}
|
||||
|
||||
private static String hmacSha256(String key, String value)
|
||||
{
|
||||
try
|
||||
{
|
||||
Mac mac = Mac.getInstance("HmacSHA256");
|
||||
mac.init(new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
|
||||
return toHex(mac.doFinal(value.getBytes(StandardCharsets.UTF_8)));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new ServiceException("虚拟支付签名失败");
|
||||
}
|
||||
}
|
||||
|
||||
private static String sha1(String value)
|
||||
{
|
||||
try
|
||||
{
|
||||
return toHex(MessageDigest.getInstance("SHA-1")
|
||||
.digest(value.getBytes(StandardCharsets.UTF_8)));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
private static String toHex(byte[] bytes)
|
||||
{
|
||||
StringBuilder result = new StringBuilder(bytes.length * 2);
|
||||
for (byte value : bytes)
|
||||
{
|
||||
result.append(String.format("%02x", value & 0xff));
|
||||
}
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
private static Date fromUnixSeconds(long seconds)
|
||||
{
|
||||
return seconds > 0 ? new Date(seconds * 1000L) : new Date();
|
||||
}
|
||||
|
||||
private static String text(JsonNode node, String... fieldNames)
|
||||
{
|
||||
for (String fieldName : fieldNames)
|
||||
{
|
||||
String value = node.path(fieldName).asText();
|
||||
if (StringUtils.isNotBlank(value))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private static int integer(JsonNode node, String... fieldNames)
|
||||
{
|
||||
for (String fieldName : fieldNames)
|
||||
{
|
||||
if (node.has(fieldName))
|
||||
{
|
||||
return node.path(fieldName).asInt(-1);
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
@@ -145,7 +145,7 @@ public interface SysUserMapper
|
||||
/**
|
||||
* 减少积分
|
||||
*/
|
||||
@Update("update sys_user SET integral = integral-#{integral} where user_id = #{userId}")
|
||||
@Update("update sys_user SET integral = integral-#{integral} where user_id = #{userId} and integral >= #{integral}")
|
||||
public int delIntegralByUserId(@Param("integral") int integral ,@Param("userId")Long userId);
|
||||
|
||||
/**
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
<result property="isShow" column="is_show" />
|
||||
<result property="isAd" column="is_ad" />
|
||||
<result property="adNumber" column="ad_number" />
|
||||
<result property="priceFen" column="price_fen" />
|
||||
<result property="virtualProductId" column="virtual_product_id" />
|
||||
<result property="downNum" column="down_num" />
|
||||
<result property="weight" column="weight" />
|
||||
<result property="delFlag" column="del_flag" />
|
||||
@@ -35,27 +37,34 @@
|
||||
</resultMap>
|
||||
|
||||
<sql id="selectAppResourceVo">
|
||||
select id, resource_title, show_img, `explain`, resource_type, keyword, is_show, is_ad, ad_number, down_num, weight, del_flag, create_by, create_time, remark from app_resource
|
||||
select a.id, a.resource_title, a.show_img, a.`explain`, a.resource_type, a.keyword,
|
||||
a.is_show, a.is_ad, a.ad_number, a.price_fen,
|
||||
(select vp.product_id from app_virtual_product vp
|
||||
where vp.price_fen = a.price_fen and vp.status = 1 limit 1) as virtual_product_id,
|
||||
a.down_num, a.weight, a.del_flag, a.create_by, a.create_time, a.remark
|
||||
from app_resource a
|
||||
</sql>
|
||||
|
||||
<select id="selectAppResourceList" parameterType="AppResource" resultMap="AppResourceResult">
|
||||
<include refid="selectAppResourceVo"/>
|
||||
<where>
|
||||
<if test="resourceTitle != null and resourceTitle != ''"> and resource_title like concat('%', #{resourceTitle}, '%')</if>
|
||||
<if test="showImg != null and showImg != ''"> and show_img = #{showImg}</if>
|
||||
<if test="explain != null and explain != ''"> and `explain` = #{explain}</if>
|
||||
<if test="resourceType != null "> and resource_type = #{resourceType}</if>
|
||||
<if test="isShow != null "> and is_show = #{isShow}</if>
|
||||
<if test="isAd != null "> and is_ad = #{isAd}</if>
|
||||
<if test="adNumber != null "> and ad_number = #{adNumber}</if>
|
||||
<if test="downNum != null "> and down_num = #{downNum}</if>
|
||||
<if test="weight != null "> and weight = #{weight}</if>
|
||||
<if test="keyword != null and keyword != ''"> and (keyword like concat('%', #{keyword}, '%') or resource_title like concat('%', #{keyword}, '%'))</if>
|
||||
</where> ORDER BY weight desc, create_time desc
|
||||
<if test="resourceTitle != null and resourceTitle != ''"> and a.resource_title like concat('%', #{resourceTitle}, '%')</if>
|
||||
<if test="showImg != null and showImg != ''"> and a.show_img = #{showImg}</if>
|
||||
<if test="explain != null and explain != ''"> and a.`explain` = #{explain}</if>
|
||||
<if test="resourceType != null "> and a.resource_type = #{resourceType}</if>
|
||||
<if test="isShow != null "> and a.is_show = #{isShow}</if>
|
||||
<if test="isAd != null "> and a.is_ad = #{isAd}</if>
|
||||
<if test="adNumber != null "> and a.ad_number = #{adNumber}</if>
|
||||
<if test="downNum != null "> and a.down_num = #{downNum}</if>
|
||||
<if test="weight != null "> and a.weight = #{weight}</if>
|
||||
<if test="keyword != null and keyword != ''"> and (a.keyword like concat('%', #{keyword}, '%') or a.resource_title like concat('%', #{keyword}, '%'))</if>
|
||||
</where> ORDER BY a.weight desc, a.create_time desc
|
||||
</select>
|
||||
|
||||
<select id="selectAppResourceById" parameterType="Long" resultMap="AppResourceAppResourceListResult">
|
||||
select a.id, a.resource_title, a.show_img, a.`explain`,a.`keyword`, a.resource_type, a.is_show, a.is_ad, a.ad_number, a.down_num, a.weight, a.del_flag, a.create_by, a.create_time, a.remark,
|
||||
select a.id, a.resource_title, a.show_img, a.`explain`,a.`keyword`, a.resource_type, a.is_show, a.is_ad, a.ad_number, a.price_fen,
|
||||
(select vp.product_id from app_virtual_product vp where vp.price_fen = a.price_fen and vp.status = 1 limit 1) as virtual_product_id,
|
||||
a.down_num, a.weight, a.del_flag, a.create_by, a.create_time, a.remark,
|
||||
b.id as sub_id, b.list_name as sub_list_name, b.list_url as sub_list_url, b.password as sub_password, b.app_resource_id as sub_app_resource_id
|
||||
from app_resource a
|
||||
left join app_resource_list b on b.app_resource_id = a.id
|
||||
@@ -73,6 +82,7 @@
|
||||
<if test="isShow != null">is_show,</if>
|
||||
<if test="isAd != null">is_ad,</if>
|
||||
<if test="adNumber != null">ad_number,</if>
|
||||
<if test="priceFen != null">price_fen,</if>
|
||||
<if test="downNum != null">down_num,</if>
|
||||
<if test="weight != null">weight,</if>
|
||||
<if test="delFlag != null">del_flag,</if>
|
||||
@@ -89,6 +99,7 @@
|
||||
<if test="isShow != null">#{isShow},</if>
|
||||
<if test="isAd != null">#{isAd},</if>
|
||||
<if test="adNumber != null">#{adNumber},</if>
|
||||
<if test="priceFen != null">#{priceFen},</if>
|
||||
<if test="downNum != null">#{downNum},</if>
|
||||
<if test="weight != null">#{weight},</if>
|
||||
<if test="delFlag != null">#{delFlag},</if>
|
||||
@@ -109,6 +120,7 @@
|
||||
<if test="isShow != null">is_show = #{isShow},</if>
|
||||
<if test="isAd != null">is_ad = #{isAd},</if>
|
||||
<if test="adNumber != null">ad_number = #{adNumber},</if>
|
||||
<if test="priceFen != null">price_fen = #{priceFen},</if>
|
||||
<if test="downNum != null">down_num = #{downNum},</if>
|
||||
<if test="weight != null">weight = #{weight},</if>
|
||||
<if test="delFlag != null">del_flag = #{delFlag},</if>
|
||||
@@ -149,16 +161,35 @@
|
||||
</insert>
|
||||
|
||||
<select id="selectAppResourceByIdAndUserId" resultMap="AppResourceAppResourceListResult">
|
||||
select a.id, a.resource_title, a.show_img, a.`explain`, a.`keyword`, a.resource_type, a.is_show,
|
||||
select a.id, a.resource_title, a.show_img, a.`explain`, a.`keyword`, a.resource_type, a.is_show,
|
||||
CASE
|
||||
WHEN (SELECT COUNT(1) FROM app_integral_record WHERE resource_id = #{id} AND user_id = #{userId}) > 0 THEN 0
|
||||
WHEN #{userId} is not null and (
|
||||
(SELECT COUNT(1) FROM app_integral_record
|
||||
WHERE resource_id = #{id} AND user_id = #{userId}
|
||||
AND source = '资源兑换' AND is_add = 1) > 0
|
||||
OR
|
||||
(SELECT COUNT(1) FROM app_resource_entitlement WHERE resource_id = #{id} AND user_id = #{userId} AND status = 1) > 0
|
||||
) THEN 0
|
||||
ELSE a.is_ad
|
||||
END as is_ad,
|
||||
a.ad_number, a.down_num, a.weight, a.del_flag, a.create_by, a.create_time, a.remark,
|
||||
END as is_ad,
|
||||
a.ad_number, a.price_fen,
|
||||
(select vp.product_id from app_virtual_product vp where vp.price_fen = a.price_fen and vp.status = 1 limit 1) as virtual_product_id,
|
||||
a.down_num, a.weight, a.del_flag, a.create_by, a.create_time, a.remark,
|
||||
b.id as sub_id, b.list_name as sub_list_name, b.list_url as sub_list_url,
|
||||
b.password as sub_password, b.app_resource_id as sub_app_resource_id
|
||||
from app_resource a
|
||||
left join app_resource_list b on b.app_resource_id = a.id
|
||||
and (
|
||||
a.is_ad not in (2, 3)
|
||||
or (
|
||||
#{userId} is not null and (
|
||||
exists(select 1 from app_integral_record ir
|
||||
where ir.resource_id = a.id and ir.user_id = #{userId}
|
||||
and ir.source = '资源兑换' and ir.is_add = 1)
|
||||
or exists(select 1 from app_resource_entitlement re where re.resource_id = a.id and re.user_id = #{userId} and re.status = 1)
|
||||
)
|
||||
)
|
||||
)
|
||||
where a.id = #{id}
|
||||
</select>
|
||||
</mapper>
|
||||
</mapper>
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
<?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.app.mapper.AppVirtualOrderMapper">
|
||||
<resultMap id="AppVirtualOrderResult" type="AppVirtualOrder">
|
||||
<id property="id" column="id"/>
|
||||
<result property="orderNo" column="order_no"/>
|
||||
<result property="userId" column="user_id"/>
|
||||
<result property="userName" column="user_name"/>
|
||||
<result property="nickName" column="nick_name"/>
|
||||
<result property="resourceId" column="resource_id"/>
|
||||
<result property="resourceTitle" column="resource_title"/>
|
||||
<result property="productId" column="product_id"/>
|
||||
<result property="priceFen" column="price_fen"/>
|
||||
<result property="openId" column="open_id"/>
|
||||
<result property="status" column="status"/>
|
||||
<result property="wxOrderNo" column="wx_order_no"/>
|
||||
<result property="transactionId" column="transaction_id"/>
|
||||
<result property="createTime" column="create_time"/>
|
||||
<result property="payTime" column="pay_time"/>
|
||||
<result property="provideTime" column="provide_time"/>
|
||||
<result property="refundTime" column="refund_time"/>
|
||||
<result property="lastQueryTime" column="last_query_time"/>
|
||||
</resultMap>
|
||||
|
||||
<sql id="selectAppVirtualOrderVo">
|
||||
select vo.id, vo.order_no, vo.user_id, u.user_name, u.nick_name,
|
||||
vo.resource_id, r.resource_title, vo.product_id, vo.price_fen,
|
||||
vo.open_id, vo.status, vo.wx_order_no, vo.transaction_id,
|
||||
vo.create_time, vo.pay_time, vo.provide_time, vo.refund_time, vo.last_query_time
|
||||
from app_virtual_order vo
|
||||
left join sys_user u on u.user_id = vo.user_id
|
||||
left join app_resource r on r.id = vo.resource_id
|
||||
</sql>
|
||||
|
||||
<insert id="insertAppVirtualOrder" parameterType="AppVirtualOrder" useGeneratedKeys="true" keyProperty="id">
|
||||
insert into app_virtual_order
|
||||
(order_no, user_id, resource_id, product_id, price_fen, open_id, status, create_time)
|
||||
values
|
||||
(#{orderNo}, #{userId}, #{resourceId}, #{productId}, #{priceFen}, #{openId}, #{status}, #{createTime})
|
||||
</insert>
|
||||
|
||||
<select id="selectByOrderNo" resultMap="AppVirtualOrderResult">
|
||||
select id, order_no, user_id, resource_id, product_id, price_fen, open_id, status,
|
||||
wx_order_no, transaction_id, create_time, pay_time, provide_time, refund_time, last_query_time
|
||||
from app_virtual_order
|
||||
where order_no = #{orderNo}
|
||||
limit 1
|
||||
</select>
|
||||
|
||||
<select id="selectAppVirtualOrderById" parameterType="Long" resultMap="AppVirtualOrderResult">
|
||||
<include refid="selectAppVirtualOrderVo"/>
|
||||
where vo.id = #{id}
|
||||
</select>
|
||||
|
||||
<select id="selectAppVirtualOrderList" parameterType="AppVirtualOrder" resultMap="AppVirtualOrderResult">
|
||||
<include refid="selectAppVirtualOrderVo"/>
|
||||
<where>
|
||||
<if test="orderNo != null and orderNo != ''">
|
||||
and vo.order_no like concat('%', #{orderNo}, '%')
|
||||
</if>
|
||||
<if test="transactionId != null and transactionId != ''">
|
||||
and vo.transaction_id like concat('%', #{transactionId}, '%')
|
||||
</if>
|
||||
<if test="wxOrderNo != null and wxOrderNo != ''">
|
||||
and vo.wx_order_no like concat('%', #{wxOrderNo}, '%')
|
||||
</if>
|
||||
<if test="userId != null">and vo.user_id = #{userId}</if>
|
||||
<if test="userName != null and userName != ''">
|
||||
and (u.user_name like concat('%', #{userName}, '%')
|
||||
or u.nick_name like concat('%', #{userName}, '%'))
|
||||
</if>
|
||||
<if test="resourceId != null">and vo.resource_id = #{resourceId}</if>
|
||||
<if test="productId != null and productId != ''">
|
||||
and vo.product_id like concat('%', #{productId}, '%')
|
||||
</if>
|
||||
<if test="openId != null and openId != ''">
|
||||
and vo.open_id like concat('%', #{openId}, '%')
|
||||
</if>
|
||||
<if test="status != null">and vo.status = #{status}</if>
|
||||
<if test="params.beginTime != null and params.beginTime != ''">
|
||||
and vo.create_time >= #{params.beginTime}
|
||||
</if>
|
||||
<if test="params.endTime != null and params.endTime != ''">
|
||||
and vo.create_time <= concat(#{params.endTime}, ' 23:59:59')
|
||||
</if>
|
||||
</where>
|
||||
order by vo.create_time desc, vo.id desc
|
||||
</select>
|
||||
|
||||
<select id="countAnyEntitlement" resultType="int">
|
||||
select
|
||||
(select count(1) from app_resource_entitlement
|
||||
where user_id = #{userId} and resource_id = #{resourceId} and status = 1)
|
||||
+
|
||||
(select count(1) from app_integral_record
|
||||
where user_id = #{userId}
|
||||
and resource_id = #{resourceId}
|
||||
and source = '资源兑换'
|
||||
and is_add = 1)
|
||||
</select>
|
||||
|
||||
<update id="markPaid">
|
||||
update app_virtual_order
|
||||
set status = 1,
|
||||
wx_order_no = coalesce(#{wxOrderNo}, wx_order_no),
|
||||
transaction_id = coalesce(#{transactionId}, transaction_id),
|
||||
pay_time = coalesce(#{payTime}, pay_time),
|
||||
provide_time = now()
|
||||
where order_no = #{orderNo} and status = 0
|
||||
</update>
|
||||
|
||||
<insert id="insertEntitlement" parameterType="AppVirtualOrder">
|
||||
insert into app_resource_entitlement
|
||||
(user_id, resource_id, order_no, status, granted_time)
|
||||
values
|
||||
(#{userId}, #{resourceId}, #{orderNo}, 1, now())
|
||||
on duplicate key update
|
||||
order_no = values(order_no),
|
||||
status = 1,
|
||||
granted_time = now(),
|
||||
revoked_time = null
|
||||
</insert>
|
||||
|
||||
<update id="markRefunded">
|
||||
update app_virtual_order
|
||||
set status = 2, refund_time = #{refundTime}
|
||||
where order_no = #{orderNo} and status in (0, 1)
|
||||
</update>
|
||||
|
||||
<update id="deleteEntitlementByOrderNo">
|
||||
update app_resource_entitlement
|
||||
set status = 0, revoked_time = now()
|
||||
where order_no = #{orderNo} and status = 1
|
||||
</update>
|
||||
|
||||
<update id="markClosed">
|
||||
update app_virtual_order set status = 3
|
||||
where order_no = #{orderNo} and status = 0
|
||||
</update>
|
||||
|
||||
<update id="markQuerying">
|
||||
update app_virtual_order
|
||||
set last_query_time = now()
|
||||
where order_no = #{orderNo}
|
||||
and status = 0
|
||||
and (last_query_time is null or last_query_time < date_sub(now(), interval 5 second))
|
||||
</update>
|
||||
</mapper>
|
||||
@@ -0,0 +1,40 @@
|
||||
<?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.app.mapper.AppVirtualProductMapper">
|
||||
<resultMap id="AppVirtualProductResult" type="AppVirtualProduct">
|
||||
<id property="id" column="id"/>
|
||||
<result property="productId" column="product_id"/>
|
||||
<result property="priceFen" column="price_fen"/>
|
||||
<result property="productName" column="product_name"/>
|
||||
<result property="status" column="status"/>
|
||||
<result property="createTime" column="create_time"/>
|
||||
<result property="updateTime" column="update_time"/>
|
||||
</resultMap>
|
||||
|
||||
<select id="selectActiveByPrice" resultMap="AppVirtualProductResult">
|
||||
select id, product_id, price_fen, product_name, status, create_time, update_time
|
||||
from app_virtual_product
|
||||
where price_fen = #{priceFen} and status = 1
|
||||
limit 1
|
||||
</select>
|
||||
|
||||
<select id="selectByProductId" resultMap="AppVirtualProductResult">
|
||||
select id, product_id, price_fen, product_name, status, create_time, update_time
|
||||
from app_virtual_product
|
||||
where product_id = #{productId}
|
||||
limit 1
|
||||
</select>
|
||||
|
||||
<insert id="insertAppVirtualProduct" parameterType="AppVirtualProduct" useGeneratedKeys="true" keyProperty="id">
|
||||
insert into app_virtual_product(product_id, price_fen, product_name, status, create_time)
|
||||
values(#{productId}, #{priceFen}, #{productName}, #{status}, #{createTime})
|
||||
</insert>
|
||||
|
||||
<update id="updateProductIdByPrice" parameterType="AppVirtualProduct">
|
||||
update app_virtual_product
|
||||
set product_id = #{productId}, product_name = #{productName}, status = 1, update_time = #{updateTime}
|
||||
where price_fen = #{priceFen}
|
||||
</update>
|
||||
</mapper>
|
||||
@@ -51,7 +51,7 @@
|
||||
<sql id="selectUserVo">
|
||||
select u.user_id, u.dept_id, u.user_name, u.nick_name, u.email, u.avatar, u.phonenumber, u.password, u.sex, u.status, u.del_flag, u.login_ip, u.login_date, u.create_by, u.create_time, u.remark, u.integral,
|
||||
d.dept_id, d.parent_id, d.ancestors, d.dept_name, d.order_num, d.leader, d.status as dept_status,
|
||||
r.role_id, r.role_name, r.role_key, r.role_sort, r.data_scope, r.status as role_statusselectUserVo, u.open_id as openId
|
||||
r.role_id, r.role_name, r.role_key, r.role_sort, r.data_scope, r.status as role_status, u.open_id
|
||||
from sys_user u
|
||||
left join sys_dept d on u.dept_id = d.dept_id
|
||||
left join sys_user_role ur on u.user_id = ur.user_id
|
||||
@@ -232,4 +232,4 @@
|
||||
select date_format(create_time,'%Y-%m-%d') as date, count(*) as count from sys_user where del_flag = '0' and create_time >= DATE_SUB(CURDATE(), INTERVAL 30 DAY) group by date_format(create_time,'%Y-%m-%d') order by date
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
</mapper>
|
||||
|
||||
Reference in New Issue
Block a user