feat(auth): 实现禁用用户拦截机制防止禁用用户访问
- 新增工具函数 isUserDisabled 判断用户是否被禁用 - 新增 blockDisabledUser 函数拦截禁用用户,清除登录态弹窗提示并跳转登录页 - UserContext 启动时异步校验用户状态,禁用则拦截并退出 - UserContext 的 refreshUser、syncFromStorage、loginUser 接口均增加禁用检查 - 登录相关 API 登录前校验用户状态,禁用时拒绝登录 - 登录页面保存登录态前校验,禁用用户弹窗提示阻断登录流程 - 用户状态定义:status 0 为正常,1 为禁用 - 新增用户管理页支持查询和管理用户状态 - 文档中补充用户状态与禁用机制说明及拦截层级介绍
This commit is contained in:
@@ -13,3 +13,13 @@
|
|||||||
- 入口:`src/pages/store/center/index.tsx` FEATURE_CARDS 中,在 VIP会员审核上方加了"用户管理"卡片(蓝色主题 👥 图标)
|
- 入口:`src/pages/store/center/index.tsx` FEATURE_CARDS 中,在 VIP会员审核上方加了"用户管理"卡片(蓝色主题 👥 图标)
|
||||||
- 路由注册:`src/app.config.ts` 新增 `pages/store/users/index`
|
- 路由注册:`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)` 接口控制)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,13 @@
|
|||||||
- 门店店员身份判断:通过 `getMyClerk()` API (`/shop/shop-store-user/my`) 返回值判断
|
- 门店店员身份判断:通过 `getMyClerk()` API (`/shop/shop-store-user/my`) 返回值判断
|
||||||
- VIP/分销商申请记录表:`ShopDealerApply`,状态码 10待审核/20通过/30驳回
|
- 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 会员机制
|
||||||
- 用户在 `vip-upgrade` 页面提交门店名称和地址申请
|
- 用户在 `vip-upgrade` 页面提交门店名称和地址申请
|
||||||
- 门店店员在 `vip-review` 页面审核(通过/驳回)
|
- 门店店员在 `vip-review` 页面审核(通过/驳回)
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import type {
|
|||||||
SmsCaptchaResult
|
SmsCaptchaResult
|
||||||
} from './model';
|
} from './model';
|
||||||
import {saveStorageByLoginUser, SERVER_API_URL} from "@/utils/server";
|
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.code === 0) {
|
||||||
if (res.data?.user && res.data?.access_token) {
|
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)
|
saveStorageByLoginUser(res.data.access_token, res.data.user)
|
||||||
}
|
}
|
||||||
return res.message;
|
return res.message;
|
||||||
@@ -46,6 +50,9 @@ export async function loginBySms(data: LoginParam) {
|
|||||||
);
|
);
|
||||||
if (res.code === 0) {
|
if (res.code === 0) {
|
||||||
if(res.data?.user){
|
if(res.data?.user){
|
||||||
|
if (isUserDisabled(res.data.user)) {
|
||||||
|
return Promise.reject(new Error('您的账号已被禁用,请联系管理员'));
|
||||||
|
}
|
||||||
saveStorageByLoginUser(`${res.data?.access_token}`, res.data?.user)
|
saveStorageByLoginUser(`${res.data?.access_token}`, res.data?.user)
|
||||||
}
|
}
|
||||||
return res.message;
|
return res.message;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import Taro from '@tarojs/taro'
|
|||||||
import type { User } from '@/api/system/user/model'
|
import type { User } from '@/api/system/user/model'
|
||||||
import { getUserInfo } from '@/api/layout'
|
import { getUserInfo } from '@/api/layout'
|
||||||
import { saveStorageByLoginUser, clearStorageByLoginUser } from '@/utils/server'
|
import { saveStorageByLoginUser, clearStorageByLoginUser } from '@/utils/server'
|
||||||
|
import { isUserDisabled, blockDisabledUser } from '@/utils/auth'
|
||||||
|
|
||||||
interface UserContextType {
|
interface UserContextType {
|
||||||
user: User | null
|
user: User | null
|
||||||
@@ -25,11 +26,14 @@ export const UserProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
|
|||||||
const token = Taro.getStorageSync('access_token')
|
const token = Taro.getStorageSync('access_token')
|
||||||
const userData = Taro.getStorageSync('User')
|
const userData = Taro.getStorageSync('User')
|
||||||
|
|
||||||
// 情况1: 有本地token和用户数据,直接使用
|
// 情况1: 有本地token和用户数据,先用缓存快速显示,再异步校验状态
|
||||||
if (token && userData) {
|
if (token && userData) {
|
||||||
const parsed = typeof userData === 'string' ? JSON.parse(userData) : userData
|
const parsed = typeof userData === 'string' ? JSON.parse(userData) : userData
|
||||||
setUser(parsed as User)
|
setUser(parsed as User)
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
|
|
||||||
|
// 异步从服务端校验用户是否被禁用(防止被禁用后仍可使用)
|
||||||
|
verifyUserStatusFromServer()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,6 +41,12 @@ export const UserProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
|
|||||||
if (token) {
|
if (token) {
|
||||||
try {
|
try {
|
||||||
const userInfo = await getUserInfo()
|
const userInfo = await getUserInfo()
|
||||||
|
if (isUserDisabled(userInfo)) {
|
||||||
|
blockDisabledUser(userInfo)
|
||||||
|
setUser(null)
|
||||||
|
setLoading(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
setUser(userInfo)
|
setUser(userInfo)
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
return
|
return
|
||||||
@@ -46,13 +56,38 @@ export const UserProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 无本地token,保持未登录状态(已禁用静默登录)
|
// 无本地token,保持未登录状态
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
init()
|
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) => {
|
const loginUser = (token: string, userInfo: User) => {
|
||||||
|
if (isUserDisabled(userInfo)) {
|
||||||
|
blockDisabledUser(userInfo)
|
||||||
|
return
|
||||||
|
}
|
||||||
saveStorageByLoginUser(token, userInfo)
|
saveStorageByLoginUser(token, userInfo)
|
||||||
setUser(userInfo)
|
setUser(userInfo)
|
||||||
}
|
}
|
||||||
@@ -68,6 +103,12 @@ export const UserProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
|
|||||||
const userData = Taro.getStorageSync('User')
|
const userData = Taro.getStorageSync('User')
|
||||||
if (token && userData) {
|
if (token && userData) {
|
||||||
const parsed = typeof userData === 'string' ? JSON.parse(userData) : userData
|
const parsed = typeof userData === 'string' ? JSON.parse(userData) : userData
|
||||||
|
// 快速检查缓存中的 status,如果已禁用则拦截
|
||||||
|
if (isUserDisabled(parsed)) {
|
||||||
|
blockDisabledUser(parsed)
|
||||||
|
setUser(null)
|
||||||
|
return false
|
||||||
|
}
|
||||||
setUser(parsed as User)
|
setUser(parsed as User)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@@ -81,6 +122,11 @@ export const UserProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
|
|||||||
if (!Taro.getStorageSync('access_token')) return null
|
if (!Taro.getStorageSync('access_token')) return null
|
||||||
try {
|
try {
|
||||||
const userInfo = await getUserInfo()
|
const userInfo = await getUserInfo()
|
||||||
|
if (isUserDisabled(userInfo)) {
|
||||||
|
blockDisabledUser(userInfo)
|
||||||
|
setUser(null)
|
||||||
|
return null
|
||||||
|
}
|
||||||
const token = Taro.getStorageSync('access_token')
|
const token = Taro.getStorageSync('access_token')
|
||||||
if (token) {
|
if (token) {
|
||||||
saveStorageByLoginUser(token, userInfo)
|
saveStorageByLoginUser(token, userInfo)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import Taro from '@tarojs/taro'
|
|||||||
import { View, Text, Input, Image } from '@tarojs/components'
|
import { View, Text, Input, Image } from '@tarojs/components'
|
||||||
import { storeLogin } from '@/api/shop/shopStore'
|
import { storeLogin } from '@/api/shop/shopStore'
|
||||||
import { saveStorageByLoginUser } from '@/utils/server'
|
import { saveStorageByLoginUser } from '@/utils/server'
|
||||||
|
import { isUserDisabled } from '@/utils/auth'
|
||||||
import './index.scss'
|
import './index.scss'
|
||||||
|
|
||||||
const StoreLogin = () => {
|
const StoreLogin = () => {
|
||||||
@@ -55,6 +56,19 @@ const StoreLogin = () => {
|
|||||||
if (res?.access_token) {
|
if (res?.access_token) {
|
||||||
const token = res.access_token
|
const token = res.access_token
|
||||||
const user = res.user
|
const user = res.user
|
||||||
|
|
||||||
|
// 检查用户是否被禁用
|
||||||
|
if (isUserDisabled(user)) {
|
||||||
|
Taro.showModal({
|
||||||
|
title: '账号已被禁用',
|
||||||
|
content: '您的账号已被管理员禁用,暂时无法使用。如有疑问请联系管理员。',
|
||||||
|
showCancel: false,
|
||||||
|
confirmText: '我知道了',
|
||||||
|
confirmColor: '#ee0a24',
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
saveStorageByLoginUser(token, user)
|
saveStorageByLoginUser(token, user)
|
||||||
Taro.showToast({ title: '登录成功', icon: 'success' })
|
Taro.showToast({ title: '登录成功', icon: 'success' })
|
||||||
setTimeout(() => navigateAfterLogin(), 800)
|
setTimeout(() => navigateAfterLogin(), 800)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { TenantId } from '@/config/app'
|
|||||||
import { getWxOpenId } from '@/api/layout'
|
import { getWxOpenId } from '@/api/layout'
|
||||||
import { getUserInfo } from '@/api/layout'
|
import { getUserInfo } from '@/api/layout'
|
||||||
import { saveStorageByLoginUser } from '@/utils/server'
|
import { saveStorageByLoginUser } from '@/utils/server'
|
||||||
|
import { isUserDisabled } from '@/utils/auth'
|
||||||
import {
|
import {
|
||||||
checkAndHandleInviteRelation,
|
checkAndHandleInviteRelation,
|
||||||
hasPendingInvite,
|
hasPendingInvite,
|
||||||
@@ -201,6 +202,18 @@ const Login = () => {
|
|||||||
user = freshUserInfo
|
user = freshUserInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检查用户是否被禁用
|
||||||
|
if (isUserDisabled(user)) {
|
||||||
|
Taro.showModal({
|
||||||
|
title: '账号已被禁用',
|
||||||
|
content: '您的账号已被管理员禁用,暂时无法使用小程序。如有疑问请联系门店客服。',
|
||||||
|
showCancel: false,
|
||||||
|
confirmText: '我知道了',
|
||||||
|
confirmColor: '#ee0a24',
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
saveStorageByLoginUser(token, user)
|
saveStorageByLoginUser(token, user)
|
||||||
|
|
||||||
// 绑定 openid + 处理邀请关系
|
// 绑定 openid + 处理邀请关系
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { Button, Checkbox } from '@nutui/nutui-react-taro'
|
|||||||
import { TenantId } from '@/config/app'
|
import { TenantId } from '@/config/app'
|
||||||
import { getUserInfo, getWxOpenId } from '@/api/layout'
|
import { getUserInfo, getWxOpenId } from '@/api/layout'
|
||||||
import { saveStorageByLoginUser } from '@/utils/server'
|
import { saveStorageByLoginUser } from '@/utils/server'
|
||||||
|
import { isUserDisabled } from '@/utils/auth'
|
||||||
import {
|
import {
|
||||||
getStoredInviteParams,
|
getStoredInviteParams,
|
||||||
parseInviteParams,
|
parseInviteParams,
|
||||||
@@ -202,6 +203,18 @@ const Register = () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检查用户是否被禁用
|
||||||
|
if (isUserDisabled(user)) {
|
||||||
|
Taro.showModal({
|
||||||
|
title: '账号已被禁用',
|
||||||
|
content: '您的账号已被管理员禁用,暂时无法使用小程序。如有疑问请联系门店客服。',
|
||||||
|
showCancel: false,
|
||||||
|
confirmText: '我知道了',
|
||||||
|
confirmColor: '#ee0a24',
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
saveStorageByLoginUser(token, user)
|
saveStorageByLoginUser(token, user)
|
||||||
|
|
||||||
// 注册/登录成功后,立即补齐 openid(JSAPI 支付必需)
|
// 注册/登录成功后,立即补齐 openid(JSAPI 支付必需)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import Taro from '@tarojs/taro'
|
import Taro from '@tarojs/taro'
|
||||||
import { clearStorageByLoginUser } from '@/utils/server'
|
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() {
|
export function logout() {
|
||||||
clearStorageByLoginUser()
|
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
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user