-
Notifications
You must be signed in to change notification settings - Fork 1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[FEAT] 포인트 사용 [유저 데이트 코스 열람] API - #65 #70
Changes from all commits
c820238
6dac86c
24b575f
0415ab4
9c1ada0
3e7ce1f
5c82f2a
e3e7691
36c23ba
cb37a4b
aec51a7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
package org.dateroad.config; | ||
|
||
|
||
import io.lettuce.core.api.async.RedisAsyncCommands; | ||
import io.lettuce.core.codec.StringCodec; | ||
import io.lettuce.core.output.StatusOutput; | ||
import io.lettuce.core.protocol.CommandArgs; | ||
import io.lettuce.core.protocol.CommandKeyword; | ||
import io.lettuce.core.protocol.CommandType; | ||
import lombok.RequiredArgsConstructor; | ||
import org.dateroad.date.repository.CourseRepository; | ||
import org.dateroad.point.event.FreeEventListener; | ||
import org.dateroad.point.event.pointEventListener; | ||
import org.springframework.beans.factory.annotation.Value; | ||
import org.springframework.context.annotation.Bean; | ||
import org.springframework.context.annotation.Configuration; | ||
import org.springframework.data.redis.connection.RedisConnectionFactory; | ||
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; | ||
import org.springframework.data.redis.connection.stream.*; | ||
import org.springframework.data.redis.core.RedisTemplate; | ||
import org.springframework.data.redis.serializer.StringRedisSerializer; | ||
import org.springframework.data.redis.stream.StreamMessageListenerContainer; | ||
import org.springframework.data.redis.stream.Subscription; | ||
|
||
import java.time.Duration; | ||
import java.util.Iterator; | ||
import java.util.Objects; | ||
|
||
@Configuration | ||
@RequiredArgsConstructor | ||
public class RedisStreamConfig { | ||
private final pointEventListener pointEventListener; | ||
private final FreeEventListener freeEventListener; | ||
@Value("${spring.data.redis.host}") | ||
private String host; | ||
@Value("${spring.data.redis.port}") | ||
private int port; | ||
|
||
@Bean | ||
public RedisTemplate<String, Object> redisTemplate() { | ||
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>(); | ||
redisTemplate.setConnectionFactory(redisConnectionFactory()); | ||
redisTemplate.setKeySerializer(new StringRedisSerializer()); | ||
redisTemplate.setValueSerializer(new StringRedisSerializer()); | ||
return redisTemplate; | ||
} | ||
|
||
@Bean | ||
public RedisConnectionFactory redisConnectionFactory() { | ||
return new LettuceConnectionFactory(host, port); | ||
} | ||
|
||
public void createStreamConsumerGroup(final String streamKey, final String consumerGroupName) { | ||
// Stream이 존재 하지 않으면, MKSTREAM 옵션을 통해 만들고, ConsumerGroup또한 생성한다 | ||
System.out.println(streamKey + consumerGroupName); | ||
// Stream이 존재하지 않으면, MKSTREAM 옵션을 통해 스트림과 소비자 그룹을 생성 | ||
if (Boolean.FALSE.equals(redisTemplate().hasKey(streamKey))) { | ||
RedisAsyncCommands<String, String> commands = (RedisAsyncCommands<String, String>) Objects.requireNonNull( | ||
redisTemplate() | ||
.getConnectionFactory()) | ||
.getConnection() | ||
.getNativeConnection(); | ||
|
||
CommandArgs<String, String> args = new CommandArgs<>(StringCodec.UTF8) | ||
.add(CommandKeyword.CREATE) | ||
.add(streamKey) | ||
.add(consumerGroupName) | ||
.add("0") | ||
.add("MKSTREAM"); | ||
// MKSTREAM 옵션을 사용하여 스트림과 그룹을 생성 | ||
commands.dispatch(CommandType.XGROUP, new StatusOutput<>(StringCodec.UTF8), args).toCompletableFuture() | ||
.join(); | ||
} | ||
// Stream 존재 시, ConsumerGroup 존재 여부 확인 후 ConsumerGroup을 생성 | ||
else { | ||
if (!isStreamConsumerGroupExist(streamKey, consumerGroupName)) { | ||
redisTemplate().opsForStream().createGroup(streamKey, ReadOffset.from("0"), consumerGroupName); | ||
} | ||
} | ||
} | ||
|
||
// ConsumerGroup 존재 여부 확인 | ||
public boolean isStreamConsumerGroupExist(final String streamKey, final String consumerGroupName) { | ||
Iterator<StreamInfo.XInfoGroup> iterator = redisTemplate() | ||
.opsForStream().groups(streamKey).stream().iterator(); | ||
|
||
while (iterator.hasNext()) { | ||
StreamInfo.XInfoGroup xInfoGroup = iterator.next(); | ||
if (xInfoGroup.groupName().equals(consumerGroupName)) { | ||
return true; | ||
} | ||
} | ||
return false; | ||
} | ||
|
||
@Bean | ||
public Subscription PointSubscription() { | ||
createStreamConsumerGroup("coursePoint", "courseGroup"); | ||
StreamMessageListenerContainer.StreamMessageListenerContainerOptions<String, MapRecord<String, String, String>> containerOptions = StreamMessageListenerContainer.StreamMessageListenerContainerOptions | ||
.builder().pollTimeout(Duration.ofMillis(100)).build(); | ||
|
||
StreamMessageListenerContainer<String, MapRecord<String, String, String>> container = StreamMessageListenerContainer.create( | ||
redisConnectionFactory(), | ||
containerOptions); | ||
|
||
Subscription subscription = container.receiveAutoAck(Consumer.from("courseGroup", "instance-1"), | ||
StreamOffset.create("coursePoint", ReadOffset.lastConsumed()), pointEventListener); | ||
container.start(); | ||
return subscription; | ||
} | ||
|
||
@Bean | ||
public Subscription FreeSubscription() { | ||
createStreamConsumerGroup("courseFree", "courseGroup"); | ||
StreamMessageListenerContainer.StreamMessageListenerContainerOptions<String, MapRecord<String, String, String>> containerOptions = StreamMessageListenerContainer.StreamMessageListenerContainerOptions | ||
.builder().pollTimeout(Duration.ofMillis(100)).build(); | ||
StreamMessageListenerContainer<String, MapRecord<String, String, String>> container = StreamMessageListenerContainer.create( | ||
redisConnectionFactory(), | ||
containerOptions); | ||
Subscription subscription = container.receiveAutoAck(Consumer.from("courseGroup", "instance-2"), | ||
StreamOffset.create("courseFree", ReadOffset.lastConsumed()), freeEventListener); | ||
container.start(); | ||
return subscription; | ||
} | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
package org.dateroad.course.dto.request; | ||
|
||
import lombok.AccessLevel; | ||
import lombok.Builder; | ||
import org.dateroad.point.domain.TransactionType; | ||
|
||
@Builder(access = AccessLevel.PROTECTED) | ||
public record PointUseReq( | ||
int point, | ||
TransactionType type, | ||
String description | ||
) { | ||
public static PointUseReq of(int point, TransactionType type, String description) { | ||
return PointUseReq.builder() | ||
.point(point) | ||
.type(type) | ||
.description(description) | ||
.build(); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
package org.dateroad.course.service; | ||
|
||
public enum CoursePaymentType { | ||
FREE,POINT | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,21 +10,25 @@ | |
import org.dateroad.course.dto.request.CourseGetAllReq; | ||
import org.dateroad.course.dto.request.CourseCreateReq; | ||
import org.dateroad.course.dto.request.CoursePlaceGetReq; | ||
import org.dateroad.course.dto.request.PointUseReq; | ||
import org.dateroad.course.dto.response.CourseDtoGetRes; | ||
import org.dateroad.course.dto.response.CourseGetAllRes; | ||
import org.dateroad.course.dto.response.DateAccessGetAllRes; | ||
import org.dateroad.course.facade.AsyncService; | ||
import org.dateroad.date.domain.Course; | ||
import org.dateroad.date.dto.response.CourseGetDetailRes; | ||
import org.dateroad.date.repository.CourseRepository; | ||
import org.dateroad.date.service.DateRepository; | ||
import org.dateroad.dateAccess.domain.DateAccess; | ||
import org.dateroad.dateAccess.repository.DateAccessRepository; | ||
import org.dateroad.exception.DateRoadException; | ||
import org.dateroad.exception.EntityNotFoundException; | ||
import org.dateroad.image.domain.Image; | ||
import org.dateroad.image.repository.ImageRepository; | ||
import org.dateroad.exception.ConflictException; | ||
import org.dateroad.like.domain.Like; | ||
import org.dateroad.like.repository.LikeRepository; | ||
import org.dateroad.point.domain.Point; | ||
import org.dateroad.point.repository.PointRepository; | ||
import org.dateroad.place.domain.CoursePlace; | ||
import org.dateroad.place.repository.CoursePlaceRepository; | ||
import org.dateroad.tag.domain.CourseTag; | ||
|
@@ -45,21 +49,22 @@ public class CourseService { | |
private final DateAccessRepository dateAccessRepository; | ||
private final UserRepository userRepository; | ||
private final AsyncService asyncService; | ||
private final PointRepository pointRepository; | ||
private final ImageRepository imageRepository; | ||
private final CoursePlaceRepository coursePlaceRepository; | ||
private final CourseTagRepository courseTagRepository; | ||
private final DateRepository dateRepository; | ||
|
||
|
||
public CourseGetAllRes getAllCourses(CourseGetAllReq courseGetAllReq) { | ||
public CourseGetAllRes getAllCourses(final CourseGetAllReq courseGetAllReq) { | ||
Specification<Course> spec = CourseSpecifications.filterByCriteria(courseGetAllReq); | ||
List<Course> courses = courseRepository.findAll(spec); | ||
List<CourseDtoGetRes> courseDtoGetResList = convertToDtoList(courses, Function.identity()); | ||
return CourseGetAllRes.of(courseDtoGetResList); | ||
} | ||
|
||
|
||
@Transactional | ||
public void createCourseLike(Long userId, Long courseId) { | ||
public void createCourseLike(final Long userId, final Long courseId) { | ||
User findUser = getUser(userId); | ||
Course findCourse = getCourse(courseId); | ||
validateCourseLike(findUser, findCourse); | ||
|
@@ -74,14 +79,14 @@ public void deleteCourseLike(Long userId, Long courseId) { | |
likeRepository.delete(findLike); | ||
} | ||
|
||
private <T> List<CourseDtoGetRes> convertToDtoList(List<T> entities, Function<T, Course> converter) { | ||
private <T> List<CourseDtoGetRes> convertToDtoList(final List<T> entities, final Function<T, Course> converter) { | ||
return entities.stream() | ||
.map(converter) | ||
.map(this::convertToDto) | ||
.collect(Collectors.toList()); | ||
} | ||
|
||
private CourseDtoGetRes convertToDto(Course course) { | ||
private CourseDtoGetRes convertToDto(final Course course) { | ||
int likeCount = likeRepository.countByCourse(course); | ||
Image thumbnailImage = asyncService.findFirstByCourseOrderBySequenceAsc(course); | ||
String thumbnailUrl = thumbnailImage != null ? thumbnailImage.getImageUrl() : null; | ||
|
@@ -97,7 +102,7 @@ private CourseDtoGetRes convertToDto(Course course) { | |
); | ||
} | ||
|
||
public DateAccessGetAllRes getAllDataAccessCourse(Long userId) { | ||
public DateAccessGetAllRes getAllDataAccessCourse(final Long userId) { | ||
List<Course> accesses = dateAccessRepository.findCoursesByUserId(userId); | ||
List<CourseDtoGetRes> courseDtoGetResList = convertToDtoList(accesses, Function.identity()); | ||
return DateAccessGetAllRes.of(courseDtoGetResList); | ||
|
@@ -154,6 +159,38 @@ public Course createCourse(final Long userId, final CourseCreateReq courseRegist | |
return saveCourse; | ||
} | ||
|
||
@Transactional | ||
public void openCourse(final Long userId, final Long courseId, final PointUseReq pointUseReq) { | ||
User user = getUser(userId); | ||
Course course = getCourse(courseId); | ||
Point point = Point.create(user, pointUseReq.point(), pointUseReq.type(), pointUseReq.description()); | ||
CoursePaymentType coursePaymentType = validateUserFreeOrPoint(user, pointUseReq.point()); | ||
processCoursePayment(coursePaymentType, user, point, pointUseReq); | ||
dateAccessRepository.save(DateAccess.create(course, user)); | ||
} | ||
|
||
private CoursePaymentType validateUserFreeOrPoint(final User user, final int requiredPoints) { | ||
if (user.getFree() > 0) { | ||
return CoursePaymentType.FREE; // User가 free를 갖고 있으면 true를 반환 | ||
} else if (user.getTotalPoint() < requiredPoints) { | ||
throw new DateRoadException(FailureCode.INSUFFICIENT_USER_POINTS); | ||
} | ||
return CoursePaymentType.POINT; | ||
} | ||
|
||
public void processCoursePayment(final CoursePaymentType coursePaymentType, final User user, final Point point, | ||
final PointUseReq pointUseReq) { | ||
switch (coursePaymentType) { | ||
case FREE -> { | ||
asyncService.publishEventUserFree(user); | ||
} | ||
case POINT -> { | ||
pointRepository.save(point); | ||
asyncService.publishEvenUserPoint(user, pointUseReq); | ||
} | ||
Comment on lines
+183
to
+190
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 성능측면으로 비교군이 적을 경우 switch보다 if가 낫다고 합니다!ㅎㅎ There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 엇 예전에는 if-else 가 성능적으로 좋았지만 현재는 그렇게 차이가 나지 않고 switch를 쓰는게 더 효과적이라고 합니다! |
||
} | ||
} | ||
|
||
public CourseGetDetailRes getCourseDetail(final Long userId, final Long courseId) { | ||
|
||
//course - courseId, title, date, start_at, description, totalCost, city, totalTime | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
띄어쓰기요..