Skip to content

Commit

Permalink
Merge branch 'develop' into feature/usePoint
Browse files Browse the repository at this point in the history
  • Loading branch information
decten authored Jan 27, 2024
2 parents 78964eb + 6d2ca39 commit de8998a
Show file tree
Hide file tree
Showing 14 changed files with 408 additions and 12 deletions.
16 changes: 16 additions & 0 deletions src/docs/asciidoc/coupon/coupon-api.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -105,3 +105,19 @@ include::{snippets}/coupon-backoffice-controller-docs-test/modify-coupon-request

include::{snippets}/coupon-backoffice-controller-docs-test/modify-coupon-request-test/http-response.adoc[]
include::{snippets}/coupon-backoffice-controller-docs-test/modify-coupon-request-test/response-fields.adoc[]


[[Statistics-Coupon]]

== 쿠폰 현황 통계

쿠폰 현환 통계 API

=== HttpRequest

include::{snippets}/coupon-backoffice-controller-docs-test/coupon-statistics-test/http-request.adoc[]

=== HttpResponse

include::{snippets}/coupon-backoffice-controller-docs-test/coupon-statistics-test/http-response.adoc[]
include::{snippets}/coupon-backoffice-controller-docs-test/coupon-statistics-test/response-fields.adoc[]
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.backoffice.upjuyanolja.domain.accommodation.dto.response;

import com.backoffice.upjuyanolja.domain.coupon.entity.CouponStatistics;
import lombok.Builder;

@Builder
public record CouponStatisticsResponse(
Long accommodationId,
long total,
long used,
long stock
) {
public static CouponStatisticsResponse from(CouponStatistics statistics) {
return CouponStatisticsResponse.builder()
.accommodationId(statistics.getId())
.total(statistics.getTotal())
.used(statistics.getUsed())
.stock(statistics.getStock())
.build();
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
package com.backoffice.upjuyanolja.domain.coupon.controller;

import com.backoffice.upjuyanolja.domain.accommodation.dto.response.CouponStatisticsResponse;
import com.backoffice.upjuyanolja.domain.coupon.dto.request.backoffice.CouponAddRequest;
import com.backoffice.upjuyanolja.domain.coupon.dto.request.backoffice.CouponDeleteRequest;
import com.backoffice.upjuyanolja.domain.coupon.dto.request.backoffice.CouponMakeRequest;
import com.backoffice.upjuyanolja.domain.coupon.dto.request.backoffice.CouponModifyRequest;
import com.backoffice.upjuyanolja.domain.coupon.dto.response.backoffice.CouponMakeViewResponse;
import com.backoffice.upjuyanolja.domain.coupon.dto.response.backoffice.CouponManageResponse;
import com.backoffice.upjuyanolja.domain.coupon.service.CouponBackofficeService;
import com.backoffice.upjuyanolja.domain.member.entity.Member;
import com.backoffice.upjuyanolja.domain.coupon.service.CouponStatisticsService;
import com.backoffice.upjuyanolja.domain.member.service.MemberGetService;
import com.backoffice.upjuyanolja.global.security.SecurityUtil;
import jakarta.validation.Valid;
Expand All @@ -34,8 +35,8 @@
public class CouponBackofficeController {

private final CouponBackofficeService couponService;
private final CouponStatisticsService couponStatisticsService;
private final SecurityUtil securityUtil;
private final MemberGetService memberGetService;

@GetMapping("/buy/{accommodationId}")
public ResponseEntity<CouponMakeViewResponse> responseRoomsView(
Expand Down Expand Up @@ -129,8 +130,17 @@ public ResponseEntity<Object> deleteCoupon(
return ResponseEntity.status(HttpStatus.OK).body(null);
}

private Member getCurrentMember() {
Long memberId = securityUtil.getCurrentMemberId();
return memberGetService.getMemberById(memberId);
@GetMapping("/statistics/{accommodationId}")
public ResponseEntity<CouponStatisticsResponse> getStatistics(
@PathVariable(name = "accommodationId") @Min(1) Long accommodationId
) {
long currentMemberId = securityUtil.getCurrentMemberId();
couponService.validateAccommodationRequest(
accommodationId, currentMemberId);

CouponStatisticsResponse result = couponStatisticsService.getCouponStatistics(
accommodationId);
return ResponseEntity.status(HttpStatus.OK).body(result);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.backoffice.upjuyanolja.domain.coupon.dto;

import com.backoffice.upjuyanolja.domain.accommodation.entity.Accommodation;
import com.backoffice.upjuyanolja.domain.coupon.entity.CouponStatistics;
import lombok.Builder;

@Builder
public record CouponStatisticsDto(
Accommodation accommodation,
long total,
long used,
long stock
) {

public static CouponStatistics toEntity(
Accommodation accommodation,
long total,
long used,
long stock
) {
return CouponStatistics.builder()
.accommodation(accommodation)
.total(total)
.used(used)
.stock(stock)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.backoffice.upjuyanolja.domain.coupon.dto;

public interface CouponStatisticsInterface {
Long getId();
Long getTotal();
Long getUsed();
Long getStock();
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public class CouponIssuance extends BaseTime {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Comment("쿠폰 발급 내역 식별자")
Long id;
private Long id;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(nullable = false, name = "coupon_id")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package com.backoffice.upjuyanolja.domain.coupon.entity;

import com.backoffice.upjuyanolja.domain.accommodation.entity.Accommodation;
import com.backoffice.upjuyanolja.global.common.entity.BaseTime;
import jakarta.persistence.Column;
import jakarta.persistence.ConstraintMode;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.ForeignKey;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.hibernate.annotations.Comment;

@Getter
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class CouponStatistics extends BaseTime {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Comment("쿠폰 사용량 통계 식별자")
private Long id;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(
nullable = false,
name = "accommodation_id",
unique = true,
foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT))
private Accommodation accommodation;

@Setter
@Column(nullable = false)
@Comment("발행 쿠폰")
private long total;

@Setter
@Column(nullable = false)
@Comment("사용 완료 쿠폰")
private long used;

@Setter
@Column(nullable = false)
@Comment("현재 보유 쿠폰")
private long stock;

@Builder
public CouponStatistics(
Long id,
Accommodation accommodation,
long total,
long used,
long stock
) {
this.id = id;
this.accommodation = accommodation;
this.total = total;
this.used = used;
this.stock = stock;
}
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,25 @@
package com.backoffice.upjuyanolja.domain.coupon.repository;

import static com.querydsl.core.group.GroupBy.sum;

import com.backoffice.upjuyanolja.domain.accommodation.entity.QAccommodation;
import com.backoffice.upjuyanolja.domain.accommodation.entity.QAccommodationOwnership;
import com.backoffice.upjuyanolja.domain.coupon.dto.response.backoffice.AccommodationResponse;
import com.backoffice.upjuyanolja.domain.coupon.dto.response.backoffice.CouponMakeViewResponse;
import com.backoffice.upjuyanolja.domain.coupon.dto.response.backoffice.CouponManageQueryDto;
import com.backoffice.upjuyanolja.domain.coupon.dto.response.backoffice.CouponRoomsResponse;
import com.backoffice.upjuyanolja.domain.coupon.entity.CouponStatistics;
import com.backoffice.upjuyanolja.domain.coupon.entity.CouponStatus;
import com.backoffice.upjuyanolja.domain.coupon.entity.QCoupon;
import com.backoffice.upjuyanolja.domain.coupon.entity.QCouponIssuance;
import com.backoffice.upjuyanolja.domain.coupon.entity.QCouponRedeem;
import com.backoffice.upjuyanolja.domain.reservation.entity.QReservation;
import com.backoffice.upjuyanolja.domain.reservation.entity.QReservationRoom;
import com.backoffice.upjuyanolja.domain.room.entity.QRoom;
import com.backoffice.upjuyanolja.domain.room.entity.QRoomPrice;
import com.querydsl.core.Tuple;
import com.querydsl.core.types.Projections;
import com.querydsl.core.types.dsl.BooleanExpression;
import com.querydsl.jpa.impl.JPAQuery;
import com.querydsl.jpa.impl.JPAQueryFactory;
import java.util.List;
import lombok.RequiredArgsConstructor;
Expand All @@ -32,7 +40,7 @@ public CouponMakeViewResponse findRoomsByAccommodationId(Long accommodationId) {

AccommodationResponse accommodationResponse = queryFactory.
select(Projections.constructor(AccommodationResponse.class,
qAccommodation.id, qAccommodation.name
qAccommodation.id, qAccommodation.name
))
.from(qAccommodation)
.where(qAccommodation.id.eq(accommodationId))
Expand All @@ -54,8 +62,8 @@ public CouponMakeViewResponse findRoomsByAccommodationId(Long accommodationId) {
}

/**
* accommodation_ownership 테이블에서 accommodationId와 memberId를 and 조건으로 검색하.
* 데이터가 존재하면 true, 존재하지 않으면 false 반환.
* accommodation_ownership 테이블에서 accommodationId와 memberId를 and 조건으로 검색하. 데이터가 존재하면 true, 존재하지
* 않으면 false 반환.
*
* @param accommodationId : 숙소 식별자
* @param memberId : 회원 식별자
Expand All @@ -66,7 +74,7 @@ public boolean existsAccommodationIdByMemberId(Long accommodationId, Long member
return queryFactory.selectOne()
.from(qOwnership)
.where(qOwnership.member.id.eq(memberId)
.and(qOwnership.accommodation.id.eq(accommodationId)))
.and(qOwnership.accommodation.id.eq(accommodationId)))
.fetchOne() != null;
}

Expand All @@ -77,8 +85,9 @@ public List<CouponManageQueryDto> findCouponsByAccommodationId(Long accommodatio
qAccommodation.id, qAccommodation.name, qCoupon.endDate, qRoom.id,
qRoom.name, qRoomPrice.offWeekDaysMinFee.as("roomPrice"),
qCoupon.id, qCoupon.couponStatus, qCoupon.discountType, qCoupon.discount,
qCoupon.dayLimit, qCoupon.stock, qCoupon.couponType)
qCoupon.dayLimit, qCoupon.stock, qCoupon.couponType
)
)
.from(qCoupon)
.join(qRoom).on(qCoupon.room.id.eq(qRoom.id))
.join(qAccommodation).on(qRoom.accommodation.id.eq(qAccommodation.id))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.backoffice.upjuyanolja.domain.coupon.repository;

import com.backoffice.upjuyanolja.domain.coupon.dto.CouponStatisticsInterface;
import com.backoffice.upjuyanolja.domain.coupon.entity.CouponStatistics;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;

public interface CouponStatisticsRepository extends JpaRepository<CouponStatistics, Long> {

@Query(value =
"select t.id, "
+ " t.total, "
+ " u.used, "
+ " t.total - u.used as stock "
+ "from (select ac.id as id, sum(ci.QUANTITY) as total "
+ " from COUPON_ISSUANCE ci "
+ " join room rm on ci.ROOM_ID = rm.id "
+ " join ACCOMMODATION ac on rm.accommodation_id = ac.id "
+ " group by ac.id) t "
+ " inner join "
+ " (select ac.id as id, count(*) as used "
+ " from reservation_room rr "
+ " left join reservation rv on rr.id = rv.RESERVATION_ROOM_ID "
+ " left join coupon_redeem cr on rv.id = cr.RESERVATION_ID "
+ " left join room r on rr.ROOM_ID = r.ID "
+ " left join accommodation ac on r.ACCOMMODATION_ID = ac.id "
+ " where rv.IS_COUPON_USED = true "
+ " group by ac.id) u "
+ " on t.id = u.id", nativeQuery = true)
List<CouponStatisticsInterface> createStatistics();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.backoffice.upjuyanolja.domain.coupon.service;

import com.backoffice.upjuyanolja.domain.accommodation.dto.response.CouponStatisticsResponse;
import com.backoffice.upjuyanolja.domain.accommodation.exception.AccommodationNotFoundException;
import com.backoffice.upjuyanolja.domain.accommodation.repository.AccommodationRepository;
import com.backoffice.upjuyanolja.domain.coupon.entity.CouponStatistics;
import com.backoffice.upjuyanolja.domain.coupon.repository.CouponStatisticsRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Slf4j
@Service
@RequiredArgsConstructor
@Transactional
public class CouponStatisticsService {

private final CouponStatisticsRepository couponStatisticsRepository;

public CouponStatisticsResponse getCouponStatistics(Long accommodationId) {
CouponStatistics couponStatistics = couponStatisticsRepository.findById(accommodationId)
.orElseThrow(AccommodationNotFoundException::new);
return CouponStatisticsResponse.from(couponStatistics);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.backoffice.upjuyanolja.global.scheduler;

import com.backoffice.upjuyanolja.domain.accommodation.entity.Accommodation;
import com.backoffice.upjuyanolja.domain.accommodation.exception.AccommodationNotFoundException;
import com.backoffice.upjuyanolja.domain.accommodation.repository.AccommodationRepository;
import com.backoffice.upjuyanolja.domain.coupon.dto.CouponStatisticsDto;
import com.backoffice.upjuyanolja.domain.coupon.dto.CouponStatisticsInterface;
import com.backoffice.upjuyanolja.domain.coupon.entity.CouponStatistics;
import com.backoffice.upjuyanolja.domain.coupon.repository.CouponStatisticsRepository;
import java.util.ArrayList;
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.transaction.annotation.Transactional;

@Slf4j
@EnableScheduling
@RequiredArgsConstructor
@Transactional
public class CouponStatisticsScheduler {

private final CouponStatisticsRepository couponStatisticsRepository;
private final AccommodationRepository accommodationRepository;

// 매일 새벽 2시에 통계 쿼리 실행
@Scheduled(cron = "0 0 2 * * *", zone = "Asia/Seoul")
public void makeCouponStatistics() {
List<CouponStatisticsInterface> result = couponStatisticsRepository.createStatistics();
List<CouponStatistics> statisticsList = new ArrayList<>();
for (CouponStatisticsInterface statistics : result) {
statisticsList.add(createCouponStatistics(statistics));
}
couponStatisticsRepository.saveAll(statisticsList);
log.info("쿠폰 통계 생성 성공. 총 {}건.", statisticsList.size());
}

private CouponStatistics createCouponStatistics(CouponStatisticsInterface statistics) {
Accommodation accommodation = accommodationRepository.findById(statistics.getId())
.orElseThrow(AccommodationNotFoundException::new);
return CouponStatisticsDto.toEntity(
accommodation,
statistics.getTotal(),
statistics.getUsed(),
statistics.getStock()
);
}

}
Loading

0 comments on commit de8998a

Please sign in to comment.