From c870e75b3bed858a5c319581eee9d5e617a712a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E5=BF=A0=E6=9E=97?= <170083662@qq.com> Date: Tue, 14 Jul 2026 23:11:29 +0800 Subject: [PATCH] =?UTF-8?q?feat(auth):=20=E5=AE=9E=E7=8E=B0=E7=A6=81?= =?UTF-8?q?=E7=94=A8=E7=94=A8=E6=88=B7=E6=8B=A6=E6=88=AA=E6=9C=BA=E5=88=B6?= =?UTF-8?q?=E9=98=B2=E6=AD=A2=E7=A6=81=E7=94=A8=E7=94=A8=E6=88=B7=E8=AE=BF?= =?UTF-8?q?=E9=97=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增工具函数 isUserDisabled 判断用户是否被禁用 - 新增 blockDisabledUser 函数拦截禁用用户,清除登录态弹窗提示并跳转登录页 - UserContext 启动时异步校验用户状态,禁用则拦截并退出 - UserContext 的 refreshUser、syncFromStorage、loginUser 接口均增加禁用检查 - 登录相关 API 登录前校验用户状态,禁用时拒绝登录 - 登录页面保存登录态前校验,禁用用户弹窗提示阻断登录流程 - 用户状态定义:status 0 为正常,1 为禁用 - 新增用户管理页支持查询和管理用户状态 - 文档中补充用户状态与禁用机制说明及拦截层级介绍 --- .workbuddy/memory/2026-07-14.md | 10 +++++++ .workbuddy/memory/MEMORY.md | 7 +++++ src/api/passport/login/index.ts | 7 +++++ src/contexts/UserContext.tsx | 50 +++++++++++++++++++++++++++++++-- src/pages/store/login/index.tsx | 14 +++++++++ src/passport/login.tsx | 13 +++++++++ src/passport/register.tsx | 13 +++++++++ src/utils/auth.ts | 29 +++++++++++++++++++ 8 files changed, 141 insertions(+), 2 deletions(-) diff --git a/.workbuddy/memory/2026-07-14.md b/.workbuddy/memory/2026-07-14.md index dcbec2c..3e33bd2 100644 --- a/.workbuddy/memory/2026-07-14.md +++ b/.workbuddy/memory/2026-07-14.md @@ -13,3 +13,13 @@ - 入口:`src/pages/store/center/index.tsx` FEATURE_CARDS 中,在 VIP会员审核上方加了"用户管理"卡片(蓝色主题 👥 图标) - 路由注册:`src/app.config.ts` 新增 `pages/store/users/index` +## 禁用用户拦截(防止被禁用用户仍可浏览小程序) +- 新增工具函数:`src/utils/auth.ts` 中 `isUserDisabled(user)` 和 `blockDisabledUser(user)`(清登录态+弹窗+跳转登录页) +- 拦截层级: + 1. **UserContext 启动校验**(`src/contexts/UserContext.tsx`):init 时先用缓存快速显示,再异步调 `getUserInfo()` 校验 status;若被禁用则 `blockDisabledUser` 拦截 + 2. **UserContext refreshUser / syncFromStorage / loginUser**:均加了 `isUserDisabled` 检查 + 3. **登录 API**(`src/api/passport/login/index.ts`):`login()` 和 `loginBySms()` 在 `saveStorageByLoginUser` 前检查 status,被禁用则 reject + 4. **登录页面**:`passport/login.tsx`、`passport/register.tsx`、`pages/store/login/index.tsx` 在保存登录态前检查 status,被禁用则弹窗拦截 +- 用户状态约定:`status === 0` 正常,`status === 1` 禁用(通过 `updateUserStatus(userId, status)` 接口控制) + + diff --git a/.workbuddy/memory/MEMORY.md b/.workbuddy/memory/MEMORY.md index 858e7a6..1390067 100644 --- a/.workbuddy/memory/MEMORY.md +++ b/.workbuddy/memory/MEMORY.md @@ -7,6 +7,13 @@ - 门店店员身份判断:通过 `getMyClerk()` API (`/shop/shop-store-user/my`) 返回值判断 - VIP/分销商申请记录表:`ShopDealerApply`,状态码 10待审核/20通过/30驳回 +## 用户状态与禁用机制 +- 用户状态字段:`User.status`,`0` = 正常,`1` = 禁用 +- 禁用接口:`updateUserStatus(userId, status)` → PUT `/system/user/status` +- 禁用拦截工具:`src/utils/auth.ts` 中 `isUserDisabled(user)` 和 `blockDisabledUser(user)` +- 拦截层级:UserContext 启动异步校验 + refreshUser/syncFromStorage/loginUser 检查 + 登录 API 和登录页检查 +- 用户管理页:`src/pages/store/users/index.tsx`(门店中心入口,支持搜索/筛选/查看详情/禁用启用) + ## VIP 会员机制 - 用户在 `vip-upgrade` 页面提交门店名称和地址申请 - 门店店员在 `vip-review` 页面审核(通过/驳回) diff --git a/src/api/passport/login/index.ts b/src/api/passport/login/index.ts index 0104c73..f86f4b5 100644 --- a/src/api/passport/login/index.ts +++ b/src/api/passport/login/index.ts @@ -7,6 +7,7 @@ import type { SmsCaptchaResult } from './model'; import {saveStorageByLoginUser, SERVER_API_URL} from "@/utils/server"; +import { isUserDisabled } from '@/utils/auth'; /** * 账号密码登录 @@ -18,6 +19,9 @@ export async function login(data: LoginParam) { ); if (res.code === 0) { if (res.data?.user && res.data?.access_token) { + if (isUserDisabled(res.data.user)) { + return Promise.reject(new Error('您的账号已被禁用,请联系管理员')); + } saveStorageByLoginUser(res.data.access_token, res.data.user) } return res.message; @@ -46,6 +50,9 @@ export async function loginBySms(data: LoginParam) { ); if (res.code === 0) { if(res.data?.user){ + if (isUserDisabled(res.data.user)) { + return Promise.reject(new Error('您的账号已被禁用,请联系管理员')); + } saveStorageByLoginUser(`${res.data?.access_token}`, res.data?.user) } return res.message; diff --git a/src/contexts/UserContext.tsx b/src/contexts/UserContext.tsx index 0635f1e..19beedb 100644 --- a/src/contexts/UserContext.tsx +++ b/src/contexts/UserContext.tsx @@ -3,6 +3,7 @@ import Taro from '@tarojs/taro' import type { User } from '@/api/system/user/model' import { getUserInfo } from '@/api/layout' import { saveStorageByLoginUser, clearStorageByLoginUser } from '@/utils/server' +import { isUserDisabled, blockDisabledUser } from '@/utils/auth' interface UserContextType { user: User | null @@ -25,11 +26,14 @@ export const UserProvider: React.FC<{ children: ReactNode }> = ({ children }) => const token = Taro.getStorageSync('access_token') const userData = Taro.getStorageSync('User') - // 情况1: 有本地token和用户数据,直接使用 + // 情况1: 有本地token和用户数据,先用缓存快速显示,再异步校验状态 if (token && userData) { const parsed = typeof userData === 'string' ? JSON.parse(userData) : userData setUser(parsed as User) setLoading(false) + + // 异步从服务端校验用户是否被禁用(防止被禁用后仍可使用) + verifyUserStatusFromServer() return } @@ -37,6 +41,12 @@ export const UserProvider: React.FC<{ children: ReactNode }> = ({ children }) => if (token) { try { const userInfo = await getUserInfo() + if (isUserDisabled(userInfo)) { + blockDisabledUser(userInfo) + setUser(null) + setLoading(false) + return + } setUser(userInfo) setLoading(false) return @@ -46,13 +56,38 @@ export const UserProvider: React.FC<{ children: ReactNode }> = ({ children }) => } } - // 无本地token,保持未登录状态(已禁用静默登录) + // 无本地token,保持未登录状态 setLoading(false) } init() }, []) + /** 异步从服务端拉取最新用户信息,校验是否被禁用 */ + const verifyUserStatusFromServer = async () => { + try { + const userInfo = await getUserInfo() + if (isUserDisabled(userInfo)) { + // 用户已被禁用,拦截 + blockDisabledUser(userInfo) + setUser(null) + } else { + // 状态正常,更新本地缓存和状态 + const token = Taro.getStorageSync('access_token') + if (token) { + saveStorageByLoginUser(token, userInfo) + } + setUser(userInfo) + } + } catch { + // 网络错误或token失效,静默处理(已有缓存的登录态可用) + } + } + const loginUser = (token: string, userInfo: User) => { + if (isUserDisabled(userInfo)) { + blockDisabledUser(userInfo) + return + } saveStorageByLoginUser(token, userInfo) setUser(userInfo) } @@ -68,6 +103,12 @@ export const UserProvider: React.FC<{ children: ReactNode }> = ({ children }) => const userData = Taro.getStorageSync('User') if (token && userData) { const parsed = typeof userData === 'string' ? JSON.parse(userData) : userData + // 快速检查缓存中的 status,如果已禁用则拦截 + if (isUserDisabled(parsed)) { + blockDisabledUser(parsed) + setUser(null) + return false + } setUser(parsed as User) return true } @@ -81,6 +122,11 @@ export const UserProvider: React.FC<{ children: ReactNode }> = ({ children }) => if (!Taro.getStorageSync('access_token')) return null try { const userInfo = await getUserInfo() + if (isUserDisabled(userInfo)) { + blockDisabledUser(userInfo) + setUser(null) + return null + } const token = Taro.getStorageSync('access_token') if (token) { saveStorageByLoginUser(token, userInfo) diff --git a/src/pages/store/login/index.tsx b/src/pages/store/login/index.tsx index 7e2b8bc..3bfcefd 100644 --- a/src/pages/store/login/index.tsx +++ b/src/pages/store/login/index.tsx @@ -3,6 +3,7 @@ import Taro from '@tarojs/taro' import { View, Text, Input, Image } from '@tarojs/components' import { storeLogin } from '@/api/shop/shopStore' import { saveStorageByLoginUser } from '@/utils/server' +import { isUserDisabled } from '@/utils/auth' import './index.scss' const StoreLogin = () => { @@ -55,6 +56,19 @@ const StoreLogin = () => { if (res?.access_token) { const token = res.access_token const user = res.user + + // 检查用户是否被禁用 + if (isUserDisabled(user)) { + Taro.showModal({ + title: '账号已被禁用', + content: '您的账号已被管理员禁用,暂时无法使用。如有疑问请联系管理员。', + showCancel: false, + confirmText: '我知道了', + confirmColor: '#ee0a24', + }) + return + } + saveStorageByLoginUser(token, user) Taro.showToast({ title: '登录成功', icon: 'success' }) setTimeout(() => navigateAfterLogin(), 800) diff --git a/src/passport/login.tsx b/src/passport/login.tsx index 043bb3f..dd0b217 100644 --- a/src/passport/login.tsx +++ b/src/passport/login.tsx @@ -5,6 +5,7 @@ import { TenantId } from '@/config/app' import { getWxOpenId } from '@/api/layout' import { getUserInfo } from '@/api/layout' import { saveStorageByLoginUser } from '@/utils/server' +import { isUserDisabled } from '@/utils/auth' import { checkAndHandleInviteRelation, hasPendingInvite, @@ -201,6 +202,18 @@ const Login = () => { user = freshUserInfo } + // 检查用户是否被禁用 + if (isUserDisabled(user)) { + Taro.showModal({ + title: '账号已被禁用', + content: '您的账号已被管理员禁用,暂时无法使用小程序。如有疑问请联系门店客服。', + showCancel: false, + confirmText: '我知道了', + confirmColor: '#ee0a24', + }) + return + } + saveStorageByLoginUser(token, user) // 绑定 openid + 处理邀请关系 diff --git a/src/passport/register.tsx b/src/passport/register.tsx index 2397fca..2c660d0 100644 --- a/src/passport/register.tsx +++ b/src/passport/register.tsx @@ -5,6 +5,7 @@ import { Button, Checkbox } from '@nutui/nutui-react-taro' import { TenantId } from '@/config/app' import { getUserInfo, getWxOpenId } from '@/api/layout' import { saveStorageByLoginUser } from '@/utils/server' +import { isUserDisabled } from '@/utils/auth' import { getStoredInviteParams, parseInviteParams, @@ -202,6 +203,18 @@ const Register = () => { return } + // 检查用户是否被禁用 + if (isUserDisabled(user)) { + Taro.showModal({ + title: '账号已被禁用', + content: '您的账号已被管理员禁用,暂时无法使用小程序。如有疑问请联系门店客服。', + showCancel: false, + confirmText: '我知道了', + confirmColor: '#ee0a24', + }) + return + } + saveStorageByLoginUser(token, user) // 注册/登录成功后,立即补齐 openid(JSAPI 支付必需) diff --git a/src/utils/auth.ts b/src/utils/auth.ts index f67d59c..cebbaa1 100644 --- a/src/utils/auth.ts +++ b/src/utils/auth.ts @@ -1,5 +1,6 @@ import Taro from '@tarojs/taro' import { clearStorageByLoginUser } from '@/utils/server' +import type { User } from '@/api/system/user/model' /** * 登录态 / 游客态 标识。 @@ -34,3 +35,31 @@ export function ensureLoggedIn(redirect?: string): boolean { export function logout() { clearStorageByLoginUser() } + +/** + * 判断用户是否被禁用(status === 1) + */ +export function isUserDisabled(user?: User | null): boolean { + return user?.status === 1 +} + +/** + * 拦截被禁用的用户:清除登录态、弹窗提示、跳转登录页。 + * 返回 false 表示用户已被禁用并已处理,调用方应中止后续流程。 + */ +export function blockDisabledUser(user?: User | null): boolean { + if (!isUserDisabled(user)) return true + + clearStorageByLoginUser() + Taro.showModal({ + title: '账号已被禁用', + content: '您的账号已被管理员禁用,暂时无法使用小程序。如有疑问请联系门店客服。', + showCancel: false, + confirmText: '我知道了', + confirmColor: '#ee0a24', + success: () => { + Taro.reLaunch({ url: '/passport/login' }) + }, + }) + return false +}