feat(auth): 移除多角色体验账号直登,仅保留微信手机号授权登录
- 登录页删除体验账号卡片、切换按钮与全部 demo 登录/失败回退逻辑,微信授权失败只提示重试 - completeLogin 只接受服务端签发的 token,禁止无 token 的假登录 - 删除 /api/auth/demo-accounts、/api/auth/demo-login、loadDemoAccounts 与客户端 API - 停止种子 demo_accounts 配置并清理历史配置行;117 套件/763 用例全绿;线上接口已 404
这个提交包含在:
@@ -1572,22 +1572,3 @@ export async function loginWithWechatPhone({ phoneCode, wxCode } = {}) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 体验账号列表(后台配置,DB 驱动;小程序端不再写死)
|
||||
*/
|
||||
export async function getDemoAccounts() {
|
||||
try {
|
||||
const res = await http.get('/auth/demo-accounts');
|
||||
return Array.isArray(res) ? res : [];
|
||||
} catch (err) {
|
||||
console.warn('[API] 获取体验账号异常:', err.message);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 体验登录:服务端校验账号在配置清单内后签发真实会话 token
|
||||
*/
|
||||
export async function demoLogin(userId) {
|
||||
return http.post('/auth/demo-login', { userId: Number(userId) });
|
||||
}
|
||||
|
||||
@@ -22,33 +22,6 @@
|
||||
<text class="auth-guarantee-tip">微信官方安全风控 · 个人信息严格加密存储</text>
|
||||
</view>
|
||||
|
||||
<!-- 多角色体验账号矩阵 (局长/买家/金牌大神) -->
|
||||
<view class="demo-accounts-card" v-if="demoAccounts.length > 0">
|
||||
<view class="demo-card-head">
|
||||
<text class="demo-title">多角色体验账号直登</text>
|
||||
<text class="demo-sub">评审与体验专用 · 一键切换角色</text>
|
||||
</view>
|
||||
|
||||
<view class="demo-list">
|
||||
<view
|
||||
v-for="acc in demoAccounts"
|
||||
:key="acc.userId"
|
||||
class="demo-item btn-active"
|
||||
@click="loginWithDemo(acc)"
|
||||
>
|
||||
<image class="demo-avatar" :src="acc.avatar || DEFAULT_AVATAR" mode="aspectFill" />
|
||||
<view class="demo-info">
|
||||
<view class="demo-name-row">
|
||||
<text class="demo-name">{{ acc.nickname }}</text>
|
||||
<text class="demo-badge" :class="acc.roleTag">{{ acc.roleDesc }}</text>
|
||||
</view>
|
||||
<text class="demo-feature">{{ acc.desc }}</text>
|
||||
</view>
|
||||
<button class="btn-switch-account btn-active">一键体验</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部合规声明与服务协议勾选 (单行紧凑排列) -->
|
||||
<view class="protocol-section" @click="agreedProtocol = !agreedProtocol">
|
||||
<view class="checkbox" :class="{ checked: agreedProtocol }">
|
||||
@@ -69,7 +42,7 @@ import { ref, onMounted } from 'vue';
|
||||
import { onLoad, onShow } from '@dcloudio/uni-app';
|
||||
import { ICONS } from '@/common/icons.js';
|
||||
import { getCurrentThemeKey, applyThemeToNativeUI } from '@/common/theme.js';
|
||||
import { getPlatformConfigs, loginWithWechatPhone, getDemoAccounts, demoLogin } from '@/common/api.js';
|
||||
import { getPlatformConfigs, loginWithWechatPhone } from '@/common/api.js';
|
||||
|
||||
const APP_LOGO = 'https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?w=200';
|
||||
const DEFAULT_AVATAR = 'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=200';
|
||||
@@ -107,19 +80,6 @@ const loadProtocolConfig = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const demoAccounts = ref([]);
|
||||
const loadingDemoAccounts = ref(false);
|
||||
|
||||
const loadDemoAccounts = async () => {
|
||||
if (loadingDemoAccounts.value) return;
|
||||
loadingDemoAccounts.value = true;
|
||||
try {
|
||||
demoAccounts.value = await getDemoAccounts();
|
||||
} finally {
|
||||
loadingDemoAccounts.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
onLoad((options) => {
|
||||
if (options && options.redirect) {
|
||||
try {
|
||||
@@ -134,7 +94,6 @@ onMounted(() => {
|
||||
currentThemeKey.value = getCurrentThemeKey();
|
||||
applyThemeToNativeUI(currentThemeKey.value);
|
||||
loadProtocolConfig();
|
||||
loadDemoAccounts();
|
||||
});
|
||||
|
||||
onShow(() => {
|
||||
@@ -157,15 +116,14 @@ const completeLogin = (userId, customData = {}, sessionToken = '') => {
|
||||
|
||||
setTimeout(() => {
|
||||
uni.hideLoading();
|
||||
// 仅接受服务端签发的 token,避免出现"旧 token 属主 + 新 userId"的身份错配
|
||||
if (!sessionToken) {
|
||||
uni.showToast({ title: '登录凭证缺失,请重试', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
uni.removeStorageSync('user_has_logged_out');
|
||||
uni.setStorageSync('userId', userId);
|
||||
// userId 与 token 必须保持一致:无 token 的体验登录要清掉旧 token,
|
||||
// 否则会出现"旧 token 属主 + 新 userId"→ 服务端判定以他人身份操作(403)
|
||||
if (sessionToken) {
|
||||
uni.setStorageSync('userToken', sessionToken);
|
||||
} else {
|
||||
uni.removeStorageSync('userToken');
|
||||
}
|
||||
uni.setStorageSync('userToken', sessionToken);
|
||||
uni.setStorageSync('userInfo', {
|
||||
id: userId,
|
||||
nickname: customData.nickname || '微信用户',
|
||||
@@ -220,82 +178,75 @@ const handleGetPhoneNumber = async (e) => {
|
||||
}
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
if (e && e.detail && (e.detail.code || e.detail.encryptedData)) {
|
||||
if (e.detail.code) {
|
||||
uni.showLoading({ title: '正在获取手机号...' });
|
||||
try {
|
||||
let wxCode = '';
|
||||
try {
|
||||
const loginRes = await new Promise((resolve, reject) => {
|
||||
uni.login({
|
||||
provider: 'weixin',
|
||||
success: (res) => resolve(res),
|
||||
fail: (err) => reject(err)
|
||||
});
|
||||
});
|
||||
wxCode = loginRes.code || '';
|
||||
} catch (_) {}
|
||||
|
||||
const res = await loginWithWechatPhone({
|
||||
phoneCode: e.detail.code,
|
||||
wxCode
|
||||
});
|
||||
|
||||
uni.hideLoading();
|
||||
if (res && (res.user || res.userId)) {
|
||||
const userObj = res.user || { id: res.userId, phone: res.phone };
|
||||
const sessionToken = res.userToken || res.token || '';
|
||||
completeLogin(userObj.id, userObj, sessionToken);
|
||||
return;
|
||||
}
|
||||
} catch (err) {
|
||||
uni.hideLoading();
|
||||
console.warn('[WeChat Login] 微信手机号解析未成功:', err);
|
||||
if (err.message && err.message.includes('封禁')) {
|
||||
uni.showModal({
|
||||
title: '账号已封禁',
|
||||
content: err.message,
|
||||
showCancel: false,
|
||||
confirmText: '我知道了'
|
||||
});
|
||||
return;
|
||||
}
|
||||
uni.showModal({
|
||||
title: '登录提示',
|
||||
content: `${err.message || '微信手机号解析未成功'},是否使用评审体验账号快捷登录?`,
|
||||
confirmText: '快捷登录',
|
||||
cancelText: '取消',
|
||||
confirmColor: '#10B981',
|
||||
success: (mRes) => {
|
||||
if (mRes.confirm) {
|
||||
loginFirstDemoAccount();
|
||||
}
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
loginFirstDemoAccount();
|
||||
} else {
|
||||
if (!e || !e.detail || !(e.detail.code || e.detail.encryptedData)) {
|
||||
console.warn('[WeChat Login] 未授权手机号或环境受限:', e?.detail?.errMsg);
|
||||
uni.showModal({
|
||||
title: '登录提示',
|
||||
content: '未获得微信手机号授权(需非个人主体认证小程序支持)。是否使用体验账号直接登录?',
|
||||
confirmText: '快捷登录',
|
||||
cancelText: '取消',
|
||||
confirmColor: '#10B981',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
loginFirstDemoAccount();
|
||||
}
|
||||
}
|
||||
content: '需要授权微信手机号才能登录。请点击“一键授权快捷登录”,并允许获取手机号后重试。',
|
||||
showCancel: false,
|
||||
confirmText: '我知道了'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!e.detail.code) {
|
||||
uni.showModal({
|
||||
title: '登录提示',
|
||||
content: '当前微信版本暂不支持手机号快捷登录,请升级微信后重试。',
|
||||
showCancel: false,
|
||||
confirmText: '我知道了'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
uni.showLoading({ title: '正在获取手机号...' });
|
||||
try {
|
||||
let wxCode = '';
|
||||
try {
|
||||
const loginRes = await new Promise((resolve, reject) => {
|
||||
uni.login({
|
||||
provider: 'weixin',
|
||||
success: (res) => resolve(res),
|
||||
fail: (err) => reject(err)
|
||||
});
|
||||
});
|
||||
wxCode = loginRes.code || '';
|
||||
} catch (_) {}
|
||||
|
||||
const res = await loginWithWechatPhone({
|
||||
phoneCode: e.detail.code,
|
||||
wxCode
|
||||
});
|
||||
|
||||
uni.hideLoading();
|
||||
if (res && (res.user || res.userId)) {
|
||||
const userObj = res.user || { id: res.userId, phone: res.phone };
|
||||
const sessionToken = res.userToken || res.token || '';
|
||||
completeLogin(userObj.id, userObj, sessionToken);
|
||||
}
|
||||
} catch (err) {
|
||||
uni.hideLoading();
|
||||
console.warn('[WeChat Login] 微信手机号解析未成功:', err);
|
||||
if (err.message && err.message.includes('封禁')) {
|
||||
uni.showModal({
|
||||
title: '账号已封禁',
|
||||
content: err.message,
|
||||
showCancel: false,
|
||||
confirmText: '我知道了'
|
||||
});
|
||||
return;
|
||||
}
|
||||
uni.showModal({
|
||||
title: '登录失败',
|
||||
content: `${err.message || '微信手机号解析未成功'},请稍后重试。`,
|
||||
showCancel: false,
|
||||
confirmText: '我知道了'
|
||||
});
|
||||
}
|
||||
// #endif
|
||||
|
||||
// #ifndef MP-WEIXIN
|
||||
loginFirstDemoAccount();
|
||||
uni.showToast({ title: '请在微信小程序内使用手机号快捷登录', icon: 'none' });
|
||||
// #endif
|
||||
};
|
||||
|
||||
@@ -305,36 +256,10 @@ const handleFallbackLogin = () => {
|
||||
uni.showToast({ title: '请先阅读并勾选服务协议', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
loginFirstDemoAccount();
|
||||
uni.showToast({ title: '请在微信小程序内使用手机号快捷登录', icon: 'none' });
|
||||
// #endif
|
||||
};
|
||||
|
||||
const loginWithDemo = async (acc) => {
|
||||
if (!agreedProtocol.value) {
|
||||
uni.showToast({ title: '请先阅读并勾选服务协议', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
uni.showLoading({ title: '正在登录...' });
|
||||
try {
|
||||
const res = await demoLogin(acc.userId);
|
||||
uni.hideLoading();
|
||||
completeLogin(res.userId, res.user || {}, res.userToken || '');
|
||||
} catch (e) {
|
||||
uni.hideLoading();
|
||||
uni.showToast({ title: (e && e.message) || '体验登录失败', icon: 'none' });
|
||||
}
|
||||
};
|
||||
|
||||
const loginFirstDemoAccount = async () => {
|
||||
if (!demoAccounts.value.length) await loadDemoAccounts();
|
||||
const first = demoAccounts.value[0];
|
||||
if (!first) {
|
||||
uni.showToast({ title: '体验账号未配置,请联系管理员', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
await loginWithDemo(first);
|
||||
};
|
||||
|
||||
const openUserAgreement = () => {
|
||||
uni.navigateTo({
|
||||
url: '/pages/common/protocol?type=service',
|
||||
@@ -457,115 +382,6 @@ const openPrivacyAgreement = () => {
|
||||
}
|
||||
}
|
||||
|
||||
/* 体验账号卡片 */
|
||||
.demo-accounts-card {
|
||||
background: #151D30;
|
||||
border-radius: 28rpx;
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
padding: 28rpx 28rpx 16rpx;
|
||||
margin-bottom: 40rpx;
|
||||
|
||||
.demo-card-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20rpx;
|
||||
|
||||
.demo-title {
|
||||
font-size: 26rpx;
|
||||
font-weight: 700;
|
||||
color: #F8FAFC;
|
||||
}
|
||||
.demo-sub {
|
||||
font-size: 20rpx;
|
||||
color: #64748B;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16rpx;
|
||||
|
||||
.demo-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 18rpx;
|
||||
padding: 16rpx 20rpx;
|
||||
border-radius: 20rpx;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
|
||||
.demo-avatar {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
border-radius: 36rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.demo-info {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
|
||||
.demo-name-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
margin-bottom: 4rpx;
|
||||
|
||||
.demo-name {
|
||||
font-size: 26rpx;
|
||||
font-weight: 700;
|
||||
color: #F8FAFC;
|
||||
}
|
||||
|
||||
.demo-badge {
|
||||
font-size: 18rpx;
|
||||
padding: 2rpx 10rpx;
|
||||
border-radius: 6rpx;
|
||||
font-weight: 600;
|
||||
|
||||
&.host {
|
||||
background: rgba(245, 158, 11, 0.2);
|
||||
color: #F59E0B;
|
||||
}
|
||||
&.buyer {
|
||||
background: rgba(99, 102, 241, 0.2);
|
||||
color: #818CF8;
|
||||
}
|
||||
&.expert {
|
||||
background: rgba(16, 185, 129, 0.2);
|
||||
color: #10B981;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.demo-feature {
|
||||
font-size: 20rpx;
|
||||
color: #94A3B8;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-switch-account {
|
||||
font-size: 22rpx;
|
||||
color: #6366F1;
|
||||
font-weight: 600;
|
||||
padding: 8rpx 18rpx;
|
||||
border-radius: 12rpx;
|
||||
background: rgba(99, 102, 241, 0.15);
|
||||
border: none;
|
||||
margin: 0;
|
||||
line-height: 1.4;
|
||||
|
||||
&::after {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 协议勾选 (一行紧凑完整展示) */
|
||||
.protocol-section {
|
||||
display: flex;
|
||||
@@ -636,39 +452,6 @@ const openPrivacyAgreement = () => {
|
||||
}
|
||||
}
|
||||
|
||||
.demo-accounts-card {
|
||||
background: #FFFFFF;
|
||||
border-color: #E7E5E4;
|
||||
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.06);
|
||||
|
||||
.demo-card-head {
|
||||
.demo-title {
|
||||
color: #1C1917;
|
||||
}
|
||||
.demo-sub {
|
||||
color: #A8A29E;
|
||||
}
|
||||
}
|
||||
|
||||
.demo-list .demo-item {
|
||||
background: #FDFBF7;
|
||||
border-color: #E7E5E4;
|
||||
|
||||
.demo-info .demo-name-row .demo-name {
|
||||
color: #1C1917;
|
||||
}
|
||||
|
||||
.demo-info .demo-feature {
|
||||
color: #78716C;
|
||||
}
|
||||
|
||||
.btn-switch-account {
|
||||
background: #FFF7ED;
|
||||
color: #EA580C;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.protocol-section {
|
||||
.checkbox {
|
||||
border-color: #D6D3D1;
|
||||
|
||||
@@ -1352,22 +1352,9 @@ async function seedInitialDataIfEmpty(conn: any): Promise<void> {
|
||||
`, [mapLocationConfigVal]);
|
||||
}
|
||||
|
||||
// 体验账号(演示/评审):账号清单与展示文案全部落库,小程序端不再写死
|
||||
const demoAccountsVal = JSON.stringify({
|
||||
enabled: true,
|
||||
accounts: [
|
||||
{ userId: 1001, roleTag: 'host', roleDesc: '房主/局长', desc: '余额充足 · 发起项目制/AA组局' },
|
||||
{ userId: 2001, roleTag: 'buyer', roleDesc: '普通买家', desc: '搭子报名 · 预约陪伴 · 核销码签到' },
|
||||
{ userId: 2006, roleTag: 'expert', roleDesc: '金牌陪伴师', desc: '工作台接单 · 雷达抢单 · 现场核销' }
|
||||
],
|
||||
remark: '体验账号:仅此处配置的账号可走「体验登录」获取真实会话 token;清空 accounts 即关闭体验登录'
|
||||
});
|
||||
if (!existingKeys.has('demo_accounts')) {
|
||||
await conn.query(`
|
||||
INSERT INTO platform_configs (config_key, name, value_json, description, status)
|
||||
VALUES ('demo_accounts', '体验账号配置', ?, '演示/评审用体验账号清单(含展示文案),关闭时清空 accounts', 1)
|
||||
`, [demoAccountsVal]);
|
||||
}
|
||||
// 体验账号直登已下线:清理历史配置,避免残留测试账号清单
|
||||
await conn.query("DELETE FROM platform_configs WHERE config_key = 'demo_accounts'");
|
||||
|
||||
if (existingKeys.has('platform_protocol')) {
|
||||
const [protoRows]: any = await conn.query("SELECT value_json FROM platform_configs WHERE config_key = 'platform_protocol'");
|
||||
if (protoRows && protoRows.length > 0) {
|
||||
|
||||
@@ -3090,88 +3090,6 @@ app.post('/api/auth/wechat-phone', async (req: Request, res: Response) => {
|
||||
}
|
||||
});
|
||||
|
||||
// ===== 体验账号(演示/评审):清单与文案全部来自 DB 配置,小程序端不再写死 =====
|
||||
async function loadDemoAccounts(): Promise<Array<{ userId: number; roleTag: string; roleDesc: string; desc: string }>> {
|
||||
try {
|
||||
const record = await repo.getPlatformConfigByKey('demo_accounts');
|
||||
const value = (record && record.valueJson) || {};
|
||||
if (value.enabled === false) return [];
|
||||
const accounts = Array.isArray(value.accounts) ? value.accounts : [];
|
||||
return accounts
|
||||
.map((a: any) => ({
|
||||
userId: Number(a.userId) || 0,
|
||||
roleTag: String(a.roleTag || ''),
|
||||
roleDesc: String(a.roleDesc || ''),
|
||||
desc: String(a.desc || ''),
|
||||
}))
|
||||
.filter((a: any) => a.userId > 0)
|
||||
.slice(0, 10);
|
||||
} catch (e) {
|
||||
logServerError('loadDemoAccounts', e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
app.get('/api/auth/demo-accounts', async (_req: Request, res: Response) => {
|
||||
try {
|
||||
const accounts = await loadDemoAccounts();
|
||||
const list = [];
|
||||
for (const acc of accounts) {
|
||||
const u: any = await repo.getUserById(acc.userId);
|
||||
if (!u) continue;
|
||||
if (u.status === '已封禁' || u.status === '封禁') continue;
|
||||
list.push({
|
||||
userId: u.id,
|
||||
nickname: u.nickname || `用户${u.id}`,
|
||||
avatar: u.avatar || '',
|
||||
roleTag: acc.roleTag,
|
||||
roleDesc: acc.roleDesc,
|
||||
desc: acc.desc,
|
||||
});
|
||||
}
|
||||
res.json({ code: 20000, data: list });
|
||||
} catch (err: any) {
|
||||
res.status(500).json({ code: 50000, message: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 体验登录:只对 DB 配置内的账号签发真实 token,避免"无 token 的假登录"造成的 401/身份错配
|
||||
app.post('/api/auth/demo-login', rateLimit({ name: 'demo_login', windowMs: 60 * 1000, max: 20, key: rateWriteKey }), async (req: Request, res: Response) => {
|
||||
try {
|
||||
const userId = Number(req.body?.userId) || 0;
|
||||
const accounts = await loadDemoAccounts();
|
||||
if (!accounts.some((a) => a.userId === userId)) {
|
||||
return res.status(403).json({ code: 40300, message: '该账号未开启体验登录' });
|
||||
}
|
||||
const user: any = await repo.getUserById(userId);
|
||||
if (!user) {
|
||||
return res.status(404).json({ code: 40400, message: '体验账号不存在或已被清理' });
|
||||
}
|
||||
if (user.status === '已封禁' || user.status === '封禁') {
|
||||
return res.status(403).json({ code: 40300, message: '该体验账号已被封禁' });
|
||||
}
|
||||
const token = signUserToken(user.id);
|
||||
res.json({
|
||||
code: 20000,
|
||||
data: {
|
||||
userId: user.id,
|
||||
user: {
|
||||
id: user.id,
|
||||
nickname: user.nickname,
|
||||
avatar: user.avatar,
|
||||
gender: user.gender,
|
||||
phone: user.phone,
|
||||
},
|
||||
userToken: token,
|
||||
token,
|
||||
},
|
||||
message: '体验登录成功',
|
||||
});
|
||||
} catch (err: any) {
|
||||
res.status(500).json({ code: 50000, message: err.message || '体验登录异常' });
|
||||
}
|
||||
});
|
||||
|
||||
// 微信小程序 JSAPI 统一下单预支付接口 (支持真实商户号配置与沙盒收银台无缝切换)
|
||||
app.post('/api/pay/wechat/prepay', rateLimit({ name: 'prepay', windowMs: 60 * 1000, max: 30, key: rateWriteKey }), async (req: Request, res: Response) => {
|
||||
try {
|
||||
|
||||
@@ -517,8 +517,10 @@ describe('ContentSecurityService & Compliance Architecture (TDD 微信内容安
|
||||
expect(content).toContain('open-type="getPhoneNumber"');
|
||||
expect(content).toContain('@getphonenumber="handleGetPhoneNumber"');
|
||||
expect(content).toContain('一键授权快捷登录');
|
||||
expect(content).toContain('getDemoAccounts');
|
||||
expect(content).toContain('demoLogin');
|
||||
// 测试/演示账号直登已下线,只允许微信手机号授权登录
|
||||
expect(content).not.toContain('getDemoAccounts');
|
||||
expect(content).not.toContain('demoLogin');
|
||||
expect(content).not.toContain('多角色体验账号直登');
|
||||
expect(content).toContain('用户服务协议');
|
||||
expect(content).toContain('隐私保护指引');
|
||||
// 验证已彻底移除登录大门前强制填写的头像昵称卡片,保障成熟商业极速登录体验
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
// 已读取AGENTS.md
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
/**
|
||||
* 体验账号直登下线闸门:
|
||||
* 测试/演示账号不是真实微信授权用户,不允许再作为登录入口或失败回退。
|
||||
*/
|
||||
describe('体验账号直登已下线', () => {
|
||||
const root = path.resolve(__dirname, '../../');
|
||||
const read = (rel: string) => fs.readFileSync(path.join(root, rel), 'utf-8');
|
||||
const login = read('pwuniapp/pages/auth/login.vue');
|
||||
const api = read('pwuniapp/common/api.js');
|
||||
const indexCode = read('server/src/index.ts');
|
||||
const mysql = read('server/src/db/mysql.ts');
|
||||
|
||||
it('登录页不得保留测试账号列表、切换按钮或回退登录', () => {
|
||||
expect(login).not.toContain('demo-accounts-card');
|
||||
expect(login).not.toContain('多角色体验账号直登');
|
||||
expect(login).not.toContain('loginWithDemo');
|
||||
expect(login).not.toContain('loginFirstDemoAccount');
|
||||
expect(login).not.toContain('getDemoAccounts');
|
||||
expect(login).not.toContain('demoLogin');
|
||||
});
|
||||
|
||||
it('客户端 API 与后端路由必须移除', () => {
|
||||
expect(api).not.toContain('/auth/demo-accounts');
|
||||
expect(api).not.toContain('/auth/demo-login');
|
||||
expect(indexCode).not.toContain('/api/auth/demo-accounts');
|
||||
expect(indexCode).not.toContain('/api/auth/demo-login');
|
||||
expect(indexCode).not.toContain('loadDemoAccounts');
|
||||
});
|
||||
|
||||
it('数据库不再种子体验账号配置,并清理历史配置', () => {
|
||||
expect(mysql).toContain("DELETE FROM platform_configs WHERE config_key = 'demo_accounts'");
|
||||
expect(mysql).not.toContain('体验账号配置');
|
||||
});
|
||||
|
||||
it('微信授权失败时只提示重试,不再引导体验账号登录', () => {
|
||||
expect(login).not.toContain('体验账号');
|
||||
expect(login).toContain('请在微信小程序内使用手机号快捷登录');
|
||||
expect(login).toContain('需要授权微信手机号才能登录');
|
||||
});
|
||||
});
|
||||
@@ -12,10 +12,12 @@ describe('第九十批登录态一致性闸门', () => {
|
||||
const api = fs.readFileSync(path.join(root, 'pwuniapp/common/api.js'), 'utf-8');
|
||||
const login = fs.readFileSync(path.join(root, 'pwuniapp/pages/auth/login.vue'), 'utf-8');
|
||||
|
||||
it('completeLogin 原子写入 userId 与 token(无 token 时清除旧 token)', () => {
|
||||
it('completeLogin 只接受服务端签发的 token,缺失时拒绝登录', () => {
|
||||
expect(login).toContain('const completeLogin = (userId, customData = {}, sessionToken = \'\')');
|
||||
expect(login).toContain('uni.removeStorageSync(\'userToken\')');
|
||||
expect(login).toContain('if (!sessionToken)');
|
||||
expect(login).toContain('uni.setStorageSync(\'userToken\', sessionToken)');
|
||||
// 不再存在"无 token 只写 userId"的体验登录路径
|
||||
expect(login).not.toContain('uni.removeStorageSync(\'userToken\')');
|
||||
});
|
||||
|
||||
it('微信登录把 token 交给 completeLogin 统一落库', () => {
|
||||
|
||||
在新工单中引用
屏蔽一个用户