27 KiB
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 changesendMobileCodeto 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
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:
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
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
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<String, String> 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:
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
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:
cd /Users/andy/IdeaProjects/one-feel-server
mvn -pl pig-upms/pig-upms-biz -Dtest=ImageCaptchaVerifierTest test
Expected: PASS.
- Step 5: Commit
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:
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:
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
@PostMapping("/sendMobileCode")
public R<Boolean> 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:
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:
@GetMapping("/sendMobileCode/{phone}")
public R<Boolean> 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:
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
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.tstypes and helpers
Replace the existing sendMobileCode implementation with:
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<boolean>(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:
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:
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:
import { computed, ref } from 'vue'
import { sendMobileCode } from '@/api/auth'
to:
import { computed, onMounted, ref } from 'vue'
import { getImageCodeUrl, sendMobileCode } from '@/api/auth'
- Step 2: Add captcha state
Add below const code = ref(''):
const imageCode = ref('')
const captchaRandomStr = ref('')
const captchaUrl = ref('')
- Step 3: Add captcha refresh helpers
Add below let timer: number | undefined:
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:
const canSend = computed(() => /^1\d{10}$/.test(phone.value) && seconds.value === 0)
with:
const canSend = computed(() => /^1\d{10}$/.test(phone.value) && imageCode.value.trim().length > 0 && seconds.value === 0)
- Step 5: Update
handleSendvalidation and payload
Replace the beginning of handleSend with:
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:
onMounted(refreshCaptcha)
- Step 7: Add captcha field to template
Insert this van-field between phone and SMS code fields:
<van-field
v-model="imageCode"
type="text"
name="imageCode"
maxlength="8"
placeholder="请输入图形验证码"
autocomplete="off"
clearable
>
<template #left-icon><ShieldCheck :size="18" /></template>
<template #button>
<button class="captcha-image-button" type="button" @click="refreshCaptcha">
<img v-if="captchaUrl" :src="captchaUrl" alt="图形验证码" />
<span v-else>刷新</span>
</button>
</template>
</van-field>
- Step 8: Add scoped styles
Add before the closing </style>:
.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:
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
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:
GET /auth/code/image?randomStr=manual-test
POST /admin/platformUser/sendMobileCode
POST /auth/oauth2/token
- Step 2: Start frontend
Run:
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/sendMobileCodereturns 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 ## 登录与组织关系:
- 发送短信验证码前必须先通过图形验证码校验;前端展示 `/auth/code/image` 生成的验证码图片,后端在 `/admin/platformUser/sendMobileCode` 校验 `randomStr` 和 `imageCode` 后才发送短信。
- Step 2: Run docs diff check
Run:
cd /Users/andy/IdeaProjects/hotel-biz-h5
git diff --check docs/frontend-practices.md
Expected: no output.
- Step 3: Commit docs
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:
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:
cd /Users/andy/IdeaProjects/one-feel-server
mvn -pl pig-upms/pig-upms-api -am -DskipTests compile
git show --stat --oneline HEAD
Pass criteria:
SendMobileCodeFormexists withphone,randomStr, andimageCode.- 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:
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:
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/sendMobileCodeacceptsSendMobileCodeForm. - Backend validates phone format,
clientConfigId,randomStr, andimageCode. - 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:
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:
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.
sendMobileCodesendsphone,randomStr, andimageCodetoPOST /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:
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:
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:
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
SendMobileCodePayloadfields match backendSendMobileCodeFormfields:phone,randomStr,imageCode.