feat(activity&admin): 优化组局起止时段与性别限制,PC后台核销看板与登录页体验升级
这个提交包含在:
@@ -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('大神入驻认证');
|
||||
});
|
||||
|
||||
|
||||
在新工单中引用
屏蔽一个用户