From 35d92c01487b56a2c9836cf28ece9fa33f6a2396 Mon Sep 17 00:00:00 2001 From: Zhou Weidong <502333560@qq.com> Date: Sat, 17 Oct 2026 23:39:48 +0800 Subject: [PATCH 1/2] =?UTF-8?q?fix(backend):=20=E5=90=8E=E5=8F=B0=E6=B3=A8?= =?UTF-8?q?=E5=86=8C=E9=99=90=E6=B5=81=E9=85=8D=E7=BD=AE=E4=BC=98=E5=85=88?= =?UTF-8?q?=E5=B9=B6=E4=BF=9D=E7=95=99=E6=B3=A8=E5=85=A5=E5=85=9C=E5=BA=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/api-phase1.md | 5 ++++- .../src/main/resources/application.yml | 4 ++++ .../app/service/AppAuthServiceTest.java | 7 ++++--- .../fruitshorts/user/service/AppAuthService.java | 15 +++++++++++---- 4 files changed, 23 insertions(+), 8 deletions(-) diff --git a/docs/api-phase1.md b/docs/api-phase1.md index d66f2ca..e3c9e2b 100644 --- a/docs/api-phase1.md +++ b/docs/api-phase1.md @@ -125,7 +125,10 @@ app.register.max-per-ip-per-day app.register.max-per-device-per-day ``` -键缺失或值不是整数时回退默认 5 / 3;值小于等于 0 表示对应维度不限制。 +运行时优先读取 `sys_config`。键缺失或值不是整数时,回退 `application.yml` +中的 `fruitshorts.auth.register-max-per-ip-per-day` / +`fruitshorts.auth.register-max-per-device-per-day`,缺省为 5 / 3; +值小于等于 0 表示对应维度不限制。 开发种子配置为 0 / 0,生产建议恢复为 5 / 3。 **残留风险**:IP 取自 `X-Forwarded-For`、设备号由客户端上报,都能伪造,只能挡脚本批量; diff --git a/fruitshorts-app-api/src/main/resources/application.yml b/fruitshorts-app-api/src/main/resources/application.yml index c2d1f4c..ae8eefe 100644 --- a/fruitshorts-app-api/src/main/resources/application.yml +++ b/fruitshorts-app-api/src/main/resources/application.yml @@ -43,6 +43,10 @@ springdoc: path: /v3/api-docs fruitshorts: + auth: + # 仅作 sys_config 缺失/非法时的兜底;运行时优先读取后台「系统配置」 + register-max-per-ip-per-day: ${REGISTER_MAX_PER_IP_PER_DAY:5} + register-max-per-device-per-day: ${REGISTER_MAX_PER_DEVICE_PER_DAY:3} interaction: # MACHINE_FIRST:机审通过即发布,命中 REVIEW 级敏感词才转人工 # PRE_REVIEW :全部先审后发(生产建议) diff --git a/fruitshorts-app-api/src/test/java/com/fruitshorts/app/service/AppAuthServiceTest.java b/fruitshorts-app-api/src/test/java/com/fruitshorts/app/service/AppAuthServiceTest.java index 4424ada..f3f3e4d 100644 --- a/fruitshorts-app-api/src/test/java/com/fruitshorts/app/service/AppAuthServiceTest.java +++ b/fruitshorts-app-api/src/test/java/com/fruitshorts/app/service/AppAuthServiceTest.java @@ -26,6 +26,7 @@ import com.fruitshorts.user.vo.TokenRespVO; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.test.util.ReflectionTestUtils; import java.time.Duration; import java.time.LocalDateTime; @@ -193,9 +194,9 @@ class AppAuthServiceTest { @Test void should_fallback_to_default_limit_when_config_is_not_numeric() { - setRegisterLimits(5, 3); - given(configService.get("app.register.max-per-ip-per-day", "5")).willReturn("invalid"); - given(userMapper.selectCount(any())).willReturn(5L); + ReflectionTestUtils.setField(authService, "maxRegisterPerIpPerDay", 2); + given(configService.get("app.register.max-per-ip-per-day", "2")).willReturn("invalid"); + given(userMapper.selectCount(any())).willReturn(2L); assertThatThrownBy(() -> authService.register(emailRegister("abcd1234", null), "203.0.113.8")) .isInstanceOf(ServiceException.class) diff --git a/fruitshorts-module-user/src/main/java/com/fruitshorts/user/service/AppAuthService.java b/fruitshorts-module-user/src/main/java/com/fruitshorts/user/service/AppAuthService.java index 8ed03b1..e0f8d56 100644 --- a/fruitshorts-module-user/src/main/java/com/fruitshorts/user/service/AppAuthService.java +++ b/fruitshorts-module-user/src/main/java/com/fruitshorts/user/service/AppAuthService.java @@ -26,6 +26,7 @@ import com.fruitshorts.user.vo.LogoutReqVO; import com.fruitshorts.user.vo.RegisterReqVO; import com.fruitshorts.user.vo.TokenRespVO; import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; import org.springframework.dao.DuplicateKeyException; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; @@ -61,6 +62,14 @@ public class AppAuthService { private final PasswordEncoder passwordEncoder; private final ConfigService configService; + /** 后台 sys_config 缺失或非法时使用;小于等于 0 表示不限制。 */ + @Value("${fruitshorts.auth.register-max-per-ip-per-day:5}") + private int maxRegisterPerIpPerDay = 5; + + /** 后台 sys_config 缺失或非法时使用;小于等于 0 表示不限制。 */ + @Value("${fruitshorts.auth.register-max-per-device-per-day:3}") + private int maxRegisterPerDevicePerDay = 3; + @Transactional(rollbackFor = Exception.class) public TokenRespVO register(RegisterReqVO req, String clientIp) { String targetType = AppIdentifier.normalizeType(req.getTargetType()); @@ -305,8 +314,6 @@ public class AppAuthService { private static final String REGISTER_IP_LIMIT_KEY = "app.register.max-per-ip-per-day"; private static final String REGISTER_DEVICE_LIMIT_KEY = "app.register.max-per-device-per-day"; - private static final int DEFAULT_REGISTER_IP_LIMIT = 5; - private static final int DEFAULT_REGISTER_DEVICE_LIMIT = 3; private static final int PASSWORD_MIN_LENGTH = 8; private static final int PASSWORD_MAX_LENGTH = 64; @@ -339,8 +346,8 @@ public class AppAuthService { */ private void guardRegisterRisk(String clientIp, String deviceId) { LocalDateTime since = LocalDateTime.now().minusDays(1); - int ipLimit = registerLimit(REGISTER_IP_LIMIT_KEY, DEFAULT_REGISTER_IP_LIMIT); - int deviceLimit = registerLimit(REGISTER_DEVICE_LIMIT_KEY, DEFAULT_REGISTER_DEVICE_LIMIT); + int ipLimit = registerLimit(REGISTER_IP_LIMIT_KEY, maxRegisterPerIpPerDay); + int deviceLimit = registerLimit(REGISTER_DEVICE_LIMIT_KEY, maxRegisterPerDevicePerDay); String ip = AppIdentifier.nullSafe(clientIp); if (ipLimit > 0 && StringUtils.hasText(ip)) { Long byIp = appUserMapper.selectCount(Wrappers.lambdaQuery() From 21e0d7f383ec8c0464e18c5f6f4f45c56e3670eb Mon Sep 17 00:00:00 2001 From: Zhou Weidong <502333560@qq.com> Date: Sat, 17 Oct 2026 23:40:22 +0800 Subject: [PATCH 2/2] =?UTF-8?q?docs(plans):=20=E8=AE=B0=E5=BD=95=20K16=20?= =?UTF-8?q?=E8=BF=94=E5=B7=A5=E4=B8=8E=E9=99=90=E6=B5=81=E5=BE=80=E8=BF=94?= =?UTF-8?q?=E9=AA=8C=E8=AF=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/plans/2026-10-17-K16.md | 10 +-- docs/plans/INBOX.md | 20 +++++- docs/plans/PROGRESS.md | 131 +++++++++++++++++++++++++++++++++++ 3 files changed, 156 insertions(+), 5 deletions(-) diff --git a/docs/plans/2026-10-17-K16.md b/docs/plans/2026-10-17-K16.md index a7909a9..640c39c 100644 --- a/docs/plans/2026-10-17-K16.md +++ b/docs/plans/2026-10-17-K16.md @@ -9,14 +9,15 @@ app.register.max-per-ip-per-day app.register.max-per-device-per-day ``` -键缺失或非整数时回退默认 5/3;值小于等于 0 时跳过对应 guard。注册接口、 -错误码和错误文案不变。 +运行时优先读取后台配置;键缺失或非整数时回退 `@Value` 注入值(默认 5/3); +值小于等于 0 时跳过对应 guard。注册接口、错误码和错误文案不变。 ## 实现 -1. `AppAuthService` 注入 `ConfigService`,从 `sys_config` 读取两个键。 +1. `AppAuthService` 注入 `ConfigService`,从 `sys_config` 优先读取两个键; + 再用 `@Value` 注入 yml 值作为缺失/非法时的 fallback。 2. `sql/data-phase1.sql` 幂等写入开发默认 0/0 和说明。 -3. 不使用 `application.yml` 固化限流值。 +3. `application.yml` 只声明 fallback(默认 5/3),运行时不以后台固化值为准。 4. `docs/api-phase1.md` §1.1 补充后台配置键、缺省/非法回退和 0=不限制口径。 5. 测试 mock `ConfigService`,覆盖自定义、0、缺省/非数字回退。 6. 确认后台「系统设置」页面可访问 `/admin/configs`;若现有表单未展示新键, @@ -28,6 +29,7 @@ app.register.max-per-device-per-day - `mvn -pl fruitshorts-app-api -am package -DskipTests` - 停止当前 8081 服务,启动新 jar,`/healthz` 通过 - 使用新设备号连续注册至少 4 次,确认不再 429(接口自检) +- 后台把 IP 上限临时改为 2:前两次成功,第三次返回 429;恢复 0 后继续注册成功 - `feature/backend-register-rate-config-fix` 以 `--no-ff` 合并回 `develop` 并 push - PROGRESS 记录命令、原始输出和解读 diff --git a/docs/plans/INBOX.md b/docs/plans/INBOX.md index 4ec9d76..1e42f43 100644 --- a/docs/plans/INBOX.md +++ b/docs/plans/INBOX.md @@ -55,7 +55,7 @@ - 提交作者与提交者统一 `Zhou Weidong <502333560@qq.com>`;从干净 develop 拉 `feature/backend-register-rate-config`,完成后 `git merge --no-ff` 回 develop 并 push。 - 留痕写入 `FruitServer/docs/plans/PROGRESS.md`。 -### 四、K16 完成回执(2026-10-17) +### 四、K16 首版回执(历史,已被返工回执替代) **结论**:注册 IP/设备限流已改为读取后台 `sys_config`,开发两键为 0/0, 支持 0=不限制;后台系统设置页可修改。注册接口与返回结构未变。 @@ -75,6 +75,24 @@ 命令、日志和服务自检原始输出见 `docs/plans/PROGRESS.md`「32. K16 注册限流后台配置化」。 +### 五、K16 返工回执(2026-10-17) + +**结论**:已按最新口径改为“后台 `sys_config` 优先,`@Value` fallback”, +后台「系统配置」可按 IP/设备修改;开发库两键均为 0。 + +- 返工提交 `35d92c0 fix(backend): 后台注册限流配置优先并保留注入兜底`。 +- `AppAuthService` 读取 `app.register.max-per-ip-per-day` / + `app.register.max-per-device-per-day`;缺失或非数字时回退 + `fruitshorts.auth.register-max-*` 的 `@Value` 注入值。 +- `sql/data-phase1.sql` 幂等种子已执行;查询结果为 IP 0、设备 0。 +- `/admin/configs` 实测返回两键,现有系统设置页可修改。 +- 临时把 IP 上限改为 2:前两次注册成功,第三次返回 `code=429`; + 恢复 0 后再次注册成功。 +- `AppAuthServiceTest` 18 项通过,全量 `mvn test` BUILD SUCCESS。 +- 新 jar 已重启,8081 PID `94657`,`/healthz` 返回 HTTP 200。 + +原始输出与解读见 `docs/plans/PROGRESS.md`「33. K16 返工」。 + --- ## [待处理] 2026-10-17 | K14 客户端文档:README + 任务清单 + 文件说明 diff --git a/docs/plans/PROGRESS.md b/docs/plans/PROGRESS.md index 13b2881..173c106 100644 --- a/docs/plans/PROGRESS.md +++ b/docs/plans/PROGRESS.md @@ -3158,6 +3158,137 @@ uid=UDAYJR-_E --- +## 2026-10-17 K16 返工:sys_config 优先 + @Value 兜底 + +### 33.1 返工范围 + +K16 首版合并后,用户要求明确区分后台运行配置与 yml fallback: + +- `AppAuthService` 运行时优先读取 `sys_config`: + `app.register.max-per-ip-per-day`、`app.register.max-per-device-per-day`; +- 键缺失或非整数时,回退 `@Value` 注入的 + `fruitshorts.auth.register-max-*`; +- yml 中的值只作 fallback,默认 5 / 3;≤0 仍表示不限制。 + +返工分支:`fix/k16-register-config-fallback`。 + +### 33.2 TDD 与测试 + +先把非数字配置用例改为验证 @Value fallback=2,旧实现红灯: + +```text +Could not find field 'maxRegisterPerIpPerDay' +Tests run: 18, Failures: 0, Errors: 1, Skipped: 0 +BUILD FAILURE +``` + +增加注入字段并接入 registerLimit 后,定向测试转绿: + +```text +RUNNING AppAuthServiceTest +Tests run: 18, Failures: 0, Errors: 0, Skipped: 0 +BUILD SUCCESS +``` + +全量测试: + +```text +$ mvn test +[INFO] fruitshorts-module-user ............................ SUCCESS +[INFO] fruitshorts-app-api ............................... SUCCESS +[INFO] BUILD SUCCESS +``` + +代码提交: + +```text +35d92c0 fix(backend): 后台注册限流配置优先并保留注入兜底 +``` + +### 33.3 数据库与后台接口 + +重新执行 `sql/data-phase1.sql` 中的幂等插入: + +```text +INSERT 0 0 +``` + +查询结果: + +```text +config_key | config_value | remark +app.register.max-per-device-per-day | 0 | 0=不限制;生产建议 3 +app.register.max-per-ip-per-day | 0 | 0=不限制;生产建议 5 +(2 rows) +``` + +使用本机开发管理员登录后,`GET /admin/configs` 返回: + +```json +{ + "code": 0, + "message": "ok", + "configs": [ + { + "key": "app.register.max-per-device-per-day", + "value": "0", + "remark": "0=不限制;生产建议 3" + }, + { + "key": "app.register.max-per-ip-per-day", + "value": "0", + "remark": "0=不限制;生产建议 5" + } + ] +} +``` + +现有 `SettingsView.vue` 已使用 `listConfigsApi` / `updateConfigsApi`, +页面上的两个数值输入项可直接读写这两键。 + +### 33.4 阈值 2 往返验证 + +通过后台 `PUT /admin/configs` 把 IP 上限临时设为 2、设备上限设为 0, +使用同一条测试 IP 连续注册: + +```text +attempt 1 HTTP 200 code=0 message=ok +attempt 2 HTTP 200 code=0 message=ok +attempt 3 HTTP 200 code=429 message=当前网络注册过于频繁,请稍后再试 +``` + +随后通过同一后台接口恢复 IP=0,再注册一次: + +```text +attempt 4 HTTP 200 code=0 message=ok +``` + +说明:当前统一响应包装对外使用 HTTP 200,业务码在 `code` 字段返回 429。 + +### 33.5 打包与 8081 + +```text +$ mvn -pl fruitshorts-app-api -am package -DskipTests +[INFO] BUILD SUCCESS +``` + +通过 `launchctl kickstart -k gui/501/com.fruitshorts.app-api` 重启: + +```text +before PID: 80642 +after PID: 94657 +java 94657 ... TCP *:8081 (LISTEN) +``` + +健康检查: + +```text +{"code":0,"message":"ok","data":{"status":"UP","app":"fruitshorts-app-api","profiles":"","time":"2026-10-17T23:38:32.363463"}} +HTTP 200 +``` + +--- + ## 2026-10-17 iOS 构建阻塞:固定 shared_preferences_foundation 2.5.6 ### 25.1 现象与根因