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 次删除

查看文件

@@ -0,0 +1,104 @@
// 已读取AGENTS.md
import { ContentSecurityService } from '../security/content-security.service';
export interface RechargeTierItem {
id: number;
amount: number;
bonusAmount: number;
tag?: string;
sortOrder: number;
status: number;
}
export interface CouponItem {
id: number;
name: string;
discountAmount: number;
minSpend: number;
validDays: number;
totalCount: number;
receivedCount: number;
categoryLimit: string;
description?: string;
status: number;
}
export class MarketingService {
private tiers: RechargeTierItem[] = [
{ id: 1, amount: 50, bonusAmount: 0, tag: '基础充值', sortOrder: 1, status: 1 },
{ id: 2, amount: 100, bonusAmount: 5, tag: '热门畅享', sortOrder: 2, status: 1 },
{ id: 3, amount: 200, bonusAmount: 15, tag: '超值加赠', sortOrder: 3, status: 1 },
{ id: 4, amount: 500, bonusAmount: 50, tag: '尊享特惠', sortOrder: 4, status: 1 },
{ id: 5, amount: 1000, bonusAmount: 120, tag: '土豪专享', sortOrder: 5, status: 1 }
];
private coupons: CouponItem[] = [
{ id: 1, name: '新用户注册专享立减券', discountAmount: 20, minSpend: 50, validDays: 15, totalCount: 2000, receivedCount: 128, categoryLimit: 'ALL', description: '全品类满50可用', status: 1 },
{ id: 2, name: '周末同城聚会AA抵扣券', discountAmount: 15, minSpend: 80, validDays: 7, totalCount: 1000, receivedCount: 85, categoryLimit: 'MEAL', description: '限美食探店使用', status: 1 },
{ id: 3, name: '工体K歌麦霸欢唱减免券', discountAmount: 30, minSpend: 120, validDays: 10, totalCount: 800, receivedCount: 62, categoryLimit: 'SING', description: '限KTV陪唱使用', status: 1 }
];
private nextTierId = 6;
private nextCouponId = 4;
private secService = new ContentSecurityService();
// 充值阶梯
getActiveTiers(): RechargeTierItem[] {
return this.tiers.filter((t) => t.status === 1).sort((a, b) => a.sortOrder - b.sortOrder);
}
createTier(data: { amount: number; bonusAmount?: number; tag?: string; sortOrder?: number; status?: number }): RechargeTierItem {
if (!data.amount || data.amount <= 0) {
throw new Error('充值金额必须大于0');
}
const item: RechargeTierItem = {
id: this.nextTierId++,
amount: Number(data.amount),
bonusAmount: Number(data.bonusAmount || 0),
tag: data.tag || '',
sortOrder: Number(data.sortOrder || 0),
status: data.status !== undefined ? Number(data.status) : 1
};
this.tiers.push(item);
return item;
}
// 优惠券业务
getActiveCoupons(category?: string): CouponItem[] {
return this.coupons
.filter((c) => c.status === 1)
.filter((c) => !category || category === 'ALL' || c.categoryLimit === 'ALL' || c.categoryLimit === category)
.sort((a, b) => b.discountAmount - a.discountAmount);
}
createCoupon(data: { name: string; discountAmount: number; minSpend?: number; validDays?: number; totalCount?: number; categoryLimit?: string; description?: string; status?: number }): CouponItem {
if (!data.name || !data.name.trim()) {
throw new Error('优惠券名称不能为空');
}
if (!data.discountAmount || data.discountAmount <= 0) {
throw new Error('减免面额必须大于0');
}
const sec = this.secService.checkText(data.name + ' ' + (data.description || ''));
if (!sec.pass) {
const err = new Error(sec.message);
(err as any).code = sec.code;
throw err;
}
const item: CouponItem = {
id: this.nextCouponId++,
name: data.name.trim(),
discountAmount: Number(data.discountAmount),
minSpend: Number(data.minSpend || 0),
validDays: Number(data.validDays || 7),
totalCount: Number(data.totalCount || 1000),
receivedCount: 0,
categoryLimit: data.categoryLimit || 'ALL',
description: data.description || '',
status: data.status !== undefined ? Number(data.status) : 1
};
this.coupons.push(item);
return item;
}
}

查看文件

