guomengjiao месяцев назад: 10
Родитель
Сommit
54ab1c5382
27 измененных файлов с 1209 добавлено и 59 удалено
  1. 9 0
      common/src/main/java/com/jeesite/common/constant/Constants.java
  2. 4 2
      modules/bjflapi/src/main/java/com/jeesite/modules/bjflapi/AbstractController.java
  3. 3 3
      modules/bjflapi/src/main/java/com/jeesite/modules/bjflapi/report/WebsiteUserControllerApi.java
  4. 4 4
      modules/bjflapi/src/main/java/com/jeesite/modules/bjflapi/report/WebsiteUserOrderControllerApi.java
  5. 131 0
      modules/bjflapi/src/main/java/com/jeesite/modules/bjflapi/report/WebsiteUserResearchOrderControllerApi.java
  6. 78 0
      modules/core/src/main/java/com/jeesite/modules/sys/utils/RedisLockUtil.java
  7. 6 0
      modules/report/pom.xml
  8. 4 0
      modules/report/src/main/java/com/jeesite/modules/report/dao/ResearchReportDao.java
  9. 21 0
      modules/report/src/main/java/com/jeesite/modules/report/dao/WebsiteUserResearchOrderDao.java
  10. 0 19
      modules/report/src/main/java/com/jeesite/modules/report/dto/ImportMarketScaleMessage.java
  11. 19 0
      modules/report/src/main/java/com/jeesite/modules/report/entity/ResearchReport.java
  12. 23 10
      modules/report/src/main/java/com/jeesite/modules/report/entity/WebsiteUser.java
  13. 203 0
      modules/report/src/main/java/com/jeesite/modules/report/entity/WebsiteUserResearchOrder.java
  14. 49 0
      modules/report/src/main/java/com/jeesite/modules/report/exception/CommonExceptionEnum.java
  15. 64 0
      modules/report/src/main/java/com/jeesite/modules/report/exception/ServiceException.java
  16. 101 0
      modules/report/src/main/java/com/jeesite/modules/report/service/PaypalPayService.java
  17. 9 0
      modules/report/src/main/java/com/jeesite/modules/report/service/ResearchReportService.java
  18. 10 4
      modules/report/src/main/java/com/jeesite/modules/report/service/WebsiteUserOrderService.java
  19. 273 0
      modules/report/src/main/java/com/jeesite/modules/report/service/WebsiteUserResearchOrderService.java
  20. 34 0
      modules/report/src/main/java/com/jeesite/modules/report/util/PaypalPayUtil.java
  21. 5 5
      modules/report/src/main/java/com/jeesite/modules/report/web/WebsiteUserOrderController.java
  22. 74 0
      modules/report/src/main/java/com/jeesite/modules/report/web/WebsiteUserResearchOrderController.java
  23. 14 1
      modules/report/src/main/resources/mappings/modules/report/ResearchReportDao.xml
  24. 29 7
      modules/report/src/main/resources/mappings/modules/report/WebsiteUserDao.xml
  25. 3 3
      modules/report/src/main/resources/mappings/modules/report/WebsiteUserOrderDao.xml
  26. 29 0
      modules/report/src/main/resources/mappings/modules/report/WebsiteUserResearchOrderDao.xml
  27. 10 1
      web/src/main/resources/config/application.yml

+ 9 - 0
common/src/main/java/com/jeesite/common/constant/Constants.java

