更新多个功能

This commit is contained in:
2026-06-29 07:35:07 +08:00
parent fa94f52b53
commit cf7e2560b5
41 changed files with 2388 additions and 37 deletions
@@ -0,0 +1,54 @@
package cn.novalon.gym.manage.payment.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 支付记录响应DTO
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "支付记录")
public class PaymentRecordResponse {
@Schema(description = "订单ID")
private Long id;
@Schema(description = "订单号")
private String orderNo;
@Schema(description = "用户编号(会员ID)")
private Long memberId;
@Schema(description = "支付方式(ALIPAY/WECHAT)")
private String tradeType;
@Schema(description = "购买内容(商品描述)")
private String goodsDesc;
@Schema(description = "订单类型(MEMBER_CARD/GROUP_COURSE/GOODS)")
private String orderType;
@Schema(description = "支付金额(元)")
private BigDecimal transAmt;
@Schema(description = "支付状态(PENDING/SUCCESS/FAIL/CLOSED)")
private String payStatus;
@Schema(description = "支付时间")
private String payTime;
@Schema(description = "汇付流水号")
private String hfSeqId;
@Schema(description = "创建时间")
private String createdAt;
}
@@ -0,0 +1,47 @@
package cn.novalon.gym.manage.payment.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
/**
* 营业额统计数据
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "营业额统计数据")
public class RevenueStatistics {
@Schema(description = "今日收入(元)")
private BigDecimal todayIncome;
@Schema(description = "今日退款(元)")
private BigDecimal todayRefund;
@Schema(description = "今日订单数")
private Long todayOrderCount;
@Schema(description = "当月收入(元)")
private BigDecimal monthIncome;
@Schema(description = "当月退款(元)")
private BigDecimal monthRefund;
@Schema(description = "当月订单数")
private Long monthOrderCount;
@Schema(description = "当年收入(元)")
private BigDecimal yearIncome;
@Schema(description = "当年退款(元)")
private BigDecimal yearRefund;
@Schema(description = "当年订单数")
private Long yearOrderCount;
}
@@ -0,0 +1,74 @@
package cn.novalon.gym.manage.payment.handler;
import cn.novalon.gym.manage.payment.dto.ApiResponse;
import cn.novalon.gym.manage.payment.service.PaymentService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono;
/**
* 支付营业数据 Handler
*
* @author system
* @date 2026-06-29
*/
@Slf4j
@Component
@Tag(name = "支付营业数据", description = "营业额统计与支付记录查询")
public class PaymentRevenueHandler {
private final PaymentService paymentService;
public PaymentRevenueHandler(PaymentService paymentService) {
this.paymentService = paymentService;
}
@Operation(summary = "获取营业额统计", description = "获取今日、当月、当年的收入和退款情况")
public Mono<ServerResponse> getRevenueStatistics(ServerRequest request) {
log.info("[Revenue] 查询营业额统计");
return paymentService.getRevenueStatistics()
.flatMap(stats -> ServerResponse.ok()
.bodyValue(ApiResponse.success(stats)))
.onErrorResume(e -> {
log.error("[Revenue] 查询营业额统计失败", e);
return ServerResponse.ok()
.bodyValue(ApiResponse.error("查询营业额统计失败: " + e.getMessage()));
});
}
@Operation(summary = "查询支付记录", description = "分页查询用户支付记录,支持按会员ID、支付状态、支付方式筛选")
public Mono<ServerResponse> getPaymentRecords(ServerRequest request) {
String memberIdStr = request.queryParam("memberId").orElse(null);
Long memberId = null;
if (memberIdStr != null && !memberIdStr.isEmpty()) {
try {
memberId = Long.parseLong(memberIdStr);
} catch (NumberFormatException e) {
return ServerResponse.ok()
.bodyValue(ApiResponse.error("会员ID格式不正确"));
}
}
String payStatus = request.queryParam("payStatus").orElse(null);
String tradeType = request.queryParam("tradeType").orElse(null);
int page = Integer.parseInt(request.queryParam("page").orElse("1"));
int pageSize = Integer.parseInt(request.queryParam("pageSize").orElse("20"));
log.info("[Revenue] 查询支付记录: memberId={}, payStatus={}, tradeType={}, page={}, pageSize={}",
memberId, payStatus, tradeType, page, pageSize);
return paymentService.getPaymentRecords(memberId, payStatus, tradeType, page, pageSize)
.flatMap(result -> ServerResponse.ok()
.bodyValue(ApiResponse.success(result)))
.onErrorResume(e -> {
log.error("[Revenue] 查询支付记录失败", e);
return ServerResponse.ok()
.bodyValue(ApiResponse.error("查询支付记录失败: " + e.getMessage()));
});
}
}
@@ -8,6 +8,7 @@ import org.springframework.stereotype.Repository;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Repository
@@ -38,4 +39,24 @@ public interface PaymentOrderRepository extends R2dbcRepository<PaymentOrder, Lo
Flux<PaymentOrder> findAllByDeletedAtIsNull();
Flux<PaymentOrder> findByMemberIdAndDeletedAtIsNull(Long memberId);
// ===== 营业额统计 =====
/** 统计指定时间范围内成功支付的金额总和 */
@Query("SELECT COALESCE(SUM(trans_amt), 0) FROM payment_order WHERE pay_status = 'SUCCESS' AND pay_time >= :startTime AND pay_time < :endTime AND deleted_at IS NULL")
Mono<BigDecimal> sumSuccessAmount(LocalDateTime startTime, LocalDateTime endTime);
/** 统计指定时间范围内的成功订单数 */
@Query("SELECT COUNT(*) FROM payment_order WHERE pay_status = 'SUCCESS' AND pay_time >= :startTime AND pay_time < :endTime AND deleted_at IS NULL")
Mono<Long> countSuccessOrders(LocalDateTime startTime, LocalDateTime endTime);
// ===== 支付记录分页查询 =====
/** 按条件分页查询支付记录 */
@Query("SELECT * FROM payment_order WHERE (:memberId IS NULL OR member_id = :memberId) AND (:payStatus IS NULL OR pay_status = :payStatus) AND (:tradeType IS NULL OR trade_type = :tradeType) AND deleted_at IS NULL ORDER BY created_at DESC LIMIT :limit OFFSET :offset")
Flux<PaymentOrder> findPaymentRecords(Long memberId, String payStatus, String tradeType, int limit, int offset);
/** 按条件统计支付记录总数 */
@Query("SELECT COUNT(*) FROM payment_order WHERE (:memberId IS NULL OR member_id = :memberId) AND (:payStatus IS NULL OR pay_status = :payStatus) AND (:tradeType IS NULL OR trade_type = :tradeType) AND deleted_at IS NULL")
Mono<Long> countPaymentRecords(Long memberId, String payStatus, String tradeType);
}
@@ -1,7 +1,9 @@
package cn.novalon.gym.manage.payment.service;
import cn.novalon.gym.manage.payment.dto.CreatePaymentRequest;
import cn.novalon.gym.manage.payment.dto.PaymentRecordResponse;
import cn.novalon.gym.manage.payment.dto.PaymentResponse;
import cn.novalon.gym.manage.payment.dto.RevenueStatistics;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@@ -30,4 +32,10 @@ public interface PaymentService {
Mono<PaymentResponse> getPendingOrder(Long memberId, String orderType);
Mono<Boolean> closeOrder(Long memberId, String orderId);
/** 获取营业额统计(今日/当月/当年) */
Mono<RevenueStatistics> getRevenueStatistics();
/** 分页查询支付记录 */
Mono<Map<String, Object>> getPaymentRecords(Long memberId, String payStatus, String tradeType, int page, int pageSize);
}
@@ -3,7 +3,9 @@ package cn.novalon.gym.manage.payment.service.impl;
import cn.novalon.gym.manage.common.util.RedisUtil;
import cn.novalon.gym.manage.payment.config.HuifuProperties;
import cn.novalon.gym.manage.payment.dto.CreatePaymentRequest;
import cn.novalon.gym.manage.payment.dto.PaymentRecordResponse;
import cn.novalon.gym.manage.payment.dto.PaymentResponse;
import cn.novalon.gym.manage.payment.dto.RevenueStatistics;
import cn.novalon.gym.manage.payment.entity.PaymentOrder;
import cn.novalon.gym.manage.payment.repository.PaymentOrderRepository;
import cn.novalon.gym.manage.payment.service.PaymentNotifyService;
@@ -934,4 +936,76 @@ public class PaymentServiceImpl implements PaymentService {
})
.switchIfEmpty(Mono.just(false));
}
@Override
public Mono<RevenueStatistics> getRevenueStatistics() {
LocalDateTime now = LocalDateTime.now();
// 今日
LocalDateTime todayStart = now.toLocalDate().atStartOfDay();
LocalDateTime todayEnd = todayStart.plusDays(1);
// 当月
LocalDateTime monthStart = now.toLocalDate().withDayOfMonth(1).atStartOfDay();
LocalDateTime monthEnd = monthStart.plusMonths(1);
// 当年
LocalDateTime yearStart = now.toLocalDate().withDayOfYear(1).atStartOfDay();
LocalDateTime yearEnd = yearStart.plusYears(1);
Mono<BigDecimal> todayIncomeMono = paymentOrderRepository.sumSuccessAmount(todayStart, todayEnd);
Mono<Long> todayCountMono = paymentOrderRepository.countSuccessOrders(todayStart, todayEnd);
Mono<BigDecimal> monthIncomeMono = paymentOrderRepository.sumSuccessAmount(monthStart, monthEnd);
Mono<Long> monthCountMono = paymentOrderRepository.countSuccessOrders(monthStart, monthEnd);
Mono<BigDecimal> yearIncomeMono = paymentOrderRepository.sumSuccessAmount(yearStart, yearEnd);
Mono<Long> yearCountMono = paymentOrderRepository.countSuccessOrders(yearStart, yearEnd);
return Mono.zip(todayIncomeMono, todayCountMono, monthIncomeMono, monthCountMono, yearIncomeMono, yearCountMono)
.map(tuple -> RevenueStatistics.builder()
.todayIncome(tuple.getT1() != null ? tuple.getT1() : BigDecimal.ZERO)
.todayRefund(BigDecimal.ZERO) // 退款功能暂未实现
.todayOrderCount(tuple.getT2() != null ? tuple.getT2() : 0L)
.monthIncome(tuple.getT3() != null ? tuple.getT3() : BigDecimal.ZERO)
.monthRefund(BigDecimal.ZERO) // 退款功能暂未实现
.monthOrderCount(tuple.getT4() != null ? tuple.getT4() : 0L)
.yearIncome(tuple.getT5() != null ? tuple.getT5() : BigDecimal.ZERO)
.yearRefund(BigDecimal.ZERO) // 退款功能暂未实现
.yearOrderCount(tuple.getT6() != null ? tuple.getT6() : 0L)
.build());
}
@Override
public Mono<Map<String, Object>> getPaymentRecords(Long memberId, String payStatus, String tradeType, int page, int pageSize) {
int offset = (page - 1) * pageSize;
Mono<List<PaymentRecordResponse>> recordsMono = paymentOrderRepository
.findPaymentRecords(memberId, payStatus, tradeType, pageSize, offset)
.map(order -> PaymentRecordResponse.builder()
.id(order.getId())
.orderNo(order.getOrderNo())
.memberId(order.getMemberId())
.tradeType(order.getTradeType())
.goodsDesc(order.getGoodsDesc())
.orderType(order.getOrderType())
.transAmt(order.getTransAmt())
.payStatus(order.getPayStatus())
.payTime(order.getPayTime() != null
? order.getPayTime().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))
: null)
.hfSeqId(order.getHfSeqId())
.createdAt(order.getCreatedAt() != null
? order.getCreatedAt().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))
: null)
.build())
.collectList();
Mono<Long> totalMono = paymentOrderRepository.countPaymentRecords(memberId, payStatus, tradeType);
return Mono.zip(recordsMono, totalMono)
.map(tuple -> {
Map<String, Object> result = new HashMap<>();
result.put("records", tuple.getT1());
result.put("total", tuple.getT2());
result.put("page", page);
result.put("pageSize", pageSize);
return result;
});
}
}