feat(marketing): implement recharge tiers and coupon management across admin, server, and uniapp
这个提交包含在:
@@ -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[]> {
|
||||
|
||||
在新工单中引用
屏蔽一个用户