docs: reorganize documentation structure
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
package com.gym.manage;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SpringBootApplication
|
||||
public class GymManageApplication {
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(GymManageApplication.class, args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.gym.manage.api.controller.booking;
|
||||
|
||||
import com.gym.manage.api.dto.request.BookingCreateRequest;
|
||||
import com.gym.manage.api.dto.response.BookingRecordResponse;
|
||||
import com.gym.manage.application.service.BookingService;
|
||||
import com.gym.manage.common.result.Result;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
@Tag(name = "预约管理", description = "预约管理相关接口")
|
||||
@RestController
|
||||
@RequestMapping("/bookings")
|
||||
@RequiredArgsConstructor
|
||||
public class BookingController {
|
||||
|
||||
private final BookingService bookingService;
|
||||
|
||||
@Operation(summary = "创建预约", description = "预约时段")
|
||||
@PostMapping
|
||||
public Mono<Result<BookingRecordResponse>> createBooking(@Valid @RequestBody BookingCreateRequest request) {
|
||||
return bookingService.createBooking(request)
|
||||
.map(Result::success);
|
||||
}
|
||||
|
||||
@Operation(summary = "查询预约", description = "根据ID查询预约")
|
||||
@GetMapping("/{id}")
|
||||
public Mono<Result<BookingRecordResponse>> getBooking(@PathVariable Long id) {
|
||||
return bookingService.getBooking(id)
|
||||
.map(Result::success);
|
||||
}
|
||||
|
||||
@Operation(summary = "会员预约列表", description = "查询会员的预约列表")
|
||||
@GetMapping("/members/{memberId}")
|
||||
public Mono<Result<Flux<BookingRecordResponse>>> listMemberBookings(
|
||||
@PathVariable Long memberId,
|
||||
@RequestParam(defaultValue = "0") int page,
|
||||
@RequestParam(defaultValue = "20") int size
|
||||
) {
|
||||
return Mono.just(Result.success(bookingService.listMemberBookings(memberId, page, size)));
|
||||
}
|
||||
|
||||
@Operation(summary = "取消预约", description = "取消预约")
|
||||
@DeleteMapping("/{id}")
|
||||
public Mono<Result<Void>> cancelBooking(
|
||||
@PathVariable Long id,
|
||||
@RequestParam(required = false) String reason
|
||||
) {
|
||||
return bookingService.cancelBooking(id, reason)
|
||||
.then(Mono.just(Result.success()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package com.gym.manage.api.controller.member;
|
||||
|
||||
import com.gym.manage.api.dto.request.MemberCardCreateRequest;
|
||||
import com.gym.manage.api.dto.request.MemberCreateRequest;
|
||||
import com.gym.manage.api.dto.request.MemberUpdateRequest;
|
||||
import com.gym.manage.api.dto.response.MemberCardResponse;
|
||||
import com.gym.manage.api.dto.response.MemberResponse;
|
||||
import com.gym.manage.application.service.MemberCardService;
|
||||
import com.gym.manage.application.service.MemberService;
|
||||
import com.gym.manage.common.result.Result;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
@Tag(name = "会员管理", description = "会员管理相关接口")
|
||||
@RestController
|
||||
@RequestMapping("/members")
|
||||
@RequiredArgsConstructor
|
||||
public class MemberController {
|
||||
|
||||
private final MemberService memberService;
|
||||
private final MemberCardService memberCardService;
|
||||
|
||||
@Operation(summary = "创建会员", description = "创建新会员")
|
||||
@PostMapping
|
||||
public Mono<Result<MemberResponse>> createMember(@Valid @RequestBody MemberCreateRequest request) {
|
||||
return memberService.createMember(request)
|
||||
.map(Result::success);
|
||||
}
|
||||
|
||||
@Operation(summary = "查询会员", description = "根据ID查询会员")
|
||||
@GetMapping("/{id}")
|
||||
public Mono<Result<MemberResponse>> getMember(@PathVariable Long id) {
|
||||
return memberService.getMember(id)
|
||||
.map(Result::success);
|
||||
}
|
||||
|
||||
@Operation(summary = "会员列表", description = "查询会员列表")
|
||||
@GetMapping
|
||||
public Mono<Result<Flux<MemberResponse>>> listMembers(
|
||||
@RequestParam Long tenantId,
|
||||
@RequestParam Long storeId,
|
||||
@RequestParam(defaultValue = "0") int page,
|
||||
@RequestParam(defaultValue = "20") int size
|
||||
) {
|
||||
return Mono.just(Result.success(memberService.listMembers(tenantId, storeId, page, size)));
|
||||
}
|
||||
|
||||
@Operation(summary = "更新会员", description = "更新会员信息")
|
||||
@PutMapping("/{id}")
|
||||
public Mono<Result<MemberResponse>> updateMember(
|
||||
@PathVariable Long id,
|
||||
@Valid @RequestBody MemberUpdateRequest request
|
||||
) {
|
||||
return memberService.updateMember(id, request)
|
||||
.map(Result::success);
|
||||
}
|
||||
|
||||
@Operation(summary = "删除会员", description = "删除会员(软删除)")
|
||||
@DeleteMapping("/{id}")
|
||||
public Mono<Result<Void>> deleteMember(@PathVariable Long id) {
|
||||
return memberService.deleteMember(id)
|
||||
.then(Mono.just(Result.success()));
|
||||
}
|
||||
|
||||
@Operation(summary = "创建会员卡", description = "为会员创建会员卡")
|
||||
@PostMapping("/{memberId}/cards")
|
||||
public Mono<Result<MemberCardResponse>> createMemberCard(
|
||||
@PathVariable Long memberId,
|
||||
@Valid @RequestBody MemberCardCreateRequest request
|
||||
) {
|
||||
request.setMemberId(memberId);
|
||||
return memberCardService.createMemberCard(request)
|
||||
.map(Result::success);
|
||||
}
|
||||
|
||||
@Operation(summary = "查询会员卡", description = "查询会员的会员卡列表")
|
||||
@GetMapping("/{memberId}/cards")
|
||||
public Mono<Result<Flux<MemberCardResponse>>> getMemberCards(@PathVariable Long memberId) {
|
||||
return Mono.just(Result.success(memberCardService.getMemberCards(memberId)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.gym.manage.api.dto.request;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
public class BookingCreateRequest {
|
||||
@NotNull(message = "会员ID不能为空")
|
||||
private Long memberId;
|
||||
|
||||
@NotNull(message = "时段ID不能为空")
|
||||
private Long slotId;
|
||||
|
||||
private String remark;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.gym.manage.api.dto.request;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
|
||||
@Data
|
||||
public class MemberCardCreateRequest {
|
||||
@NotNull(message = "会员ID不能为空")
|
||||
private Long memberId;
|
||||
|
||||
@NotBlank(message = "卡号不能为空")
|
||||
private String cardNo;
|
||||
|
||||
@NotBlank(message = "卡类型不能为空")
|
||||
private String cardType;
|
||||
|
||||
private String cardName;
|
||||
|
||||
private Integer totalCount;
|
||||
|
||||
private Integer totalDays;
|
||||
|
||||
private LocalDate startDate;
|
||||
|
||||
private LocalDate endDate;
|
||||
|
||||
private BigDecimal price;
|
||||
|
||||
private BigDecimal paidAmount;
|
||||
|
||||
private String paymentMethod;
|
||||
|
||||
private String remark;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.gym.manage.api.dto.request;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
@Data
|
||||
public class MemberCreateRequest {
|
||||
@NotNull(message = "租户ID不能为空")
|
||||
private Long tenantId;
|
||||
|
||||
@NotNull(message = "门店ID不能为空")
|
||||
private Long storeId;
|
||||
|
||||
@NotBlank(message = "姓名不能为空")
|
||||
private String name;
|
||||
|
||||
@NotBlank(message = "手机号不能为空")
|
||||
private String phone;
|
||||
|
||||
private String gender;
|
||||
|
||||
private LocalDate birthday;
|
||||
|
||||
private String idCard;
|
||||
|
||||
private String emergencyContact;
|
||||
|
||||
private String emergencyPhone;
|
||||
|
||||
private String level = "NORMAL";
|
||||
|
||||
private String source;
|
||||
|
||||
private String remark;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.gym.manage.api.dto.request;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
@Data
|
||||
public class MemberUpdateRequest {
|
||||
@NotBlank(message = "姓名不能为空")
|
||||
private String name;
|
||||
|
||||
private String gender;
|
||||
|
||||
private LocalDate birthday;
|
||||
|
||||
private String idCard;
|
||||
|
||||
private String emergencyContact;
|
||||
|
||||
private String emergencyPhone;
|
||||
|
||||
private String level;
|
||||
|
||||
private String status;
|
||||
|
||||
private String remark;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.gym.manage.api.dto.response;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class BookingRecordResponse {
|
||||
private Long id;
|
||||
private Long memberId;
|
||||
private Long slotId;
|
||||
private Long coachId;
|
||||
private String courseName;
|
||||
private LocalDateTime bookingTime;
|
||||
private String status;
|
||||
private String cancelReason;
|
||||
private LocalDateTime cancelTime;
|
||||
private LocalDateTime checkinTime;
|
||||
private String remark;
|
||||
private LocalDateTime createdAt;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.gym.manage.api.dto.response;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class MemberCardResponse {
|
||||
private Long id;
|
||||
private Long memberId;
|
||||
private String cardNo;
|
||||
private String cardType;
|
||||
private String cardName;
|
||||
private Integer totalCount;
|
||||
private Integer remainingCount;
|
||||
private Integer totalDays;
|
||||
private Integer remainingDays;
|
||||
private LocalDate startDate;
|
||||
private LocalDate endDate;
|
||||
private String status;
|
||||
private BigDecimal price;
|
||||
private BigDecimal paidAmount;
|
||||
private String paymentMethod;
|
||||
private String remark;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.gym.manage.api.dto.response;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class MemberResponse {
|
||||
private Long id;
|
||||
private Long tenantId;
|
||||
private Long storeId;
|
||||
private String name;
|
||||
private String phone;
|
||||
private String gender;
|
||||
private LocalDate birthday;
|
||||
private String idCard;
|
||||
private String emergencyContact;
|
||||
private String emergencyPhone;
|
||||
private String level;
|
||||
private String status;
|
||||
private String source;
|
||||
private String remark;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
package com.gym.manage.application.service;
|
||||
|
||||
import com.gym.manage.api.dto.request.BookingCreateRequest;
|
||||
import com.gym.manage.api.dto.response.BookingRecordResponse;
|
||||
import com.gym.manage.common.constant.ErrorCode;
|
||||
import com.gym.manage.common.exception.BusinessException;
|
||||
import com.gym.manage.domain.entity.BookingRecord;
|
||||
import com.gym.manage.domain.entity.BookingSlot;
|
||||
import com.gym.manage.domain.repository.BookingRecordRepository;
|
||||
import com.gym.manage.domain.repository.BookingSlotRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class BookingService {
|
||||
|
||||
private final BookingSlotRepository bookingSlotRepository;
|
||||
private final BookingRecordRepository bookingRecordRepository;
|
||||
|
||||
@Transactional
|
||||
public Mono<BookingRecordResponse> createBooking(BookingCreateRequest request) {
|
||||
log.info("创建预约: memberId={}, slotId={}", request.getMemberId(), request.getSlotId());
|
||||
|
||||
return validateAndBook(request)
|
||||
.map(this::toBookingRecordResponse)
|
||||
.doOnSuccess(response -> log.info("预约创建成功: bookingId={}", response.getId()))
|
||||
.doOnError(e -> log.error("预约创建失败: memberId={}, slotId={}, error={}",
|
||||
request.getMemberId(), request.getSlotId(), e.getMessage()));
|
||||
}
|
||||
|
||||
private Mono<BookingRecord> validateAndBook(BookingCreateRequest request) {
|
||||
return bookingSlotRepository.findByIdAndDeletedAtIsNull(request.getSlotId())
|
||||
.switchIfEmpty(Mono.error(new BusinessException(ErrorCode.SLOT_NOT_FOUND, "时段不存在")))
|
||||
.flatMap(slot -> {
|
||||
if (!"AVAILABLE".equals(slot.getStatus())) {
|
||||
return Mono.error(new BusinessException(ErrorCode.SLOT_NOT_AVAILABLE, "时段不可预约"));
|
||||
}
|
||||
|
||||
if (slot.getBookedCount() >= slot.getMaxCapacity()) {
|
||||
return Mono.error(new BusinessException(ErrorCode.SLOT_NOT_AVAILABLE, "时段已满"));
|
||||
}
|
||||
|
||||
return bookingRecordRepository.findByMemberIdAndSlotIdAndDeletedAtIsNull(
|
||||
request.getMemberId(), request.getSlotId()
|
||||
).flatMap(existing -> Mono.<BookingRecord>error(
|
||||
new BusinessException(ErrorCode.BOOKING_NOT_FOUND, "已预约该时段")
|
||||
)).switchIfEmpty(createBookingRecord(request, slot));
|
||||
});
|
||||
}
|
||||
|
||||
private Mono<BookingRecord> createBookingRecord(BookingCreateRequest request, BookingSlot slot) {
|
||||
return bookingSlotRepository.incrementBookedCount(request.getSlotId())
|
||||
.flatMap(rows -> {
|
||||
if (rows > 0) {
|
||||
BookingRecord record = new BookingRecord();
|
||||
record.setTenantId(slot.getTenantId());
|
||||
record.setStoreId(slot.getStoreId());
|
||||
record.setMemberId(request.getMemberId());
|
||||
record.setSlotId(request.getSlotId());
|
||||
record.setCoachId(slot.getCoachId());
|
||||
record.setCourseName(slot.getCourseName());
|
||||
record.setBookingTime(LocalDateTime.now());
|
||||
record.setStatus("BOOKED");
|
||||
record.setRemark(request.getRemark());
|
||||
record.setCreatedAt(LocalDateTime.now());
|
||||
record.setUpdatedAt(LocalDateTime.now());
|
||||
|
||||
return bookingRecordRepository.save(record);
|
||||
} else {
|
||||
return Mono.error(new BusinessException(ErrorCode.SLOT_NOT_AVAILABLE, "预约失败,请重试"));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public Mono<BookingRecordResponse> getBooking(Long id) {
|
||||
log.info("查询预约: bookingId={}", id);
|
||||
|
||||
return bookingRecordRepository.findByIdAndDeletedAtIsNull(id)
|
||||
.switchIfEmpty(Mono.error(new BusinessException(ErrorCode.BOOKING_NOT_FOUND, "预约不存在")))
|
||||
.map(this::toBookingRecordResponse);
|
||||
}
|
||||
|
||||
public Flux<BookingRecordResponse> listMemberBookings(Long memberId, int page, int size) {
|
||||
log.info("查询会员预约列表: memberId={}", memberId);
|
||||
|
||||
return bookingRecordRepository.findByMemberIdAndDeletedAtIsNull(memberId,
|
||||
org.springframework.data.domain.PageRequest.of(page, size))
|
||||
.map(this::toBookingRecordResponse);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Mono<Void> cancelBooking(Long id, String reason) {
|
||||
log.info("取消预约: bookingId={}", id);
|
||||
|
||||
return bookingRecordRepository.findByIdAndDeletedAtIsNull(id)
|
||||
.switchIfEmpty(Mono.error(new BusinessException(ErrorCode.BOOKING_NOT_FOUND, "预约不存在")))
|
||||
.flatMap(record -> {
|
||||
if ("CANCELLED".equals(record.getStatus())) {
|
||||
return Mono.error(new BusinessException(ErrorCode.BOOKING_ALREADY_CANCELLED, "预约已取消"));
|
||||
}
|
||||
|
||||
return bookingSlotRepository.decrementBookedCount(record.getSlotId())
|
||||
.flatMap(rows -> {
|
||||
record.setStatus("CANCELLED");
|
||||
record.setCancelReason(reason);
|
||||
record.setCancelTime(LocalDateTime.now());
|
||||
record.setUpdatedAt(LocalDateTime.now());
|
||||
|
||||
return bookingRecordRepository.save(record);
|
||||
});
|
||||
})
|
||||
.then();
|
||||
}
|
||||
|
||||
private BookingRecordResponse toBookingRecordResponse(BookingRecord record) {
|
||||
return BookingRecordResponse.builder()
|
||||
.id(record.getId())
|
||||
.memberId(record.getMemberId())
|
||||
.slotId(record.getSlotId())
|
||||
.coachId(record.getCoachId())
|
||||
.courseName(record.getCourseName())
|
||||
.bookingTime(record.getBookingTime())
|
||||
.status(record.getStatus())
|
||||
.cancelReason(record.getCancelReason())
|
||||
.cancelTime(record.getCancelTime())
|
||||
.checkinTime(record.getCheckinTime())
|
||||
.remark(record.getRemark())
|
||||
.createdAt(record.getCreatedAt())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package com.gym.manage.application.service;
|
||||
|
||||
import com.gym.manage.api.dto.request.MemberCardCreateRequest;
|
||||
import com.gym.manage.api.dto.response.MemberCardResponse;
|
||||
import com.gym.manage.common.constant.ErrorCode;
|
||||
import com.gym.manage.common.exception.BusinessException;
|
||||
import com.gym.manage.domain.entity.Member;
|
||||
import com.gym.manage.domain.entity.MemberCard;
|
||||
import com.gym.manage.domain.repository.MemberCardRepository;
|
||||
import com.gym.manage.domain.repository.MemberRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class MemberCardService {
|
||||
|
||||
private final MemberCardRepository memberCardRepository;
|
||||
private final MemberRepository memberRepository;
|
||||
|
||||
public Mono<MemberCardResponse> createMemberCard(MemberCardCreateRequest request) {
|
||||
log.info("创建会员卡: memberId={}, cardNo={}", request.getMemberId(), request.getCardNo());
|
||||
|
||||
return memberRepository.findByIdAndDeletedAtIsNull(request.getMemberId())
|
||||
.switchIfEmpty(Mono.error(new BusinessException(ErrorCode.MEMBER_NOT_FOUND, "会员不存在")))
|
||||
.flatMap(member -> memberCardRepository.findByCardNoAndDeletedAtIsNull(request.getCardNo()))
|
||||
.flatMap(existingCard -> Mono.<MemberCard>error(
|
||||
new BusinessException(ErrorCode.MEMBER_CARD_NOT_FOUND, "卡号已存在")
|
||||
))
|
||||
.switchIfEmpty(Mono.defer(() -> {
|
||||
MemberCard card = new MemberCard();
|
||||
card.setTenantId(request.getMemberId());
|
||||
card.setStoreId(request.getMemberId());
|
||||
card.setMemberId(request.getMemberId());
|
||||
card.setCardNo(request.getCardNo());
|
||||
card.setCardType(request.getCardType());
|
||||
card.setCardName(request.getCardName());
|
||||
card.setTotalCount(request.getTotalCount());
|
||||
card.setRemainingCount(request.getTotalCount());
|
||||
card.setTotalDays(request.getTotalDays());
|
||||
card.setRemainingDays(request.getTotalDays());
|
||||
card.setStartDate(request.getStartDate());
|
||||
card.setEndDate(request.getEndDate());
|
||||
card.setStatus("ACTIVE");
|
||||
card.setPrice(request.getPrice());
|
||||
card.setPaidAmount(request.getPaidAmount());
|
||||
card.setPaymentMethod(request.getPaymentMethod());
|
||||
card.setRemark(request.getRemark());
|
||||
card.setCreatedAt(LocalDateTime.now());
|
||||
card.setUpdatedAt(LocalDateTime.now());
|
||||
|
||||
return memberCardRepository.save(card);
|
||||
}))
|
||||
.map(this::toMemberCardResponse)
|
||||
.doOnSuccess(response -> log.info("会员卡创建成功: cardId={}", response.getId()))
|
||||
.doOnError(e -> log.error("会员卡创建失败: memberId={}, error={}", request.getMemberId(), e.getMessage()));
|
||||
}
|
||||
|
||||
public Flux<MemberCardResponse> getMemberCards(Long memberId) {
|
||||
log.info("查询会员卡列表: memberId={}", memberId);
|
||||
|
||||
return memberCardRepository.findByMemberIdAndDeletedAtIsNull(memberId)
|
||||
.map(this::toMemberCardResponse);
|
||||
}
|
||||
|
||||
private MemberCardResponse toMemberCardResponse(MemberCard card) {
|
||||
return MemberCardResponse.builder()
|
||||
.id(card.getId())
|
||||
.memberId(card.getMemberId())
|
||||
.cardNo(card.getCardNo())
|
||||
.cardType(card.getCardType())
|
||||
.cardName(card.getCardName())
|
||||
.totalCount(card.getTotalCount())
|
||||
.remainingCount(card.getRemainingCount())
|
||||
.totalDays(card.getTotalDays())
|
||||
.remainingDays(card.getRemainingDays())
|
||||
.startDate(card.getStartDate())
|
||||
.endDate(card.getEndDate())
|
||||
.status(card.getStatus())
|
||||
.price(card.getPrice())
|
||||
.paidAmount(card.getPaidAmount())
|
||||
.paymentMethod(card.getPaymentMethod())
|
||||
.remark(card.getRemark())
|
||||
.createdAt(card.getCreatedAt())
|
||||
.updatedAt(card.getUpdatedAt())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
package com.gym.manage.application.service;
|
||||
|
||||
import com.gym.manage.api.dto.request.MemberCreateRequest;
|
||||
import com.gym.manage.api.dto.request.MemberUpdateRequest;
|
||||
import com.gym.manage.api.dto.response.MemberResponse;
|
||||
import com.gym.manage.common.constant.ErrorCode;
|
||||
import com.gym.manage.common.exception.BusinessException;
|
||||
import com.gym.manage.domain.entity.Member;
|
||||
import com.gym.manage.domain.repository.MemberRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.stereotype.Service;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class MemberService {
|
||||
|
||||
private final MemberRepository memberRepository;
|
||||
|
||||
public Mono<MemberResponse> createMember(MemberCreateRequest request) {
|
||||
log.info("创建会员: phone={}", request.getPhone());
|
||||
|
||||
return memberRepository.findByPhoneAndDeletedAtIsNull(request.getPhone())
|
||||
.flatMap(existingMember -> Mono.<Member>error(
|
||||
new BusinessException(ErrorCode.MEMBER_ALREADY_EXISTS, "该手机号已注册")
|
||||
))
|
||||
.switchIfEmpty(Mono.defer(() -> {
|
||||
Member member = new Member();
|
||||
member.setTenantId(request.getTenantId());
|
||||
member.setStoreId(request.getStoreId());
|
||||
member.setName(request.getName());
|
||||
member.setPhone(request.getPhone());
|
||||
member.setGender(request.getGender());
|
||||
member.setBirthday(request.getBirthday());
|
||||
member.setIdCard(request.getIdCard());
|
||||
member.setEmergencyContact(request.getEmergencyContact());
|
||||
member.setEmergencyPhone(request.getEmergencyPhone());
|
||||
member.setLevel(request.getLevel());
|
||||
member.setStatus("ACTIVE");
|
||||
member.setSource(request.getSource());
|
||||
member.setRemark(request.getRemark());
|
||||
member.setCreatedAt(LocalDateTime.now());
|
||||
member.setUpdatedAt(LocalDateTime.now());
|
||||
|
||||
return memberRepository.save(member);
|
||||
}))
|
||||
.map(this::toMemberResponse)
|
||||
.doOnSuccess(response -> log.info("会员创建成功: memberId={}", response.getId()))
|
||||
.doOnError(e -> log.error("会员创建失败: phone={}, error={}", request.getPhone(), e.getMessage()));
|
||||
}
|
||||
|
||||
public Mono<MemberResponse> getMember(Long id) {
|
||||
log.info("查询会员: memberId={}", id);
|
||||
|
||||
return memberRepository.findByIdAndDeletedAtIsNull(id)
|
||||
.switchIfEmpty(Mono.error(new BusinessException(ErrorCode.MEMBER_NOT_FOUND, "会员不存在")))
|
||||
.map(this::toMemberResponse)
|
||||
.doOnSuccess(response -> log.info("查询会员成功: memberId={}", id))
|
||||
.doOnError(e -> log.error("查询会员失败: memberId={}, error={}", id, e.getMessage()));
|
||||
}
|
||||
|
||||
public Flux<MemberResponse> listMembers(Long tenantId, Long storeId, int page, int size) {
|
||||
log.info("查询会员列表: tenantId={}, storeId={}, page={}, size={}", tenantId, storeId, page, size);
|
||||
|
||||
return memberRepository.findByTenantIdAndStoreIdAndDeletedAtIsNull(
|
||||
tenantId, storeId, PageRequest.of(page, size)
|
||||
).map(this::toMemberResponse);
|
||||
}
|
||||
|
||||
public Mono<MemberResponse> updateMember(Long id, MemberUpdateRequest request) {
|
||||
log.info("更新会员: memberId={}", id);
|
||||
|
||||
return memberRepository.findByIdAndDeletedAtIsNull(id)
|
||||
.switchIfEmpty(Mono.error(new BusinessException(ErrorCode.MEMBER_NOT_FOUND, "会员不存在")))
|
||||
.flatMap(member -> {
|
||||
member.setName(request.getName());
|
||||
member.setGender(request.getGender());
|
||||
member.setBirthday(request.getBirthday());
|
||||
member.setIdCard(request.getIdCard());
|
||||
member.setEmergencyContact(request.getEmergencyContact());
|
||||
member.setEmergencyPhone(request.getEmergencyPhone());
|
||||
member.setLevel(request.getLevel());
|
||||
member.setStatus(request.getStatus());
|
||||
member.setRemark(request.getRemark());
|
||||
member.setUpdatedAt(LocalDateTime.now());
|
||||
|
||||
return memberRepository.save(member);
|
||||
})
|
||||
.map(this::toMemberResponse)
|
||||
.doOnSuccess(response -> log.info("会员更新成功: memberId={}", id))
|
||||
.doOnError(e -> log.error("会员更新失败: memberId={}, error={}", id, e.getMessage()));
|
||||
}
|
||||
|
||||
public Mono<Void> deleteMember(Long id) {
|
||||
log.info("删除会员: memberId={}", id);
|
||||
|
||||
return memberRepository.softDeleteById(id)
|
||||
.flatMap(rows -> {
|
||||
if (rows > 0) {
|
||||
log.info("会员删除成功: memberId={}", id);
|
||||
return Mono.empty();
|
||||
} else {
|
||||
return Mono.error(new BusinessException(ErrorCode.MEMBER_NOT_FOUND, "会员不存在"));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private MemberResponse toMemberResponse(Member member) {
|
||||
return MemberResponse.builder()
|
||||
.id(member.getId())
|
||||
.tenantId(member.getTenantId())
|
||||
.storeId(member.getStoreId())
|
||||
.name(member.getName())
|
||||
.phone(member.getPhone())
|
||||
.gender(member.getGender())
|
||||
.birthday(member.getBirthday())
|
||||
.idCard(member.getIdCard())
|
||||
.emergencyContact(member.getEmergencyContact())
|
||||
.emergencyPhone(member.getEmergencyPhone())
|
||||
.level(member.getLevel())
|
||||
.status(member.getStatus())
|
||||
.source(member.getSource())
|
||||
.remark(member.getRemark())
|
||||
.createdAt(member.getCreatedAt())
|
||||
.updatedAt(member.getUpdatedAt())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.gym.manage.common.constant;
|
||||
|
||||
public class ErrorCode {
|
||||
public static final int SUCCESS = 200;
|
||||
public static final int BAD_REQUEST = 400;
|
||||
public static final int UNAUTHORIZED = 401;
|
||||
public static final int FORBIDDEN = 403;
|
||||
public static final int NOT_FOUND = 404;
|
||||
public static final int INTERNAL_ERROR = 500;
|
||||
|
||||
public static final int MEMBER_NOT_FOUND = 1001;
|
||||
public static final int MEMBER_ALREADY_EXISTS = 1002;
|
||||
public static final int MEMBER_CARD_NOT_FOUND = 1003;
|
||||
|
||||
public static final int SLOT_NOT_FOUND = 2001;
|
||||
public static final int SLOT_NOT_AVAILABLE = 2002;
|
||||
public static final int BOOKING_NOT_FOUND = 2003;
|
||||
public static final int BOOKING_ALREADY_CANCELLED = 2004;
|
||||
|
||||
public static final int BENEFIT_NOT_FOUND = 3001;
|
||||
public static final int BENEFIT_INSUFFICIENT = 3002;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.gym.manage.common.exception;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
public class BusinessException extends RuntimeException {
|
||||
private final Integer code;
|
||||
|
||||
public BusinessException(String message) {
|
||||
super(message);
|
||||
this.code = 500;
|
||||
}
|
||||
|
||||
public BusinessException(Integer code, String message) {
|
||||
super(message);
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public BusinessException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
this.code = 500;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.gym.manage.common.exception;
|
||||
|
||||
import com.gym.manage.common.result.Result;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
import org.springframework.web.bind.support.WebExchangeBindException;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
@Slf4j
|
||||
@RestControllerAdvice
|
||||
public class GlobalExceptionHandler {
|
||||
|
||||
@ExceptionHandler(BusinessException.class)
|
||||
@ResponseStatus(HttpStatus.OK)
|
||||
public Mono<Result<Void>> handleBusinessException(BusinessException e) {
|
||||
log.error("业务异常: {}", e.getMessage(), e);
|
||||
return Mono.just(Result.error(e.getCode(), e.getMessage()));
|
||||
}
|
||||
|
||||
@ExceptionHandler(WebExchangeBindException.class)
|
||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||
public Mono<Result<Void>> handleValidationException(WebExchangeBindException e) {
|
||||
String message = e.getBindingResult().getAllErrors().get(0).getDefaultMessage();
|
||||
log.error("参数验证失败: {}", message, e);
|
||||
return Mono.just(Result.error(400, message));
|
||||
}
|
||||
|
||||
@ExceptionHandler(Exception.class)
|
||||
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
public Mono<Result<Void>> handleException(Exception e) {
|
||||
log.error("系统异常: {}", e.getMessage(), e);
|
||||
return Mono.just(Result.error("系统异常,请稍后重试"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.gym.manage.common.result;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public class Result<T> {
|
||||
private Integer code;
|
||||
private String message;
|
||||
private T data;
|
||||
|
||||
public static <T> Result<T> success(T data) {
|
||||
return new Result<>(200, "success", data);
|
||||
}
|
||||
|
||||
public static <T> Result<T> success() {
|
||||
return new Result<>(200, "success", null);
|
||||
}
|
||||
|
||||
public static <T> Result<T> error(Integer code, String message) {
|
||||
return new Result<>(code, message, null);
|
||||
}
|
||||
|
||||
public static <T> Result<T> error(String message) {
|
||||
return new Result<>(500, message, null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package com.gym.manage.domain.entity;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.relational.core.mapping.Column;
|
||||
import org.springframework.data.relational.core.mapping.Table;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@Table("booking_record")
|
||||
public class BookingRecord {
|
||||
@Id
|
||||
private Long id;
|
||||
|
||||
@Column("tenant_id")
|
||||
private Long tenantId;
|
||||
|
||||
@Column("store_id")
|
||||
private Long storeId;
|
||||
|
||||
@Column("member_id")
|
||||
private Long memberId;
|
||||
|
||||
@Column("slot_id")
|
||||
private Long slotId;
|
||||
|
||||
@Column("coach_id")
|
||||
private Long coachId;
|
||||
|
||||
@Column("course_name")
|
||||
private String courseName;
|
||||
|
||||
@Column("booking_time")
|
||||
private LocalDateTime bookingTime;
|
||||
|
||||
@Column("status")
|
||||
private String status;
|
||||
|
||||
@Column("cancel_reason")
|
||||
private String cancelReason;
|
||||
|
||||
@Column("cancel_time")
|
||||
private LocalDateTime cancelTime;
|
||||
|
||||
@Column("checkin_time")
|
||||
private LocalDateTime checkinTime;
|
||||
|
||||
@Column("remark")
|
||||
private String remark;
|
||||
|
||||
@Column("created_at")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Column("updated_at")
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
@Column("deleted_at")
|
||||
private LocalDateTime deletedAt;
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package com.gym.manage.domain.entity;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.relational.core.mapping.Column;
|
||||
import org.springframework.data.relational.core.mapping.Table;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@Table("booking_slot")
|
||||
public class BookingSlot {
|
||||
@Id
|
||||
private Long id;
|
||||
|
||||
@Column("tenant_id")
|
||||
private Long tenantId;
|
||||
|
||||
@Column("store_id")
|
||||
private Long storeId;
|
||||
|
||||
@Column("coach_id")
|
||||
private Long coachId;
|
||||
|
||||
@Column("course_id")
|
||||
private Long courseId;
|
||||
|
||||
@Column("course_name")
|
||||
private String courseName;
|
||||
|
||||
@Column("slot_type")
|
||||
private String slotType;
|
||||
|
||||
@Column("start_time")
|
||||
private LocalDateTime startTime;
|
||||
|
||||
@Column("end_time")
|
||||
private LocalDateTime endTime;
|
||||
|
||||
@Column("max_capacity")
|
||||
private Integer maxCapacity;
|
||||
|
||||
@Column("booked_count")
|
||||
private Integer bookedCount;
|
||||
|
||||
@Column("status")
|
||||
private String status;
|
||||
|
||||
@Column("remark")
|
||||
private String remark;
|
||||
|
||||
@Column("created_at")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Column("updated_at")
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
@Column("deleted_at")
|
||||
private LocalDateTime deletedAt;
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package com.gym.manage.domain.entity;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.relational.core.mapping.Column;
|
||||
import org.springframework.data.relational.core.mapping.Table;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@Table("member")
|
||||
public class Member {
|
||||
@Id
|
||||
private Long id;
|
||||
|
||||
@Column("tenant_id")
|
||||
private Long tenantId;
|
||||
|
||||
@Column("store_id")
|
||||
private Long storeId;
|
||||
|
||||
@Column("name")
|
||||
private String name;
|
||||
|
||||
@Column("phone")
|
||||
private String phone;
|
||||
|
||||
@Column("gender")
|
||||
private String gender;
|
||||
|
||||
@Column("birthday")
|
||||
private LocalDate birthday;
|
||||
|
||||
@Column("id_card")
|
||||
private String idCard;
|
||||
|
||||
@Column("emergency_contact")
|
||||
private String emergencyContact;
|
||||
|
||||
@Column("emergency_phone")
|
||||
private String emergencyPhone;
|
||||
|
||||
@Column("level")
|
||||
private String level;
|
||||
|
||||
@Column("status")
|
||||
private String status;
|
||||
|
||||
@Column("source")
|
||||
private String source;
|
||||
|
||||
@Column("remark")
|
||||
private String remark;
|
||||
|
||||
@Column("created_at")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Column("updated_at")
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
@Column("deleted_at")
|
||||
private LocalDateTime deletedAt;
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package com.gym.manage.domain.entity;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.relational.core.mapping.Column;
|
||||
import org.springframework.data.relational.core.mapping.Table;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@Table("member_card")
|
||||
public class MemberCard {
|
||||
@Id
|
||||
private Long id;
|
||||
|
||||
@Column("tenant_id")
|
||||
private Long tenantId;
|
||||
|
||||
@Column("store_id")
|
||||
private Long storeId;
|
||||
|
||||
@Column("member_id")
|
||||
private Long memberId;
|
||||
|
||||
@Column("card_no")
|
||||
private String cardNo;
|
||||
|
||||
@Column("card_type")
|
||||
private String cardType;
|
||||
|
||||
@Column("card_name")
|
||||
private String cardName;
|
||||
|
||||
@Column("total_count")
|
||||
private Integer totalCount;
|
||||
|
||||
@Column("remaining_count")
|
||||
private Integer remainingCount;
|
||||
|
||||
@Column("total_days")
|
||||
private Integer totalDays;
|
||||
|
||||
@Column("remaining_days")
|
||||
private Integer remainingDays;
|
||||
|
||||
@Column("start_date")
|
||||
private LocalDate startDate;
|
||||
|
||||
@Column("end_date")
|
||||
private LocalDate endDate;
|
||||
|
||||
@Column("status")
|
||||
private String status;
|
||||
|
||||
@Column("price")
|
||||
private BigDecimal price;
|
||||
|
||||
@Column("paid_amount")
|
||||
private BigDecimal paidAmount;
|
||||
|
||||
@Column("payment_method")
|
||||
private String paymentMethod;
|
||||
|
||||
@Column("remark")
|
||||
private String remark;
|
||||
|
||||
@Column("created_at")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Column("updated_at")
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
@Column("deleted_at")
|
||||
private LocalDateTime deletedAt;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.gym.manage.domain.repository;
|
||||
|
||||
import com.gym.manage.domain.entity.BookingRecord;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.r2dbc.repository.R2dbcRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
@Repository
|
||||
public interface BookingRecordRepository extends R2dbcRepository<BookingRecord, Long> {
|
||||
|
||||
Mono<BookingRecord> findByIdAndDeletedAtIsNull(Long id);
|
||||
|
||||
Flux<BookingRecord> findByMemberIdAndDeletedAtIsNull(Long memberId, Pageable pageable);
|
||||
|
||||
Mono<BookingRecord> findByMemberIdAndSlotIdAndDeletedAtIsNull(Long memberId, Long slotId);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.gym.manage.domain.repository;
|
||||
|
||||
import com.gym.manage.domain.entity.BookingSlot;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.r2dbc.repository.Query;
|
||||
import org.springframework.data.r2dbc.repository.R2dbcRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Repository
|
||||
public interface BookingSlotRepository extends R2dbcRepository<BookingSlot, Long> {
|
||||
|
||||
Mono<BookingSlot> findByIdAndDeletedAtIsNull(Long id);
|
||||
|
||||
Flux<BookingSlot> findByTenantIdAndStoreIdAndDeletedAtIsNull(Long tenantId, Long storeId, Pageable pageable);
|
||||
|
||||
@Query("SELECT * FROM booking_slot WHERE tenant_id = :tenantId AND store_id = :storeId " +
|
||||
"AND start_time >= :startTime AND end_time <= :endTime AND deleted_at IS NULL " +
|
||||
"AND status = 'AVAILABLE' ORDER BY start_time")
|
||||
Flux<BookingSlot> findAvailableSlots(Long tenantId, Long storeId, LocalDateTime startTime, LocalDateTime endTime);
|
||||
|
||||
@Query("UPDATE booking_slot SET booked_count = booked_count + 1, updated_at = CURRENT_TIMESTAMP " +
|
||||
"WHERE id = :id AND deleted_at IS NULL AND booked_count < max_capacity")
|
||||
Mono<Integer> incrementBookedCount(Long id);
|
||||
|
||||
@Query("UPDATE booking_slot SET booked_count = booked_count - 1, updated_at = CURRENT_TIMESTAMP " +
|
||||
"WHERE id = :id AND deleted_at IS NULL AND booked_count > 0")
|
||||
Mono<Integer> decrementBookedCount(Long id);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.gym.manage.domain.repository;
|
||||
|
||||
import com.gym.manage.domain.entity.MemberCard;
|
||||
import org.springframework.data.r2dbc.repository.R2dbcRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
@Repository
|
||||
public interface MemberCardRepository extends R2dbcRepository<MemberCard, Long> {
|
||||
|
||||
Flux<MemberCard> findByMemberIdAndDeletedAtIsNull(Long memberId);
|
||||
|
||||
Mono<MemberCard> findByIdAndMemberIdAndDeletedAtIsNull(Long id, Long memberId);
|
||||
|
||||
Mono<MemberCard> findByCardNoAndDeletedAtIsNull(String cardNo);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.gym.manage.domain.repository;
|
||||
|
||||
import com.gym.manage.domain.entity.Member;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.r2dbc.repository.Query;
|
||||
import org.springframework.data.r2dbc.repository.R2dbcRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
@Repository
|
||||
public interface MemberRepository extends R2dbcRepository<Member, Long> {
|
||||
|
||||
Mono<Member> findByIdAndDeletedAtIsNull(Long id);
|
||||
|
||||
Flux<Member> findByTenantIdAndStoreIdAndDeletedAtIsNull(Long tenantId, Long storeId, Pageable pageable);
|
||||
|
||||
Mono<Member> findByPhoneAndDeletedAtIsNull(String phone);
|
||||
|
||||
@Query("SELECT COUNT(*) FROM member WHERE tenant_id = :tenantId AND store_id = :storeId AND deleted_at IS NULL")
|
||||
Mono<Long> countByTenantIdAndStoreId(Long tenantId, Long storeId);
|
||||
|
||||
@Query("UPDATE member SET deleted_at = CURRENT_TIMESTAMP WHERE id = :id AND deleted_at IS NULL")
|
||||
Mono<Integer> softDeleteById(Long id);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
spring:
|
||||
application:
|
||||
name: gym-manage
|
||||
|
||||
r2dbc:
|
||||
url: r2dbc:postgresql://localhost:5432/gym_manage
|
||||
username: postgres
|
||||
password: postgres
|
||||
pool:
|
||||
initial-size: 5
|
||||
max-size: 20
|
||||
max-idle-time: 30m
|
||||
max-life-time: 1h
|
||||
acquire-timeout: 5s
|
||||
|
||||
webflux:
|
||||
base-path: /api/v1
|
||||
|
||||
codec:
|
||||
max-in-memory-size: 10MB
|
||||
|
||||
server:
|
||||
port: 8080
|
||||
netty:
|
||||
connection-timeout: 5s
|
||||
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: health,metrics,httptrace
|
||||
metrics:
|
||||
tags:
|
||||
application: gym-manage
|
||||
environment: dev
|
||||
|
||||
logging:
|
||||
level:
|
||||
root: INFO
|
||||
com.gym.manage: DEBUG
|
||||
org.springframework.r2dbc: DEBUG
|
||||
reactor.netty: INFO
|
||||
pattern:
|
||||
console: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"
|
||||
@@ -0,0 +1,255 @@
|
||||
-- 健身房管理系统数据库初始化脚本
|
||||
|
||||
-- 会员表
|
||||
CREATE TABLE IF NOT EXISTS member (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
tenant_id BIGINT NOT NULL COMMENT '租户ID',
|
||||
store_id BIGINT NOT NULL COMMENT '门店ID',
|
||||
name VARCHAR(100) NOT NULL COMMENT '姓名',
|
||||
phone VARCHAR(20) NOT NULL COMMENT '手机号',
|
||||
gender VARCHAR(10) COMMENT '性别',
|
||||
birthday DATE COMMENT '生日',
|
||||
id_card VARCHAR(20) COMMENT '身份证号',
|
||||
emergency_contact VARCHAR(100) COMMENT '紧急联系人',
|
||||
emergency_phone VARCHAR(20) COMMENT '紧急联系电话',
|
||||
level VARCHAR(20) DEFAULT 'NORMAL' COMMENT '会员等级',
|
||||
status VARCHAR(20) DEFAULT 'ACTIVE' COMMENT '状态',
|
||||
source VARCHAR(50) COMMENT '来源',
|
||||
remark TEXT COMMENT '备注',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
deleted_at TIMESTAMP COMMENT '删除时间'
|
||||
);
|
||||
|
||||
CREATE INDEX idx_member_tenant ON member(tenant_id) WHERE deleted_at IS NULL;
|
||||
CREATE INDEX idx_member_store ON member(store_id) WHERE deleted_at IS NULL;
|
||||
CREATE INDEX idx_member_phone ON member(phone) WHERE deleted_at IS NULL;
|
||||
CREATE INDEX idx_member_level ON member(level) WHERE deleted_at IS NULL;
|
||||
CREATE INDEX idx_member_status ON member(status) WHERE deleted_at IS NULL;
|
||||
|
||||
COMMENT ON TABLE member IS '会员表';
|
||||
COMMENT ON COLUMN member.id IS '会员ID';
|
||||
COMMENT ON COLUMN member.tenant_id IS '租户ID';
|
||||
COMMENT ON COLUMN member.store_id IS '门店ID';
|
||||
COMMENT ON COLUMN member.name IS '姓名';
|
||||
COMMENT ON COLUMN member.phone IS '手机号';
|
||||
COMMENT ON COLUMN member.gender IS '性别';
|
||||
COMMENT ON COLUMN member.birthday IS '生日';
|
||||
COMMENT ON COLUMN member.id_card IS '身份证号';
|
||||
COMMENT ON COLUMN member.emergency_contact IS '紧急联系人';
|
||||
COMMENT ON COLUMN member.emergency_phone IS '紧急联系电话';
|
||||
COMMENT ON COLUMN member.level IS '会员等级';
|
||||
COMMENT ON COLUMN member.status IS '状态';
|
||||
COMMENT ON COLUMN member.source IS '来源';
|
||||
COMMENT ON COLUMN member.remark IS '备注';
|
||||
COMMENT ON COLUMN member.created_at IS '创建时间';
|
||||
COMMENT ON COLUMN member.updated_at IS '更新时间';
|
||||
COMMENT ON COLUMN member.deleted_at IS '删除时间';
|
||||
|
||||
-- 会员卡表
|
||||
CREATE TABLE IF NOT EXISTS member_card (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
tenant_id BIGINT NOT NULL COMMENT '租户ID',
|
||||
store_id BIGINT NOT NULL COMMENT '门店ID',
|
||||
member_id BIGINT NOT NULL COMMENT '会员ID',
|
||||
card_no VARCHAR(50) NOT NULL COMMENT '卡号',
|
||||
card_type VARCHAR(50) NOT NULL COMMENT '卡类型',
|
||||
card_name VARCHAR(100) COMMENT '卡名称',
|
||||
total_count INTEGER COMMENT '总次数',
|
||||
remaining_count INTEGER COMMENT '剩余次数',
|
||||
total_days INTEGER COMMENT '总天数',
|
||||
remaining_days INTEGER COMMENT '剩余天数',
|
||||
start_date DATE COMMENT '开始日期',
|
||||
end_date DATE COMMENT '结束日期',
|
||||
status VARCHAR(20) DEFAULT 'ACTIVE' COMMENT '状态',
|
||||
price DECIMAL(10,2) COMMENT '价格',
|
||||
paid_amount DECIMAL(10,2) COMMENT '实付金额',
|
||||
payment_method VARCHAR(50) COMMENT '支付方式',
|
||||
remark TEXT COMMENT '备注',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
deleted_at TIMESTAMP COMMENT '删除时间'
|
||||
);
|
||||
|
||||
CREATE INDEX idx_member_card_member ON member_card(member_id) WHERE deleted_at IS NULL;
|
||||
CREATE INDEX idx_member_card_no ON member_card(card_no) WHERE deleted_at IS NULL;
|
||||
CREATE INDEX idx_member_card_status ON member_card(status) WHERE deleted_at IS NULL;
|
||||
|
||||
COMMENT ON TABLE member_card IS '会员卡表';
|
||||
|
||||
-- 预约时段表
|
||||
CREATE TABLE IF NOT EXISTS booking_slot (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
tenant_id BIGINT NOT NULL COMMENT '租户ID',
|
||||
store_id BIGINT NOT NULL COMMENT '门店ID',
|
||||
coach_id BIGINT COMMENT '教练ID',
|
||||
course_id BIGINT COMMENT '课程ID',
|
||||
course_name VARCHAR(100) COMMENT '课程名称',
|
||||
slot_type VARCHAR(50) NOT NULL COMMENT '时段类型',
|
||||
start_time TIMESTAMP NOT NULL COMMENT '开始时间',
|
||||
end_time TIMESTAMP NOT NULL COMMENT '结束时间',
|
||||
max_capacity INTEGER DEFAULT 20 COMMENT '最大容量',
|
||||
booked_count INTEGER DEFAULT 0 COMMENT '已预约数量',
|
||||
status VARCHAR(20) DEFAULT 'AVAILABLE' COMMENT '状态',
|
||||
remark TEXT COMMENT '备注',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
deleted_at TIMESTAMP COMMENT '删除时间'
|
||||
);
|
||||
|
||||
CREATE INDEX idx_booking_slot_tenant ON booking_slot(tenant_id) WHERE deleted_at IS NULL;
|
||||
CREATE INDEX idx_booking_slot_store ON booking_slot(store_id) WHERE deleted_at IS NULL;
|
||||
CREATE INDEX idx_booking_slot_coach ON booking_slot(coach_id) WHERE deleted_at IS NULL;
|
||||
CREATE INDEX idx_booking_slot_time ON booking_slot(start_time, end_time) WHERE deleted_at IS NULL;
|
||||
CREATE INDEX idx_booking_slot_status ON booking_slot(status) WHERE deleted_at IS NULL;
|
||||
|
||||
COMMENT ON TABLE booking_slot IS '预约时段表';
|
||||
|
||||
-- 预约记录表
|
||||
CREATE TABLE IF NOT EXISTS booking_record (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
tenant_id BIGINT NOT NULL COMMENT '租户ID',
|
||||
store_id BIGINT NOT NULL COMMENT '门店ID',
|
||||
member_id BIGINT NOT NULL COMMENT '会员ID',
|
||||
slot_id BIGINT NOT NULL COMMENT '时段ID',
|
||||
coach_id BIGINT COMMENT '教练ID',
|
||||
course_name VARCHAR(100) COMMENT '课程名称',
|
||||
booking_time TIMESTAMP NOT NULL COMMENT '预约时间',
|
||||
status VARCHAR(20) DEFAULT 'BOOKED' COMMENT '状态',
|
||||
cancel_reason TEXT COMMENT '取消原因',
|
||||
cancel_time TIMESTAMP COMMENT '取消时间',
|
||||
checkin_time TIMESTAMP COMMENT '签到时间',
|
||||
remark TEXT COMMENT '备注',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
deleted_at TIMESTAMP COMMENT '删除时间'
|
||||
);
|
||||
|
||||
CREATE INDEX idx_booking_record_member ON booking_record(member_id) WHERE deleted_at IS NULL;
|
||||
CREATE INDEX idx_booking_record_slot ON booking_record(slot_id) WHERE deleted_at IS NULL;
|
||||
CREATE INDEX idx_booking_record_coach ON booking_record(coach_id) WHERE deleted_at IS NULL;
|
||||
CREATE INDEX idx_booking_record_status ON booking_record(status) WHERE deleted_at IS NULL;
|
||||
CREATE INDEX idx_booking_record_time ON booking_record(created_at) WHERE deleted_at IS NULL;
|
||||
|
||||
COMMENT ON TABLE booking_record IS '预约记录表';
|
||||
|
||||
-- 签到记录表
|
||||
CREATE TABLE IF NOT EXISTS checkin_record (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
tenant_id BIGINT NOT NULL COMMENT '租户ID',
|
||||
store_id BIGINT NOT NULL COMMENT '门店ID',
|
||||
member_id BIGINT NOT NULL COMMENT '会员ID',
|
||||
checkin_type VARCHAR(50) NOT NULL COMMENT '签到类型',
|
||||
checkin_time TIMESTAMP NOT NULL COMMENT '签到时间',
|
||||
checkout_time TIMESTAMP COMMENT '签退时间',
|
||||
device_id VARCHAR(100) COMMENT '设备ID',
|
||||
device_type VARCHAR(50) COMMENT '设备类型',
|
||||
status VARCHAR(20) DEFAULT 'CHECKED_IN' COMMENT '状态',
|
||||
remark TEXT COMMENT '备注',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
deleted_at TIMESTAMP COMMENT '删除时间'
|
||||
);
|
||||
|
||||
CREATE INDEX idx_checkin_record_member ON checkin_record(member_id) WHERE deleted_at IS NULL;
|
||||
CREATE INDEX idx_checkin_record_time ON checkin_record(checkin_time) WHERE deleted_at IS NULL;
|
||||
CREATE INDEX idx_checkin_record_status ON checkin_record(status) WHERE deleted_at IS NULL;
|
||||
|
||||
COMMENT ON TABLE checkin_record IS '签到记录表';
|
||||
|
||||
-- 会员权益表
|
||||
CREATE TABLE IF NOT EXISTS member_benefit (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
tenant_id BIGINT NOT NULL COMMENT '租户ID',
|
||||
store_id BIGINT NOT NULL COMMENT '门店ID',
|
||||
member_id BIGINT NOT NULL COMMENT '会员ID',
|
||||
benefit_type VARCHAR(50) NOT NULL COMMENT '权益类型',
|
||||
benefit_name VARCHAR(100) COMMENT '权益名称',
|
||||
total_amount DECIMAL(10,2) COMMENT '总数量',
|
||||
remaining_amount DECIMAL(10,2) COMMENT '剩余数量',
|
||||
unit VARCHAR(20) COMMENT '单位',
|
||||
status VARCHAR(20) DEFAULT 'ACTIVE' COMMENT '状态',
|
||||
expire_time TIMESTAMP COMMENT '过期时间',
|
||||
remark TEXT COMMENT '备注',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
deleted_at TIMESTAMP COMMENT '删除时间'
|
||||
);
|
||||
|
||||
CREATE INDEX idx_member_benefit_member ON member_benefit(member_id) WHERE deleted_at IS NULL;
|
||||
CREATE INDEX idx_member_benefit_type ON member_benefit(benefit_type) WHERE deleted_at IS NULL;
|
||||
CREATE INDEX idx_member_benefit_status ON member_benefit(status) WHERE deleted_at IS NULL;
|
||||
|
||||
COMMENT ON TABLE member_benefit IS '会员权益表';
|
||||
|
||||
-- 权益记录表
|
||||
CREATE TABLE IF NOT EXISTS benefit_record (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
tenant_id BIGINT NOT NULL COMMENT '租户ID',
|
||||
store_id BIGINT NOT NULL COMMENT '门店ID',
|
||||
member_id BIGINT NOT NULL COMMENT '会员ID',
|
||||
benefit_id BIGINT NOT NULL COMMENT '权益ID',
|
||||
change_type VARCHAR(50) NOT NULL COMMENT '变更类型',
|
||||
change_amount DECIMAL(10,2) NOT NULL COMMENT '变更数量',
|
||||
before_amount DECIMAL(10,2) COMMENT '变更前数量',
|
||||
after_amount DECIMAL(10,2) COMMENT '变更后数量',
|
||||
related_type VARCHAR(50) COMMENT '关联类型',
|
||||
related_id BIGINT COMMENT '关联ID',
|
||||
remark TEXT COMMENT '备注',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间'
|
||||
);
|
||||
|
||||
CREATE INDEX idx_benefit_record_member ON benefit_record(member_id);
|
||||
CREATE INDEX idx_benefit_record_benefit ON benefit_record(benefit_id);
|
||||
CREATE INDEX idx_benefit_record_time ON benefit_record(created_at);
|
||||
|
||||
COMMENT ON TABLE benefit_record IS '权益记录表';
|
||||
|
||||
-- 订阅记录表
|
||||
CREATE TABLE IF NOT EXISTS subscription_record (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
tenant_id BIGINT NOT NULL COMMENT '租户ID',
|
||||
store_id BIGINT COMMENT '门店ID',
|
||||
module_code VARCHAR(50) NOT NULL COMMENT '模块代码',
|
||||
module_name VARCHAR(100) COMMENT '模块名称',
|
||||
subscription_type VARCHAR(50) NOT NULL COMMENT '订阅类型',
|
||||
start_time TIMESTAMP NOT NULL COMMENT '开始时间',
|
||||
end_time TIMESTAMP NOT NULL COMMENT '结束时间',
|
||||
status VARCHAR(20) DEFAULT 'ACTIVE' COMMENT '状态',
|
||||
price DECIMAL(10,2) COMMENT '价格',
|
||||
paid_amount DECIMAL(10,2) COMMENT '实付金额',
|
||||
payment_method VARCHAR(50) COMMENT '支付方式',
|
||||
remark TEXT COMMENT '备注',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
deleted_at TIMESTAMP COMMENT '删除时间'
|
||||
);
|
||||
|
||||
CREATE INDEX idx_subscription_record_tenant ON subscription_record(tenant_id) WHERE deleted_at IS NULL;
|
||||
CREATE INDEX idx_subscription_record_module ON subscription_record(module_code) WHERE deleted_at IS NULL;
|
||||
CREATE INDEX idx_subscription_record_status ON subscription_record(status) WHERE deleted_at IS NULL;
|
||||
|
||||
COMMENT ON TABLE subscription_record IS '订阅记录表';
|
||||
|
||||
-- 营销活动表
|
||||
CREATE TABLE IF NOT EXISTS marketing_campaign (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
tenant_id BIGINT NOT NULL COMMENT '租户ID',
|
||||
store_id BIGINT COMMENT '门店ID',
|
||||
campaign_name VARCHAR(200) NOT NULL COMMENT '活动名称',
|
||||
campaign_type VARCHAR(50) NOT NULL COMMENT '活动类型',
|
||||
start_time TIMESTAMP NOT NULL COMMENT '开始时间',
|
||||
end_time TIMESTAMP NOT NULL COMMENT '结束时间',
|
||||
status VARCHAR(20) DEFAULT 'DRAFT' COMMENT '状态',
|
||||
rules JSONB COMMENT '活动规则',
|
||||
remark TEXT COMMENT '备注',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
deleted_at TIMESTAMP COMMENT '删除时间'
|
||||
);
|
||||
|
||||
CREATE INDEX idx_marketing_campaign_tenant ON marketing_campaign(tenant_id) WHERE deleted_at IS NULL;
|
||||
CREATE INDEX idx_marketing_campaign_time ON marketing_campaign(start_time, end_time) WHERE deleted_at IS NULL;
|
||||
CREATE INDEX idx_marketing_campaign_status ON marketing_campaign(status) WHERE deleted_at IS NULL;
|
||||
|
||||
COMMENT ON TABLE marketing_campaign IS '营销活动表';
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.gym.manage;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
|
||||
@SpringBootTest
|
||||
class GymManageApplicationTests {
|
||||
|
||||
@Test
|
||||
void contextLoads() {
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user