This commit is contained in:
1 parent
2fee329002
commit
64eadf1446
52 files changed
+2590
-4
No files matched your search
+11
-2
@@ -7,12 +7,14 @@ import com.fruitshorts.commerce.service.OrderService;
|
||||
import com.fruitshorts.commerce.service.ProductService;
|
||||
import com.fruitshorts.commerce.service.UnlockService;
|
||||
import com.fruitshorts.commerce.service.WalletService;
|
||||
import com.fruitshorts.commerce.reward.service.RewardTaskService;
|
||||
import com.fruitshorts.commerce.vo.ProductRespVO;
|
||||
import com.fruitshorts.common.pojo.Result;
|
||||
import com.fruitshorts.framework.security.SecurityUtils;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
@@ -41,6 +43,8 @@ public class AppCommerceController {
|
||||
private final UnlockService unlockService;
|
||||
private final OrderService orderService;
|
||||
private final ProductService productService;
|
||||
@Autowired(required = false)
|
||||
private RewardTaskService rewardTaskService;
|
||||
|
||||
/** 我的钱包:余额 + 最近流水,客户端一处渲染。 */
|
||||
@Operation(summary = "我的钱包(余额 + 最近流水)")
|
||||
@@ -92,9 +96,14 @@ public class AppCommerceController {
|
||||
@Operation(summary = "提交商店票据(校验后建单并发权益,重复提交幂等)")
|
||||
@PostMapping("/orders/verify")
|
||||
public Result<Orders> verify(@RequestBody Map<String, String> body) {
|
||||
return Result.ok(orderService.verifyAndDeliver(SecurityUtils.getAppUserId(),
|
||||
Orders order = orderService.verifyAndDeliver(SecurityUtils.getAppUserId(),
|
||||
body.getOrDefault("platform", ""),
|
||||
body.getOrDefault("productId", ""),
|
||||
body.getOrDefault("receipt", "")));
|
||||
body.getOrDefault("receipt", ""));
|
||||
if (rewardTaskService != null && OrderService.STATUS_DELIVERED.equals(order.getStatus())) {
|
||||
rewardTaskService.recordEvent(order.getUserId(), RewardTaskService.TYPE_FIRST_PURCHASE,
|
||||
order.getOrderNo(), 1L);
|
||||
}
|
||||
return Result.ok(order);
|
||||
}
|
||||
}
|
||||
+13
-1
@@ -9,16 +9,19 @@ import com.fruitshorts.user.service.AppUserService;
|
||||
import com.fruitshorts.user.vo.AppUserRespVO;
|
||||
import com.fruitshorts.user.vo.DeactivateReqVO;
|
||||
import com.fruitshorts.user.vo.ProfileUpdateReqVO;
|
||||
import com.fruitshorts.commerce.reward.service.RewardTaskService;
|
||||
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.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
@@ -32,6 +35,8 @@ import java.util.Map;
|
||||
public class AppMeController {
|
||||
|
||||
private final AppUserService userService;
|
||||
@Autowired(required = false)
|
||||
private RewardTaskService rewardTaskService;
|
||||
|
||||
@Operation(summary = "我的资料")
|
||||
@GetMapping
|
||||
@@ -42,7 +47,14 @@ public class AppMeController {
|
||||
@Operation(summary = "修改资料(昵称 / 头像 / 语言等)")
|
||||
@PutMapping
|
||||
public Result<AppUserRespVO> update(@Valid @RequestBody ProfileUpdateReqVO req) {
|
||||
return Result.ok(userService.updateProfile(SecurityUtils.getAppUserId(), req));
|
||||
Long userId = SecurityUtils.getAppUserId();
|
||||
AppUserRespVO profile = userService.updateProfile(userId, req);
|
||||
if (rewardTaskService != null && StringUtils.hasText(profile.getNickname())
|
||||
&& StringUtils.hasText(profile.getAvatarUrl())) {
|
||||
rewardTaskService.recordEvent(userId, RewardTaskService.TYPE_COMPLETE_PROFILE,
|
||||
userId.toString(), 1L);
|
||||
}
|
||||
return Result.ok(profile);
|
||||
}
|
||||
|
||||
@Operation(summary = "我的登录设备列表")
|
||||
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
package com.fruitshorts.app.controller;
|
||||
|
||||
import com.fruitshorts.commerce.reward.service.RewardCheckinService;
|
||||
import com.fruitshorts.commerce.reward.service.RewardGrantService;
|
||||
import com.fruitshorts.commerce.reward.service.RewardTaskService;
|
||||
import com.fruitshorts.commerce.reward.vo.RewardActivityVO;
|
||||
import com.fruitshorts.commerce.reward.vo.RewardCheckinRespVO;
|
||||
import com.fruitshorts.commerce.reward.vo.RewardCheckinVO;
|
||||
import com.fruitshorts.commerce.reward.vo.RewardClaimRespVO;
|
||||
import com.fruitshorts.commerce.reward.vo.RewardOverviewVO;
|
||||
import com.fruitshorts.common.pojo.Result;
|
||||
import com.fruitshorts.commerce.service.WalletService;
|
||||
import com.fruitshorts.framework.security.SecurityUtils;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/** 客户端奖励中心:总览、签到、任务领取与活动。 */
|
||||
@Tag(name = "客户端-奖励中心")
|
||||
@RestController
|
||||
@RequestMapping("/app/rewards")
|
||||
@RequiredArgsConstructor
|
||||
public class AppRewardController {
|
||||
|
||||
private final RewardTaskService taskService;
|
||||
private final RewardCheckinService checkinService;
|
||||
private final RewardGrantService grantService;
|
||||
private final WalletService walletService;
|
||||
|
||||
@Operation(summary = "奖励中心总览")
|
||||
@GetMapping("/overview")
|
||||
public Result<RewardOverviewVO> overview(@RequestParam(required = false) String locale) {
|
||||
Long userId = SecurityUtils.getAppUserIdOrNull();
|
||||
long coinBalance = userId == null ? 0L : walletService.getOrCreate(userId).getCoinBalance();
|
||||
RewardOverviewVO overview = taskService.overview(
|
||||
userId, locale, coinBalance);
|
||||
overview.setCheckin(checkinService.status(userId));
|
||||
return Result.ok(overview);
|
||||
}
|
||||
|
||||
@Operation(summary = "奖励活动列表")
|
||||
@GetMapping("/activities")
|
||||
public Result<List<RewardActivityVO>> activities(@RequestParam(required = false) String locale) {
|
||||
return Result.ok(taskService.activities(locale));
|
||||
}
|
||||
|
||||
@Operation(summary = "签到状态")
|
||||
@GetMapping("/check-in")
|
||||
public Result<RewardCheckinVO> checkInStatus() {
|
||||
return Result.ok(checkinService.status(SecurityUtils.getAppUserId()));
|
||||
}
|
||||
|
||||
@Operation(summary = "执行签到")
|
||||
@PostMapping("/check-in")
|
||||
public Result<RewardCheckinRespVO> checkIn() {
|
||||
return Result.ok(checkinService.checkIn(SecurityUtils.getAppUserId()));
|
||||
}
|
||||
|
||||
@Operation(summary = "领取已完成任务奖励")
|
||||
@PostMapping("/tasks/{taskId}/claim")
|
||||
public Result<RewardClaimRespVO> claim(
|
||||
@Parameter(description = "奖励任务 ID") @PathVariable Long taskId) {
|
||||
Long userId = SecurityUtils.getAppUserId();
|
||||
var task = taskService.getExisting(taskId);
|
||||
return Result.ok(grantService.claim(userId, task, taskService.currentProgress(userId, task)));
|
||||
}
|
||||
}
|
||||
+10
-1
@@ -9,6 +9,7 @@ import com.fruitshorts.app.service.AnonymousInteractionRateLimiter;
|
||||
import com.fruitshorts.framework.interaction.InteractionIdentityResolver;
|
||||
import com.fruitshorts.framework.security.SecurityUtils;
|
||||
import com.fruitshorts.playback.service.WatchProgressService;
|
||||
import com.fruitshorts.commerce.reward.service.RewardTaskService;
|
||||
import com.fruitshorts.playback.vo.ProgressRespVO;
|
||||
import com.fruitshorts.playback.vo.ProgressSaveReqVO;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
@@ -16,6 +17,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
@@ -33,6 +35,8 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
public class AppWatchController {
|
||||
|
||||
private final WatchProgressService progressService;
|
||||
@Autowired(required = false)
|
||||
private RewardTaskService rewardTaskService;
|
||||
private final InteractionIdentityResolver identityResolver;
|
||||
private final AnonymousInteractionRateLimiter anonymousRateLimiter;
|
||||
|
||||
@@ -46,7 +50,12 @@ public class AppWatchController {
|
||||
request.getHeader("X-Device-Id"),
|
||||
req.getDeviceId());
|
||||
anonymousRateLimiter.check(identity, "watch");
|
||||
return Result.ok(progressService.save(identity, req));
|
||||
ProgressRespVO saved = progressService.save(identity, req);
|
||||
if (rewardTaskService != null && identity.authenticated()) {
|
||||
rewardTaskService.recordWatch(identity.userId(), saved.getEpisodeId(),
|
||||
saved.getPositionMs(), Boolean.TRUE.equals(saved.getFinished()));
|
||||
}
|
||||
return Result.ok(saved);
|
||||
}
|
||||
|
||||
/** 某部剧的续看位置,进详情页时用它决定"续看第 N 集"。 */
|
||||
|
||||
+165
@@ -0,0 +1,165 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
package com.fruitshorts.app.controller;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fruitshorts.commerce.reward.service.RewardCheckinService;
|
||||
import com.fruitshorts.commerce.reward.service.RewardGrantService;
|
||||
import com.fruitshorts.commerce.reward.service.RewardTaskService;
|
||||
import com.fruitshorts.commerce.reward.vo.RewardCheckinVO;
|
||||
import com.fruitshorts.commerce.reward.vo.RewardOverviewVO;
|
||||
import com.fruitshorts.commerce.reward.vo.RewardActivityVO;
|
||||
import com.fruitshorts.commerce.service.WalletService;
|
||||
import com.fruitshorts.common.exception.ErrorCode;
|
||||
import com.fruitshorts.framework.log.AccessDeniedRecorder;
|
||||
import com.fruitshorts.framework.security.JwtAuthenticationFilter;
|
||||
import com.fruitshorts.framework.security.JwtTokenProvider;
|
||||
import com.fruitshorts.framework.security.RestAuthEntryPoint;
|
||||
import com.fruitshorts.framework.security.SecurityConfig;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.web.FilterChainProxy;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
import org.springframework.test.context.web.WebAppConfiguration;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||
import org.springframework.web.context.WebApplicationContext;
|
||||
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.isNull;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
/** 奖励中心匿名浏览与写操作鉴权的安全链契约。 */
|
||||
@ExtendWith(SpringExtension.class)
|
||||
@WebAppConfiguration
|
||||
@ContextConfiguration(classes = AppRewardAnonymousSecurityTest.TestConfig.class)
|
||||
class AppRewardAnonymousSecurityTest {
|
||||
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
@Autowired
|
||||
private RewardTaskService taskService;
|
||||
@Autowired
|
||||
private RewardCheckinService checkinService;
|
||||
@Autowired
|
||||
private WalletService walletService;
|
||||
|
||||
@Test
|
||||
void should_allow_anonymous_reward_overview_without_creating_wallet() throws Exception {
|
||||
RewardOverviewVO overview = new RewardOverviewVO();
|
||||
overview.setCoinBalance(0L);
|
||||
overview.setSections(List.of());
|
||||
overview.setActivities(List.of());
|
||||
given(taskService.overview(isNull(), org.mockito.ArgumentMatchers.eq("en"),
|
||||
org.mockito.ArgumentMatchers.eq(0L))).willReturn(overview);
|
||||
given(checkinService.status(isNull())).willReturn(new RewardCheckinVO());
|
||||
|
||||
mockMvc.perform(get("/app/rewards/overview").param("locale", "en"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data.coinBalance").value(0));
|
||||
|
||||
verify(walletService, never()).getOrCreate(org.mockito.ArgumentMatchers.any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_allow_anonymous_reward_activities() throws Exception {
|
||||
given(taskService.activities("id")).willReturn(List.of(new RewardActivityVO()));
|
||||
|
||||
mockMvc.perform(get("/app/rewards/activities").param("locale", "id"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(0))
|
||||
.andExpect(jsonPath("$.data").isArray())
|
||||
.andExpect(jsonPath("$.data", org.hamcrest.Matchers.hasSize(1)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void should_keep_personal_reward_operations_login_required() throws Exception {
|
||||
mockMvc.perform(get("/app/rewards/check-in"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(ErrorCode.UNAUTHORIZED.getCode()));
|
||||
mockMvc.perform(post("/app/rewards/check-in"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(ErrorCode.UNAUTHORIZED.getCode()));
|
||||
mockMvc.perform(post("/app/rewards/tasks/9/claim"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(ErrorCode.UNAUTHORIZED.getCode()));
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebMvc
|
||||
@EnableWebSecurity
|
||||
@Import(SecurityConfig.class)
|
||||
static class TestConfig {
|
||||
|
||||
@Bean
|
||||
RewardTaskService rewardTaskService() {
|
||||
return mock(RewardTaskService.class);
|
||||
}
|
||||
|
||||
@Bean
|
||||
RewardCheckinService rewardCheckinService() {
|
||||
return mock(RewardCheckinService.class);
|
||||
}
|
||||
|
||||
@Bean
|
||||
RewardGrantService rewardGrantService() {
|
||||
return mock(RewardGrantService.class);
|
||||
}
|
||||
|
||||
@Bean
|
||||
WalletService walletService() {
|
||||
return mock(WalletService.class);
|
||||
}
|
||||
|
||||
@Bean
|
||||
AppRewardController appRewardController(
|
||||
RewardTaskService taskService,
|
||||
RewardCheckinService checkinService,
|
||||
RewardGrantService grantService,
|
||||
WalletService walletService) {
|
||||
return new AppRewardController(taskService, checkinService, grantService, walletService);
|
||||
}
|
||||
|
||||
@Bean
|
||||
JwtAuthenticationFilter jwtAuthenticationFilter() {
|
||||
return new JwtAuthenticationFilter(mock(JwtTokenProvider.class), mock(ObjectProvider.class));
|
||||
}
|
||||
|
||||
@Bean
|
||||
RestAuthEntryPoint restAuthEntryPoint(ObjectMapper objectMapper) {
|
||||
return new RestAuthEntryPoint(objectMapper,
|
||||
new AccessDeniedRecorder(mock(ObjectProvider.class), mock(ObjectProvider.class)));
|
||||
}
|
||||
|
||||
@Bean
|
||||
ObjectMapper objectMapper() {
|
||||
return new ObjectMapper();
|
||||
}
|
||||
|
||||
@Bean
|
||||
MockMvc mockMvc(WebApplicationContext context) {
|
||||
FilterChainProxy securityFilter = context.getBean(
|
||||
"springSecurityFilterChain", FilterChainProxy.class);
|
||||
return MockMvcBuilders.webAppContextSetup(context)
|
||||
.addFilters(securityFilter)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user