feat: add dashboard and virtual pay enhancements

This commit is contained in:
王鹏
2026-07-29 14:34:54 +08:00
parent 2250f744d1
commit 349f1c0374
55 changed files with 3085 additions and 213 deletions

View File

@@ -0,0 +1,108 @@
package com.ruoyi.app.domain;
import java.io.Serializable;
import java.math.BigDecimal;
/**
* 首页业务指标汇总。
*/
public class AppDashboardSummary implements Serializable
{
private static final long serialVersionUID = 1L;
private Long resourceTotal;
private Long resourceToday;
private Long userTotal;
private Long userToday;
private Long orderTotal;
private Long orderToday;
private BigDecimal amountTotal;
private BigDecimal amountToday;
public Long getResourceTotal()
{
return resourceTotal;
}
public void setResourceTotal(Long resourceTotal)
{
this.resourceTotal = resourceTotal;
}
public Long getResourceToday()
{
return resourceToday;
}
public void setResourceToday(Long resourceToday)
{
this.resourceToday = resourceToday;
}
public Long getUserTotal()
{
return userTotal;
}
public void setUserTotal(Long userTotal)
{
this.userTotal = userTotal;
}
public Long getUserToday()
{
return userToday;
}
public void setUserToday(Long userToday)
{
this.userToday = userToday;
}
public Long getOrderTotal()
{
return orderTotal;
}
public void setOrderTotal(Long orderTotal)
{
this.orderTotal = orderTotal;
}
public Long getOrderToday()
{
return orderToday;
}
public void setOrderToday(Long orderToday)
{
this.orderToday = orderToday;
}
public BigDecimal getAmountTotal()
{
return amountTotal;
}
public void setAmountTotal(BigDecimal amountTotal)
{
this.amountTotal = amountTotal;
}
public BigDecimal getAmountToday()
{
return amountToday;
}
public void setAmountToday(BigDecimal amountToday)
{
this.amountToday = amountToday;
}
}

View File

@@ -30,6 +30,21 @@ public class AppResourceList extends BaseEntity
@Excel(name = "访问密码")
private String password;
/** 规格价格,单位分 */
@Excel(name = "规格价格(分)")
private Integer priceFen;
/** 状态1启用0停用 */
@Excel(name = "规格状态", readConverterExp = "0=停用,1=启用")
private Integer status;
/** 显示顺序 */
@Excel(name = "显示顺序")
private Integer sortOrder;
/** 当前用户是否已购买,仅用于小程序详情返回 */
private Boolean purchased;
/** 关联主表app_resource */
@Excel(name = "关联主表app_resource")
private Long appResourceId;
@@ -70,6 +85,47 @@ public class AppResourceList extends BaseEntity
{
return password;
}
public Integer getPriceFen()
{
return priceFen;
}
public void setPriceFen(Integer priceFen)
{
this.priceFen = priceFen;
}
public Integer getStatus()
{
return status;
}
public void setStatus(Integer status)
{
this.status = status;
}
public Integer getSortOrder()
{
return sortOrder;
}
public void setSortOrder(Integer sortOrder)
{
this.sortOrder = sortOrder;
}
public Boolean getPurchased()
{
return purchased;
}
public void setPurchased(Boolean purchased)
{
this.purchased = purchased;
}
public void setAppResourceId(Long appResourceId)
{
this.appResourceId = appResourceId;
@@ -87,6 +143,10 @@ public class AppResourceList extends BaseEntity
.append("listName", getListName())
.append("listUrl", getListUrl())
.append("password", getPassword())
.append("priceFen", getPriceFen())
.append("status", getStatus())
.append("sortOrder", getSortOrder())
.append("purchased", getPurchased())
.append("appResourceId", getAppResourceId())
.toString();
}

View File

