package com.tcm.training.admin.app.course.service; import com.baomidou.mybatisplus.core.MybatisConfiguration; import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; import com.tcm.training.admin.app.auth.service.AppAuthSessionService; import com.tcm.training.admin.app.course.dto.AppHealthCoursePurchaseRequest; import com.tcm.training.admin.app.course.vo.AppHealthCoursePurchaseResultVO; import com.tcm.training.admin.live.entity.CoursePurchaseOrder; import com.tcm.training.admin.live.entity.Expert; import com.tcm.training.admin.live.entity.HealthCourse; import com.tcm.training.admin.live.entity.HealthCourseFavoriteRecord; import com.tcm.training.admin.live.entity.HealthCourseMemberJoinRecord; import com.tcm.training.admin.live.entity.HealthCourseVideo; import com.tcm.training.admin.live.mapper.CoursePurchaseOrderMapper; import com.tcm.training.admin.live.mapper.ExpertMapper; import com.tcm.training.admin.live.mapper.HealthCourseFavoriteRecordMapper; import com.tcm.training.admin.live.mapper.HealthCourseMapper; import com.tcm.training.admin.live.mapper.HealthCourseMemberJoinRecordMapper; import com.tcm.training.admin.live.mapper.HealthCourseVideoMapper; import com.tcm.training.admin.live.mapper.LiveCourseCategoryMapper; import com.tcm.training.admin.live.service.HealthCourseAppService; import com.tcm.training.admin.live.vo.HealthCourseDetailVO; import com.tcm.training.admin.member.entity.AppUser; import com.tcm.training.admin.member.mapper.UserMapper; import com.tcm.training.admin.system.service.SystemSettingsAppService; import com.tcm.training.admin.system.vo.SystemSettingsVO; import org.apache.ibatis.builder.MapperBuilderAssistant; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import java.math.BigDecimal; import java.time.LocalDateTime; import java.util.Arrays; import java.util.Collections; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; /** * APP 养生课程详情与购买服务测试。 */ @ExtendWith(MockitoExtension.class) class AppHealthCourseServiceTest { @BeforeAll static void initLambdaCache() { MapperBuilderAssistant assistant = new MapperBuilderAssistant(new MybatisConfiguration(), ""); TableInfoHelper.initTableInfo(assistant, HealthCourse.class); TableInfoHelper.initTableInfo(assistant, HealthCourseVideo.class); TableInfoHelper.initTableInfo(assistant, Expert.class); TableInfoHelper.initTableInfo(assistant, HealthCourseFavoriteRecord.class); TableInfoHelper.initTableInfo(assistant, HealthCourseMemberJoinRecord.class); TableInfoHelper.initTableInfo(assistant, CoursePurchaseOrder.class); TableInfoHelper.initTableInfo(assistant, AppUser.class); } @Mock private HealthCourseAppService healthCourseAppService; @Mock private HealthCourseMapper healthCourseMapper; @Mock private HealthCourseVideoMapper healthCourseVideoMapper; @Mock private LiveCourseCategoryMapper liveCourseCategoryMapper; @Mock private ExpertMapper expertMapper; @Mock private HealthCourseFavoriteRecordMapper healthCourseFavoriteRecordMapper; @Mock private HealthCourseMemberJoinRecordMapper healthCourseMemberJoinRecordMapper; @Mock private AppAuthSessionService appAuthSessionService; @Mock private CoursePurchaseOrderMapper coursePurchaseOrderMapper; @Mock private UserMapper userMapper; @Mock private SystemSettingsAppService systemSettingsAppService; @Captor private ArgumentCaptor coursePurchaseOrderCaptor; @Captor private ArgumentCaptor memberJoinRecordCaptor; private AppHealthCourseService appHealthCourseService; @BeforeEach void setUp() { appHealthCourseService = new AppHealthCourseService( healthCourseAppService, healthCourseMapper, healthCourseVideoMapper, liveCourseCategoryMapper, expertMapper, healthCourseFavoriteRecordMapper, healthCourseMemberJoinRecordMapper, appAuthSessionService, coursePurchaseOrderMapper, userMapper, systemSettingsAppService ); } /** * 已购整课时详情页应回显整课购买态并解锁全部小节。 */ @Test void detailShouldUnlockAllVideosWhenCourseAlreadyPurchased() { when(healthCourseAppService.detail(1L)).thenReturn(buildCourseDetail(1L)); when(healthCourseVideoMapper.selectList(any())).thenReturn(Arrays.asList( buildVideo(11L, 1L, BigDecimal.ZERO, "免费小节"), buildVideo(12L, 1L, new BigDecimal("99.00"), "付费小节") )); when(appAuthSessionService.isLogin()).thenReturn(true); when(appAuthSessionService.getLoginUserId()).thenReturn(200L); when(userMapper.selectById(200L)).thenReturn(buildMemberUser(200L)); when(coursePurchaseOrderMapper.selectOne(any())).thenReturn(buildCourseOrder(1L, 200L)); when(healthCourseMemberJoinRecordMapper.selectOne(any())).thenReturn(null); when(healthCourseFavoriteRecordMapper.selectOne(any())).thenReturn(null); when(systemSettingsAppService.current()).thenReturn(buildSystemSettings("400-800-9000")); com.tcm.training.admin.app.course.vo.AppHealthCourseDetailVO detailVO = appHealthCourseService.detail(1L); assertEquals("400-800-9000", detailVO.getServicePhone()); assertTrue(Boolean.TRUE.equals(detailVO.getIsMember())); assertFalse(Boolean.TRUE.equals(detailVO.getMemberCourseJoined())); assertTrue(Boolean.TRUE.equals(detailVO.getCourseOwned())); assertTrue(Boolean.TRUE.equals(detailVO.getCoursePurchased())); assertTrue(Boolean.TRUE.equals(detailVO.getVideos().get(0).getPurchased())); assertTrue(Boolean.TRUE.equals(detailVO.getVideos().get(1).getPurchased())); assertFalse(Boolean.TRUE.equals(detailVO.getVideos().get(1).getLocked())); } /** * 仅购买单小节时详情页只应解锁对应小节。 */ @Test void detailShouldUnlockOnlyPurchasedChapterWhenOnlyVideoPurchased() { when(healthCourseAppService.detail(1L)).thenReturn(buildCourseDetail(1L)); when(healthCourseVideoMapper.selectList(any())).thenReturn(Arrays.asList( buildVideo(11L, 1L, BigDecimal.ZERO, "免费小节"), buildVideo(12L, 1L, new BigDecimal("99.00"), "已购付费小节"), buildVideo(13L, 1L, new BigDecimal("199.00"), "未购付费小节") )); when(appAuthSessionService.isLogin()).thenReturn(true); when(appAuthSessionService.getLoginUserId()).thenReturn(200L); when(userMapper.selectById(200L)).thenReturn(buildNormalUser(200L)); when(coursePurchaseOrderMapper.selectOne(any())).thenReturn(null); when(coursePurchaseOrderMapper.selectList(any())).thenReturn(Collections.singletonList(buildVideoOrder(1L, 200L, 12L))); when(healthCourseMemberJoinRecordMapper.selectOne(any())).thenReturn(null); when(healthCourseFavoriteRecordMapper.selectOne(any())).thenReturn(null); when(systemSettingsAppService.current()).thenReturn(buildSystemSettings("400-800-9000")); com.tcm.training.admin.app.course.vo.AppHealthCourseDetailVO detailVO = appHealthCourseService.detail(1L); assertFalse(Boolean.TRUE.equals(detailVO.getCoursePurchased())); assertFalse(Boolean.TRUE.equals(detailVO.getCourseOwned())); assertTrue(Boolean.TRUE.equals(detailVO.getVideos().get(1).getPurchased())); assertFalse(Boolean.TRUE.equals(detailVO.getVideos().get(1).getLocked())); assertFalse(Boolean.TRUE.equals(detailVO.getVideos().get(2).getPurchased())); assertTrue(Boolean.TRUE.equals(detailVO.getVideos().get(2).getLocked())); } /** * 有效会员未拥有课程时详情页应返回会员态但保持未拥有。 */ @Test void detailShouldExposeMemberStatusWhenMemberHasNotOwnedCourse() { when(healthCourseAppService.detail(1L)).thenReturn(buildCourseDetail(1L)); when(healthCourseVideoMapper.selectList(any())).thenReturn(Collections.singletonList( buildVideo(12L, 1L, new BigDecimal("99.00"), "付费小节") )); when(appAuthSessionService.isLogin()).thenReturn(true); when(appAuthSessionService.getLoginUserId()).thenReturn(200L); when(userMapper.selectById(200L)).thenReturn(buildMemberUser(200L)); when(coursePurchaseOrderMapper.selectOne(any())).thenReturn(null); when(coursePurchaseOrderMapper.selectList(any())).thenReturn(Collections.emptyList()); when(healthCourseMemberJoinRecordMapper.selectOne(any())).thenReturn(null); when(healthCourseFavoriteRecordMapper.selectOne(any())).thenReturn(null); when(systemSettingsAppService.current()).thenReturn(buildSystemSettings("400-800-9000")); com.tcm.training.admin.app.course.vo.AppHealthCourseDetailVO detailVO = appHealthCourseService.detail(1L); assertTrue(Boolean.TRUE.equals(detailVO.getIsMember())); assertFalse(Boolean.TRUE.equals(detailVO.getMemberCourseJoined())); assertFalse(Boolean.TRUE.equals(detailVO.getCourseOwned())); assertFalse(Boolean.TRUE.equals(detailVO.getCoursePurchased())); assertTrue(Boolean.TRUE.equals(detailVO.getVideos().get(0).getLocked())); } /** * 会员已加课时详情页应将整课视为已拥有并解锁全部小节。 */ @Test void detailShouldTreatMemberJoinedCourseAsOwned() { when(healthCourseAppService.detail(1L)).thenReturn(buildCourseDetail(1L)); when(healthCourseVideoMapper.selectList(any())).thenReturn(Arrays.asList( buildVideo(11L, 1L, BigDecimal.ZERO, "免费小节"), buildVideo(12L, 1L, new BigDecimal("99.00"), "付费小节") )); when(appAuthSessionService.isLogin()).thenReturn(true); when(appAuthSessionService.getLoginUserId()).thenReturn(200L); when(userMapper.selectById(200L)).thenReturn(buildMemberUser(200L)); when(coursePurchaseOrderMapper.selectOne(any())).thenReturn(null); when(healthCourseMemberJoinRecordMapper.selectOne(any())).thenReturn(buildMemberJoinRecord(1L, 200L)); when(healthCourseFavoriteRecordMapper.selectOne(any())).thenReturn(null); when(systemSettingsAppService.current()).thenReturn(null); com.tcm.training.admin.app.course.vo.AppHealthCourseDetailVO detailVO = appHealthCourseService.detail(1L); assertEquals("", detailVO.getServicePhone()); assertTrue(Boolean.TRUE.equals(detailVO.getIsMember())); assertTrue(Boolean.TRUE.equals(detailVO.getMemberCourseJoined())); assertTrue(Boolean.TRUE.equals(detailVO.getCourseOwned())); assertFalse(Boolean.TRUE.equals(detailVO.getCoursePurchased())); assertTrue(Boolean.TRUE.equals(detailVO.getVideos().get(1).getPurchased())); assertFalse(Boolean.TRUE.equals(detailVO.getVideos().get(1).getLocked())); } /** * 首次购买整课时应直接写入已支付订单。 */ @Test void purchaseCourseShouldInsertPaidOrderWhenFirstTime() { when(appAuthSessionService.requireLoginUserId()).thenReturn(200L); when(userMapper.selectById(200L)).thenReturn(buildNormalUser(200L)); when(healthCourseMapper.selectById(1L)).thenReturn(buildCourseEntity(1L, new BigDecimal("599.80"))); when(coursePurchaseOrderMapper.selectOne(any())).thenReturn(null); when(healthCourseVideoMapper.selectList(any())).thenReturn(Arrays.asList( buildVideo(11L, 1L, BigDecimal.ZERO, "免费小节"), buildVideo(12L, 1L, new BigDecimal("99.00"), "付费小节") )); AppHealthCoursePurchaseRequest request = new AppHealthCoursePurchaseRequest(); request.setPaymentMethod("WECHAT"); AppHealthCoursePurchaseResultVO resultVO = appHealthCourseService.purchaseCourse(1L, request); verify(coursePurchaseOrderMapper).insert(coursePurchaseOrderCaptor.capture()); assertEquals("COURSE", coursePurchaseOrderCaptor.getValue().getPurchaseTargetType()); assertEquals("WECHAT", coursePurchaseOrderCaptor.getValue().getPaymentMethod()); assertEquals("PAID", coursePurchaseOrderCaptor.getValue().getStatus()); assertTrue(Boolean.TRUE.equals(resultVO.getCoursePurchased())); assertEquals(2, resultVO.getPurchasedVideoIds().size()); } /** * 首次购买付费小节时应写入小节已支付订单。 */ @Test void purchaseVideoShouldInsertPaidOrderWhenFirstTime() { when(appAuthSessionService.requireLoginUserId()).thenReturn(200L); when(userMapper.selectById(200L)).thenReturn(buildNormalUser(200L)); when(healthCourseMapper.selectById(1L)).thenReturn(buildCourseEntity(1L, new BigDecimal("599.80"))); when(healthCourseVideoMapper.selectById(12L)).thenReturn(buildVideo(12L, 1L, new BigDecimal("99.00"), "付费小节")); when(coursePurchaseOrderMapper.selectOne(any())).thenReturn(null); when(coursePurchaseOrderMapper.selectList(any())).thenReturn(Collections.singletonList(buildVideoOrder(1L, 200L, 12L))); AppHealthCoursePurchaseRequest request = new AppHealthCoursePurchaseRequest(); request.setPaymentMethod("ALIPAY"); AppHealthCoursePurchaseResultVO resultVO = appHealthCourseService.purchaseVideo(1L, 12L, request); verify(coursePurchaseOrderMapper).insert(coursePurchaseOrderCaptor.capture()); assertEquals("COURSE_CHAPTER", coursePurchaseOrderCaptor.getValue().getPurchaseTargetType()); assertEquals(Long.valueOf(12L), coursePurchaseOrderCaptor.getValue().getCourseVideoId()); assertEquals("ALIPAY", coursePurchaseOrderCaptor.getValue().getPaymentMethod()); assertFalse(Boolean.TRUE.equals(resultVO.getCoursePurchased())); assertEquals(Collections.singletonList(12L), resultVO.getPurchasedVideoIds()); } /** * 已买整课后再买小节时应直接返回整课已购结果而不重复插单。 */ @Test void purchaseVideoShouldBeIdempotentWhenCourseAlreadyPurchased() { when(appAuthSessionService.requireLoginUserId()).thenReturn(200L); when(userMapper.selectById(200L)).thenReturn(buildNormalUser(200L)); when(healthCourseMapper.selectById(1L)).thenReturn(buildCourseEntity(1L, new BigDecimal("599.80"))); when(healthCourseVideoMapper.selectById(12L)).thenReturn(buildVideo(12L, 1L, new BigDecimal("99.00"), "付费小节")); when(coursePurchaseOrderMapper.selectOne(any())).thenReturn(buildCourseOrder(1L, 200L)); when(healthCourseVideoMapper.selectList(any())).thenReturn(Arrays.asList( buildVideo(11L, 1L, BigDecimal.ZERO, "免费小节"), buildVideo(12L, 1L, new BigDecimal("99.00"), "付费小节") )); AppHealthCoursePurchaseRequest request = new AppHealthCoursePurchaseRequest(); request.setPaymentMethod("WECHAT"); AppHealthCoursePurchaseResultVO resultVO = appHealthCourseService.purchaseVideo(1L, 12L, request); verify(coursePurchaseOrderMapper, never()).insert(any()); assertTrue(Boolean.TRUE.equals(resultVO.getCoursePurchased())); assertEquals(2, resultVO.getPurchasedVideoIds().size()); } /** * 非会员执行会员加课时应直接报错。 */ @Test void memberJoinCourseShouldFailWhenUserIsNotMember() { when(appAuthSessionService.requireLoginUserId()).thenReturn(200L); when(userMapper.selectById(200L)).thenReturn(buildNormalUser(200L)); when(healthCourseMapper.selectById(1L)).thenReturn(buildCourseEntity(1L, new BigDecimal("599.80"))); IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> appHealthCourseService.memberJoinCourse(1L)); assertEquals("当前用户不是有效会员", exception.getMessage()); verify(healthCourseMemberJoinRecordMapper, never()).insert(any()); } /** * 会员首次加课时应写入会员加课记录。 */ @Test void memberJoinCourseShouldInsertRecordWhenFirstJoin() { when(appAuthSessionService.requireLoginUserId()).thenReturn(200L); when(userMapper.selectById(200L)).thenReturn(buildMemberUser(200L)); when(healthCourseMapper.selectById(1L)).thenReturn(buildCourseEntity(1L, new BigDecimal("599.80"))); when(coursePurchaseOrderMapper.selectOne(any())).thenReturn(null); when(healthCourseMemberJoinRecordMapper.selectOne(any())).thenReturn(null); boolean joined = appHealthCourseService.memberJoinCourse(1L); assertTrue(joined); verify(healthCourseMemberJoinRecordMapper).insert(memberJoinRecordCaptor.capture()); assertEquals(Long.valueOf(1L), memberJoinRecordCaptor.getValue().getCourseId()); assertEquals(Long.valueOf(200L), memberJoinRecordCaptor.getValue().getAppUserId()); } /** * 会员重复加课时应幂等成功且不重复写记录。 */ @Test void memberJoinCourseShouldBeIdempotentWhenAlreadyJoined() { when(appAuthSessionService.requireLoginUserId()).thenReturn(200L); when(userMapper.selectById(200L)).thenReturn(buildMemberUser(200L)); when(healthCourseMapper.selectById(1L)).thenReturn(buildCourseEntity(1L, new BigDecimal("599.80"))); when(coursePurchaseOrderMapper.selectOne(any())).thenReturn(null); when(healthCourseMemberJoinRecordMapper.selectOne(any())).thenReturn(buildMemberJoinRecord(1L, 200L)); boolean joined = appHealthCourseService.memberJoinCourse(1L); assertTrue(joined); verify(healthCourseMemberJoinRecordMapper, never()).insert(any()); } /** * 会员已付费购课后再次加课应幂等成功且不写会员加课记录。 */ @Test void memberJoinCourseShouldBeIdempotentWhenCourseAlreadyPurchased() { when(appAuthSessionService.requireLoginUserId()).thenReturn(200L); when(userMapper.selectById(200L)).thenReturn(buildMemberUser(200L)); when(healthCourseMapper.selectById(1L)).thenReturn(buildCourseEntity(1L, new BigDecimal("599.80"))); when(coursePurchaseOrderMapper.selectOne(any())).thenReturn(buildCourseOrder(1L, 200L)); boolean joined = appHealthCourseService.memberJoinCourse(1L); assertTrue(joined); verify(healthCourseMemberJoinRecordMapper, never()).insert(any()); } private HealthCourseDetailVO buildCourseDetail(Long courseId) { HealthCourseDetailVO detailVO = new HealthCourseDetailVO(); detailVO.setId(courseId); detailVO.setTitle("中医针灸资格证"); detailVO.setStatus(1); detailVO.setSalePrice(new BigDecimal("599.80")); detailVO.setLearnUserCount(88); detailVO.setSummary("课程简介"); detailVO.setTargetUsers("适用人群"); detailVO.setContent("

课程内容

"); detailVO.setTotalDurationSeconds(1200); detailVO.setCoverImg("https://example.com/course.png"); detailVO.setBannerImgs(Collections.singletonList("https://example.com/banner.png")); return detailVO; } private HealthCourse buildCourseEntity(Long courseId, BigDecimal salePrice) { HealthCourse course = new HealthCourse(); course.setId(courseId); course.setTitle("中医针灸资格证"); course.setCoverImg("https://example.com/course.png"); course.setSalePrice(salePrice); course.setStatus(1); course.setDeleted(0); return course; } private HealthCourseVideo buildVideo(Long videoId, Long courseId, BigDecimal salePrice, String chapterName) { HealthCourseVideo video = new HealthCourseVideo(); video.setId(videoId); video.setCourseId(courseId); video.setChapterName(chapterName); video.setSalePrice(salePrice); video.setDurationSeconds(600); video.setStatus(1); video.setDeleted(0); return video; } private AppUser buildNormalUser(Long userId) { AppUser user = new AppUser(); user.setId(userId); user.setNickname("王美丽"); user.setPhone("14712346905"); user.setStatus(1); user.setDeleted(0); return user; } private AppUser buildMemberUser(Long userId) { AppUser user = buildNormalUser(userId); user.setMemberExpireAt(LocalDateTime.now().plusDays(7)); return user; } private CoursePurchaseOrder buildCourseOrder(Long courseId, Long userId) { CoursePurchaseOrder order = new CoursePurchaseOrder(); order.setId(1L); order.setOrderNo("CPO202604141000000200"); order.setAppUserId(userId); order.setCourseId(courseId); order.setPurchaseTargetType("COURSE"); order.setCourseType("HEALTH_COURSE"); order.setPaymentMethod("WECHAT"); order.setStatus("PAID"); order.setPaidAt(LocalDateTime.now().minusMinutes(5)); order.setDeleted(0); return order; } private CoursePurchaseOrder buildVideoOrder(Long courseId, Long userId, Long videoId) { CoursePurchaseOrder order = new CoursePurchaseOrder(); order.setId(2L); order.setOrderNo("CPO202604141005000200"); order.setAppUserId(userId); order.setCourseId(courseId); order.setCourseVideoId(videoId); order.setPurchaseTargetType("COURSE_CHAPTER"); order.setCourseType("HEALTH_COURSE"); order.setPaymentMethod("ALIPAY"); order.setStatus("PAID"); order.setPaidAt(LocalDateTime.now().minusMinutes(3)); order.setDeleted(0); return order; } private HealthCourseMemberJoinRecord buildMemberJoinRecord(Long courseId, Long userId) { HealthCourseMemberJoinRecord record = new HealthCourseMemberJoinRecord(); record.setId(3L); record.setCourseId(courseId); record.setAppUserId(userId); record.setJoinedAt(LocalDateTime.now().minusDays(1)); record.setDeleted(0); return record; } private SystemSettingsVO buildSystemSettings(String servicePhone) { SystemSettingsVO settingsVO = new SystemSettingsVO(); settingsVO.setId(1L); settingsVO.setServicePhone(servicePhone); settingsVO.setServiceOnlineTime("09:00-18:00"); return settingsVO; } }