Files
hotel-biz-h5/docs/superpowers/plans/2026-06-30-image-captcha-sms.md
2026-07-01 12:54:49 +08:00

27 KiB
Raw Permalink Blame History

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

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.ts types 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 handleSend validation 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/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 ## 登录与组织关系:

- 发送短信验证码前必须先通过图形验证码校验;前端展示 `/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:

  • 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:

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/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:

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.
  • 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:

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 SendMobileCodePayload fields match backend SendMobileCodeForm fields: phone, randomStr, imageCode.