docs: reorganize documentation structure

This commit is contained in:
张翔
2026-03-05 13:48:13 +08:00
parent 349b0a754f
commit 104fa7e7c8
59 changed files with 22859 additions and 916 deletions
@@ -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);
}
+44
View File
@@ -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"
+255
View File
@@ -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() {
}
}