feat(auth): 移除多角色体验账号直登,仅保留微信手机号授权登录

- 登录页删除体验账号卡片、切换按钮与全部 demo 登录/失败回退逻辑,微信授权失败只提示重试

- completeLogin 只接受服务端签发的 token,禁止无 token 的假登录

- 删除 /api/auth/demo-accounts、/api/auth/demo-login、loadDemoAccounts 与客户端 API

- 停止种子 demo_accounts 配置并清理历史配置行;117 套件/763 用例全绿;线上接口已 404
这个提交包含在:
xcbyy
2026-10-22 15:45:48 +08:00
父节点 e595defaaf
当前提交 fba6c5ac22
共有 7 个文件被更改,包括 124 次插入 和 407 次删除

查看文件

@@ -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 统一落库', () => {