From fba6c5ac22b3264074e8e8b500c45f7996037976 Mon Sep 17 00:00:00 2001
From: xcbyy <2578898740@qq.com>
Date: Thu, 22 Oct 2026 15:45:48 +0800
Subject: [PATCH] =?UTF-8?q?feat(auth):=20=E7=A7=BB=E9=99=A4=E5=A4=9A?=
=?UTF-8?q?=E8=A7=92=E8=89=B2=E4=BD=93=E9=AA=8C=E8=B4=A6=E5=8F=B7=E7=9B=B4?=
=?UTF-8?q?=E7=99=BB=EF=BC=8C=E4=BB=85=E4=BF=9D=E7=95=99=E5=BE=AE=E4=BF=A1?=
=?UTF-8?q?=E6=89=8B=E6=9C=BA=E5=8F=B7=E6=8E=88=E6=9D=83=E7=99=BB=E5=BD=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 登录页删除体验账号卡片、切换按钮与全部 demo 登录/失败回退逻辑,微信授权失败只提示重试
- completeLogin 只接受服务端签发的 token,禁止无 token 的假登录
- 删除 /api/auth/demo-accounts、/api/auth/demo-login、loadDemoAccounts 与客户端 API
- 停止种子 demo_accounts 配置并清理历史配置行;117 套件/763 用例全绿;线上接口已 404
---
pwuniapp/common/api.js | 19 -
pwuniapp/pages/auth/login.vue | 355 ++++--------------
server/src/db/mysql.ts | 19 +-
server/src/index.ts | 82 ----
server/tests/content-security.spec.ts | 6 +-
server/tests/demo-login-removed.spec.ts | 44 +++
.../tests/login-identity-consistency.spec.ts | 6 +-
7 files changed, 124 insertions(+), 407 deletions(-)
create mode 100644 server/tests/demo-login-removed.spec.ts
diff --git a/pwuniapp/common/api.js b/pwuniapp/common/api.js
index 588a020..18ef8bf 100644
--- a/pwuniapp/common/api.js
+++ b/pwuniapp/common/api.js
@@ -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) });
-}
diff --git a/pwuniapp/pages/auth/login.vue b/pwuniapp/pages/auth/login.vue
index 01440b7..dce3001 100644
--- a/pwuniapp/pages/auth/login.vue
+++ b/pwuniapp/pages/auth/login.vue
@@ -22,33 +22,6 @@
微信官方安全风控 · 个人信息严格加密存储
-
-
-
- 多角色体验账号直登
- 评审与体验专用 · 一键切换角色
-
-
-
-
-
-
-
- {{ acc.nickname }}
- {{ acc.roleDesc }}
-
- {{ acc.desc }}
-
-
-
-
-
-
@@ -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;
diff --git a/server/src/db/mysql.ts b/server/src/db/mysql.ts
index b4de38d..f3b5a41 100644
--- a/server/src/db/mysql.ts
+++ b/server/src/db/mysql.ts
@@ -1352,22 +1352,9 @@ async function seedInitialDataIfEmpty(conn: any): Promise {
`, [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) {
diff --git a/server/src/index.ts b/server/src/index.ts
index 30872cc..a16263a 100644
--- a/server/src/index.ts
+++ b/server/src/index.ts
@@ -3090,88 +3090,6 @@ app.post('/api/auth/wechat-phone', async (req: Request, res: Response) => {
}
});
-// ===== 体验账号(演示/评审):清单与文案全部来自 DB 配置,小程序端不再写死 =====
-async function loadDemoAccounts(): Promise> {
- 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 {
diff --git a/server/tests/content-security.spec.ts b/server/tests/content-security.spec.ts
index 9ecebcc..a591db8 100644
--- a/server/tests/content-security.spec.ts
+++ b/server/tests/content-security.spec.ts
@@ -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('隐私保护指引');
// 验证已彻底移除登录大门前强制填写的头像昵称卡片,保障成熟商业极速登录体验
diff --git a/server/tests/demo-login-removed.spec.ts b/server/tests/demo-login-removed.spec.ts
new file mode 100644
index 0000000..a6afa3f
--- /dev/null
+++ b/server/tests/demo-login-removed.spec.ts
@@ -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('需要授权微信手机号才能登录');
+ });
+});
diff --git a/server/tests/login-identity-consistency.spec.ts b/server/tests/login-identity-consistency.spec.ts
index 5eefe34..1ab647e 100644
--- a/server/tests/login-identity-consistency.spec.ts
+++ b/server/tests/login-identity-consistency.spec.ts
@@ -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 统一落库', () => {