@@ -180,6 +180,7 @@ public interface Constants {
     String WEBSITE_TOKEN = "website-token";
     String WEBSITE_VALID_CODE = "websiteValidCode";
     String WEBSITE_ORDER_URL = "websiteOrderUrl";
+    String WEBSITE_RESEARCH_ORDER_URL = "websiteResearchOrderUrl";
 
     String PREFIX_USER_FORGET_TOKEN = "bjfl:user:forget:token:";
 
@@ -209,6 +210,10 @@ public interface Constants {
         String ZFB_H5 = "zfb_h5";
     }
 
+    interface researchOrderPayMethod{
+        String PAYPAL = "paypal";
+    }
+
     interface briefReportFileType{
         String PDF = "pdf";
         String PPT = "ppt";
@@ -219,5 +224,9 @@ public interface Constants {
         String REPORT_PUBLISHDATE = "bjfl:report:publish:date";
     }
 
+    interface redisLockKey{
+        String LOCK_ORDER_PAY_PREFIX = "lockOrderPay";
+    }
+
     String textFormat = "<p>%s</p>";
 }

+ 4 - 2
modules/bjflapi/src/main/java/com/jeesite/modules/bjflapi/AbstractController.java

@@ -9,6 +9,8 @@
 package com.jeesite.modules.bjflapi;
 
 import com.jeesite.common.lang.StringUtils;
+import com.jeesite.modules.report.exception.CommonExceptionEnum;
+import com.jeesite.modules.report.exception.ServiceException;
 import org.springframework.beans.factory.annotation.Autowired;
 
 import javax.servlet.http.HttpServletRequest;
@@ -23,10 +25,10 @@ public abstract class AbstractController {
     @Autowired
     HttpServletRequest request;
 
-    protected String getUserIdByRequest() throws Exception {
+    protected String getUserIdByRequest() {
         String uid = (String) request.getAttribute("uid");
         if (StringUtils.isEmpty(uid)) {
-            throw new Exception("用户未登录");
+            throw new ServiceException(CommonExceptionEnum.USER_NOT_LOGIN.getMsg(), CommonExceptionEnum.USER_NOT_LOGIN.getCode());
         }
         return uid;
     }

+ 3 - 3
modules/bjflapi/src/main/java/com/jeesite/modules/bjflapi/report/WebsiteUserControllerApi.java

@@ -163,7 +163,7 @@ public class WebsiteUserControllerApi extends AbstractController {
      */
     @WebsiteAuth
     @PostMapping(value = "updateUser")
-    public R<String> updateUser(WebsiteUser user) throws Exception {
+    public R<String> updateUser(WebsiteUser user) {
         String userId = getUserIdByRequest();
         return websiteUserService.updateUser(userId, user);
     }
@@ -173,7 +173,7 @@ public class WebsiteUserControllerApi extends AbstractController {
      */
     @WebsiteAuth
     @PostMapping(value = "updatePwd")
-    public R<String> updatePwd(WebsiteUserDto dto) throws Exception {
+    public R<String> updatePwd(WebsiteUserDto dto) {
         String userId = getUserIdByRequest();
         WebsiteUser websiteUser = websiteUserService.get(userId);
         return updatePass(websiteUser, dto.getNewPassword(), dto.getConfirmNewPassword());
@@ -278,7 +278,7 @@ public class WebsiteUserControllerApi extends AbstractController {
      */
     @WebsiteAuth
     @GetMapping(value = "getUserInfo")
-    public R<WebsiteUser> getUserInfo() throws Exception {
+    public R<WebsiteUser> getUserInfo() {
         String userId = getUserIdByRequest();
         return R.ok(websiteUserService.get(userId));
     }

+ 4 - 4
modules/bjflapi/src/main/java/com/jeesite/modules/bjflapi/report/WebsiteUserOrderControllerApi.java

@@ -42,7 +42,7 @@ public class WebsiteUserOrderControllerApi extends AbstractController {
      */
     @WebsiteAuth
     @PostMapping(value = "orderList")
-    public R<Page<WebsiteUserOrder>> orderList(WebsiteUserOrder websiteUserOrder) throws Exception {
+    public R<Page<WebsiteUserOrder>> orderList(WebsiteUserOrder websiteUserOrder) {
         String userId = getUserIdByRequest();
         websiteUserOrder.setWebsiteUserId(userId);
         return R.ok(websiteUserOrderService.findPage(websiteUserOrder));
@@ -53,7 +53,7 @@ public class WebsiteUserOrderControllerApi extends AbstractController {
      */
     @WebsiteAuth
     @PostMapping(value = "orderDetail")
-    public R<WebsiteUserOrder> orderDetail(WebsiteUserOrder websiteUserOrder) throws Exception {
+    public R<WebsiteUserOrder> orderDetail(WebsiteUserOrder websiteUserOrder) {
         String userId = getUserIdByRequest();
         websiteUserOrder.setWebsiteUserId(userId);
         return R.ok(websiteUserOrderService.getOne(websiteUserOrder));
@@ -64,7 +64,7 @@ public class WebsiteUserOrderControllerApi extends AbstractController {
      */
     @WebsiteAuth
     @PostMapping(value = "downList")
-    public R<Page<WebsiteUserOrderDown>> downList(WebsiteUserOrderDown websiteUserOrderDown) throws Exception {
+    public R<Page<WebsiteUserOrderDown>> downList(WebsiteUserOrderDown websiteUserOrderDown) {
         String userId = getUserIdByRequest();
         websiteUserOrderDown.setWebsiteUserId(userId);
         return R.ok(websiteUserOrderDownService.findPage(websiteUserOrderDown));
@@ -136,7 +136,7 @@ public class WebsiteUserOrderControllerApi extends AbstractController {
 
     @WebsiteAuth
     @PostMapping(value = "payOrder")
-    public R<WebsiteUserOrder> payOrder(HttpServletRequest request, WebsiteUserOrder websiteUserOrder) throws Exception {
+    public R<WebsiteUserOrder> payOrder(HttpServletRequest request, WebsiteUserOrder websiteUserOrder) {
         String userId = getUserIdByRequest();
         String reportId = websiteUserOrder.getResearchBriefReportId();
         if (StringUtils.isEmpty(reportId)) {

+ 131 - 0
modules/bjflapi/src/main/java/com/jeesite/modules/bjflapi/report/WebsiteUserResearchOrderControllerApi.java

@@ -0,0 +1,131 @@
+package com.jeesite.modules.bjflapi.report;
+
+import com.jeesite.common.constant.Constants;
+import com.jeesite.common.entity.Page;
+import com.jeesite.common.lang.StringUtils;
+import com.jeesite.modules.bjflapi.AbstractController;
+import com.jeesite.modules.report.entity.ResearchReport;
+import com.jeesite.modules.report.entity.WebsiteUserResearchOrder;
+import com.jeesite.modules.report.exception.CommonExceptionEnum;
+import com.jeesite.modules.report.service.ResearchReportService;
+import com.jeesite.modules.report.service.WebsiteUserResearchOrderService;
+import com.jeesite.modules.sys.annotation.WebsiteAuth;
+import com.jeesite.modules.sys.utils.R;
+import com.jeesite.modules.sys.utils.RedisLockUtil;
+import io.swagger.annotations.Api;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.collections4.CollectionUtils;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.annotation.Resource;
+import java.util.List;
+
+@Slf4j
+@RestController
+@RequestMapping(value = "${adminPath}/api/report/websiteUserResearchOrder")
+@Api(value = "WebsiteUserResearchOrderControllerApi", tags = "网站用户订单接口")
+public class WebsiteUserResearchOrderControllerApi extends AbstractController {
+    @Resource
+    private WebsiteUserResearchOrderService websiteUserResearchOrderService;
+    @Resource
+    private ResearchReportService researchReportService;
+    @Resource
+    private RedisLockUtil lockUtil;
+    /**
+     * 我的订单
+     */
+    @WebsiteAuth
+    @PostMapping(value = "orderList")
+    public R<Page<WebsiteUserResearchOrder>> orderList(WebsiteUserResearchOrder websiteUserResearchOrder) {
+        String userId = getUserIdByRequest();
+        websiteUserResearchOrder.setWebsiteUserId(userId);
+        return R.ok(websiteUserResearchOrderService.findPage(websiteUserResearchOrder));
+    }
+
+    /**
+     * 我的订单详情
+     */
+    @WebsiteAuth
+    @PostMapping(value = "orderDetail")
+    public R<WebsiteUserResearchOrder> orderDetail(WebsiteUserResearchOrder websiteUserResearchOrder) {
+        String userId = getUserIdByRequest();
+        websiteUserResearchOrder.setWebsiteUserId(userId);
+        return R.ok(websiteUserResearchOrderService.getOne(websiteUserResearchOrder));
+    }
+
+    @WebsiteAuth
+    @PostMapping(value = "payOrder")
+    public R<WebsiteUserResearchOrder> payOrder(WebsiteUserResearchOrder websiteUserResearchOrder) {
+        String userId = getUserIdByRequest();
+        String reportId = websiteUserResearchOrder.getResearchReportId();
+        if (StringUtils.isEmpty(reportId)) {
+            return R.fail(CommonExceptionEnum.REPORT_NULL.getCode(), CommonExceptionEnum.REPORT_NULL.getMsg());
+        }
+        R<WebsiteUserResearchOrder> websiteUserResearchOrderR = null;
+        String lockKey = Constants.redisLockKey.LOCK_ORDER_PAY_PREFIX + userId + reportId;
+        try {
+            if (lockUtil.tryLock(lockKey, reportId, RedisLockUtil.DEFAULT_EXPIRE_TIME, 5000)) {
+                String payMethod = websiteUserResearchOrder.getPayMethod();
+                if (StringUtils.isEmpty(payMethod)) {
+                    return R.fail(CommonExceptionEnum.PAY_TYPE_NULL.getCode(), CommonExceptionEnum.PAY_TYPE_NULL.getMsg());
+                }
+                if (!Constants.researchOrderPayMethod.PAYPAL.equals(payMethod)) {
+                    return R.fail(CommonExceptionEnum.PAY_TYPE_ERROR.getCode(), CommonExceptionEnum.PAY_TYPE_ERROR.getMsg());
+                }
+                ResearchReport reportWhere = new ResearchReport();
+                reportWhere.setId(reportId);
+                ResearchReport researchReport = researchReportService.get(reportWhere);
+                if (researchReport == null) {
+                    return R.fail(CommonExceptionEnum.REPORT_NULL.getCode(), CommonExceptionEnum.REPORT_NULL.getMsg());
+                }
+                if (websiteUserResearchOrder.getPayPrice().compareTo(researchReportService.getPrice(websiteUserResearchOrder.getPayResearchKey())) != 0) {
+                    return R.fail(CommonExceptionEnum.PAY_PRICE_ERROR.getCode(), CommonExceptionEnum.PAY_PRICE_ERROR.getMsg());
+                }
+                WebsiteUserResearchOrder orderWhere = new WebsiteUserResearchOrder();
+                orderWhere.setWebsiteUserId(userId);
+                orderWhere.setResearchReportId(reportId);
+                orderWhere.setPayStatus(Constants.orderPayStatus.WAIT);
+                List<WebsiteUserResearchOrder> orders = websiteUserResearchOrderService.findList(orderWhere);
+                if (CollectionUtils.isNotEmpty(orders)) {
+                    return R.fail(CommonExceptionEnum.ORDER_EXIST_WAIT_PAY.getCode(), CommonExceptionEnum.ORDER_EXIST_WAIT_PAY.getMsg());
+                }
+                websiteUserResearchOrder.setWebsiteUserId(userId);
+                websiteUserResearchOrderR = websiteUserResearchOrderService.saveAndPay(websiteUserResearchOrder);
+            }
+        } catch (Exception e) {
+            log.error("payOrder lock", e);
+        } finally {
+            lockUtil.unlock(lockKey, reportId);
+        }
+        return websiteUserResearchOrderR;
+    }
+
+    /**
+     * paypal授权后通知
+     */
+    @PostMapping(value = "/pay/asyncNotify")
+    public String payAsyncNotify(WebsiteUserResearchOrder websiteUserResearchOrder) {
+        return websiteUserResearchOrderService.payAsyncNotify(websiteUserResearchOrder.getThirdTraceId());
+    }
+
+    @PostMapping(value = "refundOrder")
+    public void refundOrder(WebsiteUserResearchOrder websiteUserResearchOrder) {
+        websiteUserResearchOrderService.refundOrder(websiteUserResearchOrder);
+    }
+
+    @PostMapping(value = "cancelOrder")
+    public R<String> cancelOrder(WebsiteUserResearchOrder websiteUserResearchOrder) {
+        return websiteUserResearchOrderService.cancelOrder(websiteUserResearchOrder);
+    }
+
+    /**
+     * 修改邮箱
+     */
+    @WebsiteAuth
+    @PostMapping(value = "updateEmail")
+    public R<String> updateEmail(WebsiteUserResearchOrder websiteUserResearchOrder) {
+        return websiteUserResearchOrderService.updateEmail(websiteUserResearchOrder);
+    }
+}

+ 78 - 0
modules/core/src/main/java/com/jeesite/modules/sys/utils/RedisLockUtil.java

@@ -0,0 +1,78 @@
+package com.jeesite.modules.sys.utils;
+
+import org.springframework.data.redis.connection.ReturnType;
+import org.springframework.data.redis.core.RedisCallback;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.Resource;
+import java.util.concurrent.TimeUnit;
+
+@Component
+public class RedisLockUtil {
+    @Resource
+    private RedisTemplate redisTemplate;
+    private static final String LOCK_PREFIX = "lock:";
+    public static final long DEFAULT_EXPIRE_TIME = 30; // 默认30秒
+
+    /**
+     * 尝试获取锁
+     * @param lockKey 锁键
+     * @param requestId 请求标识(用于安全释放锁)
+     * @param expireTime 锁过期时间(秒)
+     * @param timeout 尝试获取锁的超时时间(毫秒)
+     * @return 是否获取成功
+     */
+    public boolean tryLock(String lockKey, String requestId, long expireTime, long timeout) {
+        long startTime = System.currentTimeMillis();
+        String key = LOCK_PREFIX + lockKey;
+
+        while (System.currentTimeMillis() - startTime < timeout) {
+            // 使用SETNX + EXPIRE原子操作
+            Boolean success = redisTemplate.opsForValue()
+                    .setIfAbsent(key, requestId, expireTime, TimeUnit.SECONDS);
+
+            if (Boolean.TRUE.equals(success)) {
+                return true;
+            }
+
+            try {
+                Thread.sleep(50); // 避免忙等待
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+                return false;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * 释放锁
+     * @param lockKey 锁键
+     * @param requestId 请求标识
+     * @return 是否释放成功
+     */
+    public boolean unlock(String lockKey, String requestId) {
+        String key = LOCK_PREFIX + lockKey;
+        String script =
+                "if redis.call('get', KEYS[1]) == ARGV[1] then " +
+                        "    return redis.call('del', KEYS[1]) " +
+                        "else " +
+                        "    return 0 " +
+                        "end";
+
+        Object result = redisTemplate.execute(
+                (RedisCallback<Long>) connection ->
+                        connection.eval(
+                                script.getBytes(),
+                                ReturnType.INTEGER,
+                                1,
+                                key.getBytes(),
+                                requestId.getBytes()
+                        ),
+                true
+        );
+
+        return result != null && (Long) result > 0;
+    }
+}

+ 6 - 0
modules/report/pom.xml

@@ -56,6 +56,12 @@
 			<artifactId>wechatpay-java</artifactId>
 			<version>0.2.15</version>
 		</dependency>
+		<!--paypal-->
+		<dependency>
+			<groupId>com.paypal.sdk</groupId>
+			<artifactId>checkout-sdk</artifactId>
+			<version>2.0.0</version>
+		</dependency>
 	</dependencies>
 
 	<developers>

+ 4 - 0
modules/report/src/main/java/com/jeesite/modules/report/dao/ResearchReportDao.java

@@ -114,4 +114,8 @@ public interface ResearchReportDao extends CrudDao<ResearchReport> {
     Integer updateMarketScale(@Param("researchReport") ResearchReport researchReport);
 
     Integer updateMarketDriven(@Param("researchReport") ResearchReport researchReport);
+
+    String getPrice(@Param("configKey") String configKey);
+
+    ResearchReport findTitleById(@Param("researchReportId") String researchReportId);
 }

+ 21 - 0
modules/report/src/main/java/com/jeesite/modules/report/dao/WebsiteUserResearchOrderDao.java

@@ -0,0 +1,21 @@
+package com.jeesite.modules.report.dao;
+
+import com.jeesite.common.dao.CrudDao;
+import com.jeesite.common.mybatis.annotation.MyBatisDao;
+import com.jeesite.modules.report.entity.WebsiteUserResearchOrder;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * 研究报告订单DAO接口
+ * @author gmj
+ * @version 2025-05-26
+ */
+@MyBatisDao
+public interface WebsiteUserResearchOrderDao extends CrudDao<WebsiteUserResearchOrder> {
+
+    WebsiteUserResearchOrder findByOrderNumber(@Param("orderNumber") String orderNumber);
+
+    List<WebsiteUserResearchOrder> findOrderList(@Param("websiteUserResearchOrder") WebsiteUserResearchOrder websiteUserResearchOrder);
+}

+ 0 - 19
modules/report/src/main/java/com/jeesite/modules/report/dto/ImportMarketScaleMessage.java

@@ -1,19 +0,0 @@
-package com.jeesite.modules.report.dto;
-
-
-import lombok.Data;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-
-@RequiredArgsConstructor
-@Slf4j
-@Data
-public class ImportMarketScaleMessage {
-
-   private Integer successNum;
-
-   private Integer failureNum;
-
-   private StringBuilder failureMsg = new StringBuilder();
-
-}

+ 19 - 0
modules/report/src/main/java/com/jeesite/modules/report/entity/ResearchReport.java

@@ -141,6 +141,9 @@ public class ResearchReport extends DataEntity<ResearchReport> {
 	private String marketScale; //市场规模
 	private String marketDriven; //市场驱动
 
+	private String priceKey;
+	private String priceName;
+
 	public ResearchReport() {
 		this(null);
 	}
@@ -536,6 +539,22 @@ public class ResearchReport extends DataEntity<ResearchReport> {
 		this.marketDriven = marketDriven;
 	}
 
+	public String getPriceKey() {
+		return priceKey;
+	}
+
+	public void setPriceKey(String priceKey) {
+		this.priceKey = priceKey;
+	}
+
+	public String getPriceName() {
+		return priceName;
+	}
+
+	public void setPriceName(String priceName) {
+		this.priceName = priceName;
+	}
+
 	public static boolean areAllFieldsEmptyExceptOne(Object obj, String... excludeFields) {
 		Class<?> clazz = obj.getClass();
 		Field[] fields = clazz.getDeclaredFields();

+ 23 - 10
modules/report/src/main/java/com/jeesite/modules/report/entity/WebsiteUser.java

@@ -3,11 +3,14 @@ package com.jeesite.modules.report.entity;
 import javax.validation.constraints.NotBlank;
 import javax.validation.constraints.Size;
 
+import com.fasterxml.jackson.annotation.JsonFormat;
 import com.jeesite.common.entity.DataEntity;
 import com.jeesite.common.mybatis.annotation.Column;
 import com.jeesite.common.mybatis.annotation.Table;
 import com.jeesite.common.mybatis.mapper.query.QueryType;
 
+import java.util.Date;
+
 /**
  * 网站用户Entity
  * @author gg
@@ -28,7 +31,7 @@ import com.jeesite.common.mybatis.mapper.query.QueryType;
 	}, orderBy="a.update_date DESC"
 )
 public class WebsiteUser extends DataEntity<WebsiteUser> {
-	
+
 	private static final long serialVersionUID = 1L;
 	private String loginCode;		// 登录账号
 	private String password;		// 登录密码
@@ -42,15 +45,16 @@ public class WebsiteUser extends DataEntity<WebsiteUser> {
 	private String confirmNewPassword;
 	private String validCode;
 	private String unTime;
+	private Date latestPayment;
 
 	public WebsiteUser() {
 		this(null);
 	}
-	
+
 	public WebsiteUser(String id){
 		super(id);
 	}
-	
+
 	@NotBlank(message="登录账号不能为空")
 	@Size(min=0, max=100, message="登录账号长度不能超过 100 个字符")
 	public String getLoginCode() {
@@ -60,7 +64,7 @@ public class WebsiteUser extends DataEntity<WebsiteUser> {
 	public void setLoginCode(String loginCode) {
 		this.loginCode = loginCode;
 	}
-	
+
 	@NotBlank(message="登录密码不能为空")
 	@Size(min=0, max=200, message="登录密码长度不能超过 200 个字符")
 	public String getPassword() {
@@ -88,7 +92,7 @@ public class WebsiteUser extends DataEntity<WebsiteUser> {
 	public void setName(String name) {
 		this.name = name;
 	}
-	
+
 	@Size(min=0, max=32, message="联系方式长度不能超过 32 个字符")
 	public String getContact() {
 		return contact;
@@ -97,7 +101,7 @@ public class WebsiteUser extends DataEntity<WebsiteUser> {
 	public void setContact(String contact) {
 		this.contact = contact;
 	}
-	
+
 	@Size(min=0, max=64, message="邮箱长度不能超过 64 个字符")
 	public String getEmail() {
 		return email;
@@ -106,7 +110,7 @@ public class WebsiteUser extends DataEntity<WebsiteUser> {
 	public void setEmail(String email) {
 		this.email = email;
 	}
-	
+
 	@Size(min=0, max=256, message="公司名称长度不能超过 256 个字符")
 	public String getCompanyName() {
 		return companyName;
@@ -115,7 +119,7 @@ public class WebsiteUser extends DataEntity<WebsiteUser> {
 	public void setCompanyName(String companyName) {
 		this.companyName = companyName;
 	}
-	
+
 	@Size(min=0, max=512, message="需求长度不能超过 512 个字符")
 	public String getDemand() {
 		return demand;
@@ -124,7 +128,7 @@ public class WebsiteUser extends DataEntity<WebsiteUser> {
 	public void setDemand(String demand) {
 		this.demand = demand;
 	}
-	
+
 	@Size(min=0, max=128, message="公司职务长度不能超过 128 个字符")
 	public String getAddr() {
 		return addr;
@@ -157,4 +161,13 @@ public class WebsiteUser extends DataEntity<WebsiteUser> {
 	public void setUnTime(String unTime) {
 		this.unTime = unTime;
 	}
-}
+
+	@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+	public Date getLatestPayment() {
+		return latestPayment;
+	}
+
+	public void setLatestPayment(Date latestPayment) {
+		this.latestPayment = latestPayment;
+	}
+}

+ 203 - 0
modules/report/src/main/java/com/jeesite/modules/report/entity/WebsiteUserResearchOrder.java

@@ -0,0 +1,203 @@
+package com.jeesite.modules.report.entity;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.jeesite.common.entity.DataEntity;
+import com.jeesite.common.mybatis.annotation.Column;
+import com.jeesite.common.mybatis.annotation.Table;
+
+import javax.validation.constraints.Size;
+import java.math.BigDecimal;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 研究报告订单Entity
+ * @author gmj
+ * @version 2025-05-26
+ */
+@Table(name="website_user_research_order", alias="a", label="研究报告订单信息", columns={
+		@Column(name="id", attrName="id", label="id", isPK=true),
+		@Column(includeEntity=DataEntity.class),
+		@Column(name="order_number", attrName="orderNumber", label="订单号"),
+		@Column(name="website_user_id", attrName="websiteUserId", label="网站用户id"),
+		@Column(name="pay_date", attrName="payDate", label="支付日期", isUpdateForce=true),
+		@Column(name="pay_status", attrName="payStatus", label="状态 0-待支付 1-支付成功 2-支付失败"),
+		@Column(name="pay_method", attrName="payMethod", label="支付方式 paypal"),
+		@Column(name="pay_price", attrName="payPrice", label="支付单价 美元", isUpdateForce=true),
+		@Column(name="research_report_id", attrName="researchReportId", label="研究报告id"),
+		@Column(name="third_trace_id", attrName="thirdTraceId", label="平台的追踪号"),
+		@Column(name="pay_research_key", attrName="payResearchKey", label="支付研究报告类型"),
+		@Column(name="pay_research_type", attrName="payResearchType", label="支付研究报告类型"),
+		@Column(name="pay_research_version", attrName="payResearchVersion", label="支付研究报告版本"),
+		@Column(name="email", attrName="email", label="邮箱"),
+	}, orderBy="a.update_date DESC"
+)
+public class WebsiteUserResearchOrder extends DataEntity<WebsiteUserResearchOrder> {
+
+	private static final long serialVersionUID = 1L;
+	private String orderNumber;		// 订单号
+	private String websiteUserId;		// 网站用户id
+	private Date payDate;		// 支付日期
+	private String payStatus;		// 状态 0-待支付 1-支付成功 2-支付失败
+	private String payMethod;		// 支付方式 paypal
+	private BigDecimal payPrice;		// 支付单价 美元
+	private String researchReportId;		// 研究报告id
+	private String thirdTraceId;		// 平台的追踪号
+	private String payResearchKey;      // 支付研究报告类型key
+	private String payResearchType;		// 支付研究报告类型
+	private String payResearchVersion;		// 支付研究报告版本
+	private String email;				// 邮箱
+
+	private String researchReportTitle; // 研究报告标题
+	private String orderPayUrl; 	// 订单支付链接
+	private String researchReportFileName; // 研究报告文件名
+
+	private List<String> payMethodList;
+
+	public WebsiteUserResearchOrder() {
+		this(null);
+	}
+
+	public WebsiteUserResearchOrder(String id){
+		super(id);
+	}
+
+	@Size(min=0, max=100, message="订单号长度不能超过 100 个字符")
+	public String getOrderNumber() {
+		return orderNumber;
+	}
+
+	public void setOrderNumber(String orderNumber) {
+		this.orderNumber = orderNumber;
+	}
+
+	@Size(min=0, max=64, message="网站用户id长度不能超过 64 个字符")
+	public String getWebsiteUserId() {
+		return websiteUserId;
+	}
+
+	public void setWebsiteUserId(String websiteUserId) {
+		this.websiteUserId = websiteUserId;
+	}
+
+	@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+	public Date getPayDate() {
+		return payDate;
+	}
+
+	public void setPayDate(Date payDate) {
+		this.payDate = payDate;
+	}
+
+	@Size(min=0, max=2, message="状态 0-待支付 1-支付成功 2-支付失败长度不能超过 2 个字符")
+	public String getPayStatus() {
+		return payStatus;
+	}
+
+	public void setPayStatus(String payStatus) {
+		this.payStatus = payStatus;
+	}
+
+	@Size(min=0, max=10, message="支付方式 paypal长度不能超过 10 个字符")
+	public String getPayMethod() {
+		return payMethod;
+	}
+
+	public void setPayMethod(String payMethod) {
+		this.payMethod = payMethod;
+	}
+
+	public BigDecimal getPayPrice() {
+		return payPrice;
+	}
+
+	public void setPayPrice(BigDecimal payPrice) {
+		this.payPrice = payPrice;
+	}
+
+	@Size(min=0, max=64, message="研究报告id长度不能超过 64 个字符")
+	public String getResearchReportId() {
+		return researchReportId;
+	}
+
+	public void setResearchReportId(String researchReportId) {
+		this.researchReportId = researchReportId;
+	}
+
+	@Size(min=0, max=100, message="平台的追踪号长度不能超过 100 个字符")
+	public String getThirdTraceId() {
+		return thirdTraceId;
+	}
+
+	public void setThirdTraceId(String thirdTraceId) {
+		this.thirdTraceId = thirdTraceId;
+	}
+
+	@Size(min=0, max=100, message="支付研究报告类型key长度不能超过 100 个字符")
+	public String getPayResearchKey() {
+		return payResearchKey;
+	}
+
+	public void setPayResearchKey(String payResearchKey) {
+		this.payResearchKey = payResearchKey;
+	}
+
+	@Size(min=0, max=100, message="支付研究报告类型长度不能超过 100 个字符")
+	public String getPayResearchType() {
+		return payResearchType;
+	}
+
+	public void setPayResearchType(String payResearchType) {
+		this.payResearchType = payResearchType;
+	}
+
+	@Size(min=0, max=100, message="支付研究报告版本长度不能超过 100 个字符")
+	public String getPayResearchVersion() {
+		return payResearchVersion;
+	}
+
+	public void setPayResearchVersion(String payResearchVersion) {
+		this.payResearchVersion = payResearchVersion;
+	}
+
+	@Size(min=0, max=64, message="邮箱长度不能超过 64 个字符")
+	public String getEmail() {
+		return email;
+	}
+
+	public void setEmail(String email) {
+		this.email = email;
+	}
+
+	public String getOrderPayUrl() {
+		return orderPayUrl;
+	}
+
+	public void setOrderPayUrl(String orderPayUrl) {
+		this.orderPayUrl = orderPayUrl;
+	}
+
+	public String getResearchReportTitle() {
+		return researchReportTitle;
+	}
+
+	public void setResearchReportTitle(String researchReportTitle) {
+		this.researchReportTitle = researchReportTitle;
+	}
+
+	public List<String> getPayMethodList() {
+		return payMethodList;
+	}
+
+	public void setPayMethodList(List<String> payMethodList) {
+		this.payMethodList = payMethodList;
+	}
+
+	public String getResearchReportFileName() {
+		return researchReportFileName;
+	}
+
+	public void setResearchReportFileName(String researchReportFileName) {
+		this.researchReportFileName = researchReportFileName;
+	}
+}

+ 49 - 0
modules/report/src/main/java/com/jeesite/modules/report/exception/CommonExceptionEnum.java

@@ -0,0 +1,49 @@
+package com.jeesite.modules.report.exception;
+
+/**
+ * 异常枚举
+ */
+public enum CommonExceptionEnum {
+
+    // TODO 注意检查错误码,保证系统内唯一
+    SYSTEM_ERROR(900001, "系统异常,请稍后重试"),
+
+    PAY_ERROR(100002, "支付调用第三方失败"),
+    ORDER_NOT_UNPAID(100003, "订单不是未支付状态"),
+    ORDER_NULL(100004, "订单为空"),
+    REPORT_NULL(100005, "研究报告为空"),
+    PAY_TYPE_NULL(100006, "支付方式为空"),
+    PAY_TYPE_ERROR(100007, "支付方式值无效"),
+    PAY_PRICE_ERROR(100008, "支付金额与收费金额不一致"),
+    ORDER_EXIST_WAIT_PAY(100009, "研究报告存在未支付订单,请前往个人中心-我的订单进行支付"),
+    ORDER_NOT_PAID(100010, "订单不是已支付状态"),
+
+    USER_NOT_LOGIN(200001, "用户未登录"),
+    ;
+
+    private Integer code;
+
+    private String msg;
+
+    CommonExceptionEnum(Integer code, String msg) {
+        this.code = code;
+        this.msg = msg;
+    }
+
+    public Integer getCode() {
+        return code;
+    }
+
+    public void setCode(Integer code) {
+        this.code = code;
+    }
+
+    public String getMsg() {
+        return msg;
+    }
+
+    public void setMsg(String msg) {
+        this.msg = msg;
+    }
+
+}

+ 64 - 0
modules/report/src/main/java/com/jeesite/modules/report/exception/ServiceException.java

@@ -0,0 +1,64 @@
+package com.jeesite.modules.report.exception;
+
+/**
+ * 业务异常
+ *
+ * @author ruoyi
+ */
+public final class ServiceException extends RuntimeException {
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 错误码
+     */
+    private Integer code;
+
+    /**
+     * 错误提示
+     */
+    private String message;
+
+    /**
+     * 错误明细,内部调试错误
+     * <p>
+     */
+    private String detailMessage;
+
+    /**
+     * 空构造方法,避免反序列化问题
+     */
+    public ServiceException() {
+    }
+
+    public ServiceException(String message) {
+        this.message = message;
+    }
+
+    public ServiceException(String message, Integer code) {
+        this.message = message;
+        this.code = code;
+    }
+
+    public String getDetailMessage() {
+        return detailMessage;
+    }
+
+    @Override
+    public String getMessage() {
+        return message;
+    }
+
+    public Integer getCode() {
+        return code;
+    }
+
+    public ServiceException setMessage(String message) {
+        this.message = message;
+        return this;
+    }
+
+    public ServiceException setDetailMessage(String detailMessage) {
+        this.detailMessage = detailMessage;
+        return this;
+    }
+}

+ 101 - 0
modules/report/src/main/java/com/jeesite/modules/report/service/PaypalPayService.java

@@ -0,0 +1,101 @@
+package com.jeesite.modules.report.service;
+
+import com.jeesite.modules.report.entity.WebsiteUserResearchOrder;
+import com.jeesite.modules.report.util.PaypalPayUtil;
+import com.paypal.core.PayPalHttpClient;
+import com.paypal.http.HttpRequest;
+import com.paypal.http.HttpResponse;
+import com.paypal.orders.*;
+import com.paypal.payments.CapturesRefundRequest;
+import com.paypal.payments.RefundRequest;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.math.BigDecimal;
+import java.util.Arrays;
+import java.util.UUID;
+
+@Service
+public class PaypalPayService {
+    @Resource
+    private PaypalPayUtil paypalPayUtil;
+    @Resource
+    private PayPalHttpClient client;
+    private static final String CURRENCY = "USD";
+
+
+    /**
+     * 发起支付请求
+     *
+     * @param userOrder 支付信息
+     * @return
+     */
+    public String pay(WebsiteUserResearchOrder userOrder) throws Exception {
+        OrderRequest orderRequest = new OrderRequest()
+                .checkoutPaymentIntent("CAPTURE")
+                .applicationContext(new ApplicationContext()
+                        .userAction("CONTINUE")
+                        .returnUrl(paypalPayUtil.getReturnUrl())
+                        .cancelUrl(paypalPayUtil.getCancelUrl()))
+                .purchaseUnits(Arrays.asList(new PurchaseUnitRequest()
+                        .description("Research report purchase")
+                        .amountWithBreakdown(new AmountWithBreakdown()
+                                .currencyCode(CURRENCY)
+                                .value(userOrder.getPayPrice().toString()))));
+        OrdersCreateRequest request = new OrdersCreateRequest()
+                .requestBody(orderRequest);
+        HttpResponse<Order> response = executeRequest(request);
+        userOrder.setThirdTraceId(response.result().id());
+        return response.result().links()
+                .stream()
+                .filter(link -> "approve".equals(link.rel()))
+                .findFirst()
+                .map(LinkDescription::href)
+                .orElseThrow(() -> new IllegalStateException("未找到支付链接"));
+    }
+
+    public BigDecimal capturePayment(String orderId) throws Exception {
+        OrdersCaptureRequest request = new OrdersCaptureRequest(orderId);
+        HttpResponse<Order> response = executeRequest(request);
+        // 2. 业务层验证
+        Order capturedOrder = response.result();
+        if (!"COMPLETED".equals(capturedOrder.status())) {
+            throw new Exception("扣款未完成,当前状态: " + capturedOrder.status());
+        }
+        return new BigDecimal(capturedOrder.purchaseUnits().get(0).payments().captures().get(0).amount().value());
+    }
+
+    public Order payQuery(String orderId) throws Exception {
+        OrdersGetRequest request = new OrdersGetRequest(orderId);
+        HttpResponse<Order> response = executeRequest(request);
+        return response.result();
+    }
+
+    public void refund(String orderId, BigDecimal amount) throws Exception {
+        Order order = payQuery(orderId);
+        String captureId = order.purchaseUnits().get(0).payments().captures().get(0).id();
+        RefundRequest refundRequest = new RefundRequest()
+                .amount(new com.paypal.payments.Money()
+                        .currencyCode(CURRENCY)
+                        .value(amount.toString()))
+                .invoiceId("REFUND-" + UUID.randomUUID().toString().substring(0, 8))
+                .noteToPayer("User requests refund");
+        CapturesRefundRequest request = new CapturesRefundRequest(captureId)
+                .requestBody(refundRequest);
+        executeRequest(request);
+    }
+
+    // 统一请求执行
+    private <T> HttpResponse<T> executeRequest(HttpRequest<T> request) throws Exception {
+        HttpResponse<T> response = client.execute(request);
+        validateResponse(response);
+        return response;
+    }
+
+    // 响应验证
+    private void validateResponse(HttpResponse<?> response) throws Exception {
+        if (response.statusCode() >= 400) {
+            throw new Exception("API请求失败,状态码: " + response.statusCode());
+        }
+    }
+}

+ 9 - 0
modules/report/src/main/java/com/jeesite/modules/report/service/ResearchReportService.java

@@ -33,6 +33,7 @@ import org.springframework.transaction.annotation.Transactional;
 import org.springframework.web.multipart.MultipartFile;
 
 import javax.annotation.Resource;
+import java.math.BigDecimal;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.regex.Matcher;
@@ -1072,4 +1073,12 @@ public class ResearchReportService extends CrudService<ResearchReportDao, Resear
 	public DictData getDictDataByType(String dictType,String dictValue){
 		return dictCusDataDao.getDictDataByType(dictType,dictValue);
 	}
+
+	public BigDecimal getPrice(String configKey) {
+		String price = dao.getPrice(configKey);
+		if (StringUtils.isEmpty(price)) {
+			return BigDecimal.ZERO;
+		}
+		return new BigDecimal(price);
+	}
 }

+ 10 - 4
modules/report/src/main/java/com/jeesite/modules/report/service/WebsiteUserOrderService.java

@@ -80,6 +80,12 @@ public class WebsiteUserOrderService extends CrudService<WebsiteUserOrderDao, We
 		return page;
 	}
 
+	public Page<WebsiteUserOrder> findOrderPage(WebsiteUserOrder websiteUserOrder) {
+		Page<WebsiteUserOrder> page = super.findPage(websiteUserOrder);
+		page.setList(page.getList().stream().map(w -> convert(w)).collect(Collectors.toList()));
+		return page;
+	}
+
 	private WebsiteUserOrder convert(WebsiteUserOrder r) {
 		if (StringUtils.isEmpty(r.getResearchBriefReportId())) {
 			return r;
@@ -114,7 +120,7 @@ public class WebsiteUserOrderService extends CrudService<WebsiteUserOrderDao, We
 	public List<WebsiteUserOrder> findList(WebsiteUserOrder websiteUserOrder) {
 		return super.findList(websiteUserOrder);
 	}
-	
+
 	/**
 	 * 保存数据(插入或更新)
 	 * @param websiteUserOrder
@@ -124,7 +130,7 @@ public class WebsiteUserOrderService extends CrudService<WebsiteUserOrderDao, We
 	public void save(WebsiteUserOrder websiteUserOrder) {
 		super.save(websiteUserOrder);
 	}
-	
+
 	/**
 	 * 更新状态
 	 * @param websiteUserOrder
@@ -134,7 +140,7 @@ public class WebsiteUserOrderService extends CrudService<WebsiteUserOrderDao, We
 	public void updateStatus(WebsiteUserOrder websiteUserOrder) {
 		super.updateStatus(websiteUserOrder);
 	}
-	
+
 	/**
 	 * 删除数据
 	 * @param websiteUserOrder
@@ -406,4 +412,4 @@ public class WebsiteUserOrderService extends CrudService<WebsiteUserOrderDao, We
 		super.update(order);
 		return R.ok();
 	}
-}
+}

+ 273 - 0
modules/report/src/main/java/com/jeesite/modules/report/service/WebsiteUserResearchOrderService.java

@@ -0,0 +1,273 @@
+package com.jeesite.modules.report.service;
+
+import com.jeesite.common.constant.Constants;
+import com.jeesite.common.entity.Page;
+import com.jeesite.common.lang.DateUtils;
+import com.jeesite.common.lang.StringUtils;
+import com.jeesite.common.service.CrudService;
+import com.jeesite.modules.report.dao.ResearchReportDao;
+import com.jeesite.modules.report.dao.WebsiteUserResearchOrderDao;
+import com.jeesite.modules.report.entity.ResearchReport;
+import com.jeesite.modules.report.entity.WebsiteUserResearchOrder;
+import com.jeesite.modules.report.exception.CommonExceptionEnum;
+import com.jeesite.modules.sys.utils.R;
+import com.jeesite.modules.sys.utils.RedisUtil;
+import com.paypal.orders.Order;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.annotation.Resource;
+import java.math.BigDecimal;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.List;
+import java.util.Random;
+import java.util.stream.Collectors;
+
+/**
+ * 研究报告订单Service
+ * @author gmj
+ * @version 2025-05-26
+ */
+@Service
+public class WebsiteUserResearchOrderService extends CrudService<WebsiteUserResearchOrderDao, WebsiteUserResearchOrder> {
+
+	@Resource
+	private PaypalPayService paypalPayService;
+	@Resource
+	private RedisUtil redisUtil;
+	@Resource
+	private ResearchReportDao researchReportDao;
+
+	/**
+	 * 获取单条数据
+	 * @param websiteUserResearchOrder
+	 * @return
+	 */
+	@Override
+	public WebsiteUserResearchOrder get(WebsiteUserResearchOrder websiteUserResearchOrder) {
+		return super.get(websiteUserResearchOrder);
+	}
+
+	/**
+	 * 查询分页数据
+	 * @param websiteUserResearchOrder 查询条件
+	 * @param websiteUserResearchOrder.page 分页对象
+	 * @return
+	 */
+	@Override
+	public Page<WebsiteUserResearchOrder> findPage(WebsiteUserResearchOrder websiteUserResearchOrder) {
+		int pageSize = websiteUserResearchOrder.getPageSize();
+		Page<WebsiteUserResearchOrder> page = super.findPage(websiteUserResearchOrder);
+		page.setList(page.getList().stream().map(w -> convert(w)).collect(Collectors.toList()));
+		long total = super.findCount(websiteUserResearchOrder);
+		long count = total / pageSize;
+		if(total % pageSize > 0){
+			count = count +1;
+		}
+		page.setCount(count);
+		return page;
+	}
+
+	private WebsiteUserResearchOrder convert(WebsiteUserResearchOrder w) {
+		if (StringUtils.isEmpty(w.getResearchReportId())) {
+			return w;
+		}
+		ResearchReport researchReport = researchReportDao.findTitleById(w.getResearchReportId());
+		if (researchReport != null) {
+			w.setResearchReportTitle(researchReport.getTitle());
+			w.setResearchReportFileName(researchReport.getFileName());
+		}
+		if (w.getPayStatus().equals(Constants.orderPayStatus.WAIT)) {
+			w.setOrderPayUrl((String)redisUtil.get(Constants.WEBSITE_RESEARCH_ORDER_URL + w.getId()));
+		}
+		return w;
+	}
+
+	/**
+	 * 查询列表数据
+	 * @param websiteUserResearchOrder
+	 * @return
+	 */
+	@Override
+	public List<WebsiteUserResearchOrder> findList(WebsiteUserResearchOrder websiteUserResearchOrder) {
+		return super.findList(websiteUserResearchOrder);
+	}
+
+	/**
+	 * 保存数据(插入或更新)
+	 * @param websiteUserResearchOrder
+	 */
+	@Override
+	@Transactional
+	public void save(WebsiteUserResearchOrder websiteUserResearchOrder) {
+		super.save(websiteUserResearchOrder);
+	}
+
+	/**
+	 * 更新状态
+	 * @param websiteUserResearchOrder
+	 */
+	@Override
+	@Transactional
+	public void updateStatus(WebsiteUserResearchOrder websiteUserResearchOrder) {
+		super.updateStatus(websiteUserResearchOrder);
+	}
+
+	/**
+	 * 删除数据
+	 * @param websiteUserResearchOrder
+	 */
+	@Override
+	@Transactional
+	public void delete(WebsiteUserResearchOrder websiteUserResearchOrder) {
+		super.delete(websiteUserResearchOrder);
+	}
+
+	public WebsiteUserResearchOrder getOne(WebsiteUserResearchOrder websiteUserResearchOrder) {
+		return dao.getByEntity(websiteUserResearchOrder);
+	}
+
+	public R<WebsiteUserResearchOrder> saveAndPay(WebsiteUserResearchOrder websiteUserResearchOrder) {
+		websiteUserResearchOrder.setOrderNumber(generateOrderNumber());
+		websiteUserResearchOrder.setPayStatus(Constants.orderPayStatus.WAIT);
+		//查询订单号是否重复
+		WebsiteUserResearchOrder findOrder = dao.findByOrderNumber(websiteUserResearchOrder.getOrderNumber());
+		if (findOrder != null) {
+			return R.fail(CommonExceptionEnum.SYSTEM_ERROR.getCode(), CommonExceptionEnum.SYSTEM_ERROR.getMsg());
+		}
+        String payResult = null;
+        try {
+            payResult = paypalPayService.pay(websiteUserResearchOrder);
+        } catch (Exception e) {
+            logger.error("paypal saveAndPay error", e);
+        }
+        if (StringUtils.isEmpty(payResult)) {
+			return R.fail(CommonExceptionEnum.PAY_ERROR.getCode(), CommonExceptionEnum.PAY_ERROR.getMsg());
+		}
+		super.save(websiteUserResearchOrder);
+		redisUtil.set(Constants.WEBSITE_RESEARCH_ORDER_URL + websiteUserResearchOrder.getId(), payResult);
+		redisUtil.expire(Constants.WEBSITE_RESEARCH_ORDER_URL + websiteUserResearchOrder.getId(), 1800);
+		websiteUserResearchOrder.setOrderPayUrl(payResult);
+		return R.ok(websiteUserResearchOrder);
+	}
+
+	public String payAsyncNotify(String thirdTraceId) {
+        try {
+			WebsiteUserResearchOrder where = new WebsiteUserResearchOrder();
+			where.setThirdTraceId(thirdTraceId);
+			where.setPayStatus(Constants.orderPayStatus.WAIT);
+			WebsiteUserResearchOrder websiteUserResearchOrder = dao.getByEntity(where);
+			if (websiteUserResearchOrder == null) {
+				logger.error("payAsyncNotify error {}订单不存在未支付", thirdTraceId);
+			} else {
+				return capturePayment(websiteUserResearchOrder);
+			}
+		} catch (Exception e) {
+			logger.error("payAsyncNotify error {}", e);
+        }
+		return "failure";
+	}
+
+	private String capturePayment(WebsiteUserResearchOrder websiteUserResearchOrder) throws Exception {
+		//执行扣款
+		BigDecimal total_amount = paypalPayService.capturePayment(websiteUserResearchOrder.getThirdTraceId());
+		if (total_amount.compareTo(websiteUserResearchOrder.getPayPrice()) != 0) {
+			logger.error("payAsyncNotify amount不相等 {} {}", total_amount, websiteUserResearchOrder.getPayPrice().toString());
+			return "failure";
+		}
+		websiteUserResearchOrder.setPayStatus(Constants.orderPayStatus.PAY_SUCCESS);
+		websiteUserResearchOrder.setPayDate(new Date());
+		super.update(websiteUserResearchOrder);
+		return "success";
+	}
+
+	//定时任务会调用此方法
+	@Scheduled(cron ="0 0/5 * * * ?")
+	public void payPalPayQuery() {
+		WebsiteUserResearchOrder where = new WebsiteUserResearchOrder();
+		where.setPayMethodList(Arrays.asList(Constants.researchOrderPayMethod.PAYPAL));
+		where.setPayStatus(Constants.orderPayStatus.WAIT);
+		List<WebsiteUserResearchOrder> list = dao.findOrderList(where);
+		for (WebsiteUserResearchOrder order : list) {
+			//查订单状态
+			try {
+				Order payQuery = paypalPayService.payQuery(order.getThirdTraceId());
+				if (payQuery != null && "APPROVED".equals(payQuery.status())) {
+					//批准但是未执行扣款
+					capturePayment(order);
+					continue;
+				}
+				//查是否超时
+				if (DateUtils.getMinDistanceOfTwoDate(order.getCreateDate(), new Date()) > 30) {
+					order.setPayStatus(Constants.orderPayStatus.PAY_ERROR);
+					super.update(order);
+				}
+			} catch (Exception e) {
+				logger.error("payPalPayQuery payQuery error", e);
+			}
+		}
+	}
+
+	public void refundOrder(WebsiteUserResearchOrder websiteUserResearchOrder) {
+		WebsiteUserResearchOrder order = get(websiteUserResearchOrder);
+		logger.info("refundOrder ===>{}", websiteUserResearchOrder.getId());
+		if (!websiteUserResearchOrder.getOrderPayUrl().equals(Constants.WEBSITE_RESEARCH_ORDER_URL)) {
+			logger.error("refundOrder not ===>{}", websiteUserResearchOrder.getOrderPayUrl());
+			return;
+		}
+		if (!order.getPayStatus().equals(Constants.orderPayStatus.PAY_SUCCESS)) {
+			return;
+		}
+        try {
+            paypalPayService.refund(order.getThirdTraceId(), order.getPayPrice());
+        } catch (Exception e) {
+			logger.error("paypal refund error", e);
+        }
+    }
+
+	public String generateOrderNumber() {
+		String random = String.valueOf(new Random().nextInt(9000) + 1000);
+		return "WURO" + DateUtils.formatDate(new Date(), "yyyyMMddHHmmss") + random;
+	}
+
+	public R<String> cancelOrder(WebsiteUserResearchOrder websiteUserResearchOrder) {
+		if (websiteUserResearchOrder.getId() == null) {
+			return R.fail(CommonExceptionEnum.ORDER_NULL.getCode(), CommonExceptionEnum.ORDER_NULL.getMsg());
+		}
+		WebsiteUserResearchOrder order = get(websiteUserResearchOrder);
+		if (order == null) {
+			return R.fail(CommonExceptionEnum.ORDER_NULL.getCode(), CommonExceptionEnum.ORDER_NULL.getMsg());
+		}
+		if (!order.getPayStatus().equals(Constants.orderPayStatus.WAIT)) {
+			return R.fail(CommonExceptionEnum.ORDER_NOT_UNPAID.getCode(), CommonExceptionEnum.ORDER_NOT_UNPAID.getMsg());
+		}
+		//订单关闭成功
+		order.setPayStatus(Constants.orderPayStatus.PAY_ERROR);
+		super.update(order);
+		return R.ok();
+	}
+
+	public R<String> updateEmail(WebsiteUserResearchOrder websiteUserResearchOrder) {
+		if (websiteUserResearchOrder.getId() == null) {
+			return R.fail(CommonExceptionEnum.ORDER_NULL.getCode(), CommonExceptionEnum.ORDER_NULL.getMsg());
+		}
+		WebsiteUserResearchOrder order = get(websiteUserResearchOrder);
+		if (order == null) {
+			return R.fail(CommonExceptionEnum.ORDER_NULL.getCode(), CommonExceptionEnum.ORDER_NULL.getMsg());
+		}
+		if (!order.getPayStatus().equals(Constants.orderPayStatus.PAY_SUCCESS)) {
+			return R.fail(CommonExceptionEnum.ORDER_NOT_PAID.getCode(), CommonExceptionEnum.ORDER_NOT_PAID.getMsg());
+		}
+		order.setEmail(websiteUserResearchOrder.getEmail());
+		super.update(order);
+		return R.ok();
+	}
+
+	public Page<WebsiteUserResearchOrder> findOrderPage(WebsiteUserResearchOrder websiteUserResearchOrder) {
+		Page<WebsiteUserResearchOrder> page = super.findPage(websiteUserResearchOrder);
+		page.setList(page.getList().stream().map(w -> convert(w)).collect(Collectors.toList()));
+		return page;
+	}
+}

+ 34 - 0
modules/report/src/main/java/com/jeesite/modules/report/util/PaypalPayUtil.java

@@ -0,0 +1,34 @@
+package com.jeesite.modules.report.util;
+
+import com.paypal.core.PayPalEnvironment;
+import com.paypal.core.PayPalHttpClient;
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.stereotype.Component;
+
+@ConfigurationProperties(prefix = "paypal")
+@Component
+@Data
+public class PaypalPayUtil {
+    private String clientId;
+    private String clientSecret;
+    /**
+     * 沙箱 sandbox 生产环境 live
+     */
+    private String mode;
+    /**
+     * 支付成功跳转地址
+     */
+    private String returnUrl;
+    /**
+     * 取消支付跳转地址
+     */
+    private String cancelUrl;
+
+    @Bean
+    public PayPalHttpClient payPalClient() {
+        PayPalEnvironment environment = mode.equals("live") ? new PayPalEnvironment.Live(clientId, clientSecret) : new PayPalEnvironment.Sandbox(clientId, clientSecret);
+        return new PayPalHttpClient(environment);
+    }
+}

+ 5 - 5
modules/report/src/main/java/com/jeesite/modules/report/web/WebsiteUserOrderController.java

@@ -27,7 +27,7 @@ public class WebsiteUserOrderController extends BaseController {
 
 	@Autowired
 	private WebsiteUserOrderService websiteUserOrderService;
-	
+
 	/**
 	 * 获取数据
 	 */
@@ -35,7 +35,7 @@ public class WebsiteUserOrderController extends BaseController {
 	public WebsiteUserOrder get(String id, boolean isNewRecord) {
 		return websiteUserOrderService.get(id, isNewRecord);
 	}
-	
+
 	/**
 	 * 查询列表
 	 */
@@ -45,7 +45,7 @@ public class WebsiteUserOrderController extends BaseController {
 		model.addAttribute("websiteUserOrder", websiteUserOrder);
 		return "modules/report/websiteUserOrderList";
 	}
-	
+
 	/**
 	 * 查询列表数据
 	 */
@@ -57,7 +57,7 @@ public class WebsiteUserOrderController extends BaseController {
 			return null;
 		}
 		websiteUserOrder.setPage(new Page<>(request, response));
-		Page<WebsiteUserOrder> page = websiteUserOrderService.findPage(websiteUserOrder);
+		Page<WebsiteUserOrder> page = websiteUserOrderService.findOrderPage(websiteUserOrder);
 		return page;
 	}
 
@@ -71,4 +71,4 @@ public class WebsiteUserOrderController extends BaseController {
 		return "modules/report/websiteUserOrderForm";
 	}
 
-}
+}

+ 74 - 0
modules/report/src/main/java/com/jeesite/modules/report/web/WebsiteUserResearchOrderController.java

@@ -0,0 +1,74 @@
+package com.jeesite.modules.report.web;
+
+import com.jeesite.common.entity.Page;
+import com.jeesite.common.web.BaseController;
+import com.jeesite.modules.report.entity.WebsiteUserResearchOrder;
+import com.jeesite.modules.report.service.WebsiteUserResearchOrderService;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.shiro.authz.annotation.RequiresPermissions;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
+import org.springframework.web.bind.annotation.ModelAttribute;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.ResponseBody;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * 研究报告订单Controller
+ * @author gmj
+ * @version 2025-05-26
+ */
+@Controller
+@RequestMapping(value = "${adminPath}/report/websiteUserResearchOrder")
+public class WebsiteUserResearchOrderController extends BaseController {
+
+	@Autowired
+	private WebsiteUserResearchOrderService websiteUserResearchOrderService;
+
+	/**
+	 * 获取数据
+	 */
+	@ModelAttribute
+	public WebsiteUserResearchOrder get(String id, boolean isNewRecord) {
+		return websiteUserResearchOrderService.get(id, isNewRecord);
+	}
+
+	/**
+	 * 查询列表
+	 */
+	@RequiresPermissions("report:websiteUserResearchOrder:view")
+	@RequestMapping(value = {"list", ""})
+	public String list(WebsiteUserResearchOrder websiteUserResearchOrder, Model model) {
+		model.addAttribute("websiteUserResearchOrder", websiteUserResearchOrder);
+		return "modules/report/websiteUserResearchOrderList";
+	}
+
+	/**
+	 * 查询列表数据
+	 */
+	@RequiresPermissions("report:websiteUserResearchOrder:view")
+	@RequestMapping(value = "listData")
+	@ResponseBody
+	public Page<WebsiteUserResearchOrder> listData(WebsiteUserResearchOrder websiteUserResearchOrder, HttpServletRequest request, HttpServletResponse response) {
+		if (StringUtils.isEmpty(websiteUserResearchOrder.getWebsiteUserId())) {
+			return null;
+		}
+		websiteUserResearchOrder.setPage(new Page<>(request, response));
+		Page<WebsiteUserResearchOrder> page = websiteUserResearchOrderService.findOrderPage(websiteUserResearchOrder);
+		return page;
+	}
+
+	/**
+	 * 查看编辑表单
+	 */
+	@RequiresPermissions("report:websiteUserResearchOrder:view")
+	@RequestMapping(value = "form")
+	public String form(WebsiteUserResearchOrder websiteUserResearchOrder, Model model) {
+		model.addAttribute("websiteUserResearchOrder", websiteUserResearchOrder);
+		return "modules/report/websiteUserResearchOrderForm";
+	}
+
+}

+ 14 - 1
modules/report/src/main/resources/mappings/modules/report/ResearchReportDao.xml

@@ -51,7 +51,9 @@
 	<select id="findReportList" parameterType="com.jeesite.modules.report.entity.ResearchReport"
 		resultType="com.jeesite.modules.report.entity.ResearchReport">
 		select <include refid="query_columns"/>,
-			coalesce(s1.config_value,s2.config_value,s3.config_value,s4.config_value) as price
+			coalesce(s1.config_value,s2.config_value,s3.config_value,s4.config_value) as price,
+			coalesce(s1.config_key,s2.config_key,s3.config_key,s4.config_key) as priceKey,
+			coalesce(s1.config_name,s2.config_name,s3.config_name,s4.config_name) as priceName
 		from research_report b
 		left join report_attachment c on c.report_code = b.market_type and c.lang = b.lang and c.status = '0'
 		left join bjfl_config.js_sys_config s1 on b.lang = 'zh-CN' and b.report_region = '0' and s1.config_key = 'cn.china.full.single.price'
@@ -305,4 +307,15 @@
 		WHERE status = '0' and report_code = #{researchReport.reportCode}
 		  AND report_region = #{researchReport.reportRegion}
 	</update>
+
+	<select id="getPrice" resultType="java.lang.String">
+		select config_value from bjfl_config.js_sys_config where config_key = #{configKey}
+	</select>
+
+	<select id="findTitleById" resultType="com.jeesite.modules.report.entity.ResearchReport">
+		select b.title,c.name fileName
+		from research_report b
+				 left join report_attachment c on c.report_code = b.market_type and c.lang = b.lang and c.status = '0'
+		where b.id = #{researchReportId}
+	</select>
 </mapper>

+ 29 - 7
modules/report/src/main/resources/mappings/modules/report/WebsiteUserDao.xml

@@ -1,7 +1,7 @@
 <?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.jeesite.modules.report.dao.WebsiteUserDao">
-	
+
 	<!-- 查询数据
 	<select id="findList" resultType="WebsiteUser">
 		SELECT ${sqlMap.column.toSql()}
@@ -17,18 +17,40 @@
     </update>
 
     <select id="findUserCount" resultType="java.lang.Long">
-        select count(1) from website_user where status in('0','2')
+        select count(1) from website_user u
         <include refid="user_where"/>
     </select>
 
     <select id="findUserList" resultType="com.jeesite.modules.report.entity.WebsiteUser">
-        select * from website_user where status in('0','2')
+        select u.*,
+            NULLIF(
+                GREATEST(
+                COALESCE ( a.latest_payment , '1970-01-01'),
+                COALESCE ( b.latest_payment , '1970-01-01')),
+            '1970-01-01') AS latestPayment
+        from website_user u
+        LEFT JOIN (
+        SELECT website_user_id, MAX(pay_date) AS latest_payment
+        FROM website_user_order
+        where pay_status = '1'
+        GROUP BY website_user_id
+        ) a ON u.id = a.website_user_id
+        LEFT JOIN (
+        SELECT website_user_id, MAX(pay_date) AS latest_payment
+        FROM website_user_research_order
+        where pay_status = '1'
+        GROUP BY website_user_id
+        ) b ON u.id = b.website_user_id
         <include refid="user_where"/>
+        order by latestPayment desc
     </select>
 
     <sql id="user_where">
-        <if test="rr.name != null and rr.name != ''">and name like concat('%',#{rr.name},'%')</if>
-        <if test="rr.contact != null and rr.contact != ''">and contact = #{rr.contact}</if>
-        <if test="rr.email != null and rr.email != ''">and email = #{rr.email}</if>
+        where u.status in('0','2')
+        <if test="rr.name != null and rr.name != ''">and u.name like concat('%',#{rr.name},'%')</if>
+        <if test="rr.contact != null and rr.contact != ''">and u.contact like concat('%',#{rr.contact},'%')</if>
+        <if test="rr.email != null and rr.email != ''">and u.email like concat('%',#{rr.email},'%')</if>
+        <if test="rr.companyName != null and rr.companyName != ''">and u.company_name like concat('%',#{rr.companyName},'%')</if>
+        <if test="rr.status != null and rr.status != ''">and u.status = #{rr.status}</if>
     </sql>
-</mapper>
+</mapper>

+ 3 - 3
modules/report/src/main/resources/mappings/modules/report/WebsiteUserOrderDao.xml

@@ -1,7 +1,7 @@
 <?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.jeesite.modules.report.dao.WebsiteUserOrderDao">
-	
+
 	<!-- 查询数据
 	<select id="findList" resultType="WebsiteUserOrder">
 		SELECT ${sqlMap.column.toSql()}
@@ -13,7 +13,7 @@
 	</select> -->
 
     <select id="findByOrderNumber" resultType="com.jeesite.modules.report.entity.WebsiteUserOrder">
-        select * from website_user_order where order_number = #{orderNumber}
+        select * from website_user_order where status = '0' and order_number = #{orderNumber}
     </select>
 
     <select id="findOrderList" resultType="com.jeesite.modules.report.entity.WebsiteUserOrder">
@@ -26,4 +26,4 @@
             </foreach>
         </if>
     </select>
-</mapper>
+</mapper>

+ 29 - 0
modules/report/src/main/resources/mappings/modules/report/WebsiteUserResearchOrderDao.xml

@@ -0,0 +1,29 @@
+<?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.jeesite.modules.report.dao.WebsiteUserResearchOrderDao">
+
+	<!-- 查询数据
+	<select id="findList" resultType="WebsiteUserResearchOrder">
+		SELECT ${sqlMap.column.toSql()}
+		FROM ${sqlMap.table.toSql()}
+		<where>
+			${sqlMap.where.toSql()}
+		</where>
+		ORDER BY ${sqlMap.order.toSql()}
+	</select> -->
+
+    <select id="findByOrderNumber" resultType="com.jeesite.modules.report.entity.WebsiteUserResearchOrder">
+        select * from website_user_research_order where status = '0' and order_number = #{orderNumber}
+    </select>
+
+    <select id="findOrderList" resultType="com.jeesite.modules.report.entity.WebsiteUserResearchOrder">
+        select * from website_user_research_order where status = '0'
+        <if test="websiteUserResearchOrder.payStatus != null and websiteUserResearchOrder.payStatus != ''">and pay_status = #{websiteUserResearchOrder.payStatus}</if>
+        <if test="websiteUserResearchOrder.payMethodList != null">
+            AND pay_method in
+            <foreach item="str" collection="websiteUserResearchOrder.payMethodList" open="(" separator="," close=")">
+                #{str}
+            </foreach>
+        </if>
+    </select>
+</mapper>

+ 10 - 1
web/src/main/resources/config/application.yml

@@ -963,4 +963,13 @@ wxpay:
   apiV3Key: gaux2PjT2vwtcRNvvH5EKpuhIUNe1djN
 
 websiteUser:
-  resetUrl: http://192.168.0.247:8080/mine/updatePwd
+  resetUrl: http://192.168.0.247:8080/mine/updatePwd
+
+paypal:
+  clientId: ARhzp9aql8hudBMyxANRSRJ9Xcp6cpIBn8alP2OBHKrlucMLYQyZ1tIkz-nrGZ8VnExnwlBgHoggYSwk
+  clientSecret: EJszPbFOa4qAwzfzEUGFWR9IYY6hcOEWRBC-RmWJOD895Yqw2T_ZRIZ0Orw2JB2XlOJ0_s1z7BO9meAD
+  mode: sandbox # 生产环境改为 live
+  # 页面跳转同步通知页面路径
+  returnUrl: http://192.168.0.135:3000/personalCenter/myInfo
+  # 取消支付回调地址
+  cancelUrl: http://192.168.0.135:3000/personalCenter/myInfo