@@ -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[]> {

查看文件

@@ -863,6 +863,194 @@ app.delete('/api/admin/nav-categories/:id', async (req: Request, res: Response)
}
});
// ===================== 营销中心 API =====================
// 1. 充值阶梯接口
// 移动端:获取启用的充值阶梯
app.get('/api/marketing/recharge-tiers', async (_req: Request, res: Response) => {
try {
const list = await repo.getRechargeTiers();
res.json({ code: 20000, data: list });
} catch (err: any) {
res.status(500).json({ code: 50000, message: err.message });
}
});
// 管理后台:获取全部充值阶梯
app.get('/api/admin/recharge-tiers', async (_req: Request, res: Response) => {
try {
const list = await repo.getAdminRechargeTiers();
res.json({ code: 20000, data: list });
} catch (err: any) {
res.status(500).json({ code: 50000, message: err.message });
}
});
// 管理后台:新增充值阶梯
app.post('/api/admin/recharge-tiers', async (req: Request, res: Response) => {
try {
const { amount, bonusAmount, tag, sortOrder, status } = req.body;
if (amount === undefined || Number(amount) <= 0) {
return res.status(400).json({ code: 40000, message: '充值金额必须大于0' });
}
const tier = await repo.createRechargeTier({
amount: Number(amount),
bonusAmount: bonusAmount !== undefined ? Number(bonusAmount) : 0,
tag: tag || '',
sortOrder: sortOrder !== undefined ? Number(sortOrder) : 0,
status: status !== undefined ? Number(status) : 1
});
res.json({ code: 20000, data: tier, message: '充值阶梯创建成功' });
} catch (err: any) {
res.status(400).json({ code: 40000, message: err.message });
}
});
// 管理后台:修改充值阶梯
app.put('/api/admin/recharge-tiers/:id', async (req: Request, res: Response) => {
try {
const tierId = String(req.params.id);
const { amount, bonusAmount, tag, sortOrder, status } = req.body;
const updated = await repo.updateRechargeTier(tierId, {
amount: amount !== undefined ? Number(amount) : undefined,
bonusAmount: bonusAmount !== undefined ? Number(bonusAmount) : undefined,
tag,
sortOrder: sortOrder !== undefined ? Number(sortOrder) : undefined,
status: status !== undefined ? Number(status) : undefined
});
if (!updated) {
return res.status(404).json({ code: 40400, message: '充值阶梯不存在或已被删除' });
}
res.json({ code: 20000, data: updated, message: '充值阶梯修改成功' });
} catch (err: any) {
res.status(400).json({ code: 40000, message: err.message });
}
});
// 管理后台:删除充值阶梯
app.delete('/api/admin/recharge-tiers/:id', async (req: Request, res: Response) => {
try {
const tierId = String(req.params.id);
const ok = await repo.deleteRechargeTier(tierId);
if (!ok) {
return res.status(404).json({ code: 40400, message: '充值阶梯不存在或已被删除' });
}
res.json({ code: 20000, data: null, message: '充值阶梯已删除' });
} catch (err: any) {
res.status(400).json({ code: 40000, message: err.message });
}
});
// 2. 优惠券接口
// 移动端:领券中心或下单结算可用券列表
app.get('/api/marketing/coupons', async (req: Request, res: Response) => {
try {
const category = req.query.category ? String(req.query.category) : undefined;
const list = await repo.getCoupons(category);
res.json({ code: 20000, data: list });
} catch (err: any) {
res.status(500).json({ code: 50000, message: err.message });
}
});
// 管理后台:获取全部优惠券
app.get('/api/admin/coupons', async (_req: Request, res: Response) => {
try {
const list = await repo.getAdminCoupons();
res.json({ code: 20000, data: list });
} catch (err: any) {
res.status(500).json({ code: 50000, message: err.message });
}
});
// 管理后台:新增优惠券
app.post('/api/admin/coupons', async (req: Request, res: Response) => {
try {
const { name, discountAmount, minSpend, validDays, totalCount, categoryLimit, description, status } = req.body;
if (!name || !name.trim()) {
return res.status(400).json({ code: 40000, message: '优惠券名称不能为空' });
}
if (!discountAmount || Number(discountAmount) <= 0) {
return res.status(400).json({ code: 40000, message: '减免面额必须大于0' });
}
// 微信内容安全检测
const sec = contentSecurityService.checkText(name + ' ' + (description || ''));
if (!sec.pass) {
return res.status(400).json({ code: sec.code, message: sec.message });
}
const coupon = await repo.createCoupon({
name: name.trim(),
discountAmount: Number(discountAmount),
minSpend: minSpend !== undefined ? Number(minSpend) : 0,
validDays: validDays !== undefined ? Number(validDays) : 7,
totalCount: totalCount !== undefined ? Number(totalCount) : 1000,
categoryLimit: categoryLimit || 'ALL',
description: description || '',
status: status !== undefined ? Number(status) : 1
});
res.json({ code: 20000, data: coupon, message: '优惠券创建成功' });
} catch (err: any) {
res.status(400).json({ code: 40000, message: err.message });
}
});
// 管理后台:修改优惠券
app.put('/api/admin/coupons/:id', async (req: Request, res: Response) => {
try {
const couponId = String(req.params.id);
const { name, discountAmount, minSpend, validDays, totalCount, categoryLimit, description, status } = req.body;
if (name) {
const sec = contentSecurityService.checkText(name + ' ' + (description || ''));
if (!sec.pass) {
return res.status(400).json({ code: sec.code, message: sec.message });
}
}
const updated = await repo.updateCoupon(couponId, {
name: name ? name.trim() : undefined,
discountAmount: discountAmount !== undefined ? Number(discountAmount) : undefined,
minSpend: minSpend !== undefined ? Number(minSpend) : undefined,
validDays: validDays !== undefined ? Number(validDays) : undefined,
totalCount: totalCount !== undefined ? Number(totalCount) : undefined,
categoryLimit,
description,
status: status !== undefined ? Number(status) : undefined
});
if (!updated) {
return res.status(404).json({ code: 40400, message: '优惠券不存在或已被删除' });
}
res.json({ code: 20000, data: updated, message: '优惠券修改成功' });
} catch (err: any) {
res.status(400).json({ code: 40000, message: err.message });
}
});
// 管理后台:删除优惠券
app.delete('/api/admin/coupons/:id', async (req: Request, res: Response) => {
try {
const couponId = String(req.params.id);
const ok = await repo.deleteCoupon(couponId);
if (!ok) {
return res.status(404).json({ code: 40400, message: '优惠券不存在或已被删除' });
}
res.json({ code: 20000, data: null, message: '优惠券已删除' });
} catch (err: any) {
res.status(400).json({ code: 40000, message: err.message });
}
});
// 创建新订单 (落盘 MySQL)
app.post('/api/orders', async (req: Request, res: Response) => {
try {