feat(activity): implement P4 member self-service leave and ticket refund channel
这个提交包含在:
@@ -152,6 +152,7 @@ export async function initDatabase(): Promise<void> {
|
||||
CREATE TABLE IF NOT EXISTS finance_records (
|
||||
id VARCHAR(64) PRIMARY KEY,
|
||||
record_no VARCHAR(64) UNIQUE NOT NULL,
|
||||
user_id INT,
|
||||
type VARCHAR(64) NOT NULL,
|
||||
amount DECIMAL(10,2) NOT NULL,
|
||||
direction VARCHAR(8) NOT NULL DEFAULT 'IN',
|
||||
@@ -161,6 +162,10 @@ export async function initDatabase(): Promise<void> {
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
`);
|
||||
|
||||
try {
|
||||
await conn.query('ALTER TABLE finance_records ADD COLUMN user_id INT AFTER record_no');
|
||||
} catch (_) {}
|
||||
|
||||
// 7. 提现申请表
|
||||
await conn.query(`
|
||||
CREATE TABLE IF NOT EXISTS withdrawals (
|
||||
|
||||
@@ -396,7 +396,7 @@ export class MysqlRepository {
|
||||
await conn.query(`
|
||||
INSERT INTO finance_records (id, record_no, user_id, type, amount, direction, counterparty, related_id)
|
||||
VALUES (?, ?, ?, '成员退席原路退款', ?, 'IN', ?, ?)
|
||||
`, [finId, recordNo, userId, (refundAmount / 100).toFixed(2), `组局 ${id}`, id]);
|
||||
`, [finId, recordNo, userId, (refundAmount / 100).toFixed(2), `用户 ${userId} (组局退款)`, id]);
|
||||
|
||||
await conn.commit();
|
||||
return {
|
||||
|
||||
@@ -318,5 +318,62 @@ describe('ContentSecurityService & Compliance Architecture (TDD 微信内容安
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('9. 组局满员前「搭子自主退席与全额退款」与席位释放闭环验证 (P4 体验优化专项目标)', () => {
|
||||
it('MySQL 数据库必须支持 finance_records 表的 user_id 字段补丁', () => {
|
||||
const mysqlPath = path.join(rootDir, 'server', 'src', 'db', 'mysql.ts');
|
||||
const content = fs.readFileSync(mysqlPath, 'utf-8');
|
||||
expect(content).toContain('ALTER TABLE finance_records ADD COLUMN user_id');
|
||||
});
|
||||
|
||||
it('Repository leaveActivity 必须支持原路退款至用户钱包并记入资金流水', () => {
|
||||
const repoPath = path.join(rootDir, 'server', 'src', 'db', 'repository.ts');
|
||||
const content = fs.readFileSync(repoPath, 'utf-8');
|
||||
expect(content).toContain('async leaveActivity(id: string, userId: number)');
|
||||
expect(content).toContain('DELETE FROM participants WHERE id = ?');
|
||||
expect(content).toContain('UPDATE users SET balance = balance + ? WHERE id = ?');
|
||||
expect(content).toContain('成员退席原路退款');
|
||||
});
|
||||
|
||||
it('服务端路由必须提供 POST /api/activities/:id/leave 接口', () => {
|
||||
const serverIndexPath = path.join(rootDir, 'server', 'src', 'index.ts');
|
||||
const content = fs.readFileSync(serverIndexPath, 'utf-8');
|
||||
expect(content).toContain("app.post('/api/activities/:id/leave'");
|
||||
expect(content).toContain('leaveActivity');
|
||||
});
|
||||
|
||||
it('小程序活动详情页必须支持普通搭子申请退席全额退款与本人席位标识', () => {
|
||||
const actDetailPath = path.join(rootDir, 'pwuniapp', 'pages', 'activity', 'detail.vue');
|
||||
const content = fs.readFileSync(actDetailPath, 'utf-8');
|
||||
expect(content).toContain('leaveActivity');
|
||||
expect(content).toContain('handleLeave');
|
||||
expect(content).toContain('申请退席 (全额退款)');
|
||||
expect(content).toContain('my-badge');
|
||||
expect(content).toContain('我的席位');
|
||||
});
|
||||
|
||||
it('小程序门票页必须支持未核销门票自助申请退票退款', () => {
|
||||
const myTicketPath = path.join(rootDir, 'pwuniapp', 'pages', 'activity', 'my-ticket.vue');
|
||||
const content = fs.readFileSync(myTicketPath, 'utf-8');
|
||||
expect(content).toContain('leaveActivity');
|
||||
expect(content).toContain('handleCancelTicket');
|
||||
expect(content).toContain('申请退票退款 (释放席位)');
|
||||
});
|
||||
|
||||
it('所有 P4 新增代码严禁包含任何 Emoji 违规字符', () => {
|
||||
const p4Files = [
|
||||
path.join(rootDir, 'server', 'src', 'db', 'mysql.ts'),
|
||||
path.join(rootDir, 'server', 'src', 'db', 'repository.ts'),
|
||||
path.join(rootDir, 'server', 'src', 'index.ts'),
|
||||
path.join(rootDir, 'pwuniapp', 'pages', 'activity', 'detail.vue'),
|
||||
path.join(rootDir, 'pwuniapp', 'pages', 'activity', 'my-ticket.vue')
|
||||
];
|
||||
const emojiRegex = /(?:\p{Extended_Pictographic}|\p{Emoji_Presentation}|[\u2600-\u27BF])/u;
|
||||
for (const f of p4Files) {
|
||||
const text = fs.readFileSync(f, 'utf-8');
|
||||
expect(emojiRegex.test(text)).toBe(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
在新工单中引用
屏蔽一个用户