feat(activity&admin): 优化组局起止时段与性别限制,PC后台核销看板与登录页体验升级
这个提交包含在:
@@ -36,6 +36,7 @@ export async function initDatabase(): Promise<void> {
|
||||
max_participants INT DEFAULT 4,
|
||||
allow_dynamic_price TINYINT DEFAULT 1,
|
||||
price_adjust_rule VARCHAR(32) DEFAULT 'UNIFIED_HIGH',
|
||||
gender_requirement VARCHAR(32) DEFAULT 'ALL',
|
||||
status VARCHAR(32) DEFAULT 'RECRUITING',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
@@ -47,6 +48,12 @@ export async function initDatabase(): Promise<void> {
|
||||
// 字段已存在忽略
|
||||
}
|
||||
|
||||
try {
|
||||
await conn.query(`ALTER TABLE activities ADD COLUMN gender_requirement VARCHAR(32) DEFAULT 'ALL' AFTER price_adjust_rule`);
|
||||
} catch (_) {
|
||||
// 字段已存在忽略
|
||||
}
|
||||
|
||||
try {
|
||||
await conn.query(`ALTER TABLE activities ADD COLUMN latitude DECIMAL(10,6) DEFAULT NULL AFTER location`);
|
||||
} catch (_) {
|
||||
|
||||
@@ -48,6 +48,7 @@ export interface ActivityRow {
|
||||
totalFeeCents: number;
|
||||
perPersonFeeCents: number;
|
||||
maxParticipants: number;
|
||||
genderRequirement?: string;
|
||||
allowDynamicPrice: boolean;
|
||||
priceAdjustRule: string;
|
||||
joinedCount?: number;
|
||||
@@ -143,6 +144,7 @@ export class MysqlRepository {
|
||||
a.location, COALESCE(a.start_time, '') as startTime, a.pay_type as payType, a.total_fee_cents as totalFeeCents,
|
||||
a.per_person_fee_cents as perPersonFeeCents, a.max_participants as maxParticipants,
|
||||
a.allow_dynamic_price as allowDynamicPrice, a.price_adjust_rule as priceAdjustRule,
|
||||
COALESCE(a.gender_requirement, 'ALL') as genderRequirement,
|
||||
COALESCE(a.status, 'RECRUITING') as status,
|
||||
a.created_at as createdAt,
|
||||
u.nickname as creatorNickname,
|
||||
@@ -217,6 +219,7 @@ export class MysqlRepository {
|
||||
a.location, COALESCE(a.start_time, '') as startTime, a.pay_type as payType, a.total_fee_cents as totalFeeCents,
|
||||
a.per_person_fee_cents as perPersonFeeCents, a.max_participants as maxParticipants,
|
||||
a.allow_dynamic_price as allowDynamicPrice, a.price_adjust_rule as priceAdjustRule,
|
||||
COALESCE(a.gender_requirement, 'ALL') as genderRequirement,
|
||||
COALESCE(a.status, 'RECRUITING') as status,
|
||||
u.nickname as creatorNickname,
|
||||
u.avatar as creatorAvatar
|
||||
@@ -253,6 +256,31 @@ export class MysqlRepository {
|
||||
};
|
||||
}
|
||||
|
||||
async getAllActivityParticipants(): Promise<any[]> {
|
||||
const [ptRows]: any = await pool.query(`
|
||||
SELECT
|
||||
p.id, p.activity_id as activityId, p.user_id as userId,
|
||||
COALESCE(u.nickname, '同城组局参与者') as nickname,
|
||||
COALESCE(u.avatar, '') as avatar,
|
||||
p.pay_amount_cents as payAmountCents, p.verify_code as verifyCode,
|
||||
p.checkin_status as checkinStatus, p.verified_at as verifiedAt,
|
||||
p.verified_by as verifiedBy, p.created_at as createdAt,
|
||||
COALESCE(a.title, '未知活动') as activityTitle,
|
||||
COALESCE(a.game_category, '组局') as gameCategory,
|
||||
COALESCE(a.status, 'RECRUITING') as activityStatus,
|
||||
COALESCE(a.location, '') as activityLocation
|
||||
FROM participants p
|
||||
LEFT JOIN users u ON u.id = p.user_id
|
||||
LEFT JOIN activities a ON a.id = p.activity_id
|
||||
ORDER BY p.created_at DESC
|
||||
`);
|
||||
|
||||
return (ptRows || []).map((p: any) => ({
|
||||
...p,
|
||||
checkinStatus: Boolean(p.checkinStatus),
|
||||
}));
|
||||
}
|
||||
|
||||
async createActivity(data: {
|
||||
creatorId: number;
|
||||
title: string;
|
||||
@@ -267,6 +295,7 @@ export class MysqlRepository {
|
||||
maxParticipants?: number;
|
||||
allowDynamicPrice?: number | boolean;
|
||||
priceAdjustRule?: string;
|
||||
genderRequirement?: string;
|
||||
}): Promise<any> {
|
||||
const [userRows]: any = await pool.query('SELECT status FROM users WHERE id = ?', [data.creatorId]);
|
||||
if (userRows && userRows.length > 0 && (userRows[0].status === '已封禁' || userRows[0].status === '封禁')) {
|
||||
@@ -294,10 +323,18 @@ export class MysqlRepository {
|
||||
const allowDynamicPrice = data.allowDynamicPrice !== undefined ? (data.allowDynamicPrice ? 1 : 0) : 1;
|
||||
const priceAdjustRule = data.priceAdjustRule || 'UNIFIED_HIGH';
|
||||
|
||||
let genderRequirement = 'ALL';
|
||||
const rawGender = String(data.genderRequirement || '').trim();
|
||||
if (rawGender === 'FEMALE_ONLY' || rawGender === '仅女生' || rawGender === '女') {
|
||||
genderRequirement = 'FEMALE_ONLY';
|
||||
} else if (rawGender === 'MALE_ONLY' || rawGender === '仅男生' || rawGender === '男') {
|
||||
genderRequirement = 'MALE_ONLY';
|
||||
}
|
||||
|
||||
await pool.query(`
|
||||
INSERT INTO activities
|
||||
(id, creator_id, title, game_category, location, latitude, longitude, start_time, pay_type, total_fee_cents, per_person_fee_cents, max_participants, allow_dynamic_price, price_adjust_rule)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
(id, creator_id, title, game_category, location, latitude, longitude, start_time, pay_type, total_fee_cents, per_person_fee_cents, max_participants, allow_dynamic_price, price_adjust_rule, gender_requirement)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`, [
|
||||
id,
|
||||
data.creatorId,
|
||||
@@ -313,6 +350,7 @@ export class MysqlRepository {
|
||||
maxParticipants,
|
||||
allowDynamicPrice,
|
||||
priceAdjustRule,
|
||||
genderRequirement,
|
||||
]);
|
||||
|
||||
return this.getActivityById(id);
|
||||
@@ -400,7 +438,7 @@ export class MysqlRepository {
|
||||
await conn.beginTransaction();
|
||||
|
||||
// 1. 封禁校验(事务内)
|
||||
const [userRows]: any = await conn.query('SELECT status FROM users WHERE id = ?', [params.userId]);
|
||||
const [userRows]: any = await conn.query('SELECT status, gender FROM users WHERE id = ?', [params.userId]);
|
||||
if (userRows && userRows.length > 0 && (userRows[0].status === '已封禁' || userRows[0].status === '封禁')) {
|
||||
const err: any = new Error('您的账号已被管理员封禁,无法报名参加活动');
|
||||
err.status = 403;
|
||||
@@ -410,10 +448,26 @@ export class MysqlRepository {
|
||||
|
||||
// 2. 行级锁锁定活动,串行化同一活动的并发报名,从根上杜绝超卖
|
||||
const [actRows]: any = await conn.query(
|
||||
'SELECT id, max_participants, pay_type, per_person_fee_cents FROM activities WHERE id = ? FOR UPDATE',
|
||||
'SELECT id, max_participants, pay_type, per_person_fee_cents, COALESCE(gender_requirement, "ALL") as genderRequirement FROM activities WHERE id = ? FOR UPDATE',
|
||||
[activityId]
|
||||
);
|
||||
if (!actRows || actRows.length === 0) throw new Error('活动不存在');
|
||||
|
||||
// 组局性别限制合规校验
|
||||
const genderReq = String(actRows[0].genderRequirement || 'ALL');
|
||||
const userGender = String(userRows?.[0]?.gender || '').trim();
|
||||
if (genderReq === 'FEMALE_ONLY' && userGender === '男') {
|
||||
const err: any = new Error('该组局仅限女生报名参与');
|
||||
err.status = 400;
|
||||
err.code = 40011;
|
||||
throw err;
|
||||
}
|
||||
if (genderReq === 'MALE_ONLY' && userGender === '女') {
|
||||
const err: any = new Error('该组局仅限男生报名参与');
|
||||
err.status = 400;
|
||||
err.code = 40012;
|
||||
throw err;
|
||||
}
|
||||
const maxParticipants = Number(actRows[0].max_participants || 0);
|
||||
// 报名费由服务端活动配置决定(pay_type=1 为 AA 收取人均分摊,其余为房主请客 0 元),
|
||||
// 严禁信任客户端传入的 payAmountCents,否则可"免费报名 → 退款/清算套现"。
|
||||
@@ -847,6 +901,7 @@ export class MysqlRepository {
|
||||
a.location, COALESCE(a.start_time, '') as startTime, a.pay_type as payType, a.total_fee_cents as totalFeeCents,
|
||||
a.per_person_fee_cents as perPersonFeeCents, a.max_participants as maxParticipants,
|
||||
a.allow_dynamic_price as allowDynamicPrice, a.price_adjust_rule as priceAdjustRule,
|
||||
COALESCE(a.gender_requirement, 'ALL') as genderRequirement,
|
||||
COALESCE(a.status, 'RECRUITING') as status,
|
||||
a.created_at as createdAt,
|
||||
u.nickname as creatorNickname,
|
||||
|
||||
@@ -1104,6 +1104,28 @@ app.get('/api/activities', async (req: Request, res: Response) => {
|
||||
}
|
||||
});
|
||||
|
||||
// 管理端全量活动核销状态大盘数据
|
||||
app.get('/api/activities/all', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const isAdmin = isVerifiedAdminRequest(req);
|
||||
if (!isAdmin) {
|
||||
return res.status(403).json({ code: 40300, message: '仅管理员可查看全部活动核销状态' });
|
||||
}
|
||||
const participants = await repo.getAllActivityParticipants();
|
||||
return res.json({
|
||||
code: 20000,
|
||||
data: {
|
||||
id: 'all',
|
||||
title: '全部活动',
|
||||
gameCategory: '全品类',
|
||||
participants,
|
||||
},
|
||||
});
|
||||
} catch (err: any) {
|
||||
res.status(500).json({ code: 50000, message: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/activities/:id', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const act = await repo.getActivityById(String(req.params.id));
|
||||
@@ -1214,6 +1236,20 @@ app.post('/api/activities/:id/join', async (req: Request, res: Response) => {
|
||||
}
|
||||
});
|
||||
|
||||
// 管理端全量视图下4位核销码核销
|
||||
app.post('/api/activities/all/verify', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { operatorId, verifyCode } = req.body;
|
||||
let operator = resolveSecureUserId(req, Number(operatorId || 0), '核销组局门票');
|
||||
const result = await repo.universalVerifyCheckin(String(verifyCode), operator);
|
||||
res.json({ code: 20000, data: result, message: '核销成功,已完成现场到场签到' });
|
||||
} catch (err: any) {
|
||||
const statusCode = err.status || 400;
|
||||
const errCode = err.code || 40000;
|
||||
res.status(statusCode).json({ code: errCode, message: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 房主/管理员核销 4 位核销码 (防重落盘 MySQL)
|
||||
app.post('/api/activities/:id/verify', async (req: Request, res: Response) => {
|
||||
try {
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
// 已读取AGENTS.md
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
/**
|
||||
* 第一百零二批回归闸门:组局性别限制功能全链路闭环测试
|
||||
*/
|
||||
describe('第一百零二批组局性别限制闭环测试', () => {
|
||||
const rootDir = path.resolve(__dirname, '../../');
|
||||
const mysqlTsPath = path.join(rootDir, 'server/src/db/mysql.ts');
|
||||
const repoTsPath = path.join(rootDir, 'server/src/db/repository.ts');
|
||||
const apiJsPath = path.join(rootDir, 'pwuniapp/common/api.js');
|
||||
const createVuePath = path.join(rootDir, 'pwuniapp/pages/activity/create.vue');
|
||||
const detailVuePath = path.join(rootDir, 'pwuniapp/pages/activity/detail.vue');
|
||||
const listVuePath = path.join(rootDir, 'pwuniapp/pages/activity/list.vue');
|
||||
|
||||
const mysqlContent = fs.readFileSync(mysqlTsPath, 'utf-8');
|
||||
const repoContent = fs.readFileSync(repoTsPath, 'utf-8');
|
||||
const apiJsContent = fs.readFileSync(apiJsPath, 'utf-8');
|
||||
const createVueContent = fs.readFileSync(createVuePath, 'utf-8');
|
||||
const detailVueContent = fs.readFileSync(detailVuePath, 'utf-8');
|
||||
const listVueContent = fs.readFileSync(listVuePath, 'utf-8');
|
||||
|
||||
it('数据库结构必须包含 gender_requirement 字段及迁移补丁', () => {
|
||||
expect(mysqlContent).toContain('gender_requirement VARCHAR(32) DEFAULT \'ALL\'');
|
||||
expect(mysqlContent).toContain('ADD COLUMN gender_requirement VARCHAR(32)');
|
||||
});
|
||||
|
||||
it('仓储层 createActivity 必须支持并归一化 genderRequirement', () => {
|
||||
expect(repoContent).toContain('genderRequirement?: string;');
|
||||
expect(repoContent).toContain("genderRequirement = 'FEMALE_ONLY'");
|
||||
expect(repoContent).toContain("genderRequirement = 'MALE_ONLY'");
|
||||
expect(repoContent).toContain('gender_requirement');
|
||||
});
|
||||
|
||||
it('仓储层 joinActivity 必须严格执行性别限制校验,违规时拦截', () => {
|
||||
expect(repoContent).toContain('该组局仅限女生报名参与');
|
||||
expect(repoContent).toContain('该组局仅限男生报名参与');
|
||||
expect(repoContent).toContain('genderRequirement || \'ALL\'');
|
||||
});
|
||||
|
||||
it('小程序 API 层必须透传与格式化 genderRequirement', () => {
|
||||
expect(apiJsContent).toContain('genderRequirement: payload.genderRequirement || \'ALL\'');
|
||||
expect(apiJsContent).toContain('raw.genderRequirement || raw.gender_requirement || \'ALL\'');
|
||||
});
|
||||
|
||||
it('组局发布页 create.vue 必须提供「凑局性别限制」配置项及三选一胶囊', () => {
|
||||
expect(createVueContent).toContain('凑局性别限制');
|
||||
expect(createVueContent).toContain('genderOptions');
|
||||
expect(createVueContent).toContain("'仅女生'");
|
||||
expect(createVueContent).toContain("'仅男生'");
|
||||
expect(createVueContent).toContain("'不限'");
|
||||
expect(createVueContent).toContain('genderRequirement: \'ALL\'');
|
||||
expect(createVueContent).toContain('.gender-limit-block');
|
||||
});
|
||||
|
||||
it('详情页与大厅列表必须展示对应性别限制标签', () => {
|
||||
expect(detailVueContent).toContain('性别限制');
|
||||
expect(detailVueContent).toContain('formatGenderRequirement');
|
||||
expect(listVueContent).toContain('仅限女生');
|
||||
expect(listVueContent).toContain('仅限男生');
|
||||
expect(listVueContent).toContain('gender-req-tag');
|
||||
});
|
||||
|
||||
it('全量修改文件必须严格遵守零 Emoji 门禁规范', () => {
|
||||
const emojiRegex = /[\u{1F300}-\u{1F9FF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}]/u;
|
||||
expect(emojiRegex.test(mysqlContent)).toBe(false);
|
||||
expect(emojiRegex.test(repoContent)).toBe(false);
|
||||
expect(emojiRegex.test(createVueContent)).toBe(false);
|
||||
expect(emojiRegex.test(detailVueContent)).toBe(false);
|
||||
expect(emojiRegex.test(listVueContent)).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,68 @@
|
||||
// 已读取AGENTS.md
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
/**
|
||||
* 第一百零一批回归闸门:活动时段与快捷时段起止时间闭环校验
|
||||
*/
|
||||
describe('第一百零一批活动时段与快捷时段起止时间闭环', () => {
|
||||
const rootDir = path.resolve(__dirname, '../../');
|
||||
const createVuePath = path.join(rootDir, 'pwuniapp/pages/activity/create.vue');
|
||||
const detailVuePath = path.join(rootDir, 'pwuniapp/pages/activity/detail.vue');
|
||||
|
||||
const createVueContent = fs.readFileSync(createVuePath, 'utf-8');
|
||||
const detailVueContent = fs.readFileSync(detailVuePath, 'utf-8');
|
||||
|
||||
it('快捷时段必须包含起止时间,不得仅有单点开始时间', () => {
|
||||
expect(createVueContent).toContain("'今晚 19:30 - 21:30'");
|
||||
expect(createVueContent).toContain("'今晚 20:30 - 22:30'");
|
||||
expect(createVueContent).toContain("'明天 14:00 - 17:00'");
|
||||
expect(createVueContent).toContain("'周六 18:30 - 21:30'");
|
||||
expect(createVueContent).toContain("'周日 15:00 - 18:00'");
|
||||
// 确保不再包含旧的无结束时间单点快捷项
|
||||
expect(createVueContent).not.toMatch(/'今晚 19:30',/);
|
||||
expect(createVueContent).not.toMatch(/'周六 18:30',/);
|
||||
});
|
||||
|
||||
it('时间选择器必须升级为三列多列选择器(日期、开始时间、结束时间)', () => {
|
||||
expect(createVueContent).toContain('const timePickerStartSlots =');
|
||||
expect(createVueContent).toContain('const timePickerEndSlots =');
|
||||
expect(createVueContent).toContain('const timePickerRange = ref([timePickerDays, timePickerStartSlots, timePickerEndSlots]);');
|
||||
expect(createVueContent).toContain('const timePickerIndex = ref([5, 19, 24]);');
|
||||
});
|
||||
|
||||
it('时间选择变动必须输出带有起止时间范围的完整时段字符串', () => {
|
||||
expect(createVueContent).toContain('${dayStr} ${startStr} - ${endStr}');
|
||||
expect(createVueContent).toContain('toSlotMinutes');
|
||||
});
|
||||
|
||||
it('快捷时段点击必须支持解析起止时间并同步更新三列索引', () => {
|
||||
expect(createVueContent).toContain('const match = String(timeItem).trim().match(/^(\\S+)\\s+([\\d:]+)(?:\\s*-\\s*([\\d:]+))?$/);');
|
||||
expect(createVueContent).toContain('timePickerIndex.value = [');
|
||||
});
|
||||
|
||||
it('活动详情页必须将展示文案统一为「活动时段」', () => {
|
||||
expect(detailVueContent).toContain('<text class="info-label">活动时段</text>');
|
||||
expect(detailVueContent).not.toContain('<text class="info-label">开始时间</text>');
|
||||
});
|
||||
|
||||
it('时段选择器布局必须具备防挤压换行保护(无换行、省略截断与标签不压缩)', () => {
|
||||
// 占位符必须精炼,避免超长换行
|
||||
expect(createVueContent).toContain("{{ form.startTime || '请选择活动时段' }}");
|
||||
expect(createVueContent).not.toContain('请选择活动时段 (起止时间)');
|
||||
|
||||
// 标签必须 flex-shrink: 0 且 white-space: nowrap 防止文字折行
|
||||
expect(createVueContent).toContain('flex-shrink: 0;');
|
||||
expect(createVueContent).toContain('white-space: nowrap;');
|
||||
expect(createVueContent).toContain('text-overflow: ellipsis;');
|
||||
|
||||
// 基础信息卡片针对短标签优化宽度
|
||||
expect(createVueContent).toContain('&.basic-info-card');
|
||||
});
|
||||
|
||||
it('修改文件必须严格遵循零 Emoji 门禁规范', () => {
|
||||
const emojiRegex = /[\u{1F300}-\u{1F9FF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}]/u;
|
||||
expect(emojiRegex.test(createVueContent)).toBe(false);
|
||||
expect(emojiRegex.test(detailVueContent)).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
// 已读取AGENTS.md
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
/**
|
||||
* 第一百二十二批全量活动核销状态看板闸门 (查看所有活动的核销状态)
|
||||
*/
|
||||
describe('第一百二十二批全量活动核销状态看板闸门', () => {
|
||||
const rootDir = path.resolve(__dirname, '../../');
|
||||
const indexCode = fs.readFileSync(path.join(rootDir, 'server/src/index.ts'), 'utf-8');
|
||||
const repoCode = fs.readFileSync(path.join(rootDir, 'server/src/db/repository.ts'), 'utf-8');
|
||||
const verifyVuePath = path.join(
|
||||
rootDir,
|
||||
'admin/arco-design-pro-vite/src/views/activity/verify/index.vue'
|
||||
);
|
||||
const verifyVueCode = fs.readFileSync(verifyVuePath, 'utf-8');
|
||||
|
||||
it('仓储层必须实现 getAllActivityParticipants 方法并关联活动与用户详情', () => {
|
||||
expect(repoCode).toContain('getAllActivityParticipants(): Promise<any[]>');
|
||||
expect(repoCode).toContain('FROM participants p');
|
||||
expect(repoCode).toContain('LEFT JOIN activities a ON a.id = p.activity_id');
|
||||
expect(repoCode).toContain('checkinStatus: Boolean(p.checkinStatus)');
|
||||
});
|
||||
|
||||
it('服务端路由必须挂载 GET /api/activities/all 并限制管理员权限', () => {
|
||||
expect(indexCode).toContain("app.get('/api/activities/all'");
|
||||
expect(indexCode).toContain('isVerifiedAdminRequest(req)');
|
||||
expect(indexCode).toContain('repo.getAllActivityParticipants()');
|
||||
expect(indexCode).toContain("app.post('/api/activities/all/verify'");
|
||||
});
|
||||
|
||||
it('管理后台核销看板必须提供「全部」选项并支持全量核销追踪', () => {
|
||||
expect(verifyVueCode).toContain('value="all"');
|
||||
expect(verifyVueCode).toContain('全部 (查看所有活动核销状态)');
|
||||
expect(verifyVueCode).toContain('所属活动 / 组局');
|
||||
expect(verifyVueCode).toContain('filterStatus');
|
||||
expect(verifyVueCode).toContain('selectedActivityId.value === \'all\'');
|
||||
});
|
||||
|
||||
it('新增的前后端代码与测试严禁包含任何系统 Emoji 字符', () => {
|
||||
const emojiRegex = /(?:\p{Extended_Pictographic}|\p{Emoji_Presentation}|[\u2600-\u27BF])/u;
|
||||
expect(emojiRegex.test(indexCode)).toBe(false);
|
||||
expect(emojiRegex.test(verifyVueCode)).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -130,7 +130,8 @@ describe('ContentSecurityService & Compliance Architecture (TDD 微信内容安
|
||||
const raw = fs.readFileSync(pagesJsonPath, 'utf-8');
|
||||
const config = JSON.parse(raw);
|
||||
expect(Array.isArray(config.pages)).toBe(true);
|
||||
expect(config.pages.length).toBeGreaterThanOrEqual(28);
|
||||
const totalPages = config.pages.length + (config.subPackages || []).reduce((acc: number, sub: any) => acc + (sub.pages || []).length, 0);
|
||||
expect(totalPages).toBeGreaterThanOrEqual(28);
|
||||
|
||||
for (const pageItem of config.pages) {
|
||||
const vueFilePath = path.join(rootDir, 'pwuniapp', `${pageItem.path}.vue`);
|
||||
@@ -142,6 +143,19 @@ describe('ContentSecurityService & Compliance Architecture (TDD 微信内容安
|
||||
exists: true
|
||||
});
|
||||
}
|
||||
|
||||
for (const sub of (config.subPackages || [])) {
|
||||
for (const pageItem of sub.pages) {
|
||||
const vueFilePath = path.join(rootDir, 'pwuniapp', sub.root, `${pageItem.path}.vue`);
|
||||
expect({
|
||||
path: `${sub.root}/${pageItem.path}`,
|
||||
exists: fs.existsSync(vueFilePath)
|
||||
}).toEqual({
|
||||
path: `${sub.root}/${pageItem.path}`,
|
||||
exists: true
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('主包核心 TabBar 页面保持精炼且四大主页全部就绪', () => {
|
||||
@@ -484,7 +498,7 @@ describe('ContentSecurityService & Compliance Architecture (TDD 微信内容安
|
||||
expect(fs.existsSync(applyPath)).toBe(true);
|
||||
const pagesJsonPath = path.join(rootDir, 'pwuniapp', 'pages.json');
|
||||
const pagesJson = fs.readFileSync(pagesJsonPath, 'utf-8');
|
||||
expect(pagesJson).toContain('pages/expert/apply');
|
||||
expect(pagesJson.includes('pages/expert/apply') || (pagesJson.includes('pages/expert') && pagesJson.includes('"path": "apply"'))).toBe(true);
|
||||
expect(pagesJson).toContain('大神入驻认证');
|
||||
});
|
||||
|
||||
|
||||
在新工单中引用
屏蔽一个用户