feat(marketing): implement recharge tiers and coupon management across admin, server, and uniapp

这个提交包含在:
xcbyy
2026-10-10 09:22:46 +08:00
父节点 51682fffea
当前提交 d9c8388711
共有 12 个文件被更改,包括 1494 次插入 和 7 次删除

查看文件

@@ -337,6 +337,41 @@ export async function initDatabase(): Promise<void> {
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
`);
// 18. 钱包充值阶梯与赠送配置表
await conn.query(`
CREATE TABLE IF NOT EXISTS recharge_tiers (
id INT PRIMARY KEY AUTO_INCREMENT,
amount DECIMAL(10,2) NOT NULL,
bonus_amount DECIMAL(10,2) DEFAULT 0.00,
tag VARCHAR(32) DEFAULT '',
sort_order INT DEFAULT 0,
status TINYINT DEFAULT 1,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_sort (sort_order),
INDEX idx_status (status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
`);
// 19. 平台营销优惠券配置与发放表
await conn.query(`
CREATE TABLE IF NOT EXISTS coupons (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(64) NOT NULL,
discount_amount DECIMAL(10,2) NOT NULL,
min_spend DECIMAL(10,2) NOT NULL DEFAULT 0.00,
valid_days INT NOT NULL DEFAULT 7,
total_count INT NOT NULL DEFAULT 1000,
received_count INT NOT NULL DEFAULT 0,
category_limit VARCHAR(64) DEFAULT 'ALL',
description VARCHAR(255) DEFAULT '',
status TINYINT DEFAULT 1,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_status (status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
`);
// 初始化配置单条记录
const [cfgRows]: any = await conn.query('SELECT COUNT(*) as count FROM finance_config WHERE id = 1');
if (cfgRows[0].count === 0) {
@@ -564,4 +599,31 @@ async function seedInitialDataIfEmpty(conn: any): Promise<void> {
(10, 'more', '全部活动', 'gridAll', 'bg-blue', 'all', '/pages/activity/list?category=ALL', '', 10, 1);
`);
}
// 15. 种子充值阶梯
const [tierRows]: any = await conn.query('SELECT COUNT(*) as count FROM recharge_tiers');
if (tierRows[0].count === 0) {
await conn.query(`
INSERT INTO recharge_tiers (id, amount, bonus_amount, tag, sort_order, status)
VALUES
(1, 50.00, 0.00, '基础充值', 1, 1),
(2, 100.00, 5.00, '热门畅享', 2, 1),
(3, 200.00, 15.00, '超值加赠', 3, 1),
(4, 500.00, 50.00, '尊享特惠', 4, 1),
(5, 1000.00, 120.00, '土豪专享', 5, 1);
`);
}
// 16. 种子平台优惠券
const [cpnRows]: any = await conn.query('SELECT COUNT(*) as count FROM coupons');
if (cpnRows[0].count === 0) {
await conn.query(`
INSERT INTO coupons (id, name, discount_amount, min_spend, valid_days, total_count, received_count, category_limit, description, status)
VALUES
(1, '新用户注册专享立减券', 20.00, 50.00, 15, 2000, 128, 'ALL', '全品类陪玩及同城组局满50可用', 1),
(2, '周末同城聚会AA抵扣券', 15.00, 80.00, 7, 1000, 85, 'MEAL', '仅限同城美食探店组局活动使用', 1),
(3, '工体K歌麦霸欢唱减免券', 30.00, 120.00, 10, 800, 62, 'SING', '仅限KTV陪唱预约与活动使用', 1),
(4, '电竞开黑连胜冲星券', 10.00, 30.00, 30, 5000, 340, 'ALL', '线上游戏陪玩下单立减', 1);
`);
}
}

查看文件

@@ -999,6 +999,254 @@ export class MysqlRepository {
return res.affectedRows > 0;
}
// 1. 充值阶梯 (Recharge Tiers)
async getRechargeTiers(): Promise<any[]> {
const [rows]: any = await pool.query(`
SELECT
id, amount, bonus_amount as bonusAmount, tag,
sort_order as sortOrder, status,
created_at as createdAt, updated_at as updatedAt
FROM recharge_tiers
WHERE status = 1
ORDER BY sort_order ASC, amount ASC
`);
return rows;
}
async getAdminRechargeTiers(): Promise<any[]> {
const [rows]: any = await pool.query(`
SELECT
id, amount, bonus_amount as bonusAmount, tag,
sort_order as sortOrder, status,
created_at as createdAt, updated_at as updatedAt
FROM recharge_tiers
ORDER BY sort_order ASC, amount ASC
`);
return rows;
}
async createRechargeTier(data: {
amount: number;
bonusAmount?: number;
tag?: string;
sortOrder?: number;
status?: number;
}): Promise<any> {
const amount = Number(data.amount);
const bonusAmount = Number(data.bonusAmount || 0);
const tag = data.tag || '';
const sortOrder = Number(data.sortOrder || 0);
const status = data.status !== undefined ? Number(data.status) : 1;
const [res]: any = await pool.query(`
INSERT INTO recharge_tiers (amount, bonus_amount, tag, sort_order, status)
VALUES (?, ?, ?, ?, ?)
`, [amount, bonusAmount, tag, sortOrder, status]);
const newId = res.insertId;
const [rows]: any = await pool.query(`
SELECT
id, amount, bonus_amount as bonusAmount, tag,
sort_order as sortOrder, status,
created_at as createdAt, updated_at as updatedAt
FROM recharge_tiers
WHERE id = ?
`, [newId]);
return rows[0];
}
async updateRechargeTier(id: number | string, data: Partial<{
amount: number;
bonusAmount: number;
tag: string;
sortOrder: number;
status: number;
}>): Promise<any> {
const fields: string[] = [];
const values: any[] = [];
if (data.amount !== undefined) {
fields.push('amount = ?');
values.push(Number(data.amount));
}
if (data.bonusAmount !== undefined) {
fields.push('bonus_amount = ?');
values.push(Number(data.bonusAmount));
}
if (data.tag !== undefined) {
fields.push('tag = ?');
values.push(data.tag);
}
if (data.sortOrder !== undefined) {
fields.push('sort_order = ?');
values.push(Number(data.sortOrder));
}
if (data.status !== undefined) {
fields.push('status = ?');
values.push(Number(data.status));
}
if (fields.length > 0) {
values.push(id);
await pool.query(`UPDATE recharge_tiers SET ${fields.join(', ')} WHERE id = ?`, values);
}
const [rows]: any = await pool.query(`
SELECT
id, amount, bonus_amount as bonusAmount, tag,
sort_order as sortOrder, status,
created_at as createdAt, updated_at as updatedAt
FROM recharge_tiers
WHERE id = ?
`, [id]);
return rows[0] || null;
}
async deleteRechargeTier(id: number | string): Promise<boolean> {
const [res]: any = await pool.query('DELETE FROM recharge_tiers WHERE id = ?', [id]);
return res.affectedRows > 0;
}
// 2. 优惠券 (Coupons)
async getCoupons(categoryLimit?: string): Promise<any[]> {
let sql = `
SELECT
id, name, discount_amount as discountAmount, min_spend as minSpend,
valid_days as validDays, total_count as totalCount, received_count as receivedCount,
category_limit as categoryLimit, description, status,
created_at as createdAt, updated_at as updatedAt
FROM coupons
WHERE status = 1
`;
const params: any[] = [];
if (categoryLimit && categoryLimit !== 'ALL') {
sql += ' AND (category_limit = "ALL" OR category_limit = ?)';
params.push(categoryLimit);
}
sql += ' ORDER BY discount_amount DESC, id ASC';
const [rows]: any = await pool.query(sql, params);
return rows;
}
async getAdminCoupons(): Promise<any[]> {
const [rows]: any = await pool.query(`
SELECT
id, name, discount_amount as discountAmount, min_spend as minSpend,
valid_days as validDays, total_count as totalCount, received_count as receivedCount,
category_limit as categoryLimit, description, status,
created_at as createdAt, updated_at as updatedAt
FROM coupons
ORDER BY id DESC
`);
return rows;
}
async createCoupon(data: {
name: string;
discountAmount: number;
minSpend?: number;
validDays?: number;
totalCount?: number;
categoryLimit?: string;
description?: string;
status?: number;
}): Promise<any> {
const name = data.name;
const discountAmount = Number(data.discountAmount);
const minSpend = Number(data.minSpend || 0);
const validDays = Number(data.validDays || 7);
const totalCount = Number(data.totalCount || 1000);
const categoryLimit = data.categoryLimit || 'ALL';
const description = data.description || '';
const status = data.status !== undefined ? Number(data.status) : 1;
const [res]: any = await pool.query(`
INSERT INTO coupons (name, discount_amount, min_spend, valid_days, total_count, received_count, category_limit, description, status)
VALUES (?, ?, ?, ?, ?, 0, ?, ?, ?)
`, [name, discountAmount, minSpend, validDays, totalCount, categoryLimit, description, status]);
const newId = res.insertId;
const [rows]: any = await pool.query(`
SELECT
id, name, discount_amount as discountAmount, min_spend as minSpend,
valid_days as validDays, total_count as totalCount, received_count as receivedCount,
category_limit as categoryLimit, description, status,
created_at as createdAt, updated_at as updatedAt
FROM coupons
WHERE id = ?
`, [newId]);
return rows[0];
}
async updateCoupon(id: number | string, data: Partial<{
name: string;
discountAmount: number;
minSpend: number;
validDays: number;
totalCount: number;
categoryLimit: string;
description: string;
status: number;
}>): Promise<any> {
const fields: string[] = [];
const values: any[] = [];
if (data.name !== undefined) {
fields.push('name = ?');
values.push(data.name);
}
if (data.discountAmount !== undefined) {
fields.push('discount_amount = ?');
values.push(Number(data.discountAmount));
}
if (data.minSpend !== undefined) {
fields.push('min_spend = ?');
values.push(Number(data.minSpend));
}
if (data.validDays !== undefined) {
fields.push('valid_days = ?');
values.push(Number(data.validDays));
}
if (data.totalCount !== undefined) {
fields.push('total_count = ?');
values.push(Number(data.totalCount));
}
if (data.categoryLimit !== undefined) {
fields.push('category_limit = ?');
values.push(data.categoryLimit);
}
if (data.description !== undefined) {
fields.push('description = ?');
values.push(data.description);
}
if (data.status !== undefined) {
fields.push('status = ?');
values.push(Number(data.status));
}
if (fields.length > 0) {
values.push(id);
await pool.query(`UPDATE coupons SET ${fields.join(', ')} WHERE id = ?`, values);
}
const [rows]: any = await pool.query(`
SELECT
id, name, discount_amount as discountAmount, min_spend as minSpend,
valid_days as validDays, total_count as totalCount, received_count as receivedCount,
category_limit as categoryLimit, description, status,
created_at as createdAt, updated_at as updatedAt
FROM coupons
WHERE id = ?
`, [id]);
return rows[0] || null;
}
async deleteCoupon(id: number | string): Promise<boolean> {
const [res]: any = await pool.query('DELETE FROM coupons WHERE id = ?', [id]);
return res.affectedRows > 0;
}
async getArbitrations(): Promise<any[]> {