# Image Captcha SMS Protection Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Add a required image captcha before sending mobile SMS codes for the H5 verification app, reducing SMS abuse while keeping the existing mobile login flow. **Architecture:** Reuse the existing `pig-auth` image captcha endpoint `/auth/code/image?randomStr=...`. The H5 login page will fetch and display that image, then send `phone`, `randomStr`, and `imageCode` to a new POST SMS endpoint in `pig-upms`; the backend validates the image captcha from Redis before sending the SMS code. Keep the existing SMS rate limiting and SMS login verification unchanged. **Tech Stack:** Vue 3 + Vite + Vant + TypeScript frontend; Spring Boot Java backend; Redis via `StringRedisTemplate` / `RedisTemplate`; existing Pig response wrapper `R`. --- ## File Structure Frontend repository: `/Users/andy/IdeaProjects/hotel-biz-h5` - Modify `src/api/auth.ts`: add image captcha URL helper and change `sendMobileCode` to POST a structured payload. - Modify `src/views/login/LoginView.vue`: add image captcha state, input field, refresh behavior, and send-SMS validation. - Modify `src/api/mock.ts`: accept the new SMS payload shape in mock mode. - Optional modify `docs/frontend-practices.md`: record that SMS sending requires image captcha. Backend repository: `/Users/andy/IdeaProjects/one-feel-server` - Create `pig-upms/pig-upms-api/src/main/java/com/pig4cloud/pig/admin/api/form/SendMobileCodeForm.java`: request body for SMS code sending. - Modify `pig-upms/pig-upms-biz/src/main/java/com/pig4cloud/pig/admin/controller/PlatformUserController.java`: add POST endpoint and validate request body. - Create `pig-upms/pig-upms-biz/src/main/java/com/pig4cloud/pig/admin/service/impl/ImageCaptchaVerifier.java`: verify image captcha from Redis and delete it on success. - Modify `pig-upms/pig-upms-biz/src/main/java/com/pig4cloud/pig/admin/service/impl/PlatformMobileServiceImpl.java`: keep existing send logic; do not move captcha logic here unless controller becomes too large. - Add focused backend tests under `pig-upms/pig-upms-biz/src/test/java/com/pig4cloud/pig/admin/service/impl/`. --- ### Task 1: Backend Request Form **Files:** - Create: `/Users/andy/IdeaProjects/one-feel-server/pig-upms/pig-upms-api/src/main/java/com/pig4cloud/pig/admin/api/form/SendMobileCodeForm.java` - [ ] **Step 1: Create the request form** ```java package com.pig4cloud.pig.admin.api.form; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @Data @Schema(description = "发送手机验证码请求") public class SendMobileCodeForm { @Schema(description = "手机号") private String phone; @Schema(description = "图形验证码随机标识") private String randomStr; @Schema(description = "图形验证码") private String imageCode; } ``` - [ ] **Step 2: Compile API module** Run: ```bash cd /Users/andy/IdeaProjects/one-feel-server mvn -pl pig-upms/pig-upms-api -am -DskipTests compile ``` Expected: build succeeds with no compilation errors. - [ ] **Step 3: Commit** ```bash cd /Users/andy/IdeaProjects/one-feel-server git add pig-upms/pig-upms-api/src/main/java/com/pig4cloud/pig/admin/api/form/SendMobileCodeForm.java git commit -m "新增发送短信验证码请求表单" ``` --- ### Task 2: Backend Image Captcha Verifier **Files:** - Create: `/Users/andy/IdeaProjects/one-feel-server/pig-upms/pig-upms-biz/src/main/java/com/pig4cloud/pig/admin/service/impl/ImageCaptchaVerifier.java` - Test: `/Users/andy/IdeaProjects/one-feel-server/pig-upms/pig-upms-biz/src/test/java/com/pig4cloud/pig/admin/service/impl/ImageCaptchaVerifierTest.java` - [ ] **Step 1: Write the failing unit test** ```java package com.pig4cloud.pig.admin.service.impl; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.core.ValueOperations; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; class ImageCaptchaVerifierTest { private StringRedisTemplate redisTemplate; private ValueOperations valueOperations; private ImageCaptchaVerifier verifier; @BeforeEach void setUp() { redisTemplate = mock(StringRedisTemplate.class); valueOperations = mock(ValueOperations.class); when(redisTemplate.opsForValue()).thenReturn(valueOperations); verifier = new ImageCaptchaVerifier(redisTemplate); } @Test void verifyAndDeletePassesAndDeletesKeyWhenCodeMatches() { when(valueOperations.get("DEFAULT_CODE_KEY:abc123")).thenReturn("8"); assertDoesNotThrow(() -> verifier.verifyAndDelete("abc123", "8")); verify(redisTemplate).delete("DEFAULT_CODE_KEY:abc123"); } @Test void verifyAndDeleteRejectsBlankValues() { assertThrows(IllegalArgumentException.class, () -> verifier.verifyAndDelete("", "8")); assertThrows(IllegalArgumentException.class, () -> verifier.verifyAndDelete("abc123", "")); } @Test void verifyAndDeleteRejectsMissingRedisCode() { when(valueOperations.get("DEFAULT_CODE_KEY:abc123")).thenReturn(null); assertThrows(IllegalArgumentException.class, () -> verifier.verifyAndDelete("abc123", "8")); } @Test void verifyAndDeleteRejectsWrongCode() { when(valueOperations.get("DEFAULT_CODE_KEY:abc123")).thenReturn("8"); assertThrows(IllegalArgumentException.class, () -> verifier.verifyAndDelete("abc123", "9")); } } ``` - [ ] **Step 2: Run test to verify it fails** Run: ```bash cd /Users/andy/IdeaProjects/one-feel-server mvn -pl pig-upms/pig-upms-biz -Dtest=ImageCaptchaVerifierTest test ``` Expected: FAIL because `ImageCaptchaVerifier` does not exist. - [ ] **Step 3: Implement the verifier** ```java package com.pig4cloud.pig.admin.service.impl; import cn.hutool.core.util.StrUtil; import com.pig4cloud.pig.common.core.constant.CacheConstants; import lombok.RequiredArgsConstructor; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Component; @Component @RequiredArgsConstructor public class ImageCaptchaVerifier { private final StringRedisTemplate redisTemplate; public void verifyAndDelete(String randomStr, String imageCode) { if (StrUtil.hasBlank(randomStr, imageCode)) { throw new IllegalArgumentException("图形验证码不能为空"); } String key = CacheConstants.DEFAULT_CODE_KEY + randomStr; String savedCode = redisTemplate.opsForValue().get(key); if (StrUtil.isBlank(savedCode)) { throw new IllegalArgumentException("图形验证码已过期,请刷新后重试"); } if (!StrUtil.equalsIgnoreCase(savedCode, imageCode.trim())) { throw new IllegalArgumentException("图形验证码不正确"); } redisTemplate.delete(key); } } ``` - [ ] **Step 4: Run test to verify it passes** Run: ```bash cd /Users/andy/IdeaProjects/one-feel-server mvn -pl pig-upms/pig-upms-biz -Dtest=ImageCaptchaVerifierTest test ``` Expected: PASS. - [ ] **Step 5: Commit** ```bash cd /Users/andy/IdeaProjects/one-feel-server git add pig-upms/pig-upms-biz/src/main/java/com/pig4cloud/pig/admin/service/impl/ImageCaptchaVerifier.java \ pig-upms/pig-upms-biz/src/test/java/com/pig4cloud/pig/admin/service/impl/ImageCaptchaVerifierTest.java git commit -m "增加图形验证码校验器" ``` --- ### Task 3: Backend POST SMS Endpoint **Files:** - Modify: `/Users/andy/IdeaProjects/one-feel-server/pig-upms/pig-upms-biz/src/main/java/com/pig4cloud/pig/admin/controller/PlatformUserController.java` - Uses: `/Users/andy/IdeaProjects/one-feel-server/pig-upms/pig-upms-api/src/main/java/com/pig4cloud/pig/admin/api/form/SendMobileCodeForm.java` - Uses: `/Users/andy/IdeaProjects/one-feel-server/pig-upms/pig-upms-biz/src/main/java/com/pig4cloud/pig/admin/service/impl/ImageCaptchaVerifier.java` - [ ] **Step 1: Add imports** Add these imports if absent: ```java import cn.hutool.core.lang.Validator; import com.pig4cloud.pig.admin.api.form.SendMobileCodeForm; import com.pig4cloud.pig.admin.service.impl.ImageCaptchaVerifier; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.PostMapping; ``` - [ ] **Step 2: Inject the verifier** Find the controller fields and constructor style. Add a field: ```java private final ImageCaptchaVerifier imageCaptchaVerifier; ``` If `PlatformUserController` uses Lombok `@RequiredArgsConstructor`, this field is enough. If it has an explicit constructor, add `ImageCaptchaVerifier imageCaptchaVerifier` to that constructor and assign it. - [ ] **Step 3: Add the new POST endpoint next to the existing GET endpoint** ```java @PostMapping("/sendMobileCode") public R sendMobileCode(@RequestBody SendMobileCodeForm form, @RequestHeader(value = SecurityConstants.CLIENT_CONFIG_ID_PARAMETER_NAME, required = false) String clientConfigId) { assertValidSmsClientConfig(form.getPhone(), clientConfigId); assertValidImageCode(form); imageCaptchaVerifier.verifyAndDelete(form.getRandomStr(), form.getImageCode()); return platformMobileService.sendSmsCode(form.getPhone(), clientConfigId); } ``` - [ ] **Step 4: Add strict validation helpers** Add these private methods near `assertValidSmsClientConfig`: ```java private void assertValidImageCode(SendMobileCodeForm form) { if (form == null || StrUtil.hasBlank(form.getRandomStr(), form.getImageCode())) { throw new CheckedException("图形验证码不能为空"); } } private void assertValidSmsClientConfig(String phone, String clientConfigId) { if (StrUtil.hasBlank(phone, clientConfigId)) { throw new CheckedException("手机号或clientConfigId不能为空"); } if (!Validator.isMobile(phone)) { throw new CheckedException("手机号格式不正确"); } ClientConfigEntity clientConfig = clientConfigService.getById(clientConfigId); if (Objects.isNull(clientConfig) || StrUtil.isBlank(clientConfig.getClientTenantId())) { log.error("短信验证码发送失败,客户端配置不存在或未配置租户,clientConfigId={}", clientConfigId); throw new CheckedException("验证码发送失败"); } try { Long.parseLong(clientConfig.getClientTenantId()); } catch (NumberFormatException ex) { log.error("短信验证码发送失败,客户端租户配置非法,clientConfigId={}, clientTenantId={}", clientConfigId, clientConfig.getClientTenantId()); throw new CheckedException("验证码发送失败"); } } ``` Important: replace the existing `assertValidSmsClientConfig` body instead of creating a duplicate method. - [ ] **Step 5: Keep the old GET endpoint temporarily** Do not delete this method in the first rollout: ```java @GetMapping("/sendMobileCode/{phone}") public R sendMobileCode(@PathVariable String phone, @RequestHeader(value = SecurityConstants.CLIENT_CONFIG_ID_PARAMETER_NAME, required = false) String clientConfigId) { assertValidSmsClientConfig(phone, clientConfigId); return platformMobileService.sendSmsCode(phone, clientConfigId); } ``` Reason: keeping it avoids breaking other clients immediately. After H5 and other clients migrate, remove the GET endpoint in a separate cleanup. - [ ] **Step 6: Compile backend** Run: ```bash cd /Users/andy/IdeaProjects/one-feel-server mvn -pl pig-upms/pig-upms-biz -am -DskipTests compile ``` Expected: build succeeds with no compilation errors. - [ ] **Step 7: Commit** ```bash cd /Users/andy/IdeaProjects/one-feel-server git add pig-upms/pig-upms-biz/src/main/java/com/pig4cloud/pig/admin/controller/PlatformUserController.java git commit -m "短信验证码发送增加图形验证码校验" ``` --- ### Task 4: Frontend API Contract **Files:** - Modify: `/Users/andy/IdeaProjects/hotel-biz-h5/src/api/auth.ts` - Modify: `/Users/andy/IdeaProjects/hotel-biz-h5/src/api/mock.ts` - [ ] **Step 1: Update `src/api/auth.ts` types and helpers** Replace the existing `sendMobileCode` implementation with: ```ts export interface SendMobileCodePayload { phone: string randomStr: string imageCode: string } export const getImageCodeUrl = (randomStr: string) => joinUrl(env.authBase, 'code/image') + `?randomStr=${encodeURIComponent(randomStr)}&t=${Date.now()}` export const sendMobileCode = async (payload: SendMobileCodePayload) => { if (env.useMock) return mockApi.sendMobileCode(payload) return await http.post(joinUrl(env.adminBase, 'platformUser/sendMobileCode'), payload, { headers: { clientId: env.clientId, clientConfigId: env.clientConfigId } }) as unknown as boolean } ``` - [ ] **Step 2: Update `src/api/mock.ts`** Change the mock method signature: ```ts async sendMobileCode(payload: { phone: string; randomStr?: string; imageCode?: string }) { await wait() if (!payload.phone) return { data: false, msg: '请输入手机号' } if (!payload.imageCode) return { data: false, msg: '请输入图形验证码' } return { data: true, msg: '5678nianxx' } } ``` - [ ] **Step 3: Run frontend typecheck to verify expected failures** Run: ```bash cd /Users/andy/IdeaProjects/hotel-biz-h5 yarn typecheck ``` Expected: FAIL because `LoginView.vue` still calls `sendMobileCode(phone.value)`. - [ ] **Step 4: Commit only if the expected failure is documented** Do not commit a failing frontend state. Continue to Task 5 before committing frontend changes. --- ### Task 5: Frontend Login Page UI **Files:** - Modify: `/Users/andy/IdeaProjects/hotel-biz-h5/src/views/login/LoginView.vue` - [ ] **Step 1: Update imports** Change: ```ts import { computed, ref } from 'vue' import { sendMobileCode } from '@/api/auth' ``` to: ```ts import { computed, onMounted, ref } from 'vue' import { getImageCodeUrl, sendMobileCode } from '@/api/auth' ``` - [ ] **Step 2: Add captcha state** Add below `const code = ref('')`: ```ts const imageCode = ref('') const captchaRandomStr = ref('') const captchaUrl = ref('') ``` - [ ] **Step 3: Add captcha refresh helpers** Add below `let timer: number | undefined`: ```ts const createRandomStr = () => `${Date.now()}${Math.random().toString(36).slice(2, 10)}` const refreshCaptcha = () => { captchaRandomStr.value = createRandomStr() captchaUrl.value = getImageCodeUrl(captchaRandomStr.value) imageCode.value = '' } ``` - [ ] **Step 4: Update send eligibility** Replace: ```ts const canSend = computed(() => /^1\d{10}$/.test(phone.value) && seconds.value === 0) ``` with: ```ts const canSend = computed(() => /^1\d{10}$/.test(phone.value) && imageCode.value.trim().length > 0 && seconds.value === 0) ``` - [ ] **Step 5: Update `handleSend` validation and payload** Replace the beginning of `handleSend` with: ```ts const handleSend = async () => { if (!/^1\d{10}$/.test(phone.value)) { showToast('请输入正确手机号') return } if (!imageCode.value.trim()) { showToast('请输入图形验证码') return } sending.value = true try { const result = await sendMobileCode({ phone: phone.value, randomStr: captchaRandomStr.value, imageCode: imageCode.value.trim() }) const message = typeof result === 'object' && result && 'msg' in result ? result.msg : '' if (message) { code.value = String(message) showToast(`验证码:${message}`) } else { showToast('验证码已发送') } startCountdown() } catch (error) { refreshCaptcha() showToast(error instanceof Error ? error.message : '验证码发送失败') } finally { sending.value = false } } ``` - [ ] **Step 6: Initialize captcha on mount** Add below `handleLogin`: ```ts onMounted(refreshCaptcha) ``` - [ ] **Step 7: Add captcha field to template** Insert this `van-field` between phone and SMS code fields: ```vue ``` - [ ] **Step 8: Add scoped styles** Add before the closing ``: ```css .captcha-image-button { display: inline-flex; width: 100px; height: 40px; align-items: center; justify-content: center; overflow: hidden; border: 0; border-radius: var(--radius); background: var(--surface-soft); color: var(--primary-deep); padding: 0; } .captcha-image-button img { display: block; width: 100px; height: 40px; object-fit: cover; } ``` - [ ] **Step 9: Run frontend checks** Run: ```bash cd /Users/andy/IdeaProjects/hotel-biz-h5 yarn typecheck yarn build:test ``` Expected: both pass. `vite build` may still show the existing router dynamic/static import warning; that warning is unrelated. - [ ] **Step 10: Commit frontend changes** ```bash cd /Users/andy/IdeaProjects/hotel-biz-h5 git add src/api/auth.ts src/api/mock.ts src/views/login/LoginView.vue git commit -m "登录短信发送增加图形验证码" ``` --- ### Task 6: End-to-End Manual Verification **Files:** - No code changes expected. - [ ] **Step 1: Start backend services needed for auth, upms, gateway, and Redis** Use the existing local backend startup process for `/Users/andy/IdeaProjects/one-feel-server`. Confirm these routes are reachable through the gateway: ```text GET /auth/code/image?randomStr=manual-test POST /admin/platformUser/sendMobileCode POST /auth/oauth2/token ``` - [ ] **Step 2: Start frontend** Run: ```bash cd /Users/andy/IdeaProjects/hotel-biz-h5 yarn dev ``` Expected: Vite starts and prints a local URL. - [ ] **Step 3: Verify captcha image loads** Open `/login`. Expected: - Login title shows `核销端`. - Phone input is visible. - Image captcha input is visible. - Captcha image loads from `/auth/code/image`. - Clicking the image refreshes the URL and clears the image code input. - [ ] **Step 4: Verify empty captcha is blocked by frontend** Enter a valid phone number and leave image captcha empty. Click `获取验证码`. Expected: - No SMS request is sent. - Toast says `请输入图形验证码`. - [ ] **Step 5: Verify wrong captcha is rejected by backend** Enter a valid phone number and a wrong image captcha. Click `获取验证码`. Expected: - `POST /admin/platformUser/sendMobileCode` returns a failure response. - Toast shows backend error. - Captcha image refreshes. - [ ] **Step 6: Verify correct captcha sends SMS** Enter a valid phone number and the correct image captcha. Click `获取验证码`. Expected: - SMS send endpoint returns success. - Countdown starts. - Existing SMS frequency limit still applies if clicking repeatedly. - [ ] **Step 7: Verify login still works** Enter the SMS code and submit login. Expected: - Login calls `/auth/oauth2/token`. - After token success, frontend calls `/hotelStaff/organizationMember/bindUserInfoAndGetUserMemberInfoByPhone`. - Bound users enter `/home`. - Unbound users see `未绑定组织,请联系管理员`. --- ### Task 7: Documentation Update **Files:** - Modify: `/Users/andy/IdeaProjects/hotel-biz-h5/docs/frontend-practices.md` - [ ] **Step 1: Add SMS captcha note** Add this bullet under `## 登录与组织关系`: ```markdown - 发送短信验证码前必须先通过图形验证码校验;前端展示 `/auth/code/image` 生成的验证码图片,后端在 `/admin/platformUser/sendMobileCode` 校验 `randomStr` 和 `imageCode` 后才发送短信。 ``` - [ ] **Step 2: Run docs diff check** Run: ```bash cd /Users/andy/IdeaProjects/hotel-biz-h5 git diff --check docs/frontend-practices.md ``` Expected: no output. - [ ] **Step 3: Commit docs** ```bash cd /Users/andy/IdeaProjects/hotel-biz-h5 git add docs/frontend-practices.md git commit -m "补充短信图形验证码约定" ``` --- ## Rollout Notes - Keep the old GET endpoint during the first deployment to avoid breaking unknown clients. - H5 should switch to the new POST endpoint immediately after backend deployment. - After production confirms no clients call `GET /admin/platformUser/sendMobileCode/{phone}`, remove the GET endpoint in a later cleanup. - Do not remove existing Redis SMS frequency limits; image captcha is an additional layer, not a replacement. - Do not rely on frontend captcha checks for security; backend validation is required. ## Execution Checkpoints ### Checkpoint 0: Baseline Safety **Purpose:** Confirm both repositories are in a known state before touching code. **Scope:** No code changes. **Verification commands:** ```bash cd /Users/andy/IdeaProjects/hotel-biz-h5 git status --short --branch yarn typecheck cd /Users/andy/IdeaProjects/one-feel-server git status --short --branch mvn -pl pig-upms/pig-upms-api -am -DskipTests compile ``` **Pass criteria:** - Current branch and dirty files are understood. - Existing frontend typecheck passes. - Backend API module compiles before changes. **Stop condition:** If baseline build fails for unrelated reasons, record the failure and do not start feature work until the owner decides whether to fix baseline first. ### Checkpoint 1: Backend Contract Ready **Purpose:** Add the new SMS request body without changing runtime behavior. **Scope:** Task 1 only. **Verification commands:** ```bash cd /Users/andy/IdeaProjects/one-feel-server mvn -pl pig-upms/pig-upms-api -am -DskipTests compile git show --stat --oneline HEAD ``` **Pass criteria:** - `SendMobileCodeForm` exists with `phone`, `randomStr`, and `imageCode`. - API module compiles. - Commit contains only the request form. **Rollback point:** Revert the request-form commit only. ### Checkpoint 2: Captcha Verifier Proven **Purpose:** Prove Redis image captcha validation works before wiring it into the SMS endpoint. **Scope:** Task 2 only. **Verification commands:** ```bash cd /Users/andy/IdeaProjects/one-feel-server mvn -pl pig-upms/pig-upms-biz -Dtest=ImageCaptchaVerifierTest test git show --stat --oneline HEAD ``` **Pass criteria:** - Matching captcha passes and deletes `DEFAULT_CODE_KEY:{randomStr}`. - Blank, missing, and wrong captcha values fail. - Unit test passes. - Commit contains only verifier and verifier test. **Stop condition:** If the project lacks test dependencies for this isolated unit test, stop and decide whether to add test dependencies or verify with an integration test instead. **Rollback point:** Revert the verifier commit only. ### Checkpoint 3: Backend Endpoint Protected **Purpose:** Make SMS sending require backend image captcha validation while preserving the old GET endpoint for compatibility. **Scope:** Task 3 only. **Verification commands:** ```bash cd /Users/andy/IdeaProjects/one-feel-server mvn -pl pig-upms/pig-upms-biz -am -DskipTests compile git diff --check git show --stat --oneline HEAD ``` **Pass criteria:** - New `POST /admin/platformUser/sendMobileCode` accepts `SendMobileCodeForm`. - Backend validates phone format, `clientConfigId`, `randomStr`, and `imageCode`. - Captcha verification runs before `platformMobileService.sendSmsCode`. - Existing `GET /sendMobileCode/{phone}` is still present. - Existing SMS rate limits are untouched. **Manual smoke check after backend starts:** ```text POST /admin/platformUser/sendMobileCode body: {"phone":"13800000000","randomStr":"wrong","imageCode":"0000"} Expected: failure response, no SMS sent. ``` **Rollback point:** Revert the backend endpoint commit. Old GET endpoint remains available. ### Checkpoint 4: Frontend Contract Switched **Purpose:** Move H5 from URL-phone GET sending to structured POST sending. **Scope:** Task 4 and Task 5. **Verification commands:** ```bash cd /Users/andy/IdeaProjects/hotel-biz-h5 yarn typecheck yarn build:test git diff --check src/api/auth.ts src/api/mock.ts src/views/login/LoginView.vue git show --stat --oneline HEAD ``` **Pass criteria:** - Login page displays phone input, image captcha input, captcha image, SMS code input, and login button. - `sendMobileCode` sends `phone`, `randomStr`, and `imageCode` to `POST /admin/platformUser/sendMobileCode`. - Empty image captcha is blocked in the frontend with `请输入图形验证码`. - Failed SMS send shows a toast and refreshes captcha. - Mock mode still works. **Manual smoke check with frontend only:** ```text 1. Start H5 in mock mode. 2. Open /login. 3. Try sending with empty image captcha. 4. Expected: toast says 请输入图形验证码 and no countdown starts. 5. Enter any image captcha in mock mode and send. 6. Expected: countdown starts and mock SMS code appears. ``` **Rollback point:** Revert the frontend commit. Backend POST endpoint can remain deployed without affecting old clients. ### Checkpoint 5: End-to-End Gate **Purpose:** Verify the complete browser -> gateway -> auth/upms -> Redis flow before release. **Scope:** Task 6. **Verification steps:** ```text 1. Start Redis, gateway, pig-auth, pig-upms, and H5. 2. Open /login. 3. Confirm captcha image loads from /auth/code/image. 4. Enter wrong image captcha and click 获取验证码. 5. Expected: SMS endpoint returns failure and no countdown starts. 6. Refresh captcha, enter correct image captcha, and click 获取验证码. 7. Expected: SMS send succeeds and countdown starts. 8. Enter SMS code and login. 9. Expected: token endpoint succeeds, organization binding endpoint runs, bound user enters /home. ``` **Pass criteria:** - Wrong captcha cannot send SMS. - Correct captcha can send SMS. - Captcha cannot be reused after one successful SMS send. - Existing SMS frequency limit still blocks repeated sends. - Login flow after receiving SMS code is unchanged. **Stop condition:** If correct captcha still fails, inspect Redis key serialization between `ImageCodeEndpoint` and `ImageCaptchaVerifier` before changing frontend code. ### Checkpoint 6: Documentation And Release Readiness **Purpose:** Lock the new behavior into project docs and prepare release notes. **Scope:** Task 7 and release review. **Verification commands:** ```bash cd /Users/andy/IdeaProjects/hotel-biz-h5 git diff --check docs/frontend-practices.md cd /Users/andy/IdeaProjects/one-feel-server git log --oneline -3 cd /Users/andy/IdeaProjects/hotel-biz-h5 git log --oneline -3 ``` **Pass criteria:** - Docs say SMS sending requires image captcha. - Backend commits and frontend commits are clearly separated. - Release notes mention that old GET SMS endpoint is temporarily retained. **Release decision:** Deploy backend first, then deploy H5. Do not deploy H5 before the backend POST endpoint is available. ## Self-Review - Spec coverage: The plan covers existing image captcha reuse, backend POST endpoint, Redis verification, frontend login UI, API contract, tests, manual verification, and documentation. - Placeholder scan: No `TBD`, `TODO`, or vague "handle edge cases" steps remain; each task includes concrete file paths and code. - Type consistency: Frontend `SendMobileCodePayload` fields match backend `SendMobileCodeForm` fields: `phone`, `randomStr`, `imageCode`.