|
|
@@ -0,0 +1,570 @@
|
|
|
+# mobile-h5 账号密码登录对接真实 API 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:** 将 `mobile-h5` 登录页账号密码从 mock 改为调用 backend `/api/web/auth/login`(RSA),微信按钮置灰不可点。
|
|
|
+
|
|
|
+**Architecture:** 新增 `config` / `request` / `login-rsa` / `auth`;登录页调 `passwordLogin`;会话写入 `token` + `refreshToken` + `userInfo`;不改后端;不做微信登录与自动 refresh。
|
|
|
+
|
|
|
+**Tech Stack:** uni-app Vue3(HBuilderX)、`jsencrypt`、`uni.request`、现有 backend Web 登录 API
|
|
|
+
|
|
|
+**Spec:** `docs/superpowers/specs/2026-08-10-mobile-h5-password-login-api-design.md`
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## File map
|
|
|
+
|
|
|
+| 路径 | 职责 |
|
|
|
+|------|------|
|
|
|
+| `mobile-h5/utils/config.js` | `API_BASE_URL` |
|
|
|
+| `mobile-h5/utils/request.js` | `get` / `post`,解析 `{ code, message, data }` |
|
|
|
+| `mobile-h5/utils/request.test.mjs` | 纯函数响应解析单测 |
|
|
|
+| `mobile-h5/utils/login-rsa.js` | 拉公钥 + JSEncrypt 加密 |
|
|
|
+| `mobile-h5/utils/auth.js` | 会话存取 + `passwordLogin`(替换 `auth-mock.js`) |
|
|
|
+| `mobile-h5/package.json` | 增加 `jsencrypt` |
|
|
|
+| `mobile-h5/pages/login/index.vue` | 真实登录;微信置灰 |
|
|
|
+| `mobile-h5/pages/index/index.vue` | import 改为 `auth.js` |
|
|
|
+| `mobile-h5/styles/components/login.scss` | `.login-wechat.is-disabled` |
|
|
|
+| `mobile-h5/utils/auth-mock.js` | 删除(调用方改完后) |
|
|
|
+
|
|
|
+**不修改:** `backend/**`、协议页、微信换票 API。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 1: `config.js`
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Create: `mobile-h5/utils/config.js`
|
|
|
+
|
|
|
+- [ ] **Step 1: 写入配置**
|
|
|
+
|
|
|
+```js
|
|
|
+/**
|
|
|
+ * 后端 API 根路径(含 /api,不含末尾斜杠)。
|
|
|
+ * 本地默认;真机 / 体验版请改为可达地址,例如 http://192.168.x.x:8000/api
|
|
|
+ */
|
|
|
+export const API_BASE_URL = 'http://127.0.0.1:8000/api'
|
|
|
+
|
|
|
+export const REQUEST_TIMEOUT_MS = 15000
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd /Users/wuzilong/work/00code/zkxw
|
|
|
+git add mobile-h5/utils/config.js
|
|
|
+git commit -m "$(cat <<'EOF'
|
|
|
+chore(mobile-h5): add API_BASE_URL config for login
|
|
|
+
|
|
|
+EOF
|
|
|
+)"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 2: `request.js` + 响应解析单测
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Create: `mobile-h5/utils/request.js`
|
|
|
+- Create: `mobile-h5/utils/request.test.mjs`
|
|
|
+
|
|
|
+- [ ] **Step 1: 写入可测的解析函数与 request 封装**
|
|
|
+
|
|
|
+`mobile-h5/utils/request.js`:
|
|
|
+
|
|
|
+```js
|
|
|
+import { API_BASE_URL, REQUEST_TIMEOUT_MS } from './config.js'
|
|
|
+
|
|
|
+/**
|
|
|
+ * 解析后端统一响应。成功返回 data;失败抛 Error(message 为业务文案)。
|
|
|
+ * @param {{ code?: number, message?: string, data?: any }} body
|
|
|
+ * @param {number} [httpStatus]
|
|
|
+ */
|
|
|
+export function unwrapResponse(body, httpStatus) {
|
|
|
+ if (body == null || typeof body !== 'object') {
|
|
|
+ const err = new Error('网络异常,请稍后重试')
|
|
|
+ err.code = 'BAD_RESPONSE'
|
|
|
+ throw err
|
|
|
+ }
|
|
|
+ const code = body.code
|
|
|
+ if (code === 200) {
|
|
|
+ return body.data
|
|
|
+ }
|
|
|
+ const msg =
|
|
|
+ (typeof body.message === 'string' && body.message.trim()) ||
|
|
|
+ '请求失败'
|
|
|
+ const err = new Error(msg)
|
|
|
+ err.code = code
|
|
|
+ err.httpStatus = httpStatus
|
|
|
+ throw err
|
|
|
+}
|
|
|
+
|
|
|
+function joinUrl(path) {
|
|
|
+ const base = (API_BASE_URL || '').replace(/\/$/, '')
|
|
|
+ const p = path.startsWith('/') ? path : `/${path}`
|
|
|
+ return `${base}${p}`
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * @param {{ url: string, method?: string, data?: any, header?: Record<string, string> }} options
|
|
|
+ * @returns {Promise<any>} data 字段
|
|
|
+ */
|
|
|
+export function request(options) {
|
|
|
+ const method = (options.method || 'GET').toUpperCase()
|
|
|
+ const url = joinUrl(options.url)
|
|
|
+ const header = Object.assign(
|
|
|
+ { 'Content-Type': 'application/json' },
|
|
|
+ options.header || {}
|
|
|
+ )
|
|
|
+
|
|
|
+ return new Promise((resolve, reject) => {
|
|
|
+ uni.request({
|
|
|
+ url,
|
|
|
+ method,
|
|
|
+ data: options.data,
|
|
|
+ header,
|
|
|
+ timeout: REQUEST_TIMEOUT_MS,
|
|
|
+ success(res) {
|
|
|
+ try {
|
|
|
+ if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
|
+ resolve(unwrapResponse(res.data, res.statusCode))
|
|
|
+ return
|
|
|
+ }
|
|
|
+ // HTTP 非 2xx:若 body 仍是业务包,优先展示 message
|
|
|
+ try {
|
|
|
+ resolve(unwrapResponse(res.data, res.statusCode))
|
|
|
+ } catch (bizErr) {
|
|
|
+ reject(bizErr)
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ reject(e)
|
|
|
+ }
|
|
|
+ },
|
|
|
+ fail() {
|
|
|
+ const err = new Error('网络异常,请稍后重试')
|
|
|
+ err.code = 'NETWORK'
|
|
|
+ reject(err)
|
|
|
+ }
|
|
|
+ })
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+export function get(url, header) {
|
|
|
+ return request({ url, method: 'GET', header })
|
|
|
+}
|
|
|
+
|
|
|
+export function post(url, data, header) {
|
|
|
+ return request({ url, method: 'POST', data, header })
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: 写入单测**
|
|
|
+
|
|
|
+`mobile-h5/utils/request.test.mjs`:
|
|
|
+
|
|
|
+```js
|
|
|
+import test from 'node:test'
|
|
|
+import assert from 'node:assert/strict'
|
|
|
+import { unwrapResponse } from './request.js'
|
|
|
+
|
|
|
+test('unwrapResponse returns data when code is 200', () => {
|
|
|
+ const data = unwrapResponse({ code: 200, message: 'success', data: { token: 't' } })
|
|
|
+ assert.deepEqual(data, { token: 't' })
|
|
|
+})
|
|
|
+
|
|
|
+test('unwrapResponse throws with backend message when code is not 200', () => {
|
|
|
+ assert.throws(
|
|
|
+ () => unwrapResponse({ code: 401, message: '用户名或密码错误', data: null }),
|
|
|
+ (e) => e instanceof Error && e.message === '用户名或密码错误' && e.code === 401
|
|
|
+ )
|
|
|
+})
|
|
|
+
|
|
|
+test('unwrapResponse throws network message on null body', () => {
|
|
|
+ assert.throws(
|
|
|
+ () => unwrapResponse(null),
|
|
|
+ (e) => e instanceof Error && e.message === '网络异常,请稍后重试'
|
|
|
+ )
|
|
|
+})
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 3: 跑单测**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd /Users/wuzilong/work/00code/zkxw/mobile-h5
|
|
|
+node --test utils/request.test.mjs
|
|
|
+```
|
|
|
+
|
|
|
+Expected: 3 passed。若因 `uni` 未定义导致 `request.js` 加载失败:把 `unwrapResponse` 抽到 `utils/unwrap-response.js` 再测该文件(保持行为不变)。优先保持单文件;仅在加载失败时拆分。
|
|
|
+
|
|
|
+- [ ] **Step 4: Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd /Users/wuzilong/work/00code/zkxw
|
|
|
+git add mobile-h5/utils/request.js mobile-h5/utils/request.test.mjs
|
|
|
+git commit -m "$(cat <<'EOF'
|
|
|
+feat(mobile-h5): add thin uni.request wrapper for API calls
|
|
|
+
|
|
|
+EOF
|
|
|
+)"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 3: 安装 `jsencrypt` + `login-rsa.js`
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `mobile-h5/package.json`
|
|
|
+- Create: `mobile-h5/utils/login-rsa.js`
|
|
|
+
|
|
|
+- [ ] **Step 1: 安装依赖**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd /Users/wuzilong/work/00code/zkxw/mobile-h5
|
|
|
+npm install jsencrypt@^3.3.2 --save
|
|
|
+```
|
|
|
+
|
|
|
+Expected: `package.json` / `package-lock.json` 含 `jsencrypt`;`node_modules/jsencrypt` 存在。
|
|
|
+
|
|
|
+HBuilderX:若项目需「工具 → 插件安装 / 使用 npm 模块」,按既有流程执行一次 npm 构建,确保小程序能 resolve `jsencrypt`。
|
|
|
+
|
|
|
+- [ ] **Step 2: 写入 `login-rsa.js`**
|
|
|
+
|
|
|
+```js
|
|
|
+import JSEncrypt from 'jsencrypt'
|
|
|
+import { get } from './request.js'
|
|
|
+
|
|
|
+/**
|
|
|
+ * 对齐 web-admin `buildWebEncryptedLoginBody`:
|
|
|
+ * 拉公钥后加密 { username, password, ts }。
|
|
|
+ * @returns {Promise<{ encryptedLogin: string }>}
|
|
|
+ */
|
|
|
+export async function buildEncryptedLoginBody(username, password) {
|
|
|
+ const data = await get('/web/auth/login-rsa-public-key')
|
|
|
+ const publicKeyPem = data && data.publicKeyPem
|
|
|
+ if (!publicKeyPem) {
|
|
|
+ throw new Error('获取登录公钥失败')
|
|
|
+ }
|
|
|
+ const crypt = new JSEncrypt()
|
|
|
+ crypt.setPublicKey(publicKeyPem)
|
|
|
+ const payload = JSON.stringify({
|
|
|
+ username,
|
|
|
+ password,
|
|
|
+ ts: Date.now()
|
|
|
+ })
|
|
|
+ const encryptedLogin = crypt.encrypt(payload)
|
|
|
+ if (!encryptedLogin) {
|
|
|
+ throw new Error('登录加密失败,请重试')
|
|
|
+ }
|
|
|
+ return { encryptedLogin }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 3: Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd /Users/wuzilong/work/00code/zkxw
|
|
|
+git add mobile-h5/package.json mobile-h5/package-lock.json mobile-h5/utils/login-rsa.js
|
|
|
+git commit -m "$(cat <<'EOF'
|
|
|
+feat(mobile-h5): add RSA login body builder with jsencrypt
|
|
|
+
|
|
|
+EOF
|
|
|
+)"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 4: `auth.js` 替换 `auth-mock.js`
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Create: `mobile-h5/utils/auth.js`
|
|
|
+- Modify: `mobile-h5/pages/login/index.vue`(仅改 import 与 `onPasswordLogin` 调用名,微信改动放 Task 5)
|
|
|
+- Modify: `mobile-h5/pages/index/index.vue`(import 路径)
|
|
|
+- Delete: `mobile-h5/utils/auth-mock.js`
|
|
|
+
|
|
|
+- [ ] **Step 1: 写入 `auth.js`**
|
|
|
+
|
|
|
+```js
|
|
|
+import { validateCredentials } from './validate-credentials.js'
|
|
|
+import { buildEncryptedLoginBody } from './login-rsa.js'
|
|
|
+import { post } from './request.js'
|
|
|
+
|
|
|
+export { validateCredentials }
|
|
|
+
|
|
|
+const TOKEN_KEY = 'zkxw_mobile_token'
|
|
|
+const REFRESH_TOKEN_KEY = 'zkxw_mobile_refresh_token'
|
|
|
+const USER_KEY = 'zkxw_mobile_user'
|
|
|
+
|
|
|
+export function getToken() {
|
|
|
+ try {
|
|
|
+ return uni.getStorageSync(TOKEN_KEY) || ''
|
|
|
+ } catch (e) {
|
|
|
+ return ''
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+export function getRefreshToken() {
|
|
|
+ try {
|
|
|
+ return uni.getStorageSync(REFRESH_TOKEN_KEY) || ''
|
|
|
+ } catch (e) {
|
|
|
+ return ''
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+export function isLoggedIn() {
|
|
|
+ return !!getToken()
|
|
|
+}
|
|
|
+
|
|
|
+export function getUser() {
|
|
|
+ try {
|
|
|
+ const raw = uni.getStorageSync(USER_KEY)
|
|
|
+ return raw ? JSON.parse(raw) : null
|
|
|
+ } catch (e) {
|
|
|
+ return null
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+export function clearSession() {
|
|
|
+ try {
|
|
|
+ uni.removeStorageSync(TOKEN_KEY)
|
|
|
+ uni.removeStorageSync(REFRESH_TOKEN_KEY)
|
|
|
+ uni.removeStorageSync(USER_KEY)
|
|
|
+ } catch (e) {
|
|
|
+ /* ignore */
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function saveSession({ token, refreshToken, userInfo }) {
|
|
|
+ uni.setStorageSync(TOKEN_KEY, token || '')
|
|
|
+ uni.setStorageSync(REFRESH_TOKEN_KEY, refreshToken || '')
|
|
|
+ uni.setStorageSync(USER_KEY, JSON.stringify(userInfo || null))
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 可选:清除历史 mock_ 前缀假会话,避免误判已登录。
|
|
|
+ */
|
|
|
+export function clearLegacyMockSessionIfNeeded() {
|
|
|
+ const token = getToken()
|
|
|
+ if (typeof token === 'string' && token.indexOf('mock_') === 0) {
|
|
|
+ clearSession()
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 真实账号密码登录。
|
|
|
+ * @param {{ username: string, password: string }} params
|
|
|
+ */
|
|
|
+export async function passwordLogin({ username, password }) {
|
|
|
+ const v = validateCredentials(username, password)
|
|
|
+ if (!v.ok) {
|
|
|
+ const err = new Error(v.message)
|
|
|
+ err.code = v.code
|
|
|
+ throw err
|
|
|
+ }
|
|
|
+ const body = await buildEncryptedLoginBody(
|
|
|
+ (username || '').trim(),
|
|
|
+ password || ''
|
|
|
+ )
|
|
|
+ const data = await post('/web/auth/login', body)
|
|
|
+ if (!data || !data.token) {
|
|
|
+ throw new Error('登录失败')
|
|
|
+ }
|
|
|
+ saveSession({
|
|
|
+ token: data.token,
|
|
|
+ refreshToken: data.refreshToken,
|
|
|
+ userInfo: data.userInfo
|
|
|
+ })
|
|
|
+ return data
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: 更新 `pages/index/index.vue` import**
|
|
|
+
|
|
|
+将:
|
|
|
+
|
|
|
+```js
|
|
|
+import { clearSession, getUser } from '../../utils/auth-mock.js'
|
|
|
+```
|
|
|
+
|
|
|
+改为:
|
|
|
+
|
|
|
+```js
|
|
|
+import { clearSession, getUser } from '../../utils/auth.js'
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 3: 更新 `pages/login/index.vue` 的 script import 与登录调用**
|
|
|
+
|
|
|
+将 import:
|
|
|
+
|
|
|
+```js
|
|
|
+import {
|
|
|
+ isLoggedIn,
|
|
|
+ mockPasswordLogin,
|
|
|
+ mockWechatLogin
|
|
|
+} from '../../utils/auth-mock.js'
|
|
|
+```
|
|
|
+
|
|
|
+改为:
|
|
|
+
|
|
|
+```js
|
|
|
+import {
|
|
|
+ isLoggedIn,
|
|
|
+ passwordLogin,
|
|
|
+ clearLegacyMockSessionIfNeeded
|
|
|
+} from '../../utils/auth.js'
|
|
|
+```
|
|
|
+
|
|
|
+在 `onShow` 开头增加:
|
|
|
+
|
|
|
+```js
|
|
|
+clearLegacyMockSessionIfNeeded()
|
|
|
+```
|
|
|
+
|
|
|
+将 `onPasswordLogin` 内:
|
|
|
+
|
|
|
+```js
|
|
|
+mockPasswordLogin({
|
|
|
+ username: this.username,
|
|
|
+ password: this.password
|
|
|
+})
|
|
|
+```
|
|
|
+
|
|
|
+改为:
|
|
|
+
|
|
|
+```js
|
|
|
+await passwordLogin({
|
|
|
+ username: this.username,
|
|
|
+ password: this.password
|
|
|
+})
|
|
|
+```
|
|
|
+
|
|
|
+(本 Task 暂保留 `onWechatLogin` 方法体;Task 5 再置灰并移除。)
|
|
|
+
|
|
|
+- [ ] **Step 4: 删除 `auth-mock.js`**
|
|
|
+
|
|
|
+```bash
|
|
|
+rm /Users/wuzilong/work/00code/zkxw/mobile-h5/utils/auth-mock.js
|
|
|
+```
|
|
|
+
|
|
|
+确认无残留引用:
|
|
|
+
|
|
|
+```bash
|
|
|
+rg "auth-mock" /Users/wuzilong/work/00code/zkxw/mobile-h5
|
|
|
+```
|
|
|
+
|
|
|
+Expected: 无匹配。
|
|
|
+
|
|
|
+- [ ] **Step 5: Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd /Users/wuzilong/work/00code/zkxw
|
|
|
+git add mobile-h5/utils/auth.js mobile-h5/pages/login/index.vue mobile-h5/pages/index/index.vue
|
|
|
+git add -u mobile-h5/utils/auth-mock.js
|
|
|
+git commit -m "$(cat <<'EOF'
|
|
|
+feat(mobile-h5): replace mock auth with real password login API
|
|
|
+
|
|
|
+EOF
|
|
|
+)"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 5: 微信按钮置灰 + 登录页收尾
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `mobile-h5/styles/components/login.scss`
|
|
|
+- Modify: `mobile-h5/pages/login/index.vue`
|
|
|
+
|
|
|
+- [ ] **Step 1: 增加禁用样式**
|
|
|
+
|
|
|
+在 `login.scss` 的 `.login-wechat` 块后追加:
|
|
|
+
|
|
|
+```scss
|
|
|
+.login-wechat.is-disabled {
|
|
|
+ opacity: 0.45;
|
|
|
+ pointer-events: none;
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: 模板置灰并去掉点击**
|
|
|
+
|
|
|
+将:
|
|
|
+
|
|
|
+```html
|
|
|
+ <view class="login-wechat" @click="onWechatLogin">
|
|
|
+ <image class="login-wechat__icon" src="/static/wechat-logo.png" mode="aspectFit" />
|
|
|
+ <text>微信授权登录</text>
|
|
|
+ </view>
|
|
|
+```
|
|
|
+
|
|
|
+改为:
|
|
|
+
|
|
|
+```html
|
|
|
+ <view class="login-wechat is-disabled">
|
|
|
+ <image class="login-wechat__icon" src="/static/wechat-logo.png" mode="aspectFit" />
|
|
|
+ <text>微信授权登录</text>
|
|
|
+ </view>
|
|
|
+```
|
|
|
+
|
|
|
+删除 `methods` 中的 `onWechatLogin` 整段方法。
|
|
|
+
|
|
|
+- [ ] **Step 3: Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd /Users/wuzilong/work/00code/zkxw
|
|
|
+git add mobile-h5/styles/components/login.scss mobile-h5/pages/login/index.vue
|
|
|
+git commit -m "$(cat <<'EOF'
|
|
|
+feat(mobile-h5): disable WeChat login button until API is ready
|
|
|
+
|
|
|
+EOF
|
|
|
+)"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 6: 联调验收
|
|
|
+
|
|
|
+**前置:** 本地 backend 已启动(如 `./start-local.sh`),`API_BASE_URL` 指向该实例。
|
|
|
+
|
|
|
+- [ ] **Step 1: 公钥接口可达**
|
|
|
+
|
|
|
+```bash
|
|
|
+curl -sS http://127.0.0.1:8000/api/web/auth/login-rsa-public-key | head -c 200
|
|
|
+```
|
|
|
+
|
|
|
+Expected: JSON 含 `"code":200` 与 `publicKeyPem`。
|
|
|
+
|
|
|
+- [ ] **Step 2: 微信开发者工具 / HBuilderX 运行 `mobile-h5`**
|
|
|
+
|
|
|
+1. 开发设置关闭「校验合法域名」(或已配置 request 合法域名)。
|
|
|
+2. 使用后台已有启用且有角色的账号登录 → 应进首页;Storage 中 `zkxw_mobile_token` 为 JWT(非 `mock_` 前缀)。
|
|
|
+3. 错误密码 → Toast「用户名或密码错误」(或后端原文)。
|
|
|
+4. 微信按钮置灰、点击无反应。
|
|
|
+5. 退出登录(若首页有出口)后会话清除,回到登录页。
|
|
|
+
|
|
|
+- [ ] **Step 3: 若联调失败,按清单排查**
|
|
|
+
|
|
|
+| 现象 | 检查 |
|
|
|
+|------|------|
|
|
|
+| 网络异常 | `API_BASE_URL`、后端是否启动、真机是否应用局域网 IP |
|
|
|
+| 登录加密失败 / 凭证过期 | 设备时间、后端 RSA 是否正常 |
|
|
|
+| jsencrypt 找不到模块 | HBuilderX npm 构建;必要时 vendor `jsencrypt.min.js` 到 `utils/vendor/` 并改 import |
|
|
|
+| CORS(仅 H5) | 后端 CORS;小程序端一般无 CORS 问题 |
|
|
|
+
|
|
|
+- [ ] **Step 4:(可选)文档交叉引用**
|
|
|
+
|
|
|
+若方便,在 `docs/superpowers/specs/2026-08-10-miniprogram-login-ui-design.md` 的非目标中注明「账号密码已对接真实 API,见 `2026-08-10-mobile-h5-password-login-api-design.md`」;无则跳过。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Spec coverage checklist
|
|
|
+
|
|
|
+| Spec 项 | Task |
|
|
|
+|---------|------|
|
|
|
+| 复用 login + RSA | 3、4 |
|
|
|
+| `API_BASE_URL` 可改 | 1 |
|
|
|
+| 存 token / refreshToken / userInfo | 4 |
|
|
|
+| 微信置灰不可点 | 5 |
|
|
|
+| 不做 refresh / 微信接口 / 后端改动 | 全计划遵守 |
|
|
|
+| 错误 Toast / 网络文案 | 2、4 |
|
|
|
+| 清除 mock_ 假会话 | 4 `clearLegacyMockSessionIfNeeded` |
|
|
|
+| 验收标准 | 6 |
|