@@ -33,6 +33,12 @@ public class AppVirtualOrder extends BaseEntity
@Excel(name = "资源标题")
private String resourceTitle;
@Excel(name = "规格ID")
private Long resourceListId;
@Excel(name = "购买规格")
private String specName;
@Excel(name = "微信道具ID")
private String productId;
@@ -137,6 +143,26 @@ public class AppVirtualOrder extends BaseEntity
this.resourceTitle = resourceTitle;
}
public Long getResourceListId()
{
return resourceListId;
}
public void setResourceListId(Long resourceListId)
{
this.resourceListId = resourceListId;
}
public String getSpecName()
{
return specName;
}
public void setSpecName(String specName)
{
this.specName = specName;
}
public String getProductId()
{
return productId;

View File

@@ -0,0 +1,131 @@
package com.ruoyi.app.domain;
import com.fasterxml.jackson.annotation.JsonFormat;
import java.io.Serializable;
import java.util.Date;
/**
* 小程序“我的订单”安全展示对象,不包含 OpenID 和微信交易号等敏感字段。
*/
public class AppVirtualOrderSummary implements Serializable
{
private static final long serialVersionUID = 1L;
private String orderNo;
private Long resourceId;
private String resourceTitle;
private Long resourceListId;
private String specName;
private Integer priceFen;
private Integer status;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date payTime;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date refundTime;
public String getOrderNo()
{
return orderNo;
}
public void setOrderNo(String orderNo)
{
this.orderNo = orderNo;
}
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 Long getResourceListId()
{
return resourceListId;
}
public void setResourceListId(Long resourceListId)
{
this.resourceListId = resourceListId;
}
public String getSpecName()
{
return specName;
}
public void setSpecName(String specName)
{
this.specName = specName;
}
public Integer getPriceFen()
{
return priceFen;
}
public void setPriceFen(Integer priceFen)
{
this.priceFen = priceFen;
}
public Integer getStatus()
{
return status;
}
public void setStatus(Integer status)
{
this.status = status;
}
public Date getCreateTime()
{
return createTime;
}
public void setCreateTime(Date createTime)
{
this.createTime = createTime;
}
public Date getPayTime()
{
return payTime;
}
public void setPayTime(Date payTime)
{
this.payTime = payTime;
}
public Date getRefundTime()
{
return refundTime;
}
public void setRefundTime(Date refundTime)
{
this.refundTime = refundTime;
}
}

View File

@@ -1,13 +1,15 @@
package com.ruoyi.app.domain.request;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
public class CreateVirtualOrderRequest
{
@NotNull(message = "资源ID不能为空")
/** 旧版小程序兼容字段;新版按 resourceListId 下单。 */
private Long resourceId;
/** 资源下载项ID一条下载项即一个付费规格。 */
private Long resourceListId;
@NotBlank(message = "微信登录凭证不能为空")
private String code;
@@ -21,6 +23,16 @@ public class CreateVirtualOrderRequest
this.resourceId = resourceId;
}
public Long getResourceListId()
{
return resourceListId;
}
public void setResourceListId(Long resourceListId)
{
this.resourceListId = resourceListId;
}
public String getCode()
{
return code;

View File

@@ -0,0 +1,11 @@
package com.ruoyi.app.mapper;
import com.ruoyi.app.domain.AppDashboardSummary;
/**
* 首页业务指标数据访问层。
*/
public interface AppDashboardMapper
{
AppDashboardSummary selectDashboardSummary();
}

View File

@@ -2,6 +2,7 @@ package com.ruoyi.app.mapper;
import java.util.List;
import com.ruoyi.app.domain.AppIntegralRecord;
import org.apache.ibatis.annotations.Param;
/**
* 积分记录Mapper接口
@@ -27,6 +28,8 @@ public interface AppIntegralRecordMapper
*/
public List<AppIntegralRecord> selectAppIntegralRecordList(AppIntegralRecord appIntegralRecord);
public List<AppIntegralRecord> selectMyIntegralRecordList(@Param("userId") Long userId);
public int selectAppIntegralRecordCount(AppIntegralRecord appIntegralRecord);
/**

View File

@@ -22,6 +22,20 @@ public interface AppResourceMapper
*/
public AppResource selectAppResourceById(Long id);
/**
* 根据下载项ID查询规格。
*
* @param id 下载项ID
* @return 资源规格
*/
public AppResourceList selectAppResourceListById(Long id);
/**
* 查询用户当前拥有的最高资源规格,用于计算版本升级差价。
*/
public AppResourceList selectHighestEntitledResourceList(@Param("userId") Long userId,
@Param("resourceId") Long resourceId);
/**
* 查询资源列表
*
@@ -87,6 +101,17 @@ public interface AppResourceMapper
*/
public int deleteAppResourceListByAppResourceId(Long id);
/**
* 查询本次保存将删除且已经产生订单的规格数量。
*/
public int countOrderedRemovedResourceLists(@Param("resourceId") Long resourceId,
@Param("retainedIds") List<Long> retainedIds);
/**
* 查询资源是否已经产生虚拟支付订单。
*/
public int countVirtualOrdersByResourceId(Long resourceId);
/**
* 查询资源(根据用户判断是否需要广告)
*

View File

@@ -1,6 +1,7 @@
package com.ruoyi.app.mapper;
import com.ruoyi.app.domain.AppVirtualOrder;
import com.ruoyi.app.domain.AppVirtualOrderSummary;
import org.apache.ibatis.annotations.Param;
import java.util.Date;
@@ -8,15 +9,25 @@ import java.util.List;
public interface AppVirtualOrderMapper
{
String lockOpenIdForOrder(@Param("userId") Long userId);
int insertAppVirtualOrder(AppVirtualOrder order);
AppVirtualOrder selectByOrderNo(String orderNo);
AppVirtualOrder selectPendingByPurchase(@Param("userId") Long userId,
@Param("resourceId") Long resourceId,
@Param("resourceListId") Long resourceListId);
AppVirtualOrder selectAppVirtualOrderById(Long id);
List<AppVirtualOrder> selectAppVirtualOrderList(AppVirtualOrder order);
int countAnyEntitlement(@Param("userId") Long userId, @Param("resourceId") Long resourceId);
List<AppVirtualOrderSummary> selectMyOrderList(@Param("userId") Long userId);
int countAnyEntitlement(@Param("userId") Long userId,
@Param("resourceId") Long resourceId,
@Param("resourceListId") Long resourceListId);
int markPaid(@Param("orderNo") String orderNo,
@Param("wxOrderNo") String wxOrderNo,
@@ -31,5 +42,7 @@ public interface AppVirtualOrderMapper
int markClosed(String orderNo);
int cancelPendingOrder(@Param("orderNo") String orderNo, @Param("userId") Long userId);
int markQuerying(String orderNo);
}

View File

@@ -0,0 +1,11 @@
package com.ruoyi.app.service;
import com.ruoyi.app.domain.AppDashboardSummary;
/**
* 首页业务指标服务。
*/
public interface IAppDashboardService
{
AppDashboardSummary selectDashboardSummary();
}

View File

@@ -1,8 +1,10 @@
package com.ruoyi.app.service;
import com.fasterxml.jackson.databind.JsonNode;
import com.ruoyi.app.domain.AppVirtualOrderSummary;
import com.ruoyi.app.domain.request.CreateVirtualOrderRequest;
import java.util.List;
import java.util.Map;
public interface IAppVirtualPayService
@@ -11,6 +13,10 @@ public interface IAppVirtualPayService
Map<String, Object> queryOrder(String orderNo, boolean sync);
List<AppVirtualOrderSummary> listMyOrders();
void cancelOrder(String orderNo);
boolean verifyCallbackSignature(String signature, String timestamp, String nonce);
void handleCallback(JsonNode body);

View File

@@ -0,0 +1,23 @@
package com.ruoyi.app.service.impl;
import com.ruoyi.app.domain.AppDashboardSummary;
import com.ruoyi.app.mapper.AppDashboardMapper;
import com.ruoyi.app.service.IAppDashboardService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* 首页业务指标服务实现。
*/
@Service
public class AppDashboardServiceImpl implements IAppDashboardService
{
@Autowired
private AppDashboardMapper appDashboardMapper;
@Override
public AppDashboardSummary selectDashboardSummary()
{
return appDashboardMapper.selectDashboardSummary();
}
}

View File

@@ -1,10 +1,12 @@
package com.ruoyi.app.service.impl;
import java.util.List;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import com.ruoyi.common.utils.DateUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import com.ruoyi.common.utils.StringUtils;
import org.springframework.transaction.annotation.Transactional;
import com.ruoyi.app.domain.AppResourceList;
@@ -82,6 +84,7 @@ public class AppResourceServiceImpl implements IAppResourceService
public int updateAppResource(AppResource appResource)
{
configureVirtualProduct(appResource);
ensureNoOrderedSpecRemoved(appResource);
appResourceMapper.deleteAppResourceListByAppResourceId(appResource.getId());
insertAppResourceList(appResource);
return appResourceMapper.updateAppResource(appResource);
@@ -97,6 +100,13 @@ public class AppResourceServiceImpl implements IAppResourceService
@Override
public int deleteAppResourceByIds(Long[] ids)
{
for (Long id : ids)
{
if (appResourceMapper.countVirtualOrdersByResourceId(id) > 0)
{
throw new ServiceException("资源已产生支付订单,不能删除;可以将资源隐藏");
}
}
appResourceMapper.deleteAppResourceListByAppResourceIds(ids);
return appResourceMapper.deleteAppResourceByIds(ids);
}
@@ -111,6 +121,10 @@ public class AppResourceServiceImpl implements IAppResourceService
@Override
public int deleteAppResourceById(Long id)
{
if (appResourceMapper.countVirtualOrdersByResourceId(id) > 0)
{
throw new ServiceException("资源已产生支付订单,不能删除;可以将资源隐藏");
}
appResourceMapper.deleteAppResourceListByAppResourceId(id);
return appResourceMapper.deleteAppResourceById(id);
}
@@ -127,9 +141,18 @@ public class AppResourceServiceImpl implements IAppResourceService
if (StringUtils.isNotNull(appResourceListList))
{
List<AppResourceList> list = new ArrayList<AppResourceList>();
for (AppResourceList appResourceList : appResourceListList)
for (int index = 0; index < appResourceListList.size(); index++)
{
AppResourceList appResourceList = appResourceListList.get(index);
appResourceList.setAppResourceId(id);
if (appResourceList.getStatus() == null)
{
appResourceList.setStatus(1);
}
if (appResourceList.getSortOrder() == null)
{
appResourceList.setSortOrder(index);
}
list.add(appResourceList);
}
if (list.size() > 0)
@@ -144,20 +167,170 @@ public class AppResourceServiceImpl implements IAppResourceService
*/
private void configureVirtualProduct(AppResource resource)
{
List<AppResourceList> resourceSpecs = resource.getAppResourceListList();
validateResourceAccessPasswords(resourceSpecs);
if (resource.getIsAd() == null || resource.getIsAd() != 3L)
{
resource.setPriceFen(0);
if (resourceSpecs != null)
{
for (int index = 0; index < resourceSpecs.size(); index++)
{
AppResourceList resourceSpec = resourceSpecs.get(index);
resourceSpec.setPriceFen(0);
if (resourceSpec.getStatus() == null)
{
resourceSpec.setStatus(1);
}
if (resourceSpec.getSortOrder() == null)
{
resourceSpec.setSortOrder(index);
}
}
}
return;
}
if (resource.getPriceFen() == null || resource.getPriceFen() <= 0)
if (resourceSpecs == null || resourceSpecs.isEmpty())
{
throw new ServiceException("付费资源价格必须大于0分");
throw new ServiceException("付费资源至少需要配置一个购买规格");
}
AppVirtualProduct priceProduct = virtualProductMapper.selectActiveByPrice(resource.getPriceFen());
if (priceProduct == null || StringUtils.isBlank(priceProduct.getProductId()))
Integer minimumActivePrice = null;
String minimumPriceProductId = null;
for (int index = 0; index < resourceSpecs.size(); index++)
{
throw new ServiceException("当前价格档位未配置微信道具,请先配置价格与道具映射");
AppResourceList resourceSpec = resourceSpecs.get(index);
if (StringUtils.isBlank(resourceSpec.getListName()))
{
throw new ServiceException("" + (index + 1) + "个规格名称不能为空");
}
if (StringUtils.isBlank(resourceSpec.getListUrl()))
{
throw new ServiceException("规格“" + resourceSpec.getListName() + "”的资源地址不能为空");
}
if (resourceSpec.getPriceFen() == null || resourceSpec.getPriceFen() <= 0)
{
throw new ServiceException("规格“" + resourceSpec.getListName() + "”的价格必须大于0分");
}
if (resourceSpec.getStatus() == null)
{
resourceSpec.setStatus(1);
}
if (resourceSpec.getSortOrder() == null)
{
resourceSpec.setSortOrder(index);
}
if (resourceSpec.getStatus() != 1)
{
continue;
}
AppVirtualProduct priceProduct = virtualProductMapper.selectActiveByPrice(resourceSpec.getPriceFen());
if (priceProduct == null || StringUtils.isBlank(priceProduct.getProductId()))
{
throw new ServiceException("规格“" + resourceSpec.getListName()
+ "”的价格档位未配置微信道具");
}
if (minimumActivePrice == null || resourceSpec.getPriceFen() < minimumActivePrice)
{
minimumActivePrice = resourceSpec.getPriceFen();
minimumPriceProductId = priceProduct.getProductId();
}
}
if (minimumActivePrice == null)
{
throw new ServiceException("付费资源至少需要启用一个购买规格");
}
List<AppResourceList> orderedSpecs = new ArrayList<AppResourceList>(resourceSpecs);
Collections.sort(orderedSpecs, new Comparator<AppResourceList>()
{
@Override
public int compare(AppResourceList left, AppResourceList right)
{
return left.getSortOrder().compareTo(right.getSortOrder());
}
});
for (int index = 0; index < orderedSpecs.size(); index++)
{
AppResourceList current = orderedSpecs.get(index);
if (index > 0)
{
AppResourceList previous = orderedSpecs.get(index - 1);
if (current.getSortOrder().equals(previous.getSortOrder()))
{
throw new ServiceException("版本排序不能重复,请按版本等级从低到高填写");
}
if (current.getPriceFen() <= previous.getPriceFen())
{
throw new ServiceException("版本价格必须随排序等级递增");
}
}
if (current.getStatus() != 1)
{
continue;
}
for (int lowerIndex = 0; lowerIndex < index; lowerIndex++)
{
AppResourceList lower = orderedSpecs.get(lowerIndex);
int upgradePrice = current.getPriceFen() - lower.getPriceFen();
AppVirtualProduct upgradeProduct = virtualProductMapper.selectActiveByPrice(upgradePrice);
if (upgradeProduct == null || StringUtils.isBlank(upgradeProduct.getProductId()))
{
throw new ServiceException("从“" + lower.getListName() + "”升级到“"
+ current.getListName() + "”的差价" + upgradePrice + "分未配置微信道具");
}
}
}
// 主表价格仅用于列表展示和兼容旧版小程序,实际下单始终读取规格价格。
resource.setPriceFen(minimumActivePrice);
resource.setVirtualProductId(minimumPriceProductId);
}
/**
* 防止浏览器自动填充把资源链接误写到访问密码字段。
*/
private void validateResourceAccessPasswords(List<AppResourceList> resourceSpecs)
{
if (resourceSpecs == null)
{
return;
}
for (AppResourceList resourceSpec : resourceSpecs)
{
String password = resourceSpec.getPassword();
if (StringUtils.isNotBlank(password) && password.trim().matches("(?i)^https?://.*"))
{
throw new ServiceException("规格“" + resourceSpec.getListName()
+ "”的访问密码不能填写网盘链接,请填写到资源地址");
}
}
}
/**
* 已产生订单的规格必须保留原ID管理员可以停用但不能删除。
*/
private void ensureNoOrderedSpecRemoved(AppResource resource)
{
if (resource.getId() == null)
{
return;
}
List<Long> retainedIds = new ArrayList<Long>();
if (resource.getAppResourceListList() != null)
{
for (AppResourceList resourceSpec : resource.getAppResourceListList())
{
if (resourceSpec.getId() != null)
{
retainedIds.add(resourceSpec.getId());
}
}
}
if (appResourceMapper.countOrderedRemovedResourceLists(resource.getId(), retainedIds) > 0)
{
throw new ServiceException("已产生订单的规格不能删除,请将规格状态改为停用");
}
resource.setVirtualProductId(priceProduct.getProductId());
}
}

View File

@@ -3,17 +3,16 @@ 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.AppResourceList;
import com.ruoyi.app.domain.AppVirtualOrder;
import com.ruoyi.app.domain.AppVirtualOrderSummary;
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;
@@ -21,7 +20,6 @@ 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;
@@ -31,6 +29,7 @@ import okhttp3.Response;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -43,6 +42,7 @@ import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@@ -62,10 +62,8 @@ public class AppVirtualPayServiceImpl implements IAppVirtualPayService
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;
@@ -76,20 +74,16 @@ public class AppVirtualPayServiceImpl implements IAppVirtualPayService
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)
@@ -103,34 +97,120 @@ public class AppVirtualPayServiceImpl implements IAppVirtualPayService
{
checkEnabled();
Long userId = SecurityUtils.getUserId();
SysUser user = sysUserMapper.selectUserById(userId);
if (user == null || StringUtils.isBlank(user.getOpenId()))
WxCodeSession codeSession = wxCodeSessionService.exchange(request.getCode());
// 微信换码不占用数据库锁;换码完成后再锁定用户行,将同一用户的下单请求串行化。
String lockedOpenId = virtualOrderMapper.lockOpenIdForOrder(userId);
if (StringUtils.isBlank(lockedOpenId))
{
throw new ServiceException("当前账号未绑定微信");
}
WxCodeSession codeSession = wxCodeSessionService.exchange(request.getCode());
if (!MessageDigest.isEqual(user.getOpenId().getBytes(StandardCharsets.UTF_8),
if (!MessageDigest.isEqual(lockedOpenId.getBytes(StandardCharsets.UTF_8),
codeSession.getOpenId().getBytes(StandardCharsets.UTF_8)))
{
throw new ServiceException("微信身份与当前登录账号不一致");
}
AppResource resource = appResourceMapper.selectAppResourceById(request.getResourceId());
AppResourceList resourceSpec = null;
AppResource resource;
if (request.getResourceListId() != null)
{
resourceSpec = appResourceMapper.selectAppResourceListById(request.getResourceListId());
if (resourceSpec == null)
{
throw new ServiceException("购买规格不存在");
}
resource = appResourceMapper.selectAppResourceById(resourceSpec.getAppResourceId());
}
else
{
// 兼容已发布的旧版小程序。新版小程序必须按 resourceListId 购买具体规格。
if (request.getResourceId() == null)
{
throw new ServiceException("购买规格不能为空");
}
resource = appResourceMapper.selectAppResourceById(request.getResourceId());
}
if (resource == null || resource.getIsAd() == null || resource.getIsAd() != 3L)
{
throw new ServiceException("该资源不支持虚拟支付");
}
if (resource.getPriceFen() == null || resource.getPriceFen() <= 0)
if (resourceSpec == null)
{
throw new ServiceException("资源价格未配置");
int activeSpecCount = 0;
int specCount = resource.getAppResourceListList() == null
? 0 : resource.getAppResourceListList().size();
if (resource.getAppResourceListList() != null)
{
for (AppResourceList item : resource.getAppResourceListList())
{
if (item.getStatus() != null && item.getStatus() == 1)
{
activeSpecCount++;
resourceSpec = item;
}
}
}
if (specCount != 1 || activeSpecCount != 1)
{
throw new ServiceException("该资源包含多个购买规格,请升级小程序后选择版本");
}
}
if (virtualOrderMapper.countAnyEntitlement(userId, resource.getId()) > 0)
if (resourceSpec.getStatus() == null || resourceSpec.getStatus() != 1)
{
throw new ServiceException("该购买规格已停用");
}
Integer orderPrice = resourceSpec.getPriceFen();
if (orderPrice == null || orderPrice <= 0)
{
throw new ServiceException("购买规格价格未配置");
}
Long resourceListId = resourceSpec.getId();
// 旧版整项权益和积分兑换都代表已经拥有完整资源,不能再购买任一版本。
if (virtualOrderMapper.countAnyEntitlement(userId, resource.getId(), null) > 0)
{
throw new ServiceException("该资源已经解锁");
}
AppVirtualProduct product = virtualProductMapper.selectActiveByPrice(resource.getPriceFen());
AppResourceList highestOwnedSpec = appResourceMapper.selectHighestEntitledResourceList(
userId, resource.getId());
if (highestOwnedSpec != null)
{
if (resourceSpec.getSortOrder() == null || highestOwnedSpec.getSortOrder() == null)
{
throw new ServiceException("资源版本等级未配置");
}
if (highestOwnedSpec.getSortOrder() >= resourceSpec.getSortOrder())
{
throw new ServiceException("该版本已购买或已包含在更高版本中");
}
if (highestOwnedSpec.getPriceFen() == null || highestOwnedSpec.getPriceFen() <= 0)
{
throw new ServiceException("已购版本价格未配置");
}
orderPrice = resourceSpec.getPriceFen() - highestOwnedSpec.getPriceFen();
if (orderPrice <= 0)
{
throw new ServiceException("版本价格必须随等级递增");
}
}
AppVirtualOrder pendingOrder = virtualOrderMapper.selectPendingByPurchase(
userId, resource.getId(), resourceListId);
if (pendingOrder != null)
{
if (!orderPrice.equals(pendingOrder.getPriceFen()))
{
throw new ServiceException("升级价格已变化,请先在我的订单中取消原待支付订单");
}
if (!MessageDigest.isEqual(lockedOpenId.getBytes(StandardCharsets.UTF_8),
pendingOrder.getOpenId().getBytes(StandardCharsets.UTF_8)))
{
throw new ServiceException("待支付订单与当前微信账号不一致");
}
return buildPaymentParams(pendingOrder, codeSession.getSessionKey(), true);
}
AppVirtualProduct product = virtualProductMapper.selectActiveByPrice(orderPrice);
if (product == null || StringUtils.isBlank(product.getProductId()))
{
throw new ServiceException("当前价格档位尚未配置微信道具");
@@ -140,20 +220,33 @@ public class AppVirtualPayServiceImpl implements IAppVirtualPayService
order.setOrderNo(generateOrderNo());
order.setUserId(userId);
order.setResourceId(resource.getId());
order.setResourceListId(resourceListId);
order.setSpecName(resourceSpec.getListName());
order.setProductId(product.getProductId());
order.setPriceFen(resource.getPriceFen());
order.setOpenId(codeSession.getOpenId());
order.setPriceFen(orderPrice);
order.setOpenId(lockedOpenId);
order.setStatus(0);
order.setCreateTime(DateUtils.getNowDate());
virtualOrderMapper.insertAppVirtualOrder(order);
try
{
virtualOrderMapper.insertAppVirtualOrder(order);
}
catch (DuplicateKeyException e)
{
throw new ServiceException("该规格已有支付中或已支付订单,请刷新后重试");
}
return buildPaymentParams(order, codeSession.getSessionKey(), false);
}
private Map<String, Object> buildPaymentParams(AppVirtualOrder order, String sessionKey, boolean reused)
{
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("productId", order.getProductId());
signDataMap.put("goodsPrice", order.getPriceFen());
signDataMap.put("outTradeNo", order.getOrderNo());
signDataMap.put("attach", order.getOrderNo());
@@ -162,10 +255,12 @@ public class AppVirtualPayServiceImpl implements IAppVirtualPayService
String signData = objectMapper.writeValueAsString(signDataMap);
Map<String, Object> result = new LinkedHashMap<>();
result.put("orderNo", order.getOrderNo());
result.put("reused", reused);
result.put("priceFen", order.getPriceFen());
result.put("signData", signData);
result.put("paySig", hmacSha256(virtualPayConfig.getAppKey(),
"requestVirtualPayment&" + signData));
result.put("signature", hmacSha256(codeSession.getSessionKey(), signData));
result.put("signature", hmacSha256(sessionKey, signData));
result.put("mode", "short_series_goods");
return result;
}
@@ -195,11 +290,57 @@ public class AppVirtualPayServiceImpl implements IAppVirtualPayService
Map<String, Object> result = new LinkedHashMap<>();
result.put("orderNo", order.getOrderNo());
result.put("resourceId", order.getResourceId());
result.put("resourceListId", order.getResourceListId());
result.put("specName", order.getSpecName());
result.put("status", order.getStatus());
result.put("unlocked", order.getStatus() == 1);
return result;
}
@Override
public List<AppVirtualOrderSummary> listMyOrders()
{
return virtualOrderMapper.selectMyOrderList(SecurityUtils.getUserId());
}
@Override
@Transactional
public void cancelOrder(String orderNo)
{
if (StringUtils.isBlank(orderNo))
{
throw new ServiceException("订单号不能为空");
}
Long userId = SecurityUtils.getUserId();
AppVirtualOrder order = virtualOrderMapper.selectByOrderNo(orderNo);
if (order == null || !userId.equals(order.getUserId()))
{
throw new ServiceException("订单不存在");
}
if (order.getStatus() != null && order.getStatus() == 3)
{
return;
}
if (order.getStatus() != null && order.getStatus() == 0)
{
// 先向微信核对一次,避免支付成功但回调尚未落库时误关单。
syncOrderFromWechat(order);
order = virtualOrderMapper.selectByOrderNo(orderNo);
}
if (order != null && order.getStatus() != null && order.getStatus() == 3)
{
return;
}
if (order.getStatus() == null || order.getStatus() != 0)
{
throw new ServiceException("仅待支付订单可以取消");
}
if (virtualOrderMapper.cancelPendingOrder(orderNo, userId) != 1)
{
throw new ServiceException("订单状态已更新,请刷新后重试");
}
}
@Override
public boolean verifyCallbackSignature(String signature, String timestamp, String nonce)
{
@@ -386,19 +527,6 @@ public class AppVirtualPayServiceImpl implements IAppVirtualPayService
{
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("生成资源购买记录失败");
}
}
}

View File

@@ -0,0 +1,78 @@
<?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.AppDashboardMapper">
<resultMap id="AppDashboardSummaryResult" type="AppDashboardSummary">
<result property="resourceTotal" column="resource_total"/>
<result property="resourceToday" column="resource_today"/>
<result property="userTotal" column="user_total"/>
<result property="userToday" column="user_today"/>
<result property="orderTotal" column="order_total"/>
<result property="orderToday" column="order_today"/>
<result property="amountTotal" column="amount_total"/>
<result property="amountToday" column="amount_today"/>
</resultMap>
<select id="selectDashboardSummary" resultMap="AppDashboardSummaryResult">
SELECT
(SELECT COUNT(1)
FROM app_resource
WHERE del_flag = 0) AS resource_total,
(SELECT COUNT(1)
FROM app_resource
WHERE del_flag = 0
AND create_time &gt;= CURDATE()
AND create_time &lt; DATE_ADD(CURDATE(), INTERVAL 1 DAY)) AS resource_today,
(SELECT COUNT(1)
FROM sys_user
WHERE del_flag = '0') AS user_total,
(SELECT COUNT(1)
FROM sys_user
WHERE del_flag = '0'
AND create_time &gt;= CURDATE()
AND create_time &lt; DATE_ADD(CURDATE(), INTERVAL 1 DAY)) AS user_today,
((SELECT COUNT(1)
FROM app_pay_order
WHERE status = 1)
+
(SELECT COUNT(1)
FROM app_virtual_order
WHERE status = 1)) AS order_total,
((SELECT COUNT(1)
FROM app_pay_order
WHERE status = 1
AND pay_time &gt;= CURDATE()
AND pay_time &lt; DATE_ADD(CURDATE(), INTERVAL 1 DAY))
+
(SELECT COUNT(1)
FROM app_virtual_order
WHERE status = 1
AND pay_time &gt;= CURDATE()
AND pay_time &lt; DATE_ADD(CURDATE(), INTERVAL 1 DAY))) AS order_today,
CAST(
COALESCE((SELECT SUM(amount)
FROM app_pay_order
WHERE status = 1), 0)
+
COALESCE((SELECT SUM(price_fen)
FROM app_virtual_order
WHERE status = 1), 0) / 100
AS DECIMAL(20, 2)
) AS amount_total,
CAST(
COALESCE((SELECT SUM(amount)
FROM app_pay_order
WHERE status = 1
AND pay_time &gt;= CURDATE()
AND pay_time &lt; DATE_ADD(CURDATE(), INTERVAL 1 DAY)), 0)
+
COALESCE((SELECT SUM(price_fen)
FROM app_virtual_order
WHERE status = 1
AND pay_time &gt;= CURDATE()
AND pay_time &lt; DATE_ADD(CURDATE(), INTERVAL 1 DAY)), 0) / 100
AS DECIMAL(20, 2)
) AS amount_today
</select>
</mapper>

View File

@@ -36,6 +36,13 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</where> ORDER BY a.integral_time desc
</select>
<select id="selectMyIntegralRecordList" resultMap="AppIntegralRecordResult">
<include refid="selectAppIntegralRecordVo"/>
where a.user_id = #{userId}
and a.is_add in (0, 1)
order by a.integral_time desc, a.id desc
</select>
<select id="selectAppIntegralRecordCount" parameterType="AppIntegralRecord" resultType="int">
SELECT COUNT(0) FROM app_integral_record a
<where>
@@ -96,4 +103,4 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
#{id}
</foreach>
</delete>
</mapper>
</mapper>

View File

@@ -119,13 +119,20 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
LEFT JOIN (
SELECT
DATE_FORMAT(pay_time, '%Y-%m-%d') AS date,
amount
CAST(amount AS DECIMAL(18, 2)) AS amount
FROM app_pay_order
WHERE pay_time >= DATE_SUB(CURDATE(), INTERVAL #{days} DAY)
AND status = 1
UNION ALL
SELECT
DATE_FORMAT(pay_time, '%Y-%m-%d') AS date,
CAST(price_fen AS DECIMAL(18, 2)) / 100 AS amount
FROM app_virtual_order
WHERE pay_time >= DATE_SUB(CURDATE(), INTERVAL #{days} DAY)
AND status = 1
) AS pay ON dates.date = pay.date
WHERE dates.date >= DATE_SUB(CURDATE(), INTERVAL #{days} DAY)
GROUP BY dates.date
ORDER BY dates.date
</select>
</mapper>
</mapper>

View File

@@ -33,6 +33,10 @@
<result property="listName" column="sub_list_name" />
<result property="listUrl" column="sub_list_url" />
<result property="password" column="sub_password" />
<result property="priceFen" column="sub_price_fen" />
<result property="status" column="sub_status" />
<result property="sortOrder" column="sub_sort_order" />
<result property="purchased" column="sub_purchased" />
<result property="appResourceId" column="sub_app_resource_id" />
</resultMap>
@@ -65,10 +69,36 @@
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
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.price_fen as sub_price_fen,
b.status as sub_status, b.sort_order as sub_sort_order,
0 as sub_purchased, 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
where a.id = #{id}
order by b.sort_order asc, b.id asc
</select>
<select id="selectAppResourceListById" parameterType="Long" resultType="AppResourceList">
select id, list_name as listName, list_url as listUrl, password,
price_fen as priceFen, status, sort_order as sortOrder,
app_resource_id as appResourceId
from app_resource_list
where id = #{id}
limit 1
</select>
<select id="selectHighestEntitledResourceList" resultType="AppResourceList">
select rl.id, rl.list_name as listName, rl.list_url as listUrl, rl.password,
rl.price_fen as priceFen, rl.status, rl.sort_order as sortOrder,
rl.app_resource_id as appResourceId
from app_resource_entitlement re
inner join app_resource_list rl on rl.id = re.resource_list_id
where re.user_id = #{userId}
and re.resource_id = #{resourceId}
and re.status = 1
order by rl.sort_order desc, rl.id desc
limit 1
</select>
<insert id="insertAppResource" parameterType="AppResource" useGeneratedKeys="true" keyProperty="id">
@@ -154,42 +184,129 @@
</delete>
<insert id="batchAppResourceList">
insert into app_resource_list( id, list_name, list_url, password, app_resource_id) values
insert into app_resource_list
(id, list_name, list_url, password, price_fen, status, sort_order, app_resource_id) values
<foreach item="item" index="index" collection="list" separator=",">
( #{item.id}, #{item.listName}, #{item.listUrl}, #{item.password}, #{item.appResourceId})
(#{item.id}, #{item.listName}, #{item.listUrl}, #{item.password},
#{item.priceFen}, #{item.status}, #{item.sortOrder}, #{item.appResourceId})
</foreach>
</insert>
<select id="countOrderedRemovedResourceLists" resultType="int">
select count(1)
from app_virtual_order vo
inner join app_resource_list rl on rl.id = vo.resource_list_id
where rl.app_resource_id = #{resourceId}
<if test="retainedIds != null and retainedIds.size() > 0">
and rl.id not in
<foreach item="id" collection="retainedIds" open="(" separator="," close=")">
#{id}
</foreach>
</if>
</select>
<select id="countVirtualOrdersByResourceId" parameterType="Long" resultType="int">
select count(1) from app_virtual_order where resource_id = #{resourceId}
</select>
<select id="selectAppResourceByIdAndUserId" resultMap="AppResourceAppResourceListResult">
select a.id, a.resource_title, a.show_img, a.`explain`, a.`keyword`, a.resource_type, a.is_show,
CASE
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,
case
when #{userId} is not null
and (
(a.is_ad in (2, 3)
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
(a.is_ad = 3
and exists(select 1 from app_resource_entitlement re
where re.resource_id = a.id and re.user_id = #{userId}
and re.status = 1 and re.resource_list_id is null))
)
then 0
else a.is_ad
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,
(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
b.id as sub_id, b.list_name as sub_list_name,
case when
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
left join app_resource_list owned_rl
on owned_rl.id = re.resource_list_id
where re.resource_id = a.id and re.user_id = #{userId}
and re.status = 1
and (re.resource_list_id is null
or (owned_rl.app_resource_id = a.id
and owned_rl.sort_order &gt;= b.sort_order)))
)
)
then b.list_url else null end as sub_list_url,
case when
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
left join app_resource_list owned_rl
on owned_rl.id = re.resource_list_id
where re.resource_id = a.id and re.user_id = #{userId}
and re.status = 1
and (re.resource_list_id is null
or (owned_rl.app_resource_id = a.id
and owned_rl.sort_order &gt;= b.sort_order)))
)
)
then b.password else null end as sub_password,
b.price_fen as sub_price_fen, b.status as sub_status,
b.sort_order as sub_sort_order,
case when
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
left join app_resource_list owned_rl
on owned_rl.id = re.resource_list_id
where re.resource_id = a.id and re.user_id = #{userId}
and re.status = 1
and (re.resource_list_id is null
or (owned_rl.app_resource_id = a.id
and owned_rl.sort_order &gt;= b.sort_order)))
)
)
then 1 else 0 end as sub_purchased,
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)
a.is_ad != 3
or b.status = 1
or (
#{userId} is not null
and exists(select 1 from app_resource_entitlement re
left join app_resource_list owned_rl
on owned_rl.id = re.resource_list_id
where re.resource_id = a.id and re.user_id = #{userId}
and re.status = 1
and (re.resource_list_id is null
or (owned_rl.app_resource_id = a.id
and owned_rl.sort_order &gt;= b.sort_order)))
)
)
)
where a.id = #{id}
order by b.sort_order asc, b.id asc
</select>
</mapper>

View File

@@ -11,6 +11,8 @@
<result property="nickName" column="nick_name"/>
<result property="resourceId" column="resource_id"/>
<result property="resourceTitle" column="resource_title"/>
<result property="resourceListId" column="resource_list_id"/>
<result property="specName" column="spec_name"/>
<result property="productId" column="product_id"/>
<result property="priceFen" column="price_fen"/>
<result property="openId" column="open_id"/>
@@ -24,31 +26,77 @@
<result property="lastQueryTime" column="last_query_time"/>
</resultMap>
<resultMap id="AppVirtualOrderSummaryResult" type="com.ruoyi.app.domain.AppVirtualOrderSummary">
<result property="orderNo" column="order_no"/>
<result property="resourceId" column="resource_id"/>
<result property="resourceTitle" column="resource_title"/>
<result property="resourceListId" column="resource_list_id"/>
<result property="specName" column="spec_name"/>
<result property="priceFen" column="price_fen"/>
<result property="status" column="status"/>
<result property="createTime" column="create_time"/>
<result property="payTime" column="pay_time"/>
<result property="refundTime" column="refund_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.resource_id, r.resource_title, vo.resource_list_id,
coalesce(vo.spec_name_snapshot, rl.list_name, '旧版整项资源') as spec_name,
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
left join app_resource_list rl on rl.id = vo.resource_list_id
</sql>
<select id="lockOpenIdForOrder" parameterType="Long" resultType="java.lang.String">
select open_id
from sys_user
where user_id = #{userId} and del_flag = '0'
for update
</select>
<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)
(order_no, user_id, resource_id, resource_list_id, spec_name_snapshot,
product_id, price_fen, open_id, status, create_time)
values
(#{orderNo}, #{userId}, #{resourceId}, #{productId}, #{priceFen}, #{openId}, #{status}, #{createTime})
(#{orderNo}, #{userId}, #{resourceId}, #{resourceListId}, #{specName},
#{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,
select id, order_no, user_id, resource_id, resource_list_id,
spec_name_snapshot as spec_name, 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="selectPendingByPurchase" resultMap="AppVirtualOrderResult">
select id, order_no, user_id, resource_id, resource_list_id,
spec_name_snapshot as spec_name, 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 user_id = #{userId}
and resource_id = #{resourceId}
and status = 0
<choose>
<when test="resourceListId != null">
and (resource_list_id = #{resourceListId} or resource_list_id is null)
</when>
<otherwise>
and resource_list_id is null
</otherwise>
</choose>
order by id desc
limit 1
</select>
<select id="selectAppVirtualOrderById" parameterType="Long" resultMap="AppVirtualOrderResult">
<include refid="selectAppVirtualOrderVo"/>
where vo.id = #{id}
@@ -72,6 +120,7 @@
or u.nick_name like concat('%', #{userName}, '%'))
</if>
<if test="resourceId != null">and vo.resource_id = #{resourceId}</if>
<if test="resourceListId != null">and vo.resource_list_id = #{resourceListId}</if>
<if test="productId != null and productId != ''">
and vo.product_id like concat('%', #{productId}, '%')
</if>
@@ -89,10 +138,30 @@
order by vo.create_time desc, vo.id desc
</select>
<select id="selectMyOrderList" resultMap="AppVirtualOrderSummaryResult">
select vo.order_no, vo.resource_id, coalesce(r.resource_title, '资源已删除') as resource_title,
vo.resource_list_id,
coalesce(vo.spec_name_snapshot, rl.list_name, '旧版整项资源') as spec_name,
vo.price_fen, vo.status, vo.create_time, vo.pay_time, vo.refund_time
from app_virtual_order vo
left join app_resource r on r.id = vo.resource_id
left join app_resource_list rl on rl.id = vo.resource_list_id
where vo.user_id = #{userId}
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)
where user_id = #{userId} and resource_id = #{resourceId} and status = 1
<choose>
<when test="resourceListId != null">
and (resource_list_id is null or resource_list_id = #{resourceListId})
</when>
<otherwise>
and resource_list_id is null
</otherwise>
</choose>)
+
(select count(1) from app_integral_record
where user_id = #{userId}
@@ -108,14 +177,14 @@
transaction_id = coalesce(#{transactionId}, transaction_id),
pay_time = coalesce(#{payTime}, pay_time),
provide_time = now()
where order_no = #{orderNo} and status = 0
where order_no = #{orderNo} and status in (0, 3)
</update>
<insert id="insertEntitlement" parameterType="AppVirtualOrder">
insert into app_resource_entitlement
(user_id, resource_id, order_no, status, granted_time)
(user_id, resource_id, resource_list_id, order_no, status, granted_time)
values
(#{userId}, #{resourceId}, #{orderNo}, 1, now())
(#{userId}, #{resourceId}, #{resourceListId}, #{orderNo}, 1, now())
on duplicate key update
order_no = values(order_no),
status = 1,
@@ -140,6 +209,14 @@
where order_no = #{orderNo} and status = 0
</update>
<update id="cancelPendingOrder">
update app_virtual_order
set status = 3
where order_no = #{orderNo}
and user_id = #{userId}
and status = 0
</update>
<update id="markQuerying">
update app_virtual_order
set last_query_time = now()