日常平台在运营的过程中,为了促进用户消费,提高平台的营收,通常会提供优惠券的功能。后台可以配置优惠券的发放规则,用户进入小程序可以自动发放优惠券。结合线下的拉新推广活动,用户可以提供优惠券的兑换码进行线上领取。本篇我们介绍一下优惠券的具体的开发过程。

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._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
// 描述: 用户领取优惠券
家政维修平台实战32优惠券系统
// 参数:
//   - 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,搭建了管理员的后台功能。