feat: 新增虚拟支付订单与网盘链接检测功能

This commit is contained in:
王鹏
2026-07-28 13:42:30 +08:00
parent 4070179f46
commit 2250f744d1
60 changed files with 3668 additions and 106 deletions

View File

@@ -2,6 +2,7 @@ package com.ruoyi.web.controller.app;
import com.ruoyi.app.domain.AppBlogArticle;
import com.ruoyi.app.mapper.AppBlogArticleMapper;
import com.ruoyi.app.service.ImageUrlService;
import com.ruoyi.app.service.IAppBlogArticleService;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.core.controller.BaseController;
@@ -32,6 +33,9 @@ public class AppBlogArticleController extends BaseController {
@Autowired
private IAppBlogArticleService appBlogArticleService;
@Autowired
private ImageUrlService imageUrlService;
@Resource
private AppBlogArticleMapper appBlogArticleMapper;
@@ -45,6 +49,7 @@ public class AppBlogArticleController extends BaseController {
public TableDataInfo list(AppBlogArticle appBlogArticle) {
startPage();
List<AppBlogArticle> list = appBlogArticleService.selectAppBlogArticleList(appBlogArticle);
imageUrlService.decorateArticles(list);
return getDataTable(list);
}
@@ -72,6 +77,7 @@ public class AppBlogArticleController extends BaseController {
@GetMapping(value = "/app/{id}")
public AjaxResult appGetInfo(@PathVariable("id") Long id) {
AppBlogArticle appBlogArticle = appBlogArticleService.selectAppBlogArticleById(id);
imageUrlService.decorateArticle(appBlogArticle);
List<String> list = new ArrayList<>();
List<TtFile> picList = fileService.selectTtFileByCodeName(appBlogArticle.getTitle());
for (TtFile ttFile : picList) {

View File

@@ -4,6 +4,8 @@ import com.ruoyi.app.domain.AppIntegralRecord;
import com.ruoyi.app.domain.AppLotteryGoods;
import com.ruoyi.app.domain.AppLotteryLog;
import com.ruoyi.app.mapper.AppIntegralRecordMapper;
import com.ruoyi.app.mapper.AppResourceMapper;
import com.ruoyi.app.domain.AppResource;
import com.ruoyi.app.service.IAppIntegralRecordService;
import com.ruoyi.app.service.IAppLotteryGoodsService;
import com.ruoyi.app.service.IAppLotteryLogService;
@@ -42,6 +44,8 @@ public class AppIntegralRecordController extends BaseController
private AppIntegralRecordMapper appIntegralRecordMapper;
@Resource
private SysUserMapper sysUserMapper;
@Resource
private AppResourceMapper appResourceMapper;
@Autowired
private IAppLotteryGoodsService appLotteryGoodsService;
@Autowired
@@ -187,19 +191,38 @@ public class AppIntegralRecordController extends BaseController
* 积分记录,增减用户积分通用
*/
@Transactional
@PreAuthorize("isAuthenticated()")
@PostMapping("/resource")
public AjaxResult resource(@RequestBody AppIntegralRecord appIntegralRecord)
{
Date date = new Date();
appIntegralRecord.setIntegralTime(date);
appIntegralRecordService.insertAppIntegralRecord(appIntegralRecord);
if (appIntegralRecord.getIsAdd() == 0){
sysUserMapper.addIntegralByUserId(Math.toIntExact(appIntegralRecord.getIntegralNumber()), appIntegralRecord.getUserId());
}else {
sysUserMapper.delIntegralByUserId(Math.toIntExact(appIntegralRecord.getIntegralNumber()), appIntegralRecord.getUserId());
Long userId = getUserId();
AppResource resource = appResourceMapper.selectAppResourceById(appIntegralRecord.getResourceId());
if (resource == null || resource.getIsAd() == null || resource.getIsAd() != 2L
|| resource.getAdNumber() == null || resource.getAdNumber() <= 0)
{
return error("该资源不支持积分兑换");
}
return toAjax(1);
AppIntegralRecord exists = new AppIntegralRecord();
exists.setUserId(userId);
exists.setResourceId(resource.getId());
if (appIntegralRecordMapper.selectAppIntegralRecordCount(exists) > 0)
{
return success("资源已解锁");
}
int points = Math.toIntExact(resource.getAdNumber());
if (sysUserMapper.delIntegralByUserId(points, userId) != 1)
{
return error("积分不足");
}
appIntegralRecord.setSource("资源兑换");
appIntegralRecord.setIsAdd(1L);
appIntegralRecord.setIntegralNumber(resource.getAdNumber());
appIntegralRecord.setUserId(userId);
appIntegralRecord.setIntegralTime(new Date());
appIntegralRecordService.insertAppIntegralRecord(appIntegralRecord);
return success("兑换成功");
}
/**

View File

@@ -2,6 +2,7 @@ package com.ruoyi.web.controller.app;
import com.ruoyi.app.domain.AppResource;
import com.ruoyi.app.mapper.AppResourceMapper;
import com.ruoyi.app.service.ImageUrlService;
import com.ruoyi.app.service.IAppResourceService;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.core.controller.BaseController;
@@ -16,6 +17,9 @@ import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import java.util.List;
import org.springframework.security.core.Authentication;
import com.ruoyi.common.core.domain.model.LoginUser;
import com.ruoyi.common.utils.SecurityUtils;
/**
* 资源Controller
@@ -30,6 +34,9 @@ public class AppResourceController extends BaseController
@Autowired
private IAppResourceService appResourceService;
@Autowired
private ImageUrlService imageUrlService;
@Resource
private AppResourceMapper appResourceMapper;
@@ -41,6 +48,7 @@ public class AppResourceController extends BaseController
{
startPage();
List<AppResource> list = appResourceService.selectAppResourceList(appResource);
imageUrlService.decorateResources(list);
return getDataTable(list);
}
@@ -70,16 +78,28 @@ public class AppResourceController extends BaseController
@GetMapping(value = "/app/{id}")
public AjaxResult getInfoApp(@PathVariable("id") Long id)
{
return success(appResourceService.selectAppResourceById(id));
return success(imageUrlService.decorateResource(
appResourceMapper.selectAppResourceByIdAndUserId(id, currentUserId())));
}
/**
* 根据用户ID获取资源详细信息
*/
@GetMapping(value = "/app/user/{id}/{userId}")
public AjaxResult getResourceByUserId(@PathVariable("id") Long id, @PathVariable("userId") Long userId)
public AjaxResult getResourceByUserId(@PathVariable("id") Long id, @PathVariable("userId") Long ignoredUserId)
{
return success(appResourceMapper.selectAppResourceByIdAndUserId(id, userId));
return success(imageUrlService.decorateResource(
appResourceMapper.selectAppResourceByIdAndUserId(id, currentUserId())));
}
/**
* 小程序资源详情。用户身份只从 JWT 读取,不接收客户端 userId。
*/
@GetMapping(value = "/app/user/{id}")
public AjaxResult getResourceForCurrentUser(@PathVariable("id") Long id)
{
return success(imageUrlService.decorateResource(
appResourceMapper.selectAppResourceByIdAndUserId(id, currentUserId())));
}
/**
@@ -123,4 +143,14 @@ public class AppResourceController extends BaseController
{
return toAjax( appResourceMapper.lookAddNumber(appResource.getId()));
}
private Long currentUserId()
{
Authentication authentication = SecurityUtils.getAuthentication();
if (authentication != null && authentication.getPrincipal() instanceof LoginUser)
{
return ((LoginUser) authentication.getPrincipal()).getUserId();
}
return null;
}
}

View File

@@ -0,0 +1,59 @@
package com.ruoyi.web.controller.app;
import com.ruoyi.app.domain.AppVirtualOrder;
import com.ruoyi.app.service.IAppVirtualOrderService;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.utils.poi.ExcelUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletResponse;
import java.util.List;
/**
* 虚拟支付订单管理接口。
*
* 支付订单由微信回调和主动查单流程维护,管理端仅提供只读查询和导出。
*/
@RestController
@RequestMapping("/app/virtualOrder")
public class AppVirtualOrderController extends BaseController
{
@Autowired
private IAppVirtualOrderService appVirtualOrderService;
@PreAuthorize("@ss.hasPermi('app:virtualOrder:list')")
@GetMapping("/list")
public TableDataInfo list(AppVirtualOrder order)
{
startPage();
List<AppVirtualOrder> list = appVirtualOrderService.selectAppVirtualOrderList(order);
return getDataTable(list);
}
@PreAuthorize("@ss.hasPermi('app:virtualOrder:query')")
@GetMapping("/{id}")
public AjaxResult getInfo(@PathVariable Long id)
{
return success(appVirtualOrderService.selectAppVirtualOrderById(id));
}
@PreAuthorize("@ss.hasPermi('app:virtualOrder:export')")
@Log(title = "虚拟支付订单", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(HttpServletResponse response, AppVirtualOrder order)
{
List<AppVirtualOrder> list = appVirtualOrderService.selectAppVirtualOrderList(order);
ExcelUtil<AppVirtualOrder> util = new ExcelUtil<>(AppVirtualOrder.class);
util.exportExcel(response, list, "虚拟支付订单数据");
}
}

View File

@@ -0,0 +1,107 @@
package com.ruoyi.web.controller.app;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ruoyi.app.domain.request.CreateVirtualOrderRequest;
import com.ruoyi.app.service.IAppVirtualPayService;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.MediaType;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.LinkedHashMap;
import java.util.Map;
@RestController
@RequestMapping("/app/virtual-pay")
public class AppVirtualPayController extends BaseController
{
private static final Logger log = LoggerFactory.getLogger(AppVirtualPayController.class);
private final IAppVirtualPayService virtualPayService;
private final ObjectMapper objectMapper;
public AppVirtualPayController(IAppVirtualPayService virtualPayService, ObjectMapper objectMapper)
{
this.virtualPayService = virtualPayService;
this.objectMapper = objectMapper;
}
@PreAuthorize("isAuthenticated()")
@PostMapping("/resource-orders")
public AjaxResult createOrder(@Validated @RequestBody CreateVirtualOrderRequest request)
{
return success(virtualPayService.createOrder(request));
}
@PreAuthorize("isAuthenticated()")
@GetMapping("/resource-orders/{orderNo}")
public AjaxResult queryOrder(@PathVariable String orderNo,
@RequestParam(defaultValue = "false") boolean sync)
{
return success(virtualPayService.queryOrder(orderNo, sync));
}
/**
* 在小程序后台保存消息推送配置时使用。
*/
@GetMapping(value = "/callback", produces = MediaType.TEXT_PLAIN_VALUE)
public String verifyCallback(@RequestParam String signature,
@RequestParam String timestamp,
@RequestParam String nonce,
@RequestParam String echostr)
{
if (!virtualPayService.verifyCallbackSignature(signature, timestamp, nonce))
{
log.warn("微信虚拟支付回调 URL 验证失败");
return "";
}
return echostr;
}
/**
* 小程序消息推送请配置为 JSON 数据格式。
*/
@PostMapping("/callback")
public Map<String, Object> callback(@RequestParam String signature,
@RequestParam String timestamp,
@RequestParam String nonce,
@RequestBody String body)
{
if (!virtualPayService.verifyCallbackSignature(signature, timestamp, nonce))
{
log.warn("拒绝签名无效的微信虚拟支付回调");
return callbackResponse(1, "invalid signature");
}
try
{
JsonNode json = objectMapper.readTree(body);
virtualPayService.handleCallback(json);
return callbackResponse(0, "success");
}
catch (Exception e)
{
// 返回非零 ErrCode 让微信重试,异常详情只记录在服务端日志中。
log.error("微信虚拟支付回调处理失败", e);
return callbackResponse(1, "callback failed");
}
}
private static Map<String, Object> callbackResponse(int code, String message)
{
Map<String, Object> response = new LinkedHashMap<>();
response.put("ErrCode", code);
response.put("ErrMsg", message);
return response;
}
}

View File

@@ -4,7 +4,6 @@ import com.ruoyi.app.domain.ShuiYinVo;
import com.ruoyi.app.mapper.AppPunlicMapper;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.utils.http.HttpUtils;
import com.ruoyi.system.service.ISysConfigService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
@@ -21,20 +20,6 @@ public class PublicController extends BaseController {
AppPunlicMapper appPunlicMapper;
@Autowired
private ISysConfigService configService;
/**
* 获取微信openid信息
*/
@GetMapping(value = "/autoLoginWx/{code}")
public AjaxResult autoLoginWx(@PathVariable("code") String code)
{
HttpUtils httpUtils = new HttpUtils();
String appid = configService.selectConfigByKey("miniapp.wx.appId");
String secret = configService.selectConfigByKey("miniapp.wx.secret");
String param = "appid="+appid+"&secret="+secret+"&js_code="+code+"&grant_type=authorization_code";
String s = httpUtils.httpGet("https://api.weixin.qq.com/sns/jscode2session", param);
return success(s);
}
/**
* 去水印 key 免费申请联系作者微信yimoziyuan666
* @return
@@ -43,7 +28,7 @@ public class PublicController extends BaseController {
private String delWatermarkKey;
@PostMapping(value = "/delSHuiYin")
public AjaxResult delSHuiYin(HttpServletRequest request, @RequestBody ShuiYinVo shuiYinVo) {
HttpUtils httpUtils = new HttpUtils();
com.ruoyi.common.utils.http.HttpUtils httpUtils = new com.ruoyi.common.utils.http.HttpUtils();
String key = configService.selectConfigByKey("miniapp.shuiyin.key");
String param = "key="+key+"&url="+shuiYinVo.getUrl();
String s = httpUtils.httpGet("https://api.emoboy.vip/api/shuiyin/delWatermark", param);

View File

@@ -1,13 +1,12 @@
package com.ruoyi.web.controller.app;
import com.ruoyi.app.service.IAppPayOrderService;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.wx.*;
import com.wechat.pay.java.service.payments.jsapi.model.PrepayWithRequestPaymentResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.security.access.prepost.PreAuthorize;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
@@ -26,20 +25,6 @@ import java.io.IOException;
public class WxMiniappPayController extends BaseController {
@Autowired
private WxMiniappPayService wxMiniappPayService;
@Autowired
private IAppPayOrderService appPayOrderService;
/**
* 预支付订单/统一下单
* @param req
* @return
*/
@RequestMapping("/createOrder")
public Response<PrepayWithRequestPaymentResponse> createOrder(@Validated @RequestBody CreateOrderReq req) {
log.info("------预支付订单/统一下单------");
//微信小程序登录用户openid用户标识 说明用户在商户appid下的唯一标识。
return this.wxMiniappPayService.createOrder(req);
}
/**
* 支付回调
@@ -65,6 +50,7 @@ public class WxMiniappPayController extends BaseController {
* @return
*/
@PostMapping("/queryOrder")
@PreAuthorize("@ss.hasPermi('app:order:query')")
public Response queryOrder(@Validated @RequestBody QueryOrderReq req) {
log.info("------根据支付订单号查询订单------");
return this.wxMiniappPayService.queryOrder(req);
@@ -81,6 +67,7 @@ public class WxMiniappPayController extends BaseController {
* @return
*/
@PostMapping("/queryOrderByOutTradeNo")
@PreAuthorize("@ss.hasPermi('app:order:query')")
public Response queryOrderByOutTradeNo(@Validated @RequestBody QueryOrderReq req) {
log.info("------根据商户订单号查询订单------");
return this.wxMiniappPayService.queryOrderByOutTradeNo(req);
@@ -93,6 +80,7 @@ public class WxMiniappPayController extends BaseController {
*/
@PostMapping("/closeOrder")
@PreAuthorize("@ss.hasPermi('app:order:edit')")
public Response closeOrder(@Validated @RequestBody QueryOrderReq req) {
log.info("------微信小程序支付关闭订单------");
return this.wxMiniappPayService.closeOrder(req);
@@ -104,6 +92,7 @@ public class WxMiniappPayController extends BaseController {
* @return
*/
@PostMapping("/refund")
@PreAuthorize("@ss.hasPermi('app:order:refund')")
public Response refund(@Validated @RequestBody RefundOrderReq req) {
log.info("------微信支付退款------");
return this.wxMiniappPayService.refund(req);
@@ -115,6 +104,7 @@ public class WxMiniappPayController extends BaseController {
* @return
*/
@GetMapping("/queryByOutRefundNo")
@PreAuthorize("@ss.hasPermi('app:order:query')")
public Response queryByOutRefundNo(String outRefundNo) {
log.info("------微信支付查询单笔退款------");
return this.wxMiniappPayService.queryByOutRefundNo(outRefundNo);
@@ -134,4 +124,4 @@ public class WxMiniappPayController extends BaseController {
}
}

View File

@@ -16,6 +16,8 @@ import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.framework.web.service.SysLoginService;
import com.ruoyi.framework.web.service.SysPermissionService;
import com.ruoyi.system.service.ISysMenuService;
import com.ruoyi.common.wx.WxCodeSession;
import com.ruoyi.common.wx.WxCodeSessionService;
import javax.servlet.http.HttpServletRequest;
@@ -36,6 +38,9 @@ public class SysLoginController
@Autowired
private SysPermissionService permissionService;
@Autowired
private WxCodeSessionService wxCodeSessionService;
/**
* 登录方法
*
@@ -55,15 +60,13 @@ public class SysLoginController
/**
* 微信openID登陆
* @param
* @return
* 使用微信临时 code 登录。OpenID 与 session_key 只在服务端获取。
*/
@PostMapping("/wxLogin")
public AjaxResult wxLogin(@RequestBody LoginBody loginBody) {
AjaxResult ajax = AjaxResult.success();
// 生成令牌
String token = loginService.wxLogin(loginBody.getOpenId(),loginBody.getOldUserId());
WxCodeSession codeSession = wxCodeSessionService.exchange(loginBody.getCode());
String token = loginService.wxLogin(codeSession.getOpenId(), loginBody.getOldUserId());
ajax.put(Constants.TOKEN, token);
return ajax;
}

View File

@@ -9,8 +9,8 @@ ruoyi:
# 实例演示开关
demoEnabled: true
# 文件路径 示例( Windows配置D:/ruoyi/uploadPathLinux配置 /home/ruoyi/uploadPath
# profile: /home/upload
profilee: D:/ruoyi/uploadPath
profile: /home/upload
# profilee: D:/ruoyi/uploadPath
# 获取ip地址开关
addressEnabled: false
# 验证码类型 math 数字计算 char 字符验证
@@ -42,10 +42,10 @@ wx:
appid: wx17b46f75e762c184 # 微信小程序appid
secret: 1fa844dc33e70f7d813f24cd2af7678b # 微信小程序密钥
merchantId: 1704387455 # 商户号
privateKeyPath: D:\wxcert\WXCertUtil\cert\1704387455_20250112_cert\apiclient_key.pem # 商户API私钥路径测试环境
publicKeyPath: D:\wxcert\WXCertUtil\cert\1704387455_20250112_cert\pub_key.pem # 商户API公钥路径测试环境
# privateKeyPath: /home/cert/apiclient_key.pem # 商户API私钥路径正式环境
# publicKeyPath: /home/cert/pub_key.pem # 商户API公钥路径正式环境
# privateKeyPath: D:\wxcert\WXCertUtil\cert\1704387455_20250112_cert\apiclient_key.pem # 商户API私钥路径测试环境
# publicKeyPath: D:\wxcert\WXCertUtil\cert\1704387455_20250112_cert\pub_key.pem # 商户API公钥路径测试环境
privateKeyPath: /home/cert/apiclient_key.pem # 商户API私钥路径正式环境
publicKeyPath: /home/cert/pub_key.pem # 商户API公钥路径正式环境
publicKeyId: PUB_KEY_ID_0117043874552025011100188700000234
merchantSerialNumber: 743FBCB9F5DFD76104A468C6AC6EDD41268634A3 # 商户API证书序列号
apiV3Key: G7kL2mN8pQ4rT1vX9yZ3bC5dF6hJ0sW1 # 商户APIV3密钥
@@ -53,6 +53,13 @@ wx:
# refundNotifyUrl: http://www.yidaima.cn:6001/app/pay/refundNotify # 退款通知地址(测试环境)
payNotifyUrl: https://feast.yidaima.cn/prod-api/app/pay/payNotify # 支付通知地址(正式环境)
refundNotifyUrl: https://feast.yidaima.cn/prod-api/app/pay/refundNotify # 退款通知地址(正式环境)
# 虚拟支付配置。生产环境请通过环境变量注入,不要提交真实 AppKey。
virtual-pay:
enabled: ${WX_VIRTUAL_PAY_ENABLED:true}
offer-id: ${WX_VIRTUAL_PAY_OFFER_ID:1450602603}
app-key: ${WX_VIRTUAL_PAY_APP_KEY:iVRy8soX3JTHd8dWQxruDtOIDjrcIKYL}
env: ${WX_VIRTUAL_PAY_ENV:0}
callback-token: ${WX_VIRTUAL_PAY_CALLBACK_TOKEN:zSgMvGAa289XyYoWDnpc1gUUPEBQ2me9}
# 日志配置
logging:

View File

@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- 日志存放路径 -->
<property name="log.path" value="D:/ruoyi/log" />
<!-- <property name="log.path" value="/home/ruoyi/logs" />-->
<!-- <property name="log.path" value="D:/ruoyi/log" />-->
<property name="log.path" value="/home/ruoyi/logs" />
<!-- 日志输出格式 -->
<property name="log.pattern" value="%d{HH:mm:ss.SSS} [%thread] %-5level %logger{20} - [%method,%line] - %msg%n" />