Commit 97b6a8fc by Jony.L

Merge remote-tracking branch 'origin/new-pay' into develop

# Conflicts:
#	computility-module-compute/computility-module-compute-biz/src/main/java/com/luhu/computility/module/compute/service/resourceorder/ResourceOrderServiceImpl.java
#	computility-module-compute/computility-module-compute-biz/src/main/java/com/luhu/computility/module/compute/service/resourcespu/ResourceSpuServiceImpl.java
parents 863f273c 2a046d3a
......@@ -35,10 +35,6 @@ public class ApiOrderRespDTO {
private String statusName;
private Integer payStatus;
private String payStatusName;
private Long payOrderId;
private LocalDateTime payTime;
......
......@@ -38,8 +38,6 @@ public class ApiOrderSaveReqDTO {
private LocalDateTime cancelTime;
private Integer payStatus;
private String remark;
}
\ No newline at end of file
package com.luhu.computility.module.apihub.api.order;
import com.luhu.computility.module.apihub.api.order.dto.ApiOrderStatisticsDTO;
import java.time.LocalDateTime;
/**
* API订单统计API接口
*
* @author jony
*/
public interface ApiHubOrderStatisticsApi {
/**
* 获取今日已支付的API订单统计
*
* @param timeRange 时间范围 [开始时间, 结束时间]
* @return 订单统计信息(数量和总金额)
*/
ApiOrderStatisticsDTO getTodayOrderStatistics(LocalDateTime[] timeRange);
}
\ No newline at end of file
package com.luhu.computility.module.apihub.api.order.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* API订单统计DTO
*
* @author jony
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ApiOrderStatisticsDTO {
/**
* 订单数量
*/
private Integer count;
/**
* 订单总金额(分)
*/
private Integer totalAmount;
}
\ No newline at end of file
package com.luhu.computility.module.apihub.enums;
/**
* 订单支付状态
*/
public enum ApiOrderPayStatus {
// 请根据实际情况改成对应的值和备注
WAITING(0, "未支付"),
SUCCESS(10, "支付成功"),
REFUND(20, "已退款"),
CLOSED(30, "支付关闭"), // 注意:全部退款后,还是 REFUND 状态
;
private int value;
private String remark;
private ApiOrderPayStatus(int value, String remark) {
this.value = value;
this.remark = remark;
}
public int getValue() {
return value;
}
public String getRemark() {
return remark;
}
public static ApiOrderPayStatus getByValue(int value) {
for (ApiOrderPayStatus o : ApiOrderPayStatus.values()) {
if (o.getValue() == value) {
return o;
}
}
return null;
}
public static String getRemarkByValue(Integer value) {
for (ApiOrderPayStatus status : values()) {
if (status.getValue() == value) {
return status.getRemark();
}
}
return null;
}
}
package com.luhu.computility.module.apihub.enums;
/**
* 订单状态
*/
public enum ApiOrderStatus {
// 请根据实际情况改成对应的值和备注
UNPAID(0, "待支付"),
PAID(10, "已支付"),
CANCELED(40, "已取消");
private int value;
private String remark;
private ApiOrderStatus(int value, String remark) {
this.value = value;
this.remark = remark;
}
public int getValue() {
return value;
}
public String getRemark() {
return remark;
}
public static ApiOrderStatus getByValue(int value) {
for (ApiOrderStatus o : ApiOrderStatus.values()) {
if (o.getValue() == value) {
return o;
}
}
return null;
}
public static String getRemarkByValue(Integer value) {
for (ApiOrderStatus status : values()) {
if (status.getValue() == value) {
return status.getRemark();
}
}
return null;
}
}
//package com.luhu.computility.module.apihub.enums;
//
//
///**
// * 订单状态
// */
//
//
//public enum ApiOrderStatus {
// // 请根据实际情况改成对应的值和备注
//
// UNPAID(0, "待支付"),
// PAID(10, "已支付"),
// CANCELED(40, "已取消");
// private int value;
// private String remark;
//
// private ApiOrderStatus(int value, String remark) {
// this.value = value;
// this.remark = remark;
// }
//
// public int getValue() {
// return value;
// }
//
// public String getRemark() {
// return remark;
// }
//
// public static ApiOrderStatus getByValue(int value) {
// for (ApiOrderStatus o : ApiOrderStatus.values()) {
// if (o.getValue() == value) {
// return o;
// }
// }
// return null;
// }
//
// public static String getRemarkByValue(Integer value) {
// for (ApiOrderStatus status : values()) {
// if (status.getValue() == value) {
// return status.getRemark();
// }
// }
// return null;
// }
//}
package com.luhu.computility.module.apihub.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* APIHub订单状态枚举
*
* @author jony
*/
@Getter
@AllArgsConstructor
public enum ApihubOrderStatusEnum {
UNPAID(0, "待支付"),
PAID(1, "已支付"),
CANCELED(2, "已取消"),
REFUNDED(3, "已退款");
private final Integer value;
private final String name;
/**
* 判断是否待支付
*/
public static boolean isUnpaid(Integer status) {
return UNPAID.getValue().equals(status);
}
/**
* 判断是否已支付
*/
public static boolean isPaid(Integer status) {
return PAID.getValue().equals(status);
}
/**
* 判断是否已取消
*/
public static boolean isCanceled(Integer status) {
return CANCELED.getValue().equals(status);
}
/**
* 判断是否已退款
*/
public static boolean isRefunded(Integer status) {
return REFUNDED.getValue().equals(status);
}
/**
* 根据值获取枚举
*/
public static ApihubOrderStatusEnum fromValue(Integer value) {
for (ApihubOrderStatusEnum status : values()) {
if (status.getValue().equals(value)) {
return status;
}
}
return null;
}
/**
* 根据值获取名称
*/
public static String getNameByValue(Integer value) {
ApihubOrderStatusEnum statusEnum = fromValue(value);
return statusEnum != null ? statusEnum.getName() : null;
}
}
\ No newline at end of file
......@@ -4,8 +4,7 @@ import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.ObjectUtil;
import com.luhu.computility.framework.common.exception.ServiceException;
import com.luhu.computility.module.apihub.controller.admin.userapiusage.vo.UserApiUsageSaveReqVO;
import com.luhu.computility.module.apihub.enums.ApiOrderPayStatus;
import com.luhu.computility.module.apihub.enums.ApiOrderStatus;
import com.luhu.computility.module.apihub.enums.ApihubOrderStatusEnum;
import com.luhu.computility.module.apihub.service.userapiusage.UserApiUsageService;
import com.luhu.computility.module.pay.api.notify.dto.PayOrderNotifyReqDTO;
import lombok.extern.slf4j.Slf4j;
......@@ -114,11 +113,7 @@ public class ApiOrderController {
PageResult<ApiOrderRespVO> pageResult = apiOrderService.getApiOrderPage(pageReqVO);
for (ApiOrderRespVO apiOrderRespVO : pageResult.getList()) {
apiOrderRespVO.setStatusName(ApiOrderStatus.getByValue(apiOrderRespVO.getStatus()).getRemark());
if (!ObjectUtil.isEmpty(apiOrderRespVO.getPayStatus())) {
apiOrderRespVO.setPayStatusName(ApiOrderPayStatus.getByValue(apiOrderRespVO.getPayStatus()).getRemark());
}
apiOrderRespVO.setStatusName(ApihubOrderStatusEnum.fromValue(apiOrderRespVO.getStatus()).getName());
}
return success(pageResult);
}
......
......@@ -69,11 +69,6 @@ public class ApiOrderRespVO {
@Schema(description = "订单状态值:0=待支付,1=已支付,2=已取消", example = "2")
private String statusName;
@Schema(description = "支付状态", example = "2")
private Integer payStatus;
@Schema(description = "支付状态值", example = "2")
private String payStatusName;
@Schema(description = "支付订单编号", example = "14961")
@ExcelProperty("支付订单编号")
......
......@@ -53,8 +53,6 @@ public class ApiOrderSaveReqVO {
@Schema(description = "订单取消时间")
private LocalDateTime cancelTime;
@Schema(description = "支付状态", example = "2")
private Integer payStatus;
@Schema(description = "发票链接", example = "www.xxx.com/a1234567.jpg")
private String invoiceUrl;
......
package com.luhu.computility.module.apihub.controller.admin.notify;
import cn.hutool.core.util.StrUtil;
import com.luhu.computility.framework.common.pojo.CommonResult;
import com.luhu.computility.framework.common.util.json.JsonUtils;
import com.luhu.computility.module.pay.controller.admin.notify.vo.WpgjPayNotifyDTO;
import com.luhu.computility.module.pay.controller.admin.notify.vo.WpgjPayNotifyRespDTO;
import com.luhu.computility.module.apihub.service.apiorder.ApiOrderService;
import com.luhu.computility.module.pay.enums.WpgjOrderStatusEnum;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RequestBody;
import javax.annotation.Resource;
import javax.annotation.security.PermitAll;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.digest.DigestUtil;
import com.luhu.computility.module.pay.framework.pay.core.client.impl.wpgj.WpgjPayProperties;
import java.util.Map;
import java.util.TreeMap;
import java.util.stream.Collectors;
/**
* APIHub模块 - WPGJ旺铺聚合支付回调Controller
*
* 独立处理WPGJ支付回调,直接更新API订单状态
*
* @author jony
*/
@Tag(name = "管理后台 - APIHub WPGJ旺铺聚合支付回调")
@RestController
@RequestMapping("/apihub/wpgj")
@Slf4j
public class ApihubWpgjPayController {
@Resource
private ApiOrderService apiOrderService;
@Resource
private WpgjPayProperties wpgjPayProperties;
@PostMapping("/notify")
@PermitAll
@Operation(summary = "WPGJ支付异步回调通知 - APIHub模块")
public WpgjPayNotifyRespDTO notifyWpgjPay(@RequestBody WpgjPayNotifyDTO notifyDTO) {
WpgjPayNotifyRespDTO response = new WpgjPayNotifyRespDTO();
try {
log.info("[notifyWpgjPay][APIHub] 收到WPGJ支付回调(新): {}", JsonUtils.toJsonString(notifyDTO));
// 1. 验证签名(对所有出现的非空字段,按 ASCII 升序拼接,排除 sign)
if (!verifyWpgjSignature(toPayloadMap(notifyDTO))) {
log.error("[notifyWpgjPay][APIHub] WPGJ回调签名验证失败");
response.setCode("99");
response.setMsg("签名验证失败");
response.setTimestamp(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS")));
return response;
}
String merOrderId = notifyDTO.getMerOrderId();
String orderStatus = notifyDTO.getOrderStatus();
log.info("[notifyWpgjPay][APIHub] 处理WPGJ支付结果,商户订单号: {}, 订单状态: {}", merOrderId, orderStatus);
// 2. 幂等处理:根据订单状态更新(与算力资源模块保持一致逻辑)
if (WpgjOrderStatusEnum.isSuccess(orderStatus)) {
apiOrderService.updateOrderPaidByWpgj(Long.parseLong(merOrderId), notifyDTO);
log.info("[notifyWpgjPay][APIHub] 支付成功处理完成,商户订单号: {}", merOrderId);
} else if (WpgjOrderStatusEnum.isFailedOrClosed(orderStatus)) {
apiOrderService.updateOrderFailedByWpgj(Long.parseLong(merOrderId), notifyDTO);
log.info("[notifyWpgjPay][APIHub] 支付失败/关闭处理完成,商户订单号: {}", merOrderId);
} else {
log.info("[notifyWpgjPay][APIHub] 订单状态无需处理,商户订单号: {}, 状态: {}", merOrderId, orderStatus);
}
// 3. 返回成功应答
response.setCode("00");
response.setMsg("成功");
response.setTimestamp(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS")));
log.info("[notifyWpgjPay][APIHub] 返回wpgj参数: {}", JsonUtils.toJsonString(response));
return response;
} catch (Exception e) {
log.error("[notifyWpgjPay][APIHub] WPGJ支付回调处理失败", e);
response.setCode("99");
response.setMsg("处理失败");
response.setTimestamp(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS")));
return response;
}
}
/**
* Map 形式的回调请求验签:对所有出现的非空字段(排除 sign)进行 ASCII 排序拼接后 + key,再 MD5(UTF-8) 大写
*/
private boolean verifyWpgjSignature(Map<String, Object> payload) {
try {
Object signObj = payload.get("sign");
String originSign = signObj == null ? null : String.valueOf(signObj);
if (StrUtil.isBlank(originSign)) {
log.warn("[verifyWpgjSignature][APIHub] 回调缺少 sign 字段");
return false;
}
// 过滤掉 sign、自身为空的字段,按 ASCII 升序
TreeMap<String, String> sorted = new TreeMap<>();
for (Map.Entry<String, Object> e : payload.entrySet()) {
String k = e.getKey();
if ("sign".equals(k)) {
continue;
}
String v = e.getValue() == null ? null : String.valueOf(e.getValue());
if (StrUtil.isBlank(v)) {
continue;
}
sorted.put(k, v);
}
String dataStr = sorted.entrySet().stream()
.map(en -> en.getKey() + "=" + en.getValue())
.collect(Collectors.joining("&"));
String signStr = dataStr + "&key=" + wpgjPayProperties.getSignKey();
String calculatedSign = DigestUtil.md5Hex(signStr).toUpperCase();
log.info("[verifyWpgjSignature][APIHub-New] 待签名字符串: {}", dataStr);
log.info("[verifyWpgjSignature][APIHub-New] 加签前(带key): {}", signStr);
log.info("[verifyWpgjSignature][APIHub-New] 计算签名: {},原始签名: {}", calculatedSign, originSign);
return calculatedSign.equalsIgnoreCase(originSign);
} catch (Exception e) {
log.error("[verifyWpgjSignature][APIHub-New] 验签异常", e);
return false;
}
}
/**
* 将 DTO 转换为 Map(以供应商字段名 snake_case 作为 key),便于统一验签
*/
private Map<String, Object> toPayloadMap(WpgjPayNotifyDTO dto) {
TreeMap<String, Object> map = new TreeMap<>();
map.put("device_no", dto.getDeviceNo());
map.put("mer_no", dto.getMerNo());
map.put("mer_code", dto.getMerCode());
map.put("payway_code", dto.getPaywayCode());
map.put("order_id", dto.getOrderId());
map.put("mer_order_id", dto.getMerOrderId());
map.put("gateway_mer_order_id", dto.getGatewayMerOrderId());
map.put("order_time", dto.getOrderTime());
map.put("order_amt", dto.getOrderAmt());
map.put("order_status", dto.getOrderStatus());
map.put("trade_no", dto.getTradeNo());
map.put("trade_time", dto.getTradeTime());
map.put("order_title", dto.getOrderTitle());
map.put("fee", dto.getFee());
map.put("act_amt", dto.getActAmt());
map.put("buyer_id", dto.getBuyerId());
map.put("trade_top_no", dto.getTradeTopNo());
map.put("card_type", dto.getCardType());
map.put("sign", dto.getSign());
return map;
}
}
\ No newline at end of file
......@@ -8,11 +8,12 @@ import com.luhu.computility.framework.common.util.object.BeanUtils;
import com.luhu.computility.framework.security.core.util.SecurityFrameworkUtils;
import com.luhu.computility.module.apihub.controller.app.apiorder.vo.AppApiOrderCreateReqVO;
import com.luhu.computility.module.apihub.controller.app.apiorder.vo.AppApiOrderCreateRespVO;
import com.luhu.computility.module.apihub.controller.app.apiorder.vo.AppApiWpgjPayOrderSubmitRespVO;
import com.luhu.computility.module.apihub.controller.app.apiorder.vo.AppApiOrderInvoiceReqVO;
import com.luhu.computility.module.apihub.controller.app.apiorder.vo.AppApiOrderPageReqVO;
import com.luhu.computility.module.apihub.controller.app.apiorder.vo.AppApiOrderRespVO;
import com.luhu.computility.module.apihub.dal.dataobject.apiorder.ApiOrderDO;
import com.luhu.computility.module.apihub.enums.ApiOrderStatus;
import com.luhu.computility.module.apihub.enums.ApihubOrderStatusEnum;
import com.luhu.computility.module.apihub.service.apiorder.ApiOrderService;
import com.luhu.computility.module.trade.enums.order.TradeOrderInvoiceStatusEnum;
import io.swagger.v3.oas.annotations.Operation;
......@@ -53,6 +54,14 @@ public class AppApiOrderController {
return success(new AppApiOrderCreateRespVO().setId(order.getId()).setPayOrderId(order.getPayOrderId()));
}
@PostMapping("/create-wpgj")
@Operation(summary = "创建api订单(旺铺聚合支付)")
public CommonResult<AppApiWpgjPayOrderSubmitRespVO> createApiOrderWithWpgj(@Valid @RequestBody AppApiOrderCreateReqVO createReqVO) {
Long userId = SecurityFrameworkUtils.getLoginUser().getId();
AppApiWpgjPayOrderSubmitRespVO respVO = apiOrderService.createUserApiOrderWithWpgj(userId, createReqVO);
return success(respVO);
}
@GetMapping("/page")
@Operation(summary = "获得api订单分页")
......@@ -63,7 +72,7 @@ public class AppApiOrderController {
List<AppApiOrderRespVO> list = pageResult.getList();
if (!CollectionUtil.isEmpty(list)) {
for (AppApiOrderRespVO vo : list) {
vo.setStatusName(ApiOrderStatus.getRemarkByValue(vo.getStatus()));
vo.setStatusName(ApihubOrderStatusEnum.getNameByValue(vo.getStatus()));
vo.setInvoiceStatusName(TradeOrderInvoiceStatusEnum.getDescriptionByStatus(vo.getInvoiceStatus()));
}
}
......
package com.luhu.computility.module.apihub.controller.app.apiorder.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import javax.validation.constraints.NotNull;
/**
* 用户 APP - API订单 WPGJ旺铺聚合支付订单提交 Response VO
* 独立于老支付系统的WPGJ支付响应
*
* @author jony
*/
@Schema(description = "用户 APP - API订单 WPGJ旺铺聚合支付订单提交 Response VO")
@Data
public class AppApiWpgjPayOrderSubmitRespVO {
@Schema(description = "订单ID(WPGJ订单ID,用于前端轮询支付状态)", requiredMode = Schema.RequiredMode.REQUIRED, example = "202510301436330934307677")
@NotNull(message = "订单ID不能为空")
private Long id;
@Schema(description = "支付状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
@NotNull(message = "支付状态不能为空")
private String status;
@Schema(description = "展示模式", example = "url")
private String displayMode;
@Schema(description = "展示内容(支付链接等)", example = "https://example.com/pay")
private String displayContent;
}
\ No newline at end of file
......@@ -11,7 +11,7 @@ import java.time.LocalDateTime;
import java.time.LocalDateTime;
import java.time.LocalDateTime;
import com.baomidou.mybatisplus.annotation.*;
import com.luhu.computility.framework.mybatis.core.dataobject.BaseDO;
import com.luhu.computility.framework.tenant.core.db.TenantBaseDO;
/**
* api订单 DO
......@@ -26,7 +26,7 @@ import com.luhu.computility.framework.mybatis.core.dataobject.BaseDO;
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ApiOrderDO extends BaseDO {
public class ApiOrderDO extends TenantBaseDO {
/**
* 订单ID
......@@ -77,10 +77,6 @@ public class ApiOrderDO extends BaseDO {
*/
private Integer status;
/**
* 支付状态:1=已支付,2=未支付
*/
private Integer payStatus;
/**
* 支付订单编号
*/
private Long payOrderId;
......
......@@ -6,7 +6,7 @@ import java.time.LocalDateTime;
import java.time.LocalDateTime;
import java.time.LocalDateTime;
import com.baomidou.mybatisplus.annotation.*;
import com.luhu.computility.framework.mybatis.core.dataobject.BaseDO;
import com.luhu.computility.framework.tenant.core.db.TenantBaseDO;
/**
* 用户 API 使用统计 DO
......@@ -21,7 +21,7 @@ import com.luhu.computility.framework.mybatis.core.dataobject.BaseDO;
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class UserApiUsageDO extends BaseDO {
public class UserApiUsageDO extends TenantBaseDO {
/**
* 记录ID
......
......@@ -19,11 +19,12 @@ import com.luhu.computility.module.apihub.controller.app.apiorder.vo.AppApiOrder
import com.luhu.computility.module.apihub.dal.dataobject.api.ApiDO;
import com.luhu.computility.module.apihub.dal.dataobject.apicategory.ApiCategoryDO;
import com.luhu.computility.module.apihub.dal.dataobject.apiorder.ApiOrderDO;
import com.luhu.computility.module.apihub.enums.ApiOrderStatus;
import com.luhu.computility.module.member.dal.dataobject.user.MemberUserDO;
import org.apache.ibatis.annotations.Mapper;
import com.luhu.computility.module.apihub.controller.admin.apiorder.vo.*;
import static com.luhu.computility.module.apihub.enums.ApihubOrderStatusEnum.PAID;
/**
* api订单 Mapper
*
......@@ -48,7 +49,7 @@ public interface ApiOrderMapper extends BaseMapperX<ApiOrderDO> {
default List<ApiOrderRespDTO> selectAllPaidList(LocalDateTime[] timePeriod) {
List<ApiOrderDO> list = selectList(new LambdaQueryWrapperX<ApiOrderDO>()
.eq(ApiOrderDO::getStatus, ApiOrderStatus.PAID.getValue())
.eq(ApiOrderDO::getStatus, PAID.getValue())
.betweenIfPresent(ApiOrderDO::getCreateTime, timePeriod));
return BeanUtil.copyToList(list, ApiOrderRespDTO.class);
}
......@@ -100,4 +101,11 @@ public interface ApiOrderMapper extends BaseMapperX<ApiOrderDO> {
.eq(ApiOrderDO::getId, id).eq(ApiOrderDO::getStatus, status));
}
default List<ApiOrderDO> selectListByStatusAndCreateTime(Integer status, LocalDateTime[] timeRange) {
return selectList(new LambdaQueryWrapperX<ApiOrderDO>()
.eq(ApiOrderDO::getStatus, status)
.betweenIfPresent(ApiOrderDO::getCreateTime, timeRange)
.orderByDesc(ApiOrderDO::getId));
}
}
\ No newline at end of file
......@@ -4,12 +4,14 @@ import java.util.*;
import javax.validation.*;
import com.luhu.computility.module.apihub.controller.admin.apiorder.vo.*;
import com.luhu.computility.module.apihub.controller.app.apiorder.vo.AppApiOrderCreateReqVO;
import com.luhu.computility.module.apihub.controller.app.apiorder.vo.AppApiWpgjPayOrderSubmitRespVO;
import com.luhu.computility.module.apihub.controller.app.apiorder.vo.AppApiOrderInvoiceReqVO;
import com.luhu.computility.module.apihub.controller.app.apiorder.vo.AppApiOrderPageReqVO;
import com.luhu.computility.module.apihub.controller.app.apiorder.vo.AppApiOrderRespVO;
import com.luhu.computility.module.apihub.dal.dataobject.apiorder.ApiOrderDO;
import com.luhu.computility.framework.common.pojo.PageResult;
import com.luhu.computility.framework.common.pojo.PageParam;
import com.luhu.computility.module.pay.controller.admin.notify.vo.WpgjPayNotifyDTO;
/**
* api订单 Service 接口
......@@ -103,4 +105,29 @@ public interface ApiOrderService {
* @return
*/
boolean updateRequestInvoice(AppApiOrderInvoiceReqVO reqVO);
/**
* 创建用户API订单(旺铺聚合支付)
*
* @param userId 用户ID
* @param createReqVO 创建请求
* @return WPGJ支付订单提交响应
*/
AppApiWpgjPayOrderSubmitRespVO createUserApiOrderWithWpgj(Long userId, AppApiOrderCreateReqVO createReqVO);
/**
* WPGJ支付成功回调处理
*
* @param apiOrderId API订单ID
* @param notifyDTO WPGJ回调数据
*/
void updateOrderPaidByWpgj(Long apiOrderId, WpgjPayNotifyDTO notifyDTO);
/**
* WPGJ支付失败/关闭回调处理
*
* @param apiOrderId API订单ID
* @param notifyDTO WPGJ回调数据
*/
void updateOrderFailedByWpgj(Long apiOrderId, WpgjPayNotifyDTO notifyDTO);
}
\ No newline at end of file
package com.luhu.computility.module.apihub.service.expire;
import com.luhu.computility.module.apihub.dal.mysql.apiorder.ApiOrderMapper;
import com.luhu.computility.module.apihub.dal.dataobject.apiorder.ApiOrderDO;
import com.luhu.computility.module.apihub.enums.ApihubOrderStatusEnum;
import com.luhu.computility.module.pay.enums.OrderBusinessTypeEnum;
import com.luhu.computility.module.pay.service.expire.BusinessOrderExpireService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.time.LocalDateTime;
/**
* APIHub订单过期服务实现类
*
* @author jony
*/
@Service
@Slf4j
public class ApiHubOrderExpireServiceImpl implements BusinessOrderExpireService {
@Resource
private ApiOrderMapper apiOrderMapper;
@Override
public OrderBusinessTypeEnum getSupportedBusinessType() {
return OrderBusinessTypeEnum.API_HUB;
}
@Override
public boolean expireBusinessOrder(Long businessOrderId) {
try {
// 查询订单
ApiOrderDO order = apiOrderMapper.selectById(businessOrderId);
if (order == null) {
log.warn("[expireBusinessOrder] API订单不存在,订单ID: {}", businessOrderId);
return false;
}
// 检查订单状态,只有待支付的订单才能过期
if (!ApihubOrderStatusEnum.UNPAID.getValue().equals(order.getStatus())) {
log.warn("[expireBusinessOrder] API订单状态不是待支付,无法过期,订单ID: {}, 当前状态: {}",
businessOrderId, order.getStatus());
return false;
}
// 更新订单状态为已取消
ApiOrderDO updateOrder = new ApiOrderDO();
updateOrder.setId(businessOrderId);
updateOrder.setStatus(ApihubOrderStatusEnum.CANCELED.getValue());
updateOrder.setCancelTime(LocalDateTime.now());
int updateCount = apiOrderMapper.updateById(updateOrder);
if (updateCount > 0) {
log.info("[expireBusinessOrder] API订单过期成功,订单ID: {}", businessOrderId);
return true;
} else {
log.error("[expireBusinessOrder] API订单过期失败,更新数据库失败,订单ID: {}", businessOrderId);
return false;
}
} catch (Exception e) {
log.error("[expireBusinessOrder] 过期API订单异常,订单ID: {}", businessOrderId, e);
return false;
}
}
}
\ No newline at end of file
package com.luhu.computility.module.apihub.service.impl;
import com.luhu.computility.module.apihub.api.order.ApiHubOrderStatisticsApi;
import com.luhu.computility.module.apihub.api.order.dto.ApiOrderStatisticsDTO;
import com.luhu.computility.module.apihub.dal.dataobject.apiorder.ApiOrderDO;
import com.luhu.computility.module.apihub.dal.mysql.apiorder.ApiOrderMapper;
import com.luhu.computility.module.apihub.enums.ApihubOrderStatusEnum;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.List;
/**
* API订单统计API实现类
*
* @author jony
*/
@Service
@Slf4j
public class ApiHubOrderStatisticsApiImpl implements ApiHubOrderStatisticsApi {
@Resource
private ApiOrderMapper apiOrderMapper;
@Override
public ApiOrderStatisticsDTO getTodayOrderStatistics(LocalDateTime[] timeRange) {
try {
List<ApiOrderDO> orders = apiOrderMapper.selectListByStatusAndCreateTime(
ApihubOrderStatusEnum.PAID.getValue(),
timeRange
);
int count = orders.size();
int totalAmount = orders.stream()
.filter(order -> order.getCostPrice() != null)
.mapToInt(ApiOrderDO::getCostPrice)
.sum();
return new ApiOrderStatisticsDTO(count, totalAmount);
} catch (Exception e) {
log.error("[getTodayOrderStatistics] 获取今日API订单统计失败", e);
throw e;
}
}
}
\ No newline at end of file
......@@ -42,6 +42,12 @@
<dependency>
<groupId>com.luhu</groupId>
<artifactId>computility-module-compute-api</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>com.luhu</groupId>
<artifactId>computility-module-trade</artifactId>
<version>${revision}</version>
</dependency>
......
......@@ -20,7 +20,7 @@ public class HomeIndexTopBarRespVO {
private Integer newOrdersCount;
/**
* 今日新增订单金额
* 今日新增订单金额(单位:元)
*/
private Integer newOrdersAmount;
private Double newOrdersAmount;
}
......@@ -9,10 +9,15 @@ import com.luhu.computility.module.apihub.api.apicalllog.dto.ApiCallLogRespDTO;
import com.luhu.computility.module.apihub.api.apiorder.ApiOrderApi;
import com.luhu.computility.module.apihub.api.apiorder.dto.ApiOrderPageReqDTO;
import com.luhu.computility.module.apihub.api.apiorder.dto.ApiOrderRespDTO;
import com.luhu.computility.module.apihub.api.order.ApiHubOrderStatisticsApi;
import com.luhu.computility.module.apihub.api.order.dto.ApiOrderStatisticsDTO;
import com.luhu.computility.module.biz.controller.admin.home.vo.HomeIndexApiCallsRespVO;
import com.luhu.computility.module.biz.controller.admin.home.vo.HomeIndexOrdersCountRespVO;
import com.luhu.computility.module.biz.controller.admin.home.vo.HomeIndexTopBarRespVO;
import com.luhu.computility.module.biz.controller.admin.home.vo.HomeIndexUsersCountRespVO;
import com.luhu.computility.module.compute.api.order.ComputeOrderStatisticsApi;
import com.luhu.computility.module.compute.api.order.dto.ComputeOrderStatisticsDTO;
import com.luhu.computility.module.compute.api.order.dto.ResourceOrderRespDTO;
import com.luhu.computility.module.member.controller.admin.user.vo.MemberUserPageReqVO;
import com.luhu.computility.module.member.dal.dataobject.user.MemberUserDO;
import com.luhu.computility.module.member.service.user.MemberUserService;
......@@ -45,6 +50,10 @@ public class HomeIndexServiceImpl implements HomeIndexService {
@Resource
TradeOrderQueryService tradeOrderQueryService;
@Resource
ApiHubOrderStatisticsApi apiHubOrderStatisticsApi;
@Resource
ComputeOrderStatisticsApi computeOrderStatisticsApi;
@Resource
ApiOrderApi apiOrderApi;
@Resource
ApiCallLogApi apiCallLogApi;
......@@ -252,27 +261,19 @@ public class HomeIndexServiceImpl implements HomeIndexService {
MemberUserPageReqVO memberUserPageReqVO = queryVO.setCreateTime(todayLocalDateTime);
List<MemberUserDO> userList = memberUserService.getUserList(memberUserPageReqVO);
TradeOrderPageReqVO computeOrderQueryVO = new TradeOrderPageReqVO();
computeOrderQueryVO.setCreateTime(todayLocalDateTime);
computeOrderQueryVO.setStatus(TradeOrderStatusEnum.COMPLETED.getStatus());
List<TradeOrderDO> computeOrderList = tradeOrderQueryService.getOrderList(computeOrderQueryVO);
// 获取今日已完成的算力资源订单统计
ComputeOrderStatisticsDTO computeStatistics = computeOrderStatisticsApi.getTodayOrderStatistics(todayLocalDateTime);
ApiOrderPageReqDTO apiOrderPageReqDTO = new ApiOrderPageReqDTO();
apiOrderPageReqDTO.setCreateTime(todayLocalDateTime);
List<ApiOrderRespDTO> apiOrderList = apiOrderApi.getPaidOrderList(todayLocalDateTime);
// 获取今日已支付的API订单统计
ApiOrderStatisticsDTO apiStatistics = apiHubOrderStatisticsApi.getTodayOrderStatistics(todayLocalDateTime);
HomeIndexTopBarRespVO homeIndexTopBarRespVO = new HomeIndexTopBarRespVO();
int newComputeOrdersAmount = computeOrderList.stream()
.mapToInt(TradeOrderDO::getPayPrice)
.sum();
int newApiOrdersAmount = apiOrderList.stream()
.mapToInt(ApiOrderRespDTO::getCostPrice)
.sum();
homeIndexTopBarRespVO.setNewUsersCount(userList.size());
homeIndexTopBarRespVO.setNewOrdersCount(computeOrderList.size() + apiOrderList.size());
homeIndexTopBarRespVO.setNewOrdersAmount(newComputeOrdersAmount + newApiOrdersAmount);
homeIndexTopBarRespVO.setNewOrdersCount(computeStatistics.getCount() + apiStatistics.getCount());
// 金额从分转换为元(除以100)
int totalAmountFen = computeStatistics.getTotalAmount() + apiStatistics.getTotalAmount();
homeIndexTopBarRespVO.setNewOrdersAmount(totalAmountFen / 100.0);
return homeIndexTopBarRespVO;
......@@ -319,19 +320,18 @@ public class HomeIndexServiceImpl implements HomeIndexService {
// 2. 查询目标订单(截止到endTime的有效订单)
LocalDateTime[] allTimePeriod = {LocalDate.of(1970, 1, 1).atStartOfDay(), endTime};
// 算力订单:已完成状态
TradeOrderPageReqVO computeQueryVO = new TradeOrderPageReqVO();
computeQueryVO.setCreateTime(allTimePeriod);
computeQueryVO.setStatus(TradeOrderStatusEnum.COMPLETED.getStatus());
List<TradeOrderDO> computeOrderList = tradeOrderQueryService.getOrderList(computeQueryVO);
// API订单:已支付状态
ApiOrderPageReqDTO apiOrderPageReqDTO = new ApiOrderPageReqDTO();
apiOrderPageReqDTO.setCreateTime(allTimePeriod);
// TradeOrderPageReqVO computeQueryVO = new TradeOrderPageReqVO();
// computeQueryVO.setCreateTime(allTimePeriod);
// computeQueryVO.setStatus(TradeOrderStatusEnum.COMPLETED.getStatus());
// List<TradeOrderDO> computeOrderList = tradeOrderQueryService.getOrderList(computeQueryVO);
List<ResourceOrderRespDTO> resourceOrderList = computeOrderStatisticsApi.getPaidOrderList(allTimePeriod);
List<ApiOrderRespDTO> apiOrderList = apiOrderApi.getPaidOrderList(allTimePeriod);
// 3. 按节点分组统计:数量 + 金额
Map<LocalDate, Long> computeCountMap = groupOrderByNode(computeOrderList, timeNodes, dateType);
Map<LocalDate, Long> computeCountMap = groupOrderByNode(resourceOrderList, timeNodes, dateType);
Map<LocalDate, Long> apiCountMap = groupOrderByNode(apiOrderList, timeNodes, dateType);
Map<LocalDate, Integer> computeAmountMap = groupComputeAmountByNode(computeOrderList, timeNodes, dateType);
Map<LocalDate, Integer> computeAmountMap = groupComputeAmountByNode(resourceOrderList, timeNodes, dateType);
Map<LocalDate, Integer> apiAmountMap = groupApiAmountByNode(apiOrderList, timeNodes, dateType);
// 4. 构建返回结果
......@@ -395,15 +395,15 @@ public class HomeIndexServiceImpl implements HomeIndexService {
/**
* 算力订单金额分组统计(支持d/m/y)
*/
private Map<LocalDate, Integer> groupComputeAmountByNode(List<TradeOrderDO> orderList, List<LocalDate> nodes, String dateType) {
private Map<LocalDate, Integer> groupComputeAmountByNode(List<ResourceOrderRespDTO> orderList, List<LocalDate> nodes, String dateType) {
if (CollectionUtils.isEmpty(orderList)) {
return new HashMap<>();
}
return nodes.stream().collect(Collectors.toMap(
node -> node,
node -> orderList.stream()
node -> (int) orderList.stream()
.filter(order -> isDateMatch(order.getCreateTime().toLocalDate(), node, dateType))
.mapToInt(TradeOrderDO::getPayPrice)
.mapToLong(order -> order.getPaymentPrice() != null ? order.getPaymentPrice() : 0L)
.sum()
));
}
......
package com.luhu.computility.module.compute.api.order;
import com.luhu.computility.module.compute.api.order.dto.ComputeOrderStatisticsDTO;
import com.luhu.computility.module.compute.api.order.dto.ResourceOrderRespDTO;
import java.time.LocalDateTime;
import java.util.List;
/**
* 算力资源订单统计API接口
*
* @author jony
*/
public interface ComputeOrderStatisticsApi {
/**
* 获取今日已完成的算力资源订单统计
*
* @param timeRange 时间范围 [开始时间, 结束时间]
* @return 订单统计信息(数量和总金额)
*/
ComputeOrderStatisticsDTO getTodayOrderStatistics(LocalDateTime[] timeRange);
List<ResourceOrderRespDTO> getPaidOrderList(LocalDateTime[] timeRange);
}
\ No newline at end of file
package com.luhu.computility.module.compute.api.order.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 算力资源订单统计DTO
*
* @author jony
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ComputeOrderStatisticsDTO {
/**
* 订单数量
*/
private Integer count;
/**
* 订单总金额(分)
*/
private Integer totalAmount;
}
\ No newline at end of file
package com.luhu.computility.module.compute.api.order.dto;
import lombok.Data;
import java.time.LocalDateTime;
/**
* @Author: jony
* @Date : 2025/11/3 09:06
* @VERSION v1.0
*/
@Data
public class ResourceOrderRespDTO {
private Long id;
/**
* 下单用户ID
*/
private Long userId;
/**
* 用户IP
*/
private String userIp;
/**
* 算力资源SKU ID
*/
private Long skuId;
/**
* 算力资源名称(下单时快照)
*/
private String spuName;
/**
* 订单编号
*/
private String orderNo;
/**
* 订单状态:0=待支付,1=已支付,2=已取消
*/
private Integer status;
/**
* 市场价格(分)
*/
private Long marketPrice;
/**
* 实付金额(分)
*/
private Long paymentPrice;
/**
* 支付订单编号
*/
private Long payOrderId;
/**
* 支付时间
*/
private LocalDateTime payTime;
/**
* 支付渠道
*/
private String payChannelCode;
/**
* 租赁开始时间
*/
private LocalDateTime rentStartTime;
/**
* 租赁结束时间
*/
private LocalDateTime rentEndTime;
/**
* 取消时间
*/
private LocalDateTime cancelTime;
/**
* 备注
*/
private String remark;
/**
* 算力资源状态:[0]未启用,[1]使用中,[2]已释放
*/
private Integer resourceStatus;
/**
* 退款状态
*/
private Integer refundStatus;
/**
* 退款金额
*/
private String refundPrice;
/**
* 开票状态:[0]未开 [1]开票中 [2]已开票
*/
private Integer invoiceStatus;
/**
* 发票链接
*/
private String invoiceUrl;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 最后更新时间
*/
private LocalDateTime updateTime;
/**
* 创建者,目前使用 SysUser 的 id 编号
*
* 使用 String 类型的原因是,未来可能会存在非数值的情况,留好拓展性。
*/
private String creator;
/**
* 更新者,目前使用 SysUser 的 id 编号
*
* 使用 String 类型的原因是,未来可能会存在非数值的情况,留好拓展性。
*/
private String updater;
/**
* 是否删除
*/
private Boolean deleted;
}
......@@ -11,6 +11,10 @@ public interface ErrorCodeConstants {
ErrorCode RESOURCE_CATEGORY_NOT_EXISTS = new ErrorCode(1_030_001_000, "算力资源分类表(仅用于算力服务器分类)不存在");
ErrorCode RESOURCE_SKU_NOT_EXISTS = new ErrorCode(1_030_002_000, "算力资源SKU表(价格和租赁信息)不存在");
ErrorCode RESOURCE_SPU_NOT_EXISTS = new ErrorCode(1_030_003_000, "算力资源SPU表(基础配置信息)不存在");
/**
* 算力资源SPU库存不足
*/
ErrorCode RESOURCE_SPU_STOCK_NOT_ENOUGH = new ErrorCode(1_030_003_001, "算力资源SPU库存不足,订单创建失败");
ErrorCode RESOURCE_ORDER_NOT_EXISTS = new ErrorCode(1_030_004_000, "算力资源订单不存在");
ErrorCode RESOURCE_ORDER_SKU_NOT_EXISTS = new ErrorCode(1_030_004_001, "算力资源SKU不存在");
ErrorCode RESOURCE_ORDER_SPU_NOT_EXISTS = new ErrorCode(1_030_004_002, "算力资源SPU不存在");
......
......@@ -14,7 +14,8 @@ public enum ResourceOrderStatus {
UNPAID(0, "待支付"),
PAID(1, "已支付"),
CANCELED(2, "已取消");
CANCELED(2, "已取消"),
REFUNDED(3, "已退款");
private final Integer value;
private final String label;
......
......@@ -11,8 +11,8 @@ import lombok.Getter;
*/
@Getter
public enum ResourceSkuStatus {
ONLINE(0, "上架"),
OFFLINE(1, "下架");
OFFLINE(0, "下架"),
ONLINE(1, "上架");
private final Integer value;
private final String label;
......
package com.luhu.computility.module.compute.controller.admin.notify;
import cn.hutool.core.util.StrUtil;
import com.luhu.computility.framework.common.pojo.CommonResult;
import com.luhu.computility.framework.common.util.json.JsonUtils;
import com.luhu.computility.module.pay.controller.admin.notify.vo.WpgjPayNotifyDTO;
import com.luhu.computility.module.pay.controller.admin.notify.vo.WpgjPayNotifyRespDTO;
import com.luhu.computility.module.compute.service.resourceorder.ResourceOrderService;
import com.luhu.computility.module.pay.enums.WpgjOrderStatusEnum;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RequestBody;
import javax.annotation.Resource;
import javax.annotation.security.PermitAll;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.digest.DigestUtil;
import com.luhu.computility.module.pay.framework.pay.core.client.impl.wpgj.WpgjPayProperties;
import java.util.Map;
import java.util.TreeMap;
import java.util.stream.Collectors;
/**
* 算力资源模块 - WPGJ旺铺聚合支付回调Controller
*
* 独立处理WPGJ支付回调,直接更新算力资源订单状态
*
* @author jony
*/
@Tag(name = "管理后台 - 算力资源WPGJ旺铺聚合支付回调")
@RestController
@RequestMapping("/compute/wpgj")
@Slf4j
public class ComputeWpgjPayController {
@Resource
private ResourceOrderService resourceOrderService;
@Resource
private WpgjPayProperties wpgjPayProperties;
@PostMapping("/notify")
@PermitAll
@Operation(summary = "WPGJ支付异步回调通知(新) - 按文档通用验签")
public WpgjPayNotifyRespDTO notifyWpgjPay(@RequestBody WpgjPayNotifyDTO notifyDTO) {
WpgjPayNotifyRespDTO response = new WpgjPayNotifyRespDTO();
try {
log.info("[notifyWpgjPay][Compute] 收到WPGJ支付回调(新): {}", JsonUtils.toJsonString(notifyDTO));
// 1. 验证签名(对所有出现的非空字段,按 ASCII 升序拼接,排除 sign)
if (!verifyWpgjSignature(toPayloadMap(notifyDTO))) {
log.error("[notifyWpgjPay][Compute] WPGJ回调签名验证失败");
response.setCode("99");
response.setMsg("签名验证失败");
response.setTimestamp(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS")));
return response;
}
String merOrderId = notifyDTO.getMerOrderId();
String orderStatus = notifyDTO.getOrderStatus();
log.info("[notifyWpgjPay] 处理WPGJ支付结果,商户订单号: {}, 订单状态: {}", merOrderId, orderStatus);
// 2. 幂等处理:根据订单状态更新(与老接口保持一致逻辑)
if (WpgjOrderStatusEnum.isSuccess(orderStatus)) {
resourceOrderService.updateOrderPaidByWpgj(Long.parseLong(merOrderId),notifyDTO);
log.info("[notifyWpgjPay] 支付成功处理完成,商户订单号: {}", merOrderId);
} else if (WpgjOrderStatusEnum.isFailedOrClosed(orderStatus)) {
resourceOrderService.updateOrderFailedByWpgj(Long.parseLong(merOrderId), notifyDTO);
log.info("[notifyWpgjPay] 支付失败/关闭处理完成,商户订单号: {}", merOrderId);
} else {
log.info("[notifyWpgjPay] 订单状态无需处理,商户订单号: {}, 状态: {}", merOrderId, orderStatus);
}
// 3. 返回成功应答
response.setCode("00");
response.setMsg("成功");
response.setTimestamp(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS")));
log.info("[notifyWpgjPay][Compute] 返回wpgj参数:{}", JsonUtils.toJsonString(response));
return response;
} catch (Exception e) {
log.error("[notifyWpgjPay][Compute] WPGJ支付回调处理失败", e);
response.setCode("99");
response.setMsg("处理失败");
response.setTimestamp(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS")));
return response;
}
}
/**
* Map 形式的回调请求验签:对所有出现的非空字段(排除 sign)进行 ASCII 排序拼接后 + key,再 MD5(UTF-8) 大写
*/
private boolean verifyWpgjSignature(Map<String, Object> payload) {
try {
Object signObj = payload.get("sign");
String originSign = signObj == null ? null : String.valueOf(signObj);
if (StrUtil.isBlank(originSign)) {
log.warn("[verifyWpgjSignature][Compute] 回调缺少 sign 字段");
return false;
}
// 过滤掉 sign、自身为空的字段,按 ASCII 升序
TreeMap<String, String> sorted = new TreeMap<>();
for (Map.Entry<String, Object> e : payload.entrySet()) {
String k = e.getKey();
if ("sign".equals(k)) {
continue;
}
String v = e.getValue() == null ? null : String.valueOf(e.getValue());
if (StrUtil.isBlank(v)) {
continue;
}
sorted.put(k, v);
}
String dataStr = sorted.entrySet().stream()
.map(en -> en.getKey() + "=" + en.getValue())
.collect(Collectors.joining("&"));
String signStr = dataStr + "&key=" + wpgjPayProperties.getSignKey();
String calculatedSign = DigestUtil.md5Hex(signStr).toUpperCase();
log.info("[verifyWpgjSignature][Compute-New] 待签名字符串: {}", dataStr);
log.info("[verifyWpgjSignature][Compute-New] 加签前(带key): {}", signStr);
log.info("[verifyWpgjSignature][Compute-New] 计算签名: {},原始签名: {}", calculatedSign, originSign);
return calculatedSign.equalsIgnoreCase(originSign);
} catch (Exception e) {
log.error("[verifyWpgjSignature][Compute-New] 验签异常", e);
return false;
}
}
/**
* 将 DTO 转换为 Map(以供应商字段名 snake_case 作为 key),便于统一验签
*/
private Map<String, Object> toPayloadMap(WpgjPayNotifyDTO dto) {
TreeMap<String, Object> map = new TreeMap<>();
map.put("device_no", dto.getDeviceNo());
map.put("mer_no", dto.getMerNo());
map.put("mer_code", dto.getMerCode());
map.put("payway_code", dto.getPaywayCode());
map.put("order_id", dto.getOrderId());
map.put("mer_order_id", dto.getMerOrderId());
map.put("gateway_mer_order_id", dto.getGatewayMerOrderId());
map.put("order_time", dto.getOrderTime());
map.put("order_amt", dto.getOrderAmt());
map.put("order_status", dto.getOrderStatus());
map.put("trade_no", dto.getTradeNo());
map.put("trade_time", dto.getTradeTime());
map.put("order_title", dto.getOrderTitle());
map.put("fee", dto.getFee());
map.put("act_amt", dto.getActAmt());
map.put("buyer_id", dto.getBuyerId());
map.put("trade_top_no", dto.getTradeTopNo());
map.put("card_type", dto.getCardType());
map.put("sign", dto.getSign());
return map;
}
}
package com.luhu.computility.module.compute.controller.admin.resourceorder;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.luhu.computility.framework.common.exception.ServiceException;
import com.luhu.computility.framework.common.pojo.CommonResult;
import com.luhu.computility.framework.common.pojo.PageResult;
import com.luhu.computility.framework.common.util.json.JsonUtils;
import com.luhu.computility.module.pay.framework.pay.core.client.dto.order.PayOrderRespDTO;
import com.luhu.computility.module.pay.service.order.PayOrderService;
import com.luhu.computility.module.compute.controller.admin.resourceorder.vo.*;
import com.luhu.computility.module.compute.enums.ResourceOrderInvoiceStatus;
import com.luhu.computility.module.compute.enums.ResourceOrderStatus;
......@@ -11,6 +15,8 @@ import io.swagger.v3.oas.annotations.Parameter;
import org.springframework.security.access.prepost.PreAuthorize;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import com.luhu.computility.framework.excel.core.util.ExcelUtils;
......@@ -32,6 +38,7 @@ import org.springframework.validation.annotation.Validated;
import static com.luhu.computility.framework.common.pojo.CommonResult.success;
import com.luhu.computility.framework.common.util.object.BeanUtils;
import com.luhu.computility.framework.tenant.core.aop.TenantIgnore;
@Tag(name = "管理后台 - 算力资源订单")
@RestController
......@@ -43,6 +50,10 @@ public class ResourceOrderController {
@Resource
private ResourceOrderService resourceOrderService;
@Resource
private PayOrderService payOrderService;
/**
* 内部支付任务回调
*/
......@@ -55,6 +66,8 @@ public class ResourceOrderController {
return success(true);
}
@PostMapping("/create")
@Operation(summary = "创建算力资源订单")
@PreAuthorize("@ss.hasPermission('compute:resource-order:create')")
......
......@@ -36,6 +36,14 @@ public class ResourceSpuRespVO {
@ExcelProperty("存储配置")
private String storage;
@Schema(description = "电源配置", requiredMode = Schema.RequiredMode.REQUIRED)
@ExcelProperty("电源配置")
private String powerSupply;
@Schema(description = "网卡配置", requiredMode = Schema.RequiredMode.REQUIRED)
@ExcelProperty("网卡配置")
private String nic;
@Schema(description = "服务器ip", requiredMode = Schema.RequiredMode.REQUIRED)
@ExcelProperty("服务器ip")
private String ip;
......
......@@ -7,6 +7,7 @@ import com.luhu.computility.module.compute.controller.app.resourceorder.vo.AppRe
import com.luhu.computility.module.compute.controller.app.resourceorder.vo.AppResourceOrderPageReqVO;
import com.luhu.computility.module.compute.controller.app.resourceorder.vo.AppResourceOrderRespVO;
import com.luhu.computility.module.compute.controller.app.resourceorder.vo.AppResourceOrderInvoiceReqVO;
import com.luhu.computility.module.compute.controller.app.resourceorder.vo.AppWpgjPayOrderSubmitRespVO;
import com.luhu.computility.module.compute.service.resourceorder.ResourceOrderService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
......@@ -39,6 +40,14 @@ public class AppResourceOrderController {
return success(respVO);
}
@PostMapping("/create-wpgj")
@Operation(summary = "创建算力资源订单(旺铺聚合支付)")
public CommonResult<AppWpgjPayOrderSubmitRespVO> createResourceOrderWithWpgj(@Valid @RequestBody AppResourceOrderCreateReqVO createReqVO) {
Long userId = getLoginUserId();
AppWpgjPayOrderSubmitRespVO respVO = resourceOrderService.createUserResourceOrderWithWpgj(userId, createReqVO);
return success(respVO);
}
@PutMapping("/cancel")
@Operation(summary = "取消算力资源订单")
public CommonResult<Boolean> cancelResourceOrder(@RequestParam("orderId") Long orderId) {
......
......@@ -16,4 +16,10 @@ public class AppResourceOrderCreateRespVO {
@Schema(description = "支付订单ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "15798")
private Long payOrderId;
@Schema(description = "支付二维码URL(WPGJ聚合支付)")
private String payQrCode;
@Schema(description = "支付展示模式(qr_code=二维码,pay_url=支付链接)")
private String displayMode;
}
\ No newline at end of file
......@@ -64,6 +64,12 @@ public class AppResourceOrderRespVO {
@Schema(description = "存储配置", example = "2TB NVMe SSD")
private String storage;
@Schema(description = "电源配置", example = "3500W")
private String powerSupply;
@Schema(description = "网卡配置", example = "2Gbps")
private String nic;
@Schema(description = "服务器IP", example = "192.168.1.100")
private String ip;
......
package com.luhu.computility.module.compute.controller.app.resourceorder.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import javax.validation.constraints.NotNull;
/**
* 用户 APP - WPGJ旺铺聚合支付订单提交 Response VO
* 独立于老支付系统的WPGJ支付响应
*
* @author jony
*/
@Schema(description = "用户 APP - WPGJ旺铺聚合支付订单提交 Response VO")
@Data
public class AppWpgjPayOrderSubmitRespVO {
@Schema(description = "订单ID(用于前端轮询支付状态)", requiredMode = Schema.RequiredMode.REQUIRED, example = "123456")
@NotNull(message = "订单ID不能为空")
private Long id;
@Schema(description = "支付状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
@NotNull(message = "支付状态不能为空")
private Integer status;
@Schema(description = "展示模式", example = "url")
private String displayMode;
@Schema(description = "展示内容(支付链接等)", example = "https://example.com/pay")
private String displayContent;
}
\ No newline at end of file
......@@ -88,7 +88,8 @@ public class AppResourceSkuController {
respVO.setGpu(spu.getGpu());
respVO.setRam(spu.getRam());
respVO.setStorage(spu.getStorage());
respVO.setIp(spu.getIp());
respVO.setPowerSupply(spu.getPowerSupply());
respVO.setNic(spu.getNic());
respVO.setLocation(spu.getLocation());
return respVO;
......
......@@ -35,8 +35,11 @@ public class AppResourceSkuRespVO {
@Schema(description = "存储配置", example = "2TB NVMe SSD")
private String storage;
@Schema(description = "服务器IP", example = "192.168.1.100")
private String ip;
@Schema(description = "电源配置", example = "3500W")
private String powerSupply;
@Schema(description = "网卡配置", example = "2Gbps")
private String nic;
@Schema(description = "服务器所在地", example = "深圳")
private String location;
......
......@@ -48,6 +48,14 @@ public class ResourceSpuDO extends BaseDO {
*/
private String storage;
/**
* 电源配置
*/
private String powerSupply;
/**
* 网卡配置
*/
private String nic;
/**
* 服务器ip
*/
private String ip;
......
package com.luhu.computility.module.compute.dal.mysql.resourceorder;
import java.time.LocalDateTime;
import java.util.*;
import cn.hutool.core.bean.BeanUtil;
import com.luhu.computility.framework.common.pojo.PageResult;
import com.luhu.computility.framework.mybatis.core.query.LambdaQueryWrapperX;
import com.luhu.computility.framework.mybatis.core.mapper.BaseMapperX;
import com.luhu.computility.framework.mybatis.core.query.MPJLambdaWrapperX;
import com.luhu.computility.module.compute.api.order.dto.ResourceOrderRespDTO;
import com.luhu.computility.module.compute.dal.dataobject.resourceorder.ResourceOrderDO;
import com.luhu.computility.module.member.dal.dataobject.user.MemberUserDO;
import org.apache.ibatis.annotations.Mapper;
import com.luhu.computility.module.compute.controller.admin.resourceorder.vo.*;
import static com.luhu.computility.module.compute.enums.ResourceOrderStatus.PAID;
/**
* 算力资源订单 Mapper
*
......@@ -23,6 +28,7 @@ public interface ResourceOrderMapper extends BaseMapperX<ResourceOrderDO> {
return selectJoinPage(reqVO, ResourceOrderRespVO.class, new MPJLambdaWrapperX<ResourceOrderDO>()
.selectAll(ResourceOrderDO.class)
.selectAs(MemberUserDO::getNickname, ResourceOrderRespVO::getNickname)
.selectAs(MemberUserDO::getMobile, ResourceOrderRespVO::getMobile)
.leftJoin(MemberUserDO.class, MemberUserDO::getId, ResourceOrderDO::getUserId)
.eqIfPresent(ResourceOrderDO::getUserId, reqVO.getUserId())
.eqIfPresent(ResourceOrderDO::getSkuId, reqVO.getSkuId())
......@@ -72,4 +78,24 @@ public interface ResourceOrderMapper extends BaseMapperX<ResourceOrderDO> {
.orderByDesc(ResourceOrderDO::getId));
}
default ResourceOrderDO selectByOrderNo(String orderNo) {
return selectOne(new LambdaQueryWrapperX<ResourceOrderDO>()
.eq(ResourceOrderDO::getOrderNo, orderNo)
.eq(ResourceOrderDO::getDeleted, false));
}
default List<ResourceOrderDO> selectListByStatusAndCreateTime(Integer status, LocalDateTime[] timeRange) {
return selectList(new LambdaQueryWrapperX<ResourceOrderDO>()
.eq(ResourceOrderDO::getStatus, status)
.betweenIfPresent(ResourceOrderDO::getCreateTime, timeRange)
.orderByDesc(ResourceOrderDO::getId));
}
default List<ResourceOrderRespDTO> getPaidOrderList(LocalDateTime[] timeRange) {
List<ResourceOrderDO> list = selectList(new LambdaQueryWrapperX<ResourceOrderDO>()
.eq(ResourceOrderDO::getStatus, PAID.getValue())
.betweenIfPresent(ResourceOrderDO::getCreateTime, timeRange));
return BeanUtil.copyToList(list, ResourceOrderRespDTO.class);
}
}
\ No newline at end of file
......@@ -28,4 +28,25 @@ public interface ResourceSkuMapper extends BaseMapperX<ResourceSkuDO> {
.orderByDesc(ResourceSkuDO::getId));
}
/**
* 将指定 SPU 下的所有 SKU 批量下架
*
* @param spuId 关联的 SPU ID
* @param status 目标状态(通常为下架 0)
* @return 受影响行数
*/
@org.apache.ibatis.annotations.Update("UPDATE compute_resource_sku SET status = #{status} WHERE spu_id = #{spuId}")
int updateStatusBySpuId(@org.apache.ibatis.annotations.Param("spuId") Long spuId,
@org.apache.ibatis.annotations.Param("status") Integer status);
/**
* 根据SPU ID删除SKU表数据
*
* @param spuId 关联的 SPU ID
* @return 受影响行数
*/
default int deleteBySpuId(Long spuId) {
return delete(new LambdaQueryWrapperX<ResourceSkuDO>().eq(ResourceSkuDO::getSpuId, spuId));
}
}
\ No newline at end of file
......@@ -44,4 +44,13 @@ public interface ResourceSpuMapper extends BaseMapperX<ResourceSpuDO> {
.orderByDesc(ResourceSpuDO::getId));
}
/**
* 原子递减库存:仅当库存大于0时将库存-1
*
* @param id SPU 主键ID
* @return 受影响行数(1 表示成功递减,0 表示库存不足或不存在)
*/
@org.apache.ibatis.annotations.Update("UPDATE compute_resource_spu SET stock = stock - 1 WHERE id = #{id} AND stock > 0")
int decrementStock(@org.apache.ibatis.annotations.Param("id") Long id);
}
\ No newline at end of file
package com.luhu.computility.module.compute.service.expire;
import com.luhu.computility.module.compute.dal.mysql.resourceorder.ResourceOrderMapper;
import com.luhu.computility.module.compute.dal.dataobject.resourceorder.ResourceOrderDO;
import com.luhu.computility.module.compute.enums.ResourceOrderStatus;
import com.luhu.computility.module.pay.enums.OrderBusinessTypeEnum;
import com.luhu.computility.module.pay.service.expire.BusinessOrderExpireService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.time.LocalDateTime;
/**
* 算力资源订单过期服务实现类
*
* @author jony
*/
@Service
@Slf4j
public class ComputeOrderExpireServiceImpl implements BusinessOrderExpireService {
@Resource
private ResourceOrderMapper resourceOrderMapper;
@Override
public OrderBusinessTypeEnum getSupportedBusinessType() {
return OrderBusinessTypeEnum.COMPUTE_RESOURCE;
}
@Override
public boolean expireBusinessOrder(Long businessOrderId) {
try {
// 查询订单
ResourceOrderDO order = resourceOrderMapper.selectById(businessOrderId);
if (order == null) {
log.warn("[expireBusinessOrder] 算力资源订单不存在,订单ID: {}", businessOrderId);
return false;
}
// 检查订单状态,只有待支付的订单才能过期
if (!ResourceOrderStatus.UNPAID.getValue().equals(order.getStatus())) {
log.warn("[expireBusinessOrder] 算力资源订单状态不是待支付,无法过期,订单ID: {}, 当前状态: {}",
businessOrderId, order.getStatus());
return false;
}
// 更新订单状态为已取消
ResourceOrderDO updateOrder = new ResourceOrderDO();
updateOrder.setId(businessOrderId);
updateOrder.setStatus(ResourceOrderStatus.CANCELED.getValue());
updateOrder.setCancelTime(LocalDateTime.now());
int updateCount = resourceOrderMapper.updateById(updateOrder);
if (updateCount > 0) {
log.info("[expireBusinessOrder] 算力资源订单过期成功,订单ID: {}", businessOrderId);
return true;
} else {
log.error("[expireBusinessOrder] 算力资源订单过期失败,更新数据库失败,订单ID: {}", businessOrderId);
return false;
}
} catch (Exception e) {
log.error("[expireBusinessOrder] 过期算力资源订单异常,订单ID: {}", businessOrderId, e);
return false;
}
}
}
\ No newline at end of file
package com.luhu.computility.module.compute.service.impl;
import com.luhu.computility.module.compute.api.order.ComputeOrderStatisticsApi;
import com.luhu.computility.module.compute.api.order.dto.ComputeOrderStatisticsDTO;
import com.luhu.computility.module.compute.api.order.dto.ResourceOrderRespDTO;
import com.luhu.computility.module.compute.dal.dataobject.resourceorder.ResourceOrderDO;
import com.luhu.computility.module.compute.dal.mysql.resourceorder.ResourceOrderMapper;
import com.luhu.computility.module.compute.enums.ResourceOrderStatus;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.List;
/**
* 算力资源订单统计API实现类
*
* @author jony
*/
@Service
@Slf4j
public class ComputeOrderStatisticsApiImpl implements ComputeOrderStatisticsApi {
@Resource
private ResourceOrderMapper resourceOrderMapper;
@Override
public ComputeOrderStatisticsDTO getTodayOrderStatistics(LocalDateTime[] timeRange) {
try {
List<ResourceOrderDO> orders = resourceOrderMapper.selectListByStatusAndCreateTime(
ResourceOrderStatus.PAID.getValue(),
timeRange
);
int count = orders.size();
int totalAmount = (int) orders.stream()
.mapToLong(order -> order.getPaymentPrice() != null ? order.getPaymentPrice() : 0L)
.sum();
return new ComputeOrderStatisticsDTO(count, totalAmount);
} catch (Exception e) {
log.error("[getTodayOrderStatistics] 获取今日算力资源订单统计失败", e);
throw e;
}
}
@Override
public List<ResourceOrderRespDTO> getPaidOrderList(LocalDateTime[] timeRange) {
return resourceOrderMapper.getPaidOrderList(timeRange);
}
}
\ No newline at end of file
......@@ -36,7 +36,8 @@ public class ResourceConfigServiceImpl implements ResourceConfigService {
if (ObjectUtil.isEmpty(category)) {
queryVo.setConfigCategory(null);
} else {
queryVo.setConfigCategory(category);
//power_supply是驼峰命名,需要转换
queryVo.setConfigCategory(category.replaceAll("([a-z])([A-Z])", "$1_$2").toLowerCase());
}
queryVo.setStatus(ENABLE.getValue());
List<ResourceConfigDO> list = resourceConfigMapper.selectPage(queryVo).getList();
......
......@@ -8,6 +8,8 @@ import com.luhu.computility.module.compute.controller.app.resourceorder.vo.AppRe
import com.luhu.computility.module.compute.controller.app.resourceorder.vo.AppResourceOrderPageReqVO;
import com.luhu.computility.module.compute.controller.app.resourceorder.vo.AppResourceOrderRespVO;
import com.luhu.computility.module.compute.controller.app.resourceorder.vo.AppResourceOrderInvoiceReqVO;
import com.luhu.computility.module.compute.controller.app.resourceorder.vo.AppWpgjPayOrderSubmitRespVO;
import com.luhu.computility.module.pay.controller.admin.notify.vo.WpgjPayNotifyDTO;
import com.luhu.computility.module.compute.dal.dataobject.resourceorder.ResourceOrderDO;
import com.luhu.computility.framework.common.pojo.PageResult;
import com.luhu.computility.framework.common.pojo.PageParam;
......@@ -74,6 +76,15 @@ public interface ResourceOrderService {
AppResourceOrderCreateRespVO createUserResourceOrder(Long userId, @Valid AppResourceOrderCreateReqVO createReqVO);
/**
* 用户创建算力资源订单(旺铺聚合支付)
*
* @param userId 用户ID
* @param createReqVO 创建信息
* @return WPGJ支付提交响应
*/
AppWpgjPayOrderSubmitRespVO createUserResourceOrderWithWpgj(Long userId, @Valid AppResourceOrderCreateReqVO createReqVO);
/**
* 更新订单为已支付(支付回调使用)
*
* @param orderId 订单ID
......@@ -122,4 +133,28 @@ public interface ResourceOrderService {
*/
boolean updateRequestInvoice(AppResourceOrderInvoiceReqVO reqVO);
/**
* 根据订单号获取算力资源订单
*
* @param orderNo 订单号
* @return 算力资源订单
*/
ResourceOrderDO getResourceOrderByOrderNo(String orderNo);
/**
* WPGJ支付成功回调更新订单状态
*
* @param orderId 订单ID
* @param notifyDTO WPGJ回调通知
*/
void updateOrderPaidByWpgj(Long orderId, WpgjPayNotifyDTO notifyDTO);
/**
* WPGJ支付失败回调更新订单状态
*
* @param orderId 订单ID
* @param notifyDTO WPGJ回调通知
*/
void updateOrderFailedByWpgj(Long orderId, WpgjPayNotifyDTO notifyDTO);
}
\ No newline at end of file
......@@ -2,7 +2,7 @@ package com.luhu.computility.module.compute.service.resourcespu;
import cn.hutool.core.collection.CollUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.luhu.computility.module.compute.dal.mysql.resourcesku.ResourceSkuMapper;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import org.springframework.validation.annotation.Validated;
......@@ -36,6 +36,9 @@ public class ResourceSpuServiceImpl implements ResourceSpuService {
@Resource
private ResourceSpuMapper resourceSpuMapper;
@Resource
private ResourceSkuMapper resourceSkuMapper;
@Override
public Long createResourceSpu(ResourceSpuSaveReqVO createReqVO) {
// 插入
......@@ -47,18 +50,31 @@ public class ResourceSpuServiceImpl implements ResourceSpuService {
}
@Override
@Transactional(rollbackFor = Exception.class)
public void updateResourceSpu(ResourceSpuSaveReqVO updateReqVO) {
// 校验存在
validateResourceSpuExists(updateReqVO.getId());
// 更新
// 获取更新前的状态
ResourceSpuDO existingSpu = resourceSpuMapper.selectById(updateReqVO.getId());
Integer oldStatus = existingSpu.getStatus();
// 更新SPU
ResourceSpuDO updateObj = BeanUtils.toBean(updateReqVO, ResourceSpuDO.class);
resourceSpuMapper.updateById(updateObj);
// 如果状态发生变化,同步更新关联的SKU状态
Integer newStatus = updateObj.getStatus();
if (!Objects.equals(oldStatus, newStatus)) {
resourceSkuMapper.updateStatusBySpuId(updateReqVO.getId(), newStatus);
}
}
@Override
public void deleteResourceSpu(Long id) {
// 校验存在
validateResourceSpuExists(id);
resourceSkuMapper.deleteBySpuId(id);
// 删除
resourceSpuMapper.deleteById(id);
}
......@@ -88,8 +104,7 @@ public class ResourceSpuServiceImpl implements ResourceSpuService {
@Override
public List<ResourceSpuSimpleRespVO> getResourceSimpleSpuList() {
List<ResourceSpuDO> resourceSpuDOList = resourceSpuMapper.selectList(ResourceSpuDO::getStatus, ONLINE.getValue(),
ResourceSpuDO::getDeleted,0);
List<ResourceSpuDO> resourceSpuDOList = resourceSpuMapper.selectList(ResourceSpuDO::getDeleted,0);
return BeanUtils.toBean(resourceSpuDOList, ResourceSpuSimpleRespVO.class);
}
......@@ -121,31 +136,4 @@ public class ResourceSpuServiceImpl implements ResourceSpuService {
return resourceSpuMapper.selectList(wrapper);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void updateSalesAndStock(Long spuId) {
// 校验SPU存在
ResourceSpuDO spu = resourceSpuMapper.selectById(spuId);
if (spu == null) {
throw exception(RESOURCE_SPU_NOT_EXISTS);
}
// 检查库存是否充足
if (spu.getStock() <= 0) {
throw exception(RESOURCE_SPU_STOCK_INSUFFICIENT);
}
// 使用LambdaUpdateWrapper实现原子性的销量+1和库存-1
LambdaUpdateWrapper<ResourceSpuDO> updateWrapper = new LambdaUpdateWrapper<>();
updateWrapper.eq(ResourceSpuDO::getId, spuId)
.gt(ResourceSpuDO::getStock, 0) // 确保库存大于0
.setSql("sales = sales + 1")
.setSql("stock = stock - 1");
int updateCount = resourceSpuMapper.update(null, updateWrapper);
if (updateCount == 0) {
throw exception(RESOURCE_SPU_STOCK_INSUFFICIENT);
}
}
}
\ No newline at end of file
......@@ -31,6 +31,18 @@
<artifactId>computility-spring-boot-starter-biz-tenant</artifactId>
</dependency>
<!-- 业务模块API依赖 -->
<dependency>
<groupId>com.luhu</groupId>
<artifactId>computility-module-compute-api</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>com.luhu</groupId>
<artifactId>computility-module-apihub-api</artifactId>
<version>${revision}</version>
</dependency>
<!-- Web 相关 -->
<dependency>
<groupId>com.luhu</groupId>
......
......@@ -2,6 +2,8 @@ package com.luhu.computility.module.pay.api.order;
import com.luhu.computility.module.pay.api.order.dto.PayOrderCreateReqDTO;
import com.luhu.computility.module.pay.api.order.dto.PayOrderRespDTO;
import com.luhu.computility.module.pay.controller.app.order.vo.AppPayOrderSubmitRespVO;
import com.luhu.computility.module.pay.api.wpgj.vo.WpgjPayOrderSubmitReqVO;
import javax.validation.Valid;
......@@ -21,6 +23,7 @@ public interface PayOrderApi {
*/
Long createOrder(@Valid PayOrderCreateReqDTO reqDTO);
/**
* 获得支付单
*
......@@ -37,4 +40,12 @@ public interface PayOrderApi {
*/
void updatePayOrderPrice(Long id, Integer payPrice);
/**
* 提交WPGJ旺铺聚合支付订单
*
* @param reqVO 提交请求
* @return 支付提交响应
*/
AppPayOrderSubmitRespVO submitWpgjOrder(@Valid WpgjPayOrderSubmitReqVO reqVO);
}
package com.luhu.computility.module.pay.api.order.dto;
import lombok.Data;
/**
* WPGJ旺铺聚合支付响应 DTO
*
* @author jonyl
*/
@Data
public class WpgjPayOrderRespDTO {
/**
* 商户订单号
*/
private String merchantOrderId;
/**
* 支付状态
*/
private Integer status;
/**
* 展示模式
*/
private String displayMode;
/**
* 展示内容(支付URL)
*/
private String displayContent;
/**
* 渠道错误码
*/
private String channelErrorCode;
/**
* 渠道错误信息
*/
private String channelErrorMsg;
}
\ No newline at end of file
package com.luhu.computility.module.pay.api.wpgj;
import com.luhu.computility.module.pay.api.wpgj.dto.CommonWpgjCreateReqDTO;
import com.luhu.computility.module.pay.api.wpgj.vo.CommonWpgjPayOrderSubmitRespVO;
import javax.validation.Valid;
/**
* 通用WPGJ支付 API 接口
* 支持算力资源订单和API订单等多种业务类型
*
* @author jony
*/
public interface CommonWpgjPayApi {
/**
* 创建通用WPGJ支付订单
* 支持算力资源订单和API订单等多种业务类型
*
* @param createReqDTO 创建请求
* @return WPGJ支付订单提交响应
*/
CommonWpgjPayOrderSubmitRespVO createCommonWpgjOrder(@Valid CommonWpgjCreateReqDTO createReqDTO);
}
\ No newline at end of file
package com.luhu.computility.module.pay.api.wpgj;
import com.luhu.computility.module.pay.api.wpgj.dto.PayOrderWpgjCreateReqDTO;
import javax.validation.Valid;
/**
* WPGJ旺铺聚合支付订单 API 接口
*
* @author jony
*/
public interface PayOrderWpgjApi {
/**
* 创建WPGJ旺铺聚合支付订单
*
* @param reqDTO 创建请求
* @return WPGJ订单ID
*/
Long createOrderWpgj(@Valid PayOrderWpgjCreateReqDTO reqDTO);
}
\ No newline at end of file
package com.luhu.computility.module.pay.api.wpgj;
import com.luhu.computility.module.pay.api.wpgj.dto.PayOrderWpgjCreateReqDTO;
import com.luhu.computility.module.pay.dal.dataobject.wpgj.PayOrderWpgjDO;
import com.luhu.computility.module.pay.dal.mysql.wpgj.PayOrderWpgjMapper;
import com.luhu.computility.module.pay.framework.pay.core.client.impl.wpgj.WpgjPayProperties;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import static com.luhu.computility.module.pay.enums.WpgjOrderStatusEnum.WAITING;
/**
* WPGJ旺铺聚合支付订单 API 实现类
*
* @author jony
*/
@Service
@Validated
@Slf4j
public class PayOrderWpgjApiImpl implements PayOrderWpgjApi {
@Resource
private PayOrderWpgjMapper payOrderWpgjMapper;
@Resource
private WpgjPayProperties wpgjPayProperties;
@Override
public Long createOrderWpgj(PayOrderWpgjCreateReqDTO reqDTO) {
try {
log.info("[createOrderWpgj] 开始创建WPGJ支付订单,商户订单ID: {}, 订单金额: {}元",
reqDTO.getMerOrderId(), reqDTO.getOrderAmt());
// 1. 构建PayOrderWpgjDO
PayOrderWpgjDO wpgjOrder = new PayOrderWpgjDO();
wpgjOrder.setBusinessType(reqDTO.getBusinessType());
wpgjOrder.setMerOrderId(reqDTO.getMerOrderId());
wpgjOrder.setOrderNo(reqDTO.getOrderNo());
wpgjOrder.setOrderTitle(reqDTO.getOrderTitle());
wpgjOrder.setOrderAmt(reqDTO.getOrderAmt()); // 单位:元
wpgjOrder.setOrderStatus(WAITING.getStatus()); // 0-处理中
wpgjOrder.setOrderTime(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")));
// orderId字段在WPGJ回调时才会设置,创建时保持null
// 从配置获取WPGJ必要字段
if (wpgjPayProperties != null) {
wpgjOrder.setMerNo(wpgjPayProperties.getMerNo());
wpgjOrder.setMerCode(wpgjPayProperties.getMerCode());
}
// deviceNo、paywayCode、sign等字段在WPGJ回调时才会设置,创建时保持null
// 2. 插入pay_order_wpgj表
payOrderWpgjMapper.insert(wpgjOrder);
Long wpgjOrderId = wpgjOrder.getId();
log.info("[createOrderWpgj] WPGJ订单创建成功,商户订单ID: {}, WPGJ订单ID: {}",
reqDTO.getMerOrderId(), wpgjOrderId);
// 3. 返回WPGJ订单ID
return wpgjOrderId;
} catch (Exception e) {
log.error("[createOrderWpgj] 创建WPGJ支付订单失败", e);
throw new RuntimeException("创建支付订单失败: " + e.getMessage());
}
}
}
\ No newline at end of file
package com.luhu.computility.module.pay.api.wpgj.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
/**
* 通用WPGJ支付创建请求 DTO
* 支持算力资源订单和API订单等多种业务类型
*
* @author jony
*/
@Schema(description = "通用WPGJ支付创建请求 DTO")
@Data
public class CommonWpgjCreateReqDTO {
@Schema(description = "业务类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "业务类型不能为空")
private Integer businessType;
@Schema(description = "业务订单ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "123456")
@NotNull(message = "业务订单ID不能为空")
private Long businessOrderId;
@Schema(description = "订单标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "API服务 - 基础包")
@NotBlank(message = "订单标题不能为空")
private String orderTitle;
@Schema(description = "支付金额(分)", requiredMode = Schema.RequiredMode.REQUIRED, example = "100")
@NotNull(message = "支付金额不能为空")
private Integer payAmount;
}
\ No newline at end of file
package com.luhu.computility.module.pay.api.wpgj.dto;
import lombok.Data;
import org.hibernate.validator.constraints.Length;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import java.io.Serializable;
/**
* WPGJ旺铺聚合支付单创建 Request DTO
*/
@Data
public class PayOrderWpgjCreateReqDTO implements Serializable {
/**
* 业务类型 1-算力资源订单 2-API订单
*/
@NotNull(message = "业务类型不能为空")
private Integer businessType;
/**
* 商户订单ID(对应业务订单ID)
*/
@NotEmpty(message = "商户订单ID不能为空")
private String merOrderId;
/**
* 关联的业务订单号
*/
private String orderNo;
/**
* 订单标题
*/
@NotEmpty(message = "订单标题不能为空")
@Length(max = 64, message = "订单标题长度不能超过64")
private String orderTitle;
/**
* 订单金额,单位:元(注意:WPGJ要求单位是元)
*/
@NotEmpty(message = "订单金额不能为空")
private String orderAmt;
}
\ No newline at end of file
package com.luhu.computility.module.pay.api.wpgj.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* 通用WPGJ支付订单提交响应 VO
* 支持算力资源订单和API订单等多种业务类型
*
* @author jony
*/
@Schema(description = "通用WPGJ支付订单提交响应 VO")
@Data
public class CommonWpgjPayOrderSubmitRespVO {
@Schema(description = "WPGJ订单ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "202510301436330934307677")
private Long wpgjOrderId;
@Schema(description = "支付状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
private String status;
@Schema(description = "展示模式", example = "QR_CODE")
private String displayMode;
@Schema(description = "展示内容(支付链接或二维码内容)", example = "https://pay.example.com/qr/123")
private String displayContent;
}
\ No newline at end of file
package com.luhu.computility.module.pay.api.wpgj.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import javax.validation.constraints.NotNull;
/**
* WPGJ旺铺聚合支付订单提交 Request VO
* 独立于老支付系统的WPGJ专用VO
*
* @author jony
*/
@Schema(description = "WPGJ旺铺聚合支付订单提交 Request VO")
@Data
public class WpgjPayOrderSubmitReqVO {
@Schema(description = "WPGJ支付订单ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "123456")
@NotNull(message = "WPGJ支付订单ID不能为空")
private Long id;
}
\ No newline at end of file
......@@ -21,6 +21,7 @@ import com.luhu.computility.module.pay.service.notify.PayNotifyService;
import com.luhu.computility.module.pay.service.order.PayOrderService;
import com.luhu.computility.module.pay.service.refund.PayRefundService;
import com.luhu.computility.module.pay.service.transfer.PayTransferService;
import com.luhu.computility.module.pay.framework.pay.core.client.impl.wpgj.WpgjPayProperties;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
......
package com.luhu.computility.module.pay.controller.admin.notify.vo;
import com.fasterxml.jackson.annotation.JsonAlias;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import javax.validation.constraints.NotBlank;
/**
* WPGJ旺铺聚合支付回调DTO
*
* @author jonyl
*/
@Schema(description = "WPGJ旺铺聚合支付回调DTO")
@Data
public class WpgjPayNotifyDTO {
@Schema(description = "旺铺平台唯一设备SN号", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "设备编号不能为空")
@JsonProperty("device_no")
private String deviceNo;
@Schema(description = "内部商户号", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "商户号不能为空")
@JsonProperty("mer_no")
private String merNo;
@Schema(description = "商户代码", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "商户代码不能为空")
@JsonProperty("mer_code")
private String merCode;
@Schema(description = "支付通道代码", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "支付通道代码不能为空")
@JsonProperty("payway_code")
private String paywayCode;
@Schema(description = "旺铺订单号", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "旺铺订单号不能为空")
@JsonProperty("order_id")
private String orderId;
@Schema(description = "商户订单id,系统唯一", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "商户订单号不能为空")
@JsonProperty("mer_order_id")
private String merOrderId;
@Schema(description = "网关商户订单号")
@JsonProperty("gateway_mer_order_id")
private String gatewayMerOrderId;
@Schema(description = "下单时间", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "下单时间不能为空")
@JsonProperty("order_time")
private String orderTime;
@Schema(description = "订单金额", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "订单金额不能为空")
@JsonProperty("order_amt")
private String orderAmt;
@Schema(description = "订单状态", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "订单状态不能为空")
@JsonProperty("order_status")
private String orderStatus;
@Schema(description = "通道交易流水号")
@JsonProperty("trade_no")
private String tradeNo;
@Schema(description = "交易时间,支付成功返回")
@JsonProperty("trade_time")
private String tradeTime;
@Schema(description = "订单标题")
@JsonProperty("order_title")
private String orderTitle;
@Schema(description = "订单手续费")
@JsonProperty("fee")
private String fee;
@Schema(description = "结算金额")
@JsonProperty("act_amt")
private String actAmt;
@Schema(description = "买家用户号")
@JsonProperty("buyer_id")
@JsonAlias("buyer_user_id")
private String buyerId;
@Schema(description = "对应微信、支付宝小票上交易单号")
@JsonProperty("trade_top_no")
private String tradeTopNo;
@Schema(description = "交易账户类型 C-贷记卡,D-借记卡,U-未知")
@JsonProperty("card_type")
private String cardType;
@Schema(description = "报文签名值", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "签名不能为空")
@JsonProperty("sign")
private String sign;
}
package com.luhu.computility.module.pay.controller.admin.notify.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* WPGJ旺铺聚合支付回调响应DTO
*
* @author jonyl
*/
@Schema(description = "WPGJ旺铺聚合支付回调响应DTO")
@Data
public class WpgjPayNotifyRespDTO {
@Schema(description = "应答码", requiredMode = Schema.RequiredMode.REQUIRED, example = "00")
private String code;
@Schema(description = "对应code码内容描述", requiredMode = Schema.RequiredMode.REQUIRED, example = "成功")
private String msg;
@Schema(description = "对应时间戳", requiredMode = Schema.RequiredMode.REQUIRED, example = "20251027123456789")
private String timestamp;
}
\ No newline at end of file
package com.luhu.computility.module.pay.controller.app.order;
import cn.hutool.core.util.ObjectUtil;
import com.luhu.computility.framework.common.pojo.CommonResult;
import com.luhu.computility.framework.common.util.object.BeanUtils;
import com.luhu.computility.module.pay.controller.admin.order.vo.PayOrderRespVO;
import com.luhu.computility.module.pay.controller.admin.order.vo.PayOrderSubmitRespVO;
import com.luhu.computility.module.pay.controller.app.order.vo.AppPayOrderSubmitReqVO;
import com.luhu.computility.module.pay.controller.app.order.vo.AppPayOrderSubmitRespVO;
import com.luhu.computility.module.pay.controller.app.order.vo.PayOrderWpgjRespVO;
import com.luhu.computility.module.pay.convert.order.PayOrderConvert;
import com.luhu.computility.module.pay.dal.dataobject.order.PayOrderDO;
import com.luhu.computility.module.pay.dal.dataobject.wallet.PayWalletDO;
import com.luhu.computility.module.pay.dal.dataobject.wpgj.PayOrderWpgjDO;
import com.luhu.computility.module.pay.enums.PayChannelEnum;
import com.luhu.computility.module.pay.enums.order.PayOrderStatusEnum;
import com.luhu.computility.module.pay.framework.pay.core.client.impl.wallet.WalletPayClient;
import com.luhu.computility.module.pay.service.order.PayOrderService;
import com.luhu.computility.module.pay.service.wallet.PayWalletService;
import com.luhu.computility.module.pay.service.wpgj.PayOrderWpgjService;
import com.google.common.collect.Maps;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
......@@ -42,6 +46,8 @@ public class AppPayOrderController {
private PayOrderService payOrderService;
@Resource
private PayWalletService payWalletService;
@Resource
private PayOrderWpgjService payOrderWpgjService;
@GetMapping("/get")
@Operation(summary = "获得支付订单")
......@@ -78,4 +84,15 @@ public class AppPayOrderController {
return success(PayOrderConvert.INSTANCE.convert3(respVO));
}
@GetMapping("/wpgj-get")
@Operation(summary = "获得WPGJ旺铺聚合支付订单")
@Parameter(name = "id", description = "WPGJ订单ID", required = true, example = "1024")
public CommonResult<PayOrderWpgjRespVO> getWpgjOrder(@RequestParam("id") Long id) {
PayOrderWpgjDO order = payOrderWpgjService.getById(id);
if (order == null) {
return success(new PayOrderWpgjRespVO());
}
return success(BeanUtils.toBean(order, PayOrderWpgjRespVO.class));
}
}
......@@ -12,4 +12,10 @@ import lombok.experimental.Accessors;
@Data
public class AppPayOrderSubmitRespVO extends PayOrderSubmitRespVO {
@Schema(description = "订单扩展ID(用于前端轮询支付状态)", example = "123456")
private Long orderExtensionId;
@Schema(description = "WPGJ订单ID(用于前端轮询WPGJ支付状态)", example = "123456")
private Long wpgjOrderId;
}
package com.luhu.computility.module.pay.controller.app.order.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 用户 APP - WPGJ旺铺聚合支付订单 Response VO
*
* @author jonyl
*/
@Schema(description = "用户 APP - WPGJ旺铺聚合支付订单 Response VO")
@Data
public class PayOrderWpgjRespVO {
@Schema(description = "订单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1895381951077651000")
private Long id;
@Schema(description = "业务类型 1-算力资源订单 2-API订单", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Integer businessType;
@Schema(description = "商户订单ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "123456")
private String merOrderId;
@Schema(description = "旺铺订单号", requiredMode = Schema.RequiredMode.REQUIRED, example = "WPGJ123456789")
private String orderId;
@Schema(description = "网关商户订单号", example = "GW123456")
private String gatewayMerOrderId;
@Schema(description = "关联的业务订单号", example = "123456")
private String orderNo;
@Schema(description = "内部商户号", requiredMode = Schema.RequiredMode.REQUIRED, example = "M123456")
private String merNo;
@Schema(description = "商户代码", requiredMode = Schema.RequiredMode.REQUIRED, example = "C123456")
private String merCode;
@Schema(description = "设备SN号", requiredMode = Schema.RequiredMode.REQUIRED, example = "SN123456789")
private String deviceNo;
@Schema(description = "支付通道代码", requiredMode = Schema.RequiredMode.REQUIRED, example = "wx_native")
private String paywayCode;
@Schema(description = "下单时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "20241028143000")
private String orderTime;
@Schema(description = "订单金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "100")
private String orderAmt;
@Schema(description = "结算金额", example = "99")
private String actAmt;
@Schema(description = "手续费", example = "1")
private String fee;
@Schema(description = "订单状态 0-处理中 1-支付成功 2-支付失败", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private String orderStatus;
@Schema(description = "通道交易流水号", example = "TXN123456789")
private String tradeNo;
@Schema(description = "交易时间", example = "20241028143030")
private String tradeTime;
@Schema(description = "订单标题", example = "算力资源 - 7天")
private String orderTitle;
@Schema(description = "买家用户号", example = "U123456")
private String buyerId;
@Schema(description = "微信/支付宝小票交易单号", example = "TXN123456789")
private String tradeTopNo;
@Schema(description = "交易账户类型 C-贷记卡 D-借记卡 U-未知", example = "D")
private String cardType;
@Schema(description = "报文签名值", requiredMode = Schema.RequiredMode.REQUIRED, example = "ABC123")
private String sign;
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime createTime;
@Schema(description = "更新时间", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime updateTime;
}
\ No newline at end of file
package com.luhu.computility.module.pay.dal.dataobject.wpgj;
import com.luhu.computility.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
/**
* 旺铺聚合支付订单 DO
*
* @author jonyl
*/
@TableName("pay_order_wpgj")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PayOrderWpgjDO extends BaseDO {
/**
* 订单编号,自增
*/
@TableId
private Long id;
/**
* 业务类型 1-算力资源订单 2-API订单
*/
private Integer businessType;
/**
* 商户订单ID
*/
private String merOrderId;
/**
* 旺铺订单号
*/
private String orderId;
/**
* 网关商户订单号
*/
private String gatewayMerOrderId;
/**
* 关联的业务订单号
*/
private String orderNo;
/**
* 内部商户号
*/
private String merNo;
/**
* 商户代码
*/
private String merCode;
/**
* 设备SN号
*/
private String deviceNo;
/**
* 支付通道代码
*/
private String paywayCode;
/**
* 下单时间
*/
private String orderTime;
/**
* 订单金额
*/
private String orderAmt;
/**
* 结算金额
*/
private String actAmt;
/**
* 手续费
*/
private String fee;
/**
* 订单状态 0-处理中 1-支付成功 2-支付失败
*/
private String orderStatus;
/**
* 通道交易流水号
*/
private String tradeNo;
/**
* 交易时间
*/
private String tradeTime;
/**
* 订单标题
*/
private String orderTitle;
/**
* 买家用户号
*/
private String buyerId;
/**
* 微信/支付宝小票交易单号
*/
private String tradeTopNo;
/**
* 交易账户类型 C-贷记卡 D-借记卡 U-未知
*/
private String cardType;
/**
* 报文签名值
*/
private String sign;
}
\ No newline at end of file
package com.luhu.computility.module.pay.dal.mysql.wpgj;
import com.luhu.computility.framework.mybatis.core.mapper.BaseMapperX;
import com.luhu.computility.framework.mybatis.core.query.LambdaQueryWrapperX;
import com.luhu.computility.module.pay.dal.dataobject.wpgj.PayOrderWpgjDO;
import org.apache.ibatis.annotations.Mapper;
import java.time.LocalDateTime;
import java.util.List;
/**
* 旺铺聚合支付订单 Mapper
*
* @author jonyl
*/
@Mapper
public interface PayOrderWpgjMapper extends BaseMapperX<PayOrderWpgjDO> {
default PayOrderWpgjDO selectByMerOrderId(String merOrderId) {
return selectOne(PayOrderWpgjDO::getMerOrderId, merOrderId);
}
default PayOrderWpgjDO selectByOrderId(String orderId) {
return selectOne(PayOrderWpgjDO::getOrderId, orderId);
}
default PayOrderWpgjDO selectByOrderNo(String orderNo) {
return selectOne(PayOrderWpgjDO::getOrderNo, orderNo);
}
/**
* 查询过期的处理中订单
*
* @param orderStatus 订单状态
* @param expireTime 过期时间
* @return 过期订单列表
*/
default List<PayOrderWpgjDO> selectListByOrderStatusAndCreateTimeLt(String orderStatus, LocalDateTime expireTime) {
return selectList(new LambdaQueryWrapperX<PayOrderWpgjDO>()
.eq(PayOrderWpgjDO::getOrderStatus, orderStatus)
.lt(PayOrderWpgjDO::getCreateTime, expireTime));
}
}
\ No newline at end of file
package com.luhu.computility.module.pay.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 订单业务类型枚举
*
* @author jony
*/
@Getter
@AllArgsConstructor
public enum OrderBusinessTypeEnum {
COMPUTE_RESOURCE(1, "算力资源订单"),
API_HUB(2, "API订单");
private final Integer value;
private final String name;
/**
* 根据值获取枚举
*/
public static OrderBusinessTypeEnum fromValue(Integer value) {
for (OrderBusinessTypeEnum type : values()) {
if (type.getValue().equals(value)) {
return type;
}
}
return null;
}
/**
* 根据值获取名称
*/
public static String getNameByValue(Integer value) {
OrderBusinessTypeEnum typeEnum = fromValue(value);
return typeEnum != null ? typeEnum.getName() : null;
}
/**
* 判断是否是算力资源订单
*/
public static boolean isComputeResource(Integer businessType) {
return COMPUTE_RESOURCE.getValue().equals(businessType);
}
/**
* 判断是否是API订单
*/
public static boolean isApiHub(Integer businessType) {
return API_HUB.getValue().equals(businessType);
}
}
package com.luhu.computility.module.pay.enums;
import com.luhu.computility.framework.common.core.ArrayValuable;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* WPGJ旺铺聚合支付订单状态枚举
*
* @author jonyl
*/
@Getter
@AllArgsConstructor
public enum WpgjOrderStatusEnum implements ArrayValuable<String> {
WAITING("0", "待支付"),
SUCCESS("1", "已支付"),
CLOSED("2", "已取消"), // 支付关闭
REFUND("3", "已退款"),
;
private final String status;
private final String name;
@Override
public String[] array() {
return new String[0];
}
/**
* 判断是否支付成功
*
* @param status 状态
* @return 是否支付成功
*/
public static boolean isSuccess(String status) {
return SUCCESS.getStatus().equals(status);
}
/**
* 判断是否支付失败或关闭
*
* @param status 状态
* @return 是否支付失败或关闭
*/
public static boolean isFailedOrClosed(String status) {
return WAITING.getStatus().equals(status) || CLOSED.getStatus().equals(status);
}
}
\ No newline at end of file
......@@ -30,6 +30,11 @@ public class PayOrderRespDTO {
private String outTradeNo;
/**
* 支付渠道编码
*/
private String channelCode;
/**
* 支付渠道编号
*/
private String channelOrderNo;
......
package com.luhu.computility.module.pay.framework.pay.core.client.dto.order;
import lombok.Data;
import lombok.experimental.Accessors;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
/**
* WPGJ 业务数据 DTO
*
* @author jony
*/
@Data
@Accessors(chain = true)
public class WpgjBusinessDataDTO {
/**
* 商户唯一订单号
*/
@NotBlank(message = "商户订单号不能为空")
private String merOrderId;
/**
* 订单金额(分)
*/
@NotBlank(message = "订单金额不能为空")
private String orderAmt;
/**
* 订单标题
*/
private String orderTitle;
/**
* 下单时间
*/
@NotBlank(message = "下单时间不能为空")
private String orderTime;
}
\ No newline at end of file
package com.luhu.computility.module.pay.framework.pay.core.client.dto.order;
import lombok.Data;
import lombok.experimental.Accessors;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
/**
* WPGJ 统一下单请求 DTO
*
* @author jony
*/
@Data
@Accessors(chain = true)
public class WpgjUnifiedOrderReqDTO {
/**
* 商户号
*/
@NotBlank(message = "商户号不能为空")
private String merNo;
/**
* 商户代码
*/
@NotBlank(message = "商户代码不能为空")
private String merCode;
/**
* 异步通知地址
*/
@NotBlank(message = "异步通知地址不能为空")
private String notifyUrl;
/**
* 业务数据
*/
@NotBlank(message = "业务数据不能为空")
private String businessData;
}
\ No newline at end of file
package com.luhu.computility.module.pay.framework.pay.core.client.impl.wpgj;
import cn.hutool.json.JSONObject;
import lombok.extern.slf4j.Slf4j;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Base64;
import java.util.UUID;
/**
* WPGJ旺铺聚合支付加密工具类 - 专门用于动态支付序列码接口
* 按照官方demo代码标准实现
*
* @author jonyl
*/
@Slf4j
public class WpgjCryptoUtils {
/**
* 构建加密的请求数据 - 按照demo代码的标准实现
*/
public static String buildEncryptedRequest(JSONObject businessData, String organizNo, String publicKey) throws Exception {
// 1. 生成16位随机AES密钥
String aesKey = generateLenString(16);
// 2. 构建完整的请求数据结构
JSONObject requestJson = new JSONObject();
requestJson.set("serialNo", UUID.randomUUID().toString().replaceAll("-", "").toUpperCase());
requestJson.set("version", "1.0");
requestJson.set("timestamp", new SimpleDateFormat("yyyyMMddHHmmssSSS").format(new Date()));
// 3. 用AES加密业务数据
String businessDataStr = businessData.toString();
String encryptedData = encryptData(businessDataStr, aesKey, "UTF-8");
requestJson.set("data", encryptedData);
// 4. 用RSA公钥加密AES密钥
String encryptedAesKey = encrtptKey(publicKey, aesKey, "UTF-8");
requestJson.set("signature", encryptedAesKey);
requestJson.set("extras", null);
requestJson.set("organizNo", organizNo);
return requestJson.toString();
}
/**
* 解密响应数据
*/
public static JSONObject decryptResponse(String responseStr, String privateKey) throws Exception {
JSONObject response = new JSONObject(responseStr);
if ("0000".equals(response.getStr("code"))) {
// 解密data字段
String encryptedData = response.getStr("data");
String signature = response.getStr("signature");
// 用RSA私钥解密AES密钥
byte[] aesKeyBytes = decryptKey(privateKey, signature, "UTF-8");
// 用AES密钥解密响应数据
String decryptedData = decryptData(encryptedData, aesKeyBytes, "UTF-8");
response.set("data", new JSONObject(decryptedData));
}
return response;
}
/**
* 通过AES秘钥加密数据 - 按照demo实现
*/
private static String encryptData(String data, String aesKey, String charset) throws Exception {
byte[] plainBytes = data.getBytes(charset);
byte[] keyBytes = aesKey.getBytes(charset);
byte[] encryptedBytes = AESEncrypt(plainBytes, keyBytes);
return new String(Base64.getEncoder().encode(encryptedBytes), charset);
}
/**
* 通过RSA公钥加密AES秘钥 - 按照demo实现
*/
private static String encrtptKey(String publicRSAKey, String aesKey, String charset) throws Exception {
byte[] keyBytes = aesKey.getBytes(charset);
PublicKey publicKey = buildRSAPublicKeyByStr(publicRSAKey);
byte[] encryptedBytes = RSAEncrypt(keyBytes, publicKey);
return new String(Base64.getEncoder().encode(encryptedBytes), charset);
}
/**
* 通过RSA私钥解密AES秘钥 - 按照demo实现
*/
private static byte[] decryptKey(String privateRSAKey, String aesEncryptKey, String charset) throws Exception {
byte[] decodeBase64KeyBytes = Base64.getDecoder().decode(aesEncryptKey.getBytes(charset));
PrivateKey privateKey = buildRSAPrivateKeyByStr(privateRSAKey);
return RSADecrypt(decodeBase64KeyBytes, privateKey);
}
/**
* 通过AES秘钥解密数据 - 按照demo实现
*/
private static String decryptData(String encryptedData, byte[] aesKeyBytes, String charset) throws Exception {
byte[] decodeBase64DataBytes = Base64.getDecoder().decode(encryptedData.getBytes(charset));
byte[] decryptedBytes = AESDecrypt(decodeBase64DataBytes, aesKeyBytes);
return new String(decryptedBytes, charset);
}
// ========== AES加密/解密方法 ==========
private static byte[] AESEncrypt(byte[] plainBytes, byte[] keyBytes) throws Exception {
if (keyBytes.length != 16) {
throw new Exception("AES密钥必须是16位");
}
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
SecretKeySpec secretKey = new SecretKeySpec(keyBytes, "AES");
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
return cipher.doFinal(plainBytes);
}
private static byte[] AESDecrypt(byte[] encryptedBytes, byte[] keyBytes) throws Exception {
if (keyBytes.length != 16) {
throw new Exception("AES密钥必须是16位");
}
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
SecretKeySpec secretKey = new SecretKeySpec(keyBytes, "AES");
cipher.init(Cipher.DECRYPT_MODE, secretKey);
return cipher.doFinal(encryptedBytes);
}
// ========== RSA加密/解密方法 ==========
private static byte[] RSAEncrypt(byte[] plainBytes, PublicKey publicKey) throws Exception {
Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
return cipher.doFinal(plainBytes);
}
private static byte[] RSADecrypt(byte[] encryptedBytes, PrivateKey privateKey) throws Exception {
Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
cipher.init(Cipher.DECRYPT_MODE, privateKey);
return cipher.doFinal(encryptedBytes);
}
// ========== RSA密钥构建方法 ==========
private static PublicKey buildRSAPublicKeyByStr(String key) throws Exception {
String cleanKey = key
.replace("-----BEGIN PUBLIC KEY-----", "")
.replace("-----END PUBLIC KEY-----", "")
.replaceAll("\\s+", "");
X509EncodedKeySpec pubX509 = new X509EncodedKeySpec(Base64.getDecoder().decode(cleanKey));
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
return keyFactory.generatePublic(pubX509);
}
private static PrivateKey buildRSAPrivateKeyByStr(String key) throws Exception {
String cleanKey = key
.replace("-----BEGIN PRIVATE KEY-----", "")
.replace("-----END PRIVATE KEY-----", "")
.replaceAll("\\s+", "");
PKCS8EncodedKeySpec priPKCS8 = new PKCS8EncodedKeySpec(Base64.getDecoder().decode(cleanKey));
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
return keyFactory.generatePrivate(priPKCS8);
}
/**
* 生成指定位数的随机字符串 - 按照demo实现
*/
private static String generateLenString(int length) {
char[] cResult = new char[length];
int[] flag = { 0, 0, 0 }; // A-Z, a-z, 0-9
int i = 0;
while (flag[0] == 0 || flag[1] == 0 || flag[2] == 0 || i < length) {
i = i % length;
int f = (int) (Math.random() * 3 % 3);
if (f == 0)
cResult[i] = (char) ('A' + Math.random() * 26);
else if (f == 1)
cResult[i] = (char) ('a' + Math.random() * 26);
else
cResult[i] = (char) ('0' + Math.random() * 10);
flag[f] = 1;
i++;
}
return new String(cResult);
}
}
\ No newline at end of file
package com.luhu.computility.module.pay.framework.pay.core.client.impl.wpgj;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* 旺铺聚合支付配置属性
*
* @author jonyl
*/
@Data
@Component
@ConfigurationProperties(prefix = "computility.wpgj-pay")
public class WpgjPayProperties {
/**
* 组织编号
*/
private String organizNo;
/**
* 商户编号
*/
private String merNo;
/**
* 商户代码
*/
private String merCode;
/**
* 终端编号
*/
private String termCode;
/**
* API地址
*/
private String apiUrl;
/**
* 算力资源模块回调地址
*/
private String notifyUrlCompute;
/**
* APIHub模块回调地址
*/
private String notifyUrlApi;
/**
* 兼容旧的notifyUrl字段
*/
private String notifyUrl;
/**
* 公钥
*/
private String publicKey;
/**
* 私钥
*/
private String privateKey;
/**
* 签名密钥(用于回调验证)
*/
private String signKey;
}
\ No newline at end of file
......@@ -3,29 +3,29 @@ package com.luhu.computility.module.pay.job.order;
import cn.hutool.core.util.StrUtil;
import com.luhu.computility.framework.quartz.core.handler.JobHandler;
import com.luhu.computility.framework.tenant.core.job.TenantJob;
import com.luhu.computility.module.pay.service.order.PayOrderService;
import com.luhu.computility.module.pay.service.wpgj.PayOrderWpgjService;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* 支付订单的过期 Job
* WPGJ旺铺聚合支付订单的过期 Job
*
* 支付超过过期时间时,支付渠道是不会通知进行过期,所以需要定时进行过期关闭。
*
* @author 芋道源码
* @author jonyl
*/
@Component
public class PayOrderExpireJob implements JobHandler {
@Resource
private PayOrderService orderService;
private PayOrderWpgjService payOrderWpgjService;
@Override
@TenantJob
public String execute(String param) {
int count = orderService.expireOrder();
return StrUtil.format("支付过期 {} 个", count);
int count = payOrderWpgjService.expireOrder();
return StrUtil.format("WPGJ支付过期 {} 个", count);
}
}
package com.luhu.computility.module.pay.service.expire;
import com.luhu.computility.module.pay.enums.OrderBusinessTypeEnum;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import java.util.HashMap;
import java.util.Map;
/**
* 业务订单过期管理器
*
* @author jony
*/
@Service
@Slf4j
public class BusinessOrderExpireManager {
@Autowired
private ApplicationContext applicationContext;
private Map<Integer, BusinessOrderExpireService> expireServiceMap = new HashMap<>();
@PostConstruct
public void init() {
// 获取所有BusinessOrderExpireService的实现
Map<String, BusinessOrderExpireService> services = applicationContext.getBeansOfType(BusinessOrderExpireService.class);
for (BusinessOrderExpireService service : services.values()) {
OrderBusinessTypeEnum supportedType = service.getSupportedBusinessType();
if (supportedType != null) {
expireServiceMap.put(supportedType.getValue(), service);
log.info("[init] 注册业务订单过期服务: {} -> {}",
supportedType.getName(), service.getClass().getSimpleName());
}
}
log.info("[init] 业务订单过期服务注册完成,共注册 {} 个服务", expireServiceMap.size());
}
/**
* 根据业务类型过期业务订单
*
* @param businessType 业务类型
* @param businessOrderId 业务订单ID
* @return 是否过期成功
*/
public boolean expireBusinessOrder(Integer businessType, Long businessOrderId) {
BusinessOrderExpireService service = expireServiceMap.get(businessType);
if (service == null) {
log.error("[expireBusinessOrder] 未找到业务类型 {} 对应的过期服务", businessType);
return false;
}
try {
log.info("[expireBusinessOrder] 开始过期业务订单,业务类型: {}, 订单ID: {}", businessType, businessOrderId);
boolean success = service.expireBusinessOrder(businessOrderId);
if (success) {
log.info("[expireBusinessOrder] 业务订单过期成功,业务类型: {}, 订单ID: {}", businessType, businessOrderId);
} else {
log.error("[expireBusinessOrder] 业务订单过期失败,业务类型: {}, 订单ID: {}", businessType, businessOrderId);
}
return success;
} catch (Exception e) {
log.error("[expireBusinessOrder] 过期业务订单异常,业务类型: {}, 订单ID: {}", businessType, businessOrderId, e);
return false;
}
}
}
\ No newline at end of file
package com.luhu.computility.module.pay.service.expire;
import com.luhu.computility.module.pay.enums.OrderBusinessTypeEnum;
/**
* 业务订单过期服务接口
*
* @author jony
*/
public interface BusinessOrderExpireService {
/**
* 获取支持的业务类型
*
* @return 业务类型
*/
OrderBusinessTypeEnum getSupportedBusinessType();
/**
* 过期业务订单
*
* @param businessOrderId 业务订单ID
* @return 是否过期成功
*/
boolean expireBusinessOrder(Long businessOrderId);
}
\ No newline at end of file
......@@ -7,6 +7,8 @@ import com.luhu.computility.module.pay.controller.admin.order.vo.PayOrderExportR
import com.luhu.computility.module.pay.controller.admin.order.vo.PayOrderPageReqVO;
import com.luhu.computility.module.pay.controller.admin.order.vo.PayOrderSubmitReqVO;
import com.luhu.computility.module.pay.controller.admin.order.vo.PayOrderSubmitRespVO;
import com.luhu.computility.module.pay.controller.app.order.vo.AppPayOrderSubmitReqVO;
import com.luhu.computility.module.pay.controller.app.order.vo.AppPayOrderSubmitRespVO;
import com.luhu.computility.module.pay.dal.dataobject.order.PayOrderDO;
import com.luhu.computility.module.pay.dal.dataobject.order.PayOrderExtensionDO;
......@@ -80,6 +82,7 @@ public interface PayOrderService {
*/
Long createOrder(@Valid PayOrderCreateReqDTO reqDTO);
/**
* 提交支付
* 此时,会发起支付渠道的调用
......@@ -91,6 +94,7 @@ public interface PayOrderService {
PayOrderSubmitRespVO submitOrder(@Valid PayOrderSubmitReqVO reqVO,
@NotEmpty(message = "提交 IP 不能为空") String userIp);
/**
* 通知支付单成功
*
......
......@@ -4,6 +4,10 @@ import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.luhu.computility.framework.common.pojo.PageResult;
import com.luhu.computility.framework.common.util.date.LocalDateTimeUtils;
import com.luhu.computility.framework.common.util.number.MoneyUtils;
......@@ -16,6 +20,8 @@ import com.luhu.computility.module.pay.controller.admin.order.vo.PayOrderExportR
import com.luhu.computility.module.pay.controller.admin.order.vo.PayOrderPageReqVO;
import com.luhu.computility.module.pay.controller.admin.order.vo.PayOrderSubmitReqVO;
import com.luhu.computility.module.pay.controller.admin.order.vo.PayOrderSubmitRespVO;
import com.luhu.computility.module.pay.controller.app.order.vo.AppPayOrderSubmitReqVO;
import com.luhu.computility.module.pay.controller.app.order.vo.AppPayOrderSubmitRespVO;
import com.luhu.computility.module.pay.convert.order.PayOrderConvert;
import com.luhu.computility.module.pay.dal.dataobject.app.PayAppDO;
import com.luhu.computility.module.pay.dal.dataobject.channel.PayChannelDO;
......@@ -74,6 +80,7 @@ public class PayOrderServiceImpl implements PayOrderService {
@Resource
private PayNotifyService notifyService;
@Override
public PayOrderDO getOrder(Long id) {
return payOrderMapper.selectById(id);
......@@ -133,6 +140,8 @@ public class PayOrderServiceImpl implements PayOrderService {
return order.getId();
}
@Override // 注意,这里不能添加事务注解,避免调用支付渠道失败时,将 PayOrderExtensionDO 回滚了
public PayOrderSubmitRespVO submitOrder(PayOrderSubmitReqVO reqVO, String userIp) {
// 1.1 获得 PayOrderDO ,并校验其是否存在
......@@ -181,6 +190,10 @@ public class PayOrderServiceImpl implements PayOrderService {
return PayOrderConvert.INSTANCE.convert(order, unifiedOrderResp);
}
private PayOrderDO validateOrderCanSubmit(Long id) {
PayOrderDO order = payOrderMapper.selectById(id);
if (order == null) { // 是否存在
......@@ -256,12 +269,16 @@ public class PayOrderServiceImpl implements PayOrderService {
@Override
public void notifyOrder(Long channelId, PayOrderRespDTO notify) {
// 校验支付渠道是否有效
PayChannelDO channel = channelService.validPayChannel(channelId);
// 更新支付订单为已支付
TenantUtils.execute(channel.getTenantId(), () -> getSelf().notifyOrder(channel, notify));
}
/**
* 通知并更新订单的支付结果
*
......
package com.luhu.computility.module.pay.service.wpgj;
import cn.hutool.core.util.ObjectUtil;
import com.luhu.computility.framework.common.exception.ServiceException;
import com.luhu.computility.module.pay.api.wpgj.CommonWpgjPayApi;
import com.luhu.computility.module.pay.api.wpgj.PayOrderWpgjApi;
import com.luhu.computility.module.pay.api.wpgj.dto.CommonWpgjCreateReqDTO;
import com.luhu.computility.module.pay.api.wpgj.dto.PayOrderWpgjCreateReqDTO;
import com.luhu.computility.module.pay.api.wpgj.vo.CommonWpgjPayOrderSubmitRespVO;
import com.luhu.computility.module.pay.api.wpgj.vo.WpgjPayOrderSubmitReqVO;
import com.luhu.computility.module.pay.api.order.PayOrderApi;
import com.luhu.computility.module.pay.controller.app.order.vo.AppPayOrderSubmitRespVO;
import com.luhu.computility.framework.common.util.string.StrUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import javax.validation.Valid;
import static com.luhu.computility.module.pay.enums.order.PayOrderStatusEnum.SUCCESS;
/**
* 通用WPGJ支付服务实现
* 支持算力资源订单和API订单等多种业务类型
*
* @author jony
*/
@Service
@Slf4j
public class CommonWpgjPayServiceImpl implements CommonWpgjPayApi {
@Resource
private PayOrderWpgjApi payOrderWpgjApi;
@Resource
private PayOrderApi payOrderApi;
@Override
public CommonWpgjPayOrderSubmitRespVO createCommonWpgjOrder(@Valid CommonWpgjCreateReqDTO createReqDTO) {
try {
log.info("[createCommonWpgjOrder] 开始创建通用WPGJ支付订单,业务类型: {}, 业务订单ID: {}, 支付金额: {}",
createReqDTO.getBusinessType(), createReqDTO.getBusinessOrderId(), createReqDTO.getPayAmount());
// 1. 创建WPGJ支付订单记录
PayOrderWpgjCreateReqDTO wpgjCreateReqDTO = new PayOrderWpgjCreateReqDTO();
wpgjCreateReqDTO.setBusinessType(createReqDTO.getBusinessType());
wpgjCreateReqDTO.setMerOrderId(String.valueOf(createReqDTO.getBusinessOrderId()));
wpgjCreateReqDTO.setOrderTitle(StrUtils.maxLength(createReqDTO.getOrderTitle(), 64)); // WPGJ订单标题最大64位
wpgjCreateReqDTO.setOrderAmt(String.valueOf(createReqDTO.getPayAmount() / 100.0)); // 转换为元
Long wpgjOrderId = payOrderWpgjApi.createOrderWpgj(wpgjCreateReqDTO);
log.info("[createCommonWpgjOrder] WPGJ订单创建成功,业务类型: {}, 业务订单ID: {}, WPGJ订单ID: {}",
createReqDTO.getBusinessType(), createReqDTO.getBusinessOrderId(), wpgjOrderId);
// 2. 调用WPGJ支付提交API获取支付链接
WpgjPayOrderSubmitReqVO submitReqVO = new WpgjPayOrderSubmitReqVO();
submitReqVO.setId(wpgjOrderId);
AppPayOrderSubmitRespVO submitRespVO = payOrderApi.submitWpgjOrder(submitReqVO);
log.info("[createCommonWpgjOrder] WPGJ支付提交成功,业务类型: {}, 业务订单ID: {}, WPGJ订单ID: {}, 支付状态: {}",
createReqDTO.getBusinessType(), createReqDTO.getBusinessOrderId(), wpgjOrderId, submitRespVO.getStatus());
// 3. 构建返回数据 - 包含支付链接
CommonWpgjPayOrderSubmitRespVO respVO = new CommonWpgjPayOrderSubmitRespVO();
respVO.setWpgjOrderId(wpgjOrderId); // 返回WPGJ订单ID,用于前端轮询
respVO.setStatus(ObjectUtil.toString(submitRespVO.getStatus()));
respVO.setDisplayMode(submitRespVO.getDisplayMode());
respVO.setDisplayContent(submitRespVO.getDisplayContent()); // 支付链接
log.info("[createCommonWpgjOrder] 通用WPGJ支付订单创建完成,业务类型: {}, 业务订单ID: {}, WPGJ订单ID: {}, 支付链接: {}",
createReqDTO.getBusinessType(), createReqDTO.getBusinessOrderId(), wpgjOrderId, submitRespVO.getDisplayContent());
return respVO;
} catch (Exception e) {
log.error("[createCommonWpgjOrder] 创建通用WPGJ支付订单失败", e);
throw new ServiceException("创建支付订单失败: " + e.getMessage());
}
}
}
\ No newline at end of file
package com.luhu.computility.module.pay.service.wpgj;
import com.luhu.computility.module.pay.controller.admin.notify.vo.WpgjPayNotifyDTO;
import com.luhu.computility.module.pay.dal.dataobject.wpgj.PayOrderWpgjDO;
/**
* 旺铺聚合支付订单 Service 接口
*
* @author jonyl
*/
public interface PayOrderWpgjService {
/**
* 保存旺铺聚合支付订单回调数据
*
* @param notifyDTO WPGJ回调数据
* @return 订单ID
*/
Long saveOrder(WpgjPayNotifyDTO notifyDTO);
/**
* 根据商户订单ID获取旺铺聚合支付订单
*
* @param merOrderId 商户订单ID (对应compute_resource_order.id)
* @return 旺铺聚合支付订单
*/
PayOrderWpgjDO getByMerOrderId(String merOrderId);
/**
* 根据旺铺订单ID获取旺铺聚合支付订单
*
* @param orderId 旺铺订单ID
* @return 旺铺聚合支付订单
*/
PayOrderWpgjDO getByOrderId(String orderId);
/**
* 根据主键ID获取旺铺聚合支付订单
*
* @param id 主键ID
* @return 旺铺聚合支付订单
*/
PayOrderWpgjDO getById(Long id);
/**
* 过期处理中的旺铺聚合支付订单
*
* @return 过期的订单数量
*/
int expireOrder();
}
\ No newline at end of file
package com.luhu.computility.module.pay.service.wpgj;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import com.luhu.computility.framework.common.util.object.BeanUtils;
import com.luhu.computility.module.pay.controller.admin.notify.vo.WpgjPayNotifyDTO;
import com.luhu.computility.module.pay.dal.dataobject.wpgj.PayOrderWpgjDO;
import com.luhu.computility.module.pay.dal.mysql.wpgj.PayOrderWpgjMapper;
import com.luhu.computility.module.pay.enums.WpgjOrderStatusEnum;
import com.luhu.computility.module.pay.framework.pay.core.client.impl.wpgj.WpgjPayProperties;
import com.luhu.computility.module.pay.service.expire.BusinessOrderExpireManager;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.List;
/**
* 旺铺聚合支付订单 Service 实现类
*
* @author jonyl
*/
@Service
@Validated
@Slf4j
public class PayOrderWpgjServiceImpl implements PayOrderWpgjService {
@Resource
private PayOrderWpgjMapper payOrderWpgjMapper;
@Resource
private WpgjPayProperties wpgjPayProperties;
@Resource
private BusinessOrderExpireManager businessOrderExpireManager;
@Override
public Long saveOrder(WpgjPayNotifyDTO notifyDTO) {
// 查询是否已存在相同商户订单ID的记录
PayOrderWpgjDO existingOrder = payOrderWpgjMapper.selectByMerOrderId(notifyDTO.getMerOrderId());
if (existingOrder != null) {
log.info("[saveOrder] WPGJ订单已存在,商户订单ID: {}, 更新订单状态", notifyDTO.getMerOrderId());
// 更新现有订单的状态和信息
PayOrderWpgjDO updateOrder = BeanUtils.toBean(notifyDTO, PayOrderWpgjDO.class);
updateOrder.setId(existingOrder.getId());
updateOrder.setOrderStatus(notifyDTO.getOrderStatus());
payOrderWpgjMapper.updateById(updateOrder);
return existingOrder.getId();
}
// 创建新订单记录
PayOrderWpgjDO payOrderWpgj = BeanUtils.toBean(notifyDTO, PayOrderWpgjDO.class);
payOrderWpgj.setBusinessType(1); // 1-算力资源订单
payOrderWpgj.setOrderNo(notifyDTO.getMerOrderId()); // 商户订单ID作为业务订单号
// 从配置中获取可能为空的必填字段
if (StrUtil.isBlank(payOrderWpgj.getMerCode())) {
payOrderWpgj.setMerCode(wpgjPayProperties.getMerCode());
log.info("[saveOrder] 使用配置中的merCode: {}", wpgjPayProperties.getMerCode());
}
payOrderWpgjMapper.insert(payOrderWpgj);
log.info("[saveOrder] WPGJ订单保存成功,订单ID: {}, 商户订单ID: {}",
payOrderWpgj.getId(), notifyDTO.getMerOrderId());
return payOrderWpgj.getId();
}
@Override
public PayOrderWpgjDO getByMerOrderId(String merOrderId) {
return payOrderWpgjMapper.selectByMerOrderId(merOrderId);
}
@Override
public PayOrderWpgjDO getByOrderId(String orderId) {
return payOrderWpgjMapper.selectByOrderId(orderId);
}
@Override
public PayOrderWpgjDO getById(Long id) {
return payOrderWpgjMapper.selectById(id);
}
@Override
public int expireOrder() {
// 1. 查询过期的待支付订单(超过30分钟未支付的订单)
LocalDateTime expireTime = LocalDateTime.now().minusMinutes(30);
List<PayOrderWpgjDO> orders = payOrderWpgjMapper.selectListByOrderStatusAndCreateTimeLt(
WpgjOrderStatusEnum.WAITING.getStatus(), expireTime);
if (CollUtil.isEmpty(orders)) {
return 0;
}
// 2. 遍历执行,将过期的待支付订单标记为已取消,并同时过期对应的业务订单
int count = 0;
for (PayOrderWpgjDO order : orders) {
if (expireOrder(order)) {
count++;
// 同时过期对应的业务订单
expireBusinessOrder(order);
}
}
return count;
}
/**
* 过期单个WPGJ订单
*
* @param order WPGJ订单
* @return 是否过期成功
*/
private boolean expireOrder(PayOrderWpgjDO order) {
try {
// 将待支付的订单更新为已取消状态
PayOrderWpgjDO updateObj = new PayOrderWpgjDO()
.setId(order.getId())
.setOrderStatus(WpgjOrderStatusEnum.CLOSED.getStatus());
int updateCount = payOrderWpgjMapper.updateById(updateObj);
if (updateCount > 0) {
log.info("[expireOrder][WPGJ订单({}) 过期成功,状态更新为已取消]", order.getOrderId());
return true;
} else {
log.error("[expireOrder][WPGJ订单({}) 过期失败,更新状态失败]", order.getOrderId());
return false;
}
} catch (Exception e) {
log.error("[expireOrder][WPGJ订单({}) 过期异常]", order.getOrderId(), e);
return false;
}
}
/**
* 过期业务订单
*
* @param payOrder WPGJ支付订单
*/
private void expireBusinessOrder(PayOrderWpgjDO payOrder) {
try {
Integer businessType = payOrder.getBusinessType();
String merOrderId = payOrder.getMerOrderId();
if (businessType == null || merOrderId == null) {
log.warn("[expireBusinessOrder] 业务类型或商户订单ID为空,无法过期业务订单,WPGJ订单ID: {}",
payOrder.getOrderId());
return;
}
// 调用业务订单过期管理器过期业务订单
boolean success = businessOrderExpireManager.expireBusinessOrder(businessType, Long.valueOf(merOrderId));
if (success) {
log.info("[expireBusinessOrder] 业务订单过期成功,业务类型: {}, 订单ID: {}, WPGJ订单ID: {}",
businessType, merOrderId, payOrder.getOrderId());
} else {
log.error("[expireBusinessOrder] 业务订单过期失败,业务类型: {}, 订单ID: {}, WPGJ订单ID: {}",
businessType, merOrderId, payOrder.getOrderId());
}
} catch (Exception e) {
log.error("[expireBusinessOrder] 过期业务订单异常,WPGJ订单ID: {}", payOrder.getOrderId(), e);
}
}
}
\ No newline at end of file
......@@ -168,6 +168,46 @@ computility:
order-notify-url: https://phslgld.hnluchuan.com/admin-api/pay/notify/order # 支付渠道的【支付】回调地址
refund-notify-url: https://phslgld.hnluchuan.com/admin-api/pay/notify/refund # 支付渠道的【退款】回调地址
transfer-notify-url: https://phslgld.hnluchuan.com/admin-api/pay/notify/transfer # 支付渠道的【转账】回调地址
wpgj-pay:
# WPGJ旺铺聚合支付配置
# organiz-no: 105549
# mer-no: 99911325651RE1R
# mer-code: K20241200111267
# term-code: 1011215692596
# api-url: https://stg5-qr.wpgjcs.com/industrial/payment/dynamic
# notify-url-compute: https://phslgld.hnluchuan.com/admin-api/compute/wpgj/notify # 算力资源模块WPGJ回调地址
# notify-url-api: https://phslgld.hnluchuan.com/admin-api/apihub/wpgj/notify # APIHub模块WPGJ回调地址
# sign-key: 07714583f82b4db8b675b32cd5e0969743 # WPGJ回调签名密钥(测试用,实际需要向WPGJ确认)
# public-key: |
# -----BEGIN PUBLIC KEY-----
# MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvS43wZPiULrGVcXLIsTRcPZn1ZH3P5DN/IxdWUTijPMkruLu36Q1Dh+8x/oGnQBJFckp31YTptyXa2CxC7wxodAc7I4umDp1h43vDFItsPb0DBDtYUTdjw7YFBppwgyr8soUUfc9fDShgYbza/pqOzsvgXECubKrtpeBii0BGShfs20H/Rawzn2WXMUq5l+Cw7pXwJ87FT8rbJ/KJRmtMTiy5yczWikBETCGv/b7Wxw10c7w0NBeWW/vjj6FoJQOcoo3QEcjPAAx0pC4HBZiE4T3ouvsWjxyhq2CZ6FFVT05jPezw2/FZI26DOKm3pStXne3gsXzcuxPGIrzccrgjQIDAQAB
# -----END PUBLIC KEY-----
# private-key: |
# -----BEGIN PRIVATE KEY-----
# MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC9LjfBk+JQusZVxcsixNFw9mfVkfc/kM38jF1ZROKM8ySu4u7fpDUOH7zH+gadAEkVySnfVhOm3JdrYLELvDGh0Bzsji6YOnWHje8MUi2w9vQMEO1hRN2PDtgUGmnCDKvyyhRR9z18NKGBhvNr+mo7Oy+BcQK5squ2l4GKLQEZKF+zbQf9FrDOfZZcxSrmX4LDulfAnzsVPytsn8olGa0xOLLnJzNaKQERMIa/9vtbHDXRzvDQ0F5Zb++OPoWglA5yijdARyM8ADHSkLgcFmIThPei6+xaPHKGrYJnoUVVPTmM97PDb8VkjboM4qbelK1ed7eCxfNy7E8YivNxyuCNAgMBAAECggEADSpXvVQo1lHBNrvj8KCo1gctNQrfnYqwGHNo6wBPvIZj7FB6VjxWaDa44Q6F16GVnyYCO3rWivC5ZZgeNMEH5TPB/OEXiLGHd1kJz0jddCjOKr7M06Lta6TD+yuBZlHnxNGKYM/ndVWGUyjMl5xJjfixoXz3YcFM5jvx9byUYdHeP93qrKjJmF/lH6fAVcrk8OiwApmMC8XQ1uAZhZzbeJ4yQozWznOeF9TU0wmguNmP9zKmp3dwgBn34tmMbp7br6nAiQOmCgGl8c5QpbrN6sf2d65I0lH/hbomIQSiGve3Jen37msm/lm22WdncJVnHsgMpQq9NVqqSOZX624O0QKBgQDtqXk4FEiclRJ7wcVIBudObRtxZbQ/mEitVCk83Ka1KyHFp8dv4M22Go6+qn3FYVa7N9fQOPjBo+aInlKeRYQ7eCQMLEI0YML7D5cbV+c6fCwdY9QoQ9buyxjP3HMOJVIPXPphV/dqbSTIVkK2k4xKP0knBOUYt7CUl75gdHDldQKBgQDLxxeVWiJAIKzGt1oublEuUL91tsfrw1uV /woiDaGseVys3KK8HTH86HQ5J+yb7YmsXWdeJH1Y8s3ybkFa9wDDmOeTDbjuDXJCdT0vrzG6eDE4Vyr4wBaCzaUm0TAnjJ5ALuXXFvFgcsWwHmUkTF64+MeF5/O4AnqWII6PgOvzuQKBgA1aMidGbHXvoiQ11MGhMamkU6BhWGkiQyMJ0W9L7knLbJBQRtIwTR5oC5EGvx4xw8+s6YEXUlJ+xNr0GiwuN6mnrE7KxcVvkkTMsW6l16XfiBL2otrIRPERlAlJl2U6D4wxaxfA+3ONQj3HKuRbEcyyFYgNrFlhKJiQle+RWy5pAoGASLpPwnAe1UBUKb/6oaOhuaP8ESkhBRX+M/SIS0sOLp5J4zADrJvG4XqdMGD9Y1FN14SmhcUEL6DIvAN5s9uAHE8QJz24iM2nv+xT4Bbo6qJG/tKtdYYpApoenVH/m73mJQYX5cI8d62+vHK8qk3PaLjxjOsR4tSPp9EK0FC05ZECgYEApT3z4ITgrrMFlQ0rcCBV21ywwBFLy47AYmxNh9DQ1SQU5fB5yaCa58RX+07cSJLdirBgD6qTmRo3WI8yTkXrt3Rw+fwUoB/CUMmhtbmSm3lPWqoK6yzbjkrqkPAsMZnMYbBoAAX4S2FraZMhOth/pQ/0zcSv6VkbehMHoAlYgfE=
# -----END PRIVATE KEY-----
#机构号
organiz-no: 159491
#内部商户号
mer-no: 99955127299R9G0
#银联商户号
mer-code: 831551272990064
#SN号
term-code: 56364927
#交易域名
api-url: https://posx.worthtech.net/industrial/payment/dynamic
#异步通知key
sign-key: 45d02f85ad34b5807700bb73d4354dcb96
public-key: |
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAjgka7LBaGdBr+b7B6Uv0+UReO9j5Kakv2PMK1w93jcalrJcrUJEUJ2+P6hcVy8nFFCprT8YEGzTwDv+biaDsGxRLeE9f1Ove/zdRSWXArNMkTqCWGmrUEyzHcAAv9aXH79fS0Zm1AGNvyrGvF/sNyb3zAHzlKv3qX9YUiO97T71VB38kvusBz+BtbwYtWodjBIsyPG030eTxuMuXv6BvExlCRlgYxlAiAmk3NokYpYpEFMx0WbvLrXJzzOxqbufzH3PhDXPyWOlRAVPVip1u9lHnI7isun/mes0g8FgxVOolPZjy1O+EDAZLETT9RtK6SlWkSTBn29QhLHTk4Aw/swIDAQAB
-----END PUBLIC KEY-----
private-key: |
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDF7YNor7LvSq1GLB7P592FKoxwFJfOLHibMtFQ2wTRDCiCufYuEoRWzm6mbpRQXauzaqfdwWfjlHbBXDJ2jx/CWawwfOmLHb6KHpdRgBV4OgbSW7Z+3WL8d2kTsc8GMRl7exJtw+vxhQM+sN8ny2zFSrsJKgEtjHAtKQmNgoXMA33xyfN3MbjoPu8okMftXpc4th+uf+LxyX1CCpc7egscNKcEqlFmekt36WJ4UcWLB6Cw4tZbd7IYaqTrFNNtmPi47D5YG0CW0ko8lJajOW61BjTS1X5lh4EUnX03+02YZwB2eOG4lZC/W/NiU7tc0xin7JjubgUTaePWsRInA0CnAgMBAAECggEAON2FbLVWFnQBFnEkpRz7wv+3e5gfCUgzmnteMfnLB3iTxwNAnHoLdZk3py+MAw72fsS81/RyMat89w7THMcAG+mBlCi/PI3eKXaiiPLguDsLrLJW21olz11LXjIuxZujs5tnbwvkJO7PQNq2Mou6g3B2Dir4TarUq9TnfrWqVTOFz8j7/g0Ha+FY8w2BqYw1APbwAJnNHqJylKIw3IM4UcusF5zbZRnqvd3BKF2bRVzv51FdMeSSEPtMKN7atUAiv5PLJiGXPiuM4s6DcMqo8kA3si3eFZrZT8V7gwR1sqv0S+8m5N2NqbzSsuuVBAMnId4H/q75UcPUfMGDWXNsoQKBgQD1UkQTV3hTpwU6QHYuASDde9aT+DMaHTC7PxMK1uTLnxt3udErV8gZBPUf7iwn9RsLxNAyh6I5iRvcpiAcZngG8qq/sCncupe/Jl1T0XxauvoWo5FMmKrr/ilFQJcqUdcvKz6Ztqj02ljDf5WvD2ZPT9FnYgl4kqK+vjEMUMsusQKBgQDOix445F0SalDZdgNljNNpGOfad3mrOda5yO22NGy4cFvm7ionYHVe19R/zUKe8hbEQpgsYfhb5E4nq6kIDxIm5enmLWfAj7aC5aiwghB60Ydk3XcDUpDr51U3PS4Y7WBeJoMhmTOPPw1uuuptKox9krdw816ib+BA3U2KC5Yq1wKBgCO09KmoCqCKZ+1hopHxohn6w3HIJ4/+fbBTbu8d9jFZGENl7XcUkNBrc05ReWXbfDNLU053hXpAZajJGVVo6MGCIq5B8uXo1tuAtwbTL/l4y5vt9OEkO4Sb+t/UlewX+20nKzZuassw2Mij0mKnqCmVIZKdp2lAVqXSwwra26gRAoGAGZC+vOwHWTAvsbsZ0IgN4wRiLnh7ZuZR3c0xH0x96JZ/yaXRMe6OmJ6+ftM5W9M7Xi+gBl5aD4XC5sYotganiIkM2qDkJsGjJbCnoLF4uLsWtzVydcbSiWCo+51nB07ajszVjmMYLrLvRrV8LucFXMW8Tw7Qt+qBJ4Y9AslMXSECgYEArOeQJ2Pkw12dyGENrtETTUbZsKanO8ptoZ+PUguzQQA8OVlRbRfKRUVgUvNniVi6MPILMkpdpVOrh8nCfDo6okN7jV0FasCq172ZsjItg3LuuaZHthaTt2R61ms6lpxS8EBPuL72/IcQRKyfPMbZDF2dbT4oS7OMF+KKHMuqxFE=
-----END PRIVATE KEY-----
notify-url-compute: https://phslgld.hnluchuan.com/admin-api/compute/wpgj/notify # 算力资源模块WPGJ回调地址
notify-url-api: https://phslgld.hnluchuan.com/admin-api/apihub/wpgj/notify # APIHub模块WPGJ回调地址
access-log: # 访问日志的配置项
enable: true
demo: false # 开启演示模式
......@@ -223,3 +263,4 @@ iot:
# 插件配置
pf4j:
pluginsDir: ${user.home}/plugins # 插件目录
......@@ -13,7 +13,7 @@ spring:
# noinspection SpringBootApplicationYaml
exclude:
- com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure # 排除 Druid 的自动配置,使用 dynamic-datasource-spring-boot-starter 配置多数据源
- org.springframework.boot.autoconfigure.quartz.QuartzAutoConfiguration # 默认 local 环境,不开启 Quartz 的自动配置
# - org.springframework.boot.autoconfigure.quartz.QuartzAutoConfiguration # 默认 local 环境,不开启 Quartz 的自动配置
- de.codecentric.boot.admin.server.config.AdminServerAutoConfiguration # 禁用 Spring Boot Admin 的 Server 的自动配置
- de.codecentric.boot.admin.server.ui.config.AdminServerUiAutoConfiguration # 禁用 Spring Boot Admin 的 Server UI 的自动配置
- de.codecentric.boot.admin.client.config.SpringBootAdminClientAutoConfiguration # 禁用 Spring Boot Admin 的 Client 的自动配置
......
......@@ -281,9 +281,12 @@ computility:
- /admin-api/infra/file/preview
- /app-api/infra/file/preview
- /open-api/external/**
- /admin-api/compute/wpgj/notify
- /admin-api/apihub/wpgj/notify
ignore-visit-urls:
- /admin-api/system/user/profile/**
- /admin-api/system/auth/**
- /admin-api/compute/wpgj/new-notify
ignore-tables:
- biz_banner_info
- biz_computility_information
......
......@@ -112,6 +112,10 @@
<groupId>org.codehaus.mojo</groupId>
<artifactId>flatten-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</pluginManagement>
......
mvn clean package -Dmaven.test.skip=true
echo "laMOYtX9Fo0qjrfD"
scp /Users/jackey/Documents/code/computing-power-platform/computing-power-platform-server-new/computility-server/target/computility-server.jar root@8.136.9.68:/data/computility
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment