feat: phase1 UI prep — P1 安全(XFF防护), P2 规模化(平台索引+region保护), P3 文档补全
P1 安全: WebUtils.getClientIp 改为 remoteAddr 优先 + 可信代理 CIDR 配置 P2 规模化: DISTINCT ON 查询补匹配排序的部分索引 (migrate-k113) P2 一致性: syncCountryByIp regionCode 同语言/时区/币种加保护,不覆盖用户自定义 P3 文档: api-phase1.md 新增 platform 参数说明, IpRegionService 新增集成测试 + VERSION 新增: CountryProfile 52 国映射 + IpRegionService 离线 ip2region 解析 平台筛选: app-users/favorites/likes/watch-history 按最近一次会话 ANDROID/IOS 筛选
This commit is contained in:
1 parent
a41ab18636
commit
272aa62434
34 files changed
+748
-77
No files matched your search
+6
-4
@@ -35,13 +35,14 @@ public class AppAuthController {
|
||||
|
||||
private final AppOtpService otpService;
|
||||
private final AppAuthService authService;
|
||||
private final WebUtils webUtils;
|
||||
|
||||
/** 发送验证码。频控在服务端,客户端不需要自己限流。 */
|
||||
@Operation(summary = "发送验证码(频控在服务端)")
|
||||
@RateLimit(limit = 20, windowSeconds = 60, key = "otp")
|
||||
@PostMapping("/otp")
|
||||
public Result<Void> sendOtp(@Valid @RequestBody OtpSendReqVO req, HttpServletRequest request) {
|
||||
otpService.send(req, WebUtils.getClientIp(request));
|
||||
otpService.send(req, webUtils.clientIp(request));
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
@@ -50,7 +51,7 @@ public class AppAuthController {
|
||||
public Result<TokenRespVO> register(@Valid @RequestBody RegisterReqVO req,
|
||||
HttpServletRequest request) {
|
||||
applyHeaderDeviceId(req, request);
|
||||
return Result.ok(authService.register(req, WebUtils.getClientIp(request)));
|
||||
return Result.ok(authService.register(req, webUtils.clientIp(request)));
|
||||
}
|
||||
|
||||
@Operation(summary = "登录(验证码或密码)")
|
||||
@@ -59,7 +60,7 @@ public class AppAuthController {
|
||||
public Result<TokenRespVO> login(@Valid @RequestBody LoginReqVO req,
|
||||
HttpServletRequest request) {
|
||||
applyHeaderDeviceId(req, request);
|
||||
return Result.ok(authService.login(req, WebUtils.getClientIp(request)));
|
||||
return Result.ok(authService.login(req, webUtils.clientIp(request)));
|
||||
}
|
||||
|
||||
@Operation(summary = "Google 登录(未配置 clientId 时返回明确错误)")
|
||||
@@ -69,7 +70,8 @@ public class AppAuthController {
|
||||
String headerDeviceId = request.getHeader("X-Device-Id");
|
||||
String deviceId = StringUtils.hasText(headerDeviceId)
|
||||
? headerDeviceId : req.getDeviceId();
|
||||
return Result.ok(authService.googleLogin(req.getIdToken(), deviceId));
|
||||
return Result.ok(authService.googleLogin(req.getIdToken(), deviceId,
|
||||
webUtils.clientIp(request)));
|
||||
}
|
||||
|
||||
@Operation(summary = "刷新令牌(旧 refresh token 立即失效)")
|
||||
|
||||
+2
-1
@@ -48,6 +48,7 @@ public class AppCommentController {
|
||||
private final EpisodeService episodeService;
|
||||
private final InteractionIdentityResolver identityResolver;
|
||||
private final AnonymousInteractionRateLimiter anonymousRateLimiter;
|
||||
private final WebUtils webUtils;
|
||||
|
||||
@Operation(summary = "发布评论")
|
||||
@PostMapping
|
||||
@@ -59,7 +60,7 @@ public class AppCommentController {
|
||||
req.getDeviceId());
|
||||
anonymousRateLimiter.check(identity, "comment");
|
||||
return Result.ok(commentService.publish(
|
||||
identity, req, WebUtils.getClientIp(request)));
|
||||
identity, req, webUtils.clientIp(request)));
|
||||
}
|
||||
|
||||
@Operation(summary = "评论列表(游标翻页,可匿名浏览)")
|
||||
|
||||
+2
-1
@@ -37,6 +37,7 @@ public class AppDramaController {
|
||||
private final EpisodeService episodeService;
|
||||
private final CommentService commentService;
|
||||
private final AppEpisodeStatsService appEpisodeStatsService;
|
||||
private final WebUtils webUtils;
|
||||
|
||||
/** 详情:包含剧集列表,客户端进详情页一次请求即可渲染 */
|
||||
@Operation(summary = "短剧详情(含剧集列表,进详情页一次请求即可渲染)")
|
||||
@@ -56,7 +57,7 @@ public class AppDramaController {
|
||||
SecurityUtils.getUserIdOrNull(),
|
||||
id,
|
||||
request.getHeader("X-Device-Id"),
|
||||
WebUtils.getClientIp(request));
|
||||
webUtils.clientIp(request));
|
||||
DramaEpisode episode = episodeService.getExisting(id);
|
||||
vo.setLikeCount(episode.getLikeCount() == null ? 0L : episode.getLikeCount());
|
||||
vo.setCommentCount(commentService.countByEpisodes(List.of(id)).getOrDefault(id, 0L));
|
||||
|
||||
+2
-1
@@ -25,11 +25,12 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
public class AppEventController {
|
||||
|
||||
private final AppEventService eventService;
|
||||
private final WebUtils webUtils;
|
||||
|
||||
@Operation(summary = "埋点事件批量上报")
|
||||
@PostMapping("/events")
|
||||
public Result<Void> report(@Valid @RequestBody AppEventReqVO req, HttpServletRequest request) {
|
||||
eventService.save(req, SecurityUtils.getUserIdOrNull(), WebUtils.getClientIp(request));
|
||||
eventService.save(req, SecurityUtils.getUserIdOrNull(), webUtils.clientIp(request));
|
||||
return Result.ok();
|
||||
}
|
||||
}
|
||||
@@ -47,6 +47,11 @@ springdoc:
|
||||
path: /v3/api-docs
|
||||
|
||||
fruitshorts:
|
||||
web:
|
||||
# 可信代理 CIDR 列表(逗号分隔)。默认空 = 永不解析 XFF,直接用 request.getRemoteAddr()。
|
||||
# 配了反代(Nginx/K8s ingress)时填上其 IP 段,才会从 XFF 取最右侧可信一跳。
|
||||
# 生产若开启了 Tomcat RemoteIpValve(prod profile 已配),保持空即可。
|
||||
trusted-proxies: ${FRUITSHORTS_TRUSTED_PROXIES:}
|
||||
auth:
|
||||
# 仅作 sys_config 缺失/非法时的兜底;运行时优先读取后台「系统配置」
|
||||
register-max-per-ip-per-day: ${REGISTER_MAX_PER_IP_PER_DAY:5}
|
||||
|
||||
Reference in new issue
Block a user