日常平台在运营的过程中,为了促进用户消费,提高平台的营收,通常会提供优惠券的功能。后台可以配置优惠券的发放规则,用户进入小程序可以自动发放优惠券。结合线下的拉新推广活动,用户可以提供优惠券的兑换码进行线上领取。本篇我们介绍一下优惠券的具体的开发过程。
1 创建数据源
我们设计两个表来实现优惠券信息的存取,一个是优惠券的配置表,一个是优惠券的领用表。
Table: coupons(优惠券定义表)
Description: 存储优惠券的基本定义和规则
Column Name | Data Type | Constraints | Description |
---|---|---|---|
id | BIGINT | PRIMARY KEY, AUTO_INCREMENT | 唯一标识符 |
coupon_name | VARCHAR(255) | NOT NULL | 优惠券名称 (例如: 保姆初体验80元红包券) |
coupon_type | ENUM | NOT NULL | 优惠券类型 (RED_PACKET: 红包, DISCOUNT: 折扣券, FULL_REDUCTION: 满减券) |
discount_value | DECIMAL(10, 2) | NOT NULL | 折扣值 (例如: 80, 100) |
min_order_amount | DECIMAL(10, 2) | DEFAULT 0.00 | 最低消费金额 (0表示无门槛) |
start_time | DATETIME | NOT NULL | 优惠券生效时间 |
end_time | DATETIME | NOT NULL | 优惠券过期时间 |
total_quantity | INT | DEFAULT 0 | 优惠券总发行量 (0表示不限制) |
redeemed_quantity | INT | DEFAULT 0 | 已核销数量 |
per_user_limit | INT | DEFAULT 1 | 每用户限领/限用次数 (0表示不限制) |
is_stackable | BOOLEAN | DEFAULT FALSE | 是否可与其他优惠券叠加使用 |
status | ENUM | NOT NULL | 优惠券状态 (ACTIVE: 活跃, INACTIVE: 未启用, EXPIRED: 已过期) |
created_at | DATETIME | NOT NULL | 创建时间 |
updated_at | DATETIME | NOT NULL | 最后更新时间 |
Table: user_coupons(用户优惠券表)
Description: 记录用户持有的优惠券实例及其状态
Column Name | Data Type | Constraints | Description |
---|---|---|---|
id | BIGINT | PRIMARY KEY, AUTO_INCREMENT | 唯一标识符 |
user_id | BIGINT | NOT NULL, INDEX | 用户ID (外键关联用户表) |
coupon_id | BIGINT | NOT NULL, INDEX | 关联的优惠券定义ID (外键关联coupons 表) |
receive_time | DATETIME | NOT NULL | 用户领取时间 |
expire_time | DATETIME | NOT NULL | 用户持有的优惠券过期时间 |
status | ENUM | NOT NULL | 优惠券状态 (UNUSED: 未使用, USED: 已使用, EXPIRED: 已过期) |
order_id | BIGINT | NULL, INDEX | 如果已使用,关联的订单ID |
used_time | DATETIME | NULL | 如果已使用,使用时间 |
created_at | DATETIME | NOT NULL | 创建时间 |
updated_at | DATETIME | NOT NULL | 最后更新时间 |
2 创建API
在我们的API模块,我们继续添加一个优惠券管理的API,来管理我们具体的方法
2.1 getAvailableUserCoupons (获取用户可用优惠券列表)
入参,需要传入用户的ID和订单的金额,来查询是否有符合要求的优惠券
具体代码如下:
// API 名称: getAvailableUserCoupons
// 描述: 获取当前用户可用于新订单的优惠券列表
// 参数:
// - userId: 用户ID (必填)
// - orderAmount: 当前订单总金额 (用于判断满减条件) (可选,但在实际场景中非常有用)
// - applicableServiceIds: 订单中包含的服务/商品ID列表 (可选,用于判断适用范围)
const ErrorCode = {
SUCCESS: 0,
PARAM_ERROR: 1001,
NOT_FOUND: 1002,
SYSTEM_ERROR: 1003,
USER_NOT_EXISTS: 1005,
MEMBER_NOT_EXISTS: 1006, // 沿用,但可能实际是 user_not_exists
INVALID_AMOUNT: 1007,
INSUFFICIENT_BALANCE: 1008,
ORDER_NOT_EXISTS: 1009,
ORDER_ALREADY_PAID: 1010,
// 新增优惠券相关错误码
COUPON_NOT_EXISTS: 2001,
COUPON_EXPIRED: 2002,
COUPON_UNAVAILABLE: 2003, // 优惠券未到生效时间或已发完
COUPON_ALREADY_RECEIVED: 2004,
COUPON_NOT_BELONGS_TO_USER: 2005,
COUPON_ALREADY_USED: 2006,
COUPON_APPLICABILITY_ERROR: 2007, // 优惠券不适用于当前订单
USER_COUPON_NOT_EXISTS: 2008, // 用户未持有该优惠券实例
MAX_RECEIVE_LIMIT_REACHED: 2009, // 达到领取上限
};
module.exports = async function (params, context) {
console.log('获取用户可用优惠券API入参:', params);
const { userId, orderAmount = 0 } = params;
if (!userId) {
return { code: ErrorCode.PARAM_ERROR, message: '用户ID不能为空' };
}
try {
// 1. 查询用户持有的未使用的、未过期的优惠券实例
const userCouponsResult = await context.callModel({
name: "user_coupons",
methodName: "wedaGetRecordsV2",
params: {
filter: {
where: {
user_id: { $eq: userId },
status: { $eq: '1' }, // 未使用
expire_time: { $gt: Date.now() } // 未过期
}
},
select: { $master: true }
}
});
if (!userCouponsResult || !userCouponsResult.records || userCouponsResult.records.length === 0) {
return { code: ErrorCode.SUCCESS, data: [], message: '暂无可用优惠券,本身没领' };
}
const userCouponIds = userCouponsResult.records.map(uc => uc.coupon_id);
// 2. 查询这些优惠券的定义信息,确保优惠券本身处于活跃状态且在有效期内
const couponDefinitions = await context.callModel({
name: "coupons",
methodName: "wedaGetRecordsV2",
params: {
filter: {
where: {
_id: { $in: userCouponIds },
status: { $eq: '1' }, // 优惠券定义本身必须是活跃的
start_time: { $lte: Date.now() }, // 优惠券已生效
end_time: { $gt: Date.now() } // 优惠券未过期
}
},
select: { $master: true }
}
});
if (!couponDefinitions || !couponDefinitions.records || couponDefinitions.records.length === 0) {
return { code: ErrorCode.SUCCESS, data: [], message: '暂无可用优惠券,状态不符合' };
}
const validCouponDefinitionsMap = new Map(couponDefinitions.records.map(c => [c._id, c]));
const availableCoupons = [];
for (const uc of userCouponsResult.records) {
const couponDef = validCouponDefinitionsMap.get(uc.coupon_id);
// 检查优惠券定义是否存在且有效
if (!couponDef) {
continue;
}
// 检查订单金额是否满足满减条件 (如果有传入 orderAmount)
if (orderAmount > 0 && couponDef.min_order_amount > 0 && orderAmount < couponDef.min_order_amount) {
continue; // 不满足满减条件,跳过
}
// 这里可以添加更复杂的适用性判断,例如:
// - 优惠券是否适用于 `applicableServiceIds` 中的服务/商品
// 简化版本假设优惠券适用所有服务,或适用性判断在前端完成
availableCoupons.push({
userCouponId: uc._id, // 用户持有的优惠券实例ID
couponId: couponDef._id,
couponName: couponDef.coupon_name,
couponType: couponDef.coupon_type,
discountValue: couponDef.discount_value,
minOrderAmount: couponDef.min_order_amount,
isStackable: couponDef.is_stackable,
expireTime: uc.expire_time // 使用用户持有的具体过期时间
});
}
return {
code: ErrorCode.SUCCESS,
data: availableCoupons,
message: '获取用户可用优惠券成功'
};
} catch (error) {
console.error('获取用户可用优惠券API错误:', error);
return {
code: ErrorCode.SYSTEM_ERROR,
message: `系统错误: ${error.message}`
};
}
};
2.2 receiveCoupon (领取优惠券)
除了系统自动派发优惠券外,我们还允许用户通过促销活动页面领取优惠券,入参传入用户ID和优惠券ID
代码:
// API 名称: receiveCoupon
// 描述: 用户领取优惠券
// 参数:
// - userId: 用户ID (必填)
// - couponId: 要领取的优惠券定义ID (必填)
const ErrorCode = {
SUCCESS: 0,
PARAM_ERROR: 1001,
NOT_FOUND: 1002,
SYSTEM_ERROR: 1003,
USER_NOT_EXISTS: 1005,
MEMBER_NOT_EXISTS: 1006, // 沿用,但可能实际是 user_not_exists
INVALID_AMOUNT: 1007,
INSUFFICIENT_BALANCE: 1008,
ORDER_NOT_EXISTS: 1009,
ORDER_ALREADY_PAID: 1010,
// 新增优惠券相关错误码
COUPON_NOT_EXISTS: 2001,
COUPON_EXPIRED: 2002,
COUPON_UNAVAILABLE: 2003, // 优惠券未到生效时间或已发完
COUPON_ALREADY_RECEIVED: 2004,
COUPON_NOT_BELONGS_TO_USER: 2005,
COUPON_ALREADY_USED: 2006,
COUPON_APPLICABILITY_ERROR: 2007, // 优惠券不适用于当前订单
USER_COUPON_NOT_EXISTS: 2008, // 用户未持有该优惠券实例
MAX_RECEIVE_LIMIT_REACHED: 2009, // 达到领取上限
};
module.exports = async function (params, context) {
console.log('领取优惠券API入参:', params);
const { userId, couponId } = params;
if (!userId || !couponId) {
return { code: ErrorCode.PARAM_ERROR, message: '用户ID和优惠券ID不能为空' };
}
try {
// 1. 查询优惠券定义
const couponDef = await context.callModel({
name: "coupons",
methodName: "wedaGetItemV2",
params: {
filter: {
where: { _id: { $eq: couponId } }
},
select: { $master: true }
}
});
if (!couponDef || !couponDef._id) {
return { code: ErrorCode.COUPON_NOT_EXISTS, message: '优惠券不存在' };
}
// 2. 检查优惠券状态和有效期
if (couponDef.status !== '1') {
return { code: ErrorCode.COUPON_UNAVAILABLE, message: '优惠券当前不可领取' };
}
if (couponDef.start_time > Date.now() || couponDef.end_time < Date.now()) {
return { code: ErrorCode.COUPON_EXPIRED, message: '优惠券未到领取时间或已过期' };
}
if (couponDef.total_quantity > 0 && couponDef.redeemed_quantity >= couponDef.total_quantity) {
return { code: ErrorCode.COUPON_UNAVAILABLE, message: '优惠券已发完' };
}
// 3. 检查用户是否已达到领取上限
if (couponDef.per_user_limit > 0) {
const userReceivedCount = await context.callModel({
name: "user_coupons",
methodName: "wedaGetRecordsV2",
params: {
filter: {
where: {
user_id: { $eq: userId },
coupon_id: { $eq: couponId }
}
},
getCount:true,
pageSize:10,
pagepageNumber:1
}
});
if (userReceivedCount.total > 0 && userReceivedCount.total >= couponDef.per_user_limit) {
return { code: ErrorCode.MAX_RECEIVE_LIMIT_REACHED, message: `您已达到此优惠券的领取上限(${couponDef.per_user_limit}张)` };
}
}
// 4. 创建用户优惠券实例
const userCouponData = {
user_id: { _id: userId }, // 假设user_id在模型中是关联类型
coupon_id: { _id: couponId }, // 假设coupon_id在模型中是关联类型
receive_time: Date.now(),
expire_time: couponDef.end_time, // 用户持有的优惠券过期时间继承自定义
status: '1'
};
const newUserCoupon = await context.callModel({
name: "user_coupons",
methodName: "wedaCreateV2",
params: {
data: userCouponData
}
});
// 5. 更新优惠券已发放数量 (可选,如果total_quantity需要精确控制)
// 注意:在高并发场景下,这里需要乐观锁或分布式锁来确保 `redeemed_quantity` 的准确性
await context.callModel({
name: "coupons",
methodName: "wedaUpdateV2",
params: {
data: { redeemed_quantity: couponDef.redeemed_quantity + 1 },
filter: { where: { _id: { $eq: couponId } } }
}
});
return {
code: ErrorCode.SUCCESS,
data: {
userCouponId: newUserCoupon._id,
couponName: couponDef.coupon_name,
expireTime: userCouponData.expire_time
},
message: '优惠券领取成功'
};
} catch (error) {
console.error('领取优惠券API错误:', error);
return {
code: ErrorCode.SYSTEM_ERROR,
message: `系统错误: ${error.message}`
};
}
};
3 后台功能
有了表和API后,需要给管理员搭建一个后台功能,进行优惠券的录入。打开我们的后台应用,创建页面,选择我们的优惠券表,选择左侧导航布局
然后切换到页面布局,添加菜单
修改菜单的名称
配置筛选器
配置后的最终效果
总结
本篇我们创建了优惠券相关的表和API,搭建了管理员的后台功能,下一篇我们介绍一下用户登录系统后主动领券,在下单的时候使用优惠券的功能。