bug修复迭代
This commit is contained in:
892
docs/superpowers/plans/2026-06-30-image-captcha-sms.md
Normal file
892
docs/superpowers/plans/2026-06-30-image-captcha-sms.md
Normal file
@@ -0,0 +1,892 @@
|
||||
# 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<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:
|
||||
|
||||
```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<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`:
|
||||
|
||||
```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<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:
|
||||
|
||||
```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<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:
|
||||
|
||||
```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
|
||||
<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>`:
|
||||
|
||||
```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`.
|
||||
54
docs/superpowers/plans/2026-06-30-page-optimization.md
Normal file
54
docs/superpowers/plans/2026-06-30-page-optimization.md
Normal file
@@ -0,0 +1,54 @@
|
||||
# H5 Page Optimization 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:** Upgrade the current H5 business pages with a clearer, more polished mobile operations UI.
|
||||
|
||||
**Architecture:** Keep the Vue/Vant app structure intact. Centralize the broad visual improvements in `src/styles/main.css`, then add scoped page-level polish where the page has unique layout needs.
|
||||
|
||||
**Tech Stack:** Vue 3, Vite, Vant 4, lucide-vue-next, vanilla CSS.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Global Tokens And Components
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/styles/main.css`
|
||||
|
||||
- [ ] Add primitive, semantic, and component tokens while preserving existing variables such as `--app-bg`, `--surface`, `--primary`, and `--radius`.
|
||||
- [ ] Improve page shell, nav, panels, list cards, quick actions, buttons, tabs, fields, empty states, and fixed action bars.
|
||||
- [ ] Add tactile `:active`, keyboard focus, reduced-motion, and narrow-phone rules.
|
||||
|
||||
### Task 2: High-Frequency Page Polish
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/views/home/HomeView.vue`
|
||||
- Modify: `src/views/orders/OrderListView.vue`
|
||||
- Modify: `src/views/events/EventListView.vue`
|
||||
- Modify: `src/views/verify/VerifyView.vue`
|
||||
|
||||
- [ ] Make the home hero feel like a useful shift summary and make quick actions easier to scan.
|
||||
- [ ] Improve list loading, cards, search/tabs spacing, and empty states for orders/events.
|
||||
- [ ] Strengthen the scan panel visual hierarchy and feedback.
|
||||
|
||||
### Task 3: Detail, Confirm, Result, Account, And Login Polish
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/views/orders/OrderDetailView.vue`
|
||||
- Modify: `src/views/verify/VerifyConfirmView.vue`
|
||||
- Modify: `src/views/verify/VerifyResultView.vue`
|
||||
- Modify: `src/views/events/EventCreateView.vue`
|
||||
- Modify: `src/views/mine/MineView.vue`
|
||||
- Modify: `src/views/login/LoginView.vue`
|
||||
|
||||
- [ ] Improve detail and confirm hero sections, form panels, result panels, and account profile surface.
|
||||
- [ ] Keep all existing business logic unchanged.
|
||||
|
||||
### Task 4: Verification
|
||||
|
||||
**Files:**
|
||||
- No source files expected.
|
||||
|
||||
- [ ] Run `yarn build:prod`.
|
||||
- [ ] Start `yarn dev --host 127.0.0.1` or equivalent.
|
||||
- [ ] Inspect representative pages in a mobile viewport and fix visible layout issues.
|
||||
Reference in New Issue
Block a user