bug修复迭代

This commit is contained in:
andy
2026-07-01 12:54:49 +08:00
parent 5ac547a9d5
commit 4870b6761e
16 changed files with 1519 additions and 91 deletions

View 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`.

View 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.