'use strict'; const { CrudService } = require('naf-framework-mongoose-free/lib/service'); const { BusinessError, ErrorCode } = require('naf-core').Error; const _ = require('lodash'); const assert = require('assert'); const moment = require('moment'); const { ObjectId } = require('mongoose').Types; const Transaction = require('mongoose-transactions'); // class UserCouponService extends CrudService { constructor(ctx) { super(ctx, 'usercoupon'); this.model = this.ctx.model.User.UserCoupon; this.couponModel = this.ctx.model.Trade.Coupon; this.goodsSpecModel = this.ctx.model.Shop.GoodsSpec; this.tran = new Transaction(); } /** * 修改优惠券状态 * @param {Array} ids 用户领取的优惠券id列表 * @param {Transaction} tran 数据库事务实例 */ async useCoupon(ids, tran) { for (const id of ids) { tran.update('UserCoupon', id, { status: '1' }); } } /** * 领取优惠券 * @param {Object} params 地址参数 * @param params.coupon_id 优惠券id */ async getCoupon({ coupon_id }) { const coupon = await this.couponModel.findById(coupon_id); if (!coupon) throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '未找到优惠券'); const canGet = this.ctx.minus(coupon.num, 1) < 0; if (canGet) throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '您手慢了,没抢到'); const res = await this.checkCouponCanGet(coupon); if (!res.result) return res; const customer = _.get(this.ctx, 'user._id'); if (!customer) throw new BusinessError(ErrorCode.NOT_LOGIN, '未找到用户信息'); const data = { coupon, customer, shop: _.get(coupon, 'shop') }; try { // 领券=>减券库存 this.tran.insert('UserCoupon', data); this.tran.update('Coupon', coupon_id, { num: this.ctx.minus(coupon.num, 1) }); await this.tran.run(); } catch (error) { console.log(error); await this.tran.rollback(); throw new BusinessError(ErrorCode.SERVICE_FAULT, '领取优惠券失败'); } finally { this.tran.clean(); } } /** * 检查优惠券是否满足可领取的条件 * 可使用,就可以领取;否则只是垃圾数据,领了也用不了 * @param {Object} coupon 优惠券 */ async checkCouponCanGet(coupon) { const { expire_type, expire_config, get_limit_config, get_limit, _id } = coupon; // 1.检查优惠券是否生效: expire const tr = this.checkCanGet_expire(expire_type, expire_config); console.log(tr); if (!tr.result) return tr; // 2.不需要检查减免配置和使用配置,这两个配置是在使用的时候才检查 // 检查领取配置 const gr = await this.checkCanGet_get(get_limit, get_limit_config, _id); console.log(gr); if (!gr.result) return gr; return { result: true }; } /** * 检查优惠券的过期时间是否符合领取条件 * 是否过了 失效时间 * 只有时间段的情况才会有这个问题.领取后开始计算失效问题不需要检查 * @param {String} type 过期类型 * @param {Object} config 过期设置 */ checkCanGet_expire(type, config) { const res = { result: true }; if (type === 'fixed') { const end = _.last(_.get(config, 'fixed')); if (!end) throw new BusinessError(ErrorCode.DATA_INVALID, '优惠券设置错误'); const r = moment().isBefore(end); if (!r) { res.result = false; res.msg = '已超过优惠券可使用日期'; } } return res; } /** * 检查优惠券的领取设置是否符合领取条件 * nolimit直接放过,max需要查看confing里面的设置 * @param {String} type 领取类型 * @param {Object} config 领取设置 * @param {String} coupon 优惠券id */ async checkCanGet_get(type, config, coupon) { const res = { result: true }; if (type === 'nolimit') return res; const max = _.get(config, type); // 需要找到用户 const customer = _.get(this.ctx, 'user._id'); if (!customer) throw new BusinessError(ErrorCode.NOT_LOGIN, '未找到用户信息'); const num = await this.model.count({ 'coupon._id': coupon, customer }); if (num >= max) { res.result = false; res.msg = '该优惠券已到达领取上限'; } return res; } /** * 根据用户和下单页的商品,对用户的优惠券进行检查,整理数据 * @param {Array} goods 商品列表 */ async toMakeOrder_getList(goods) { const customer = _.get(this.ctx, 'user._id'); if (!customer) throw new BusinessError(ErrorCode.NOT_LOGIN, '未找到用户信息'); const { populate } = this.getRefMods(); let couponList = await this.model.find({ customer, status: '0' }, { customer: 0 }).populate(populate); couponList = JSON.parse(JSON.stringify(couponList)); // 接下来检查哪些优惠券符合条件 for (const uc of couponList) { // 1.检查失效问题 let r = this.checkCanUse_expire(uc); if (!r) { uc.canUse = false; continue; } // 2.检查使用问题 r = this.checkCanUse_use(uc, goods); if (!r) { uc.canUse = false; continue; } // 3.检查是否满足优惠券是用条件 r = this.checkCanUse_discount(uc, goods); if (!r) { uc.canUse = false; continue; } uc.canUse = true; } couponList = couponList.filter((f) => f.canUse); if (couponList.length <= 0) return []; couponList = await this.makeShowData(couponList); return couponList; } /** * 检查优惠券的过期时间是否符合使用条件 * @param {Object} userCoupon 用户领取的优惠券 */ checkCanUse_expire(userCoupon) { const { coupon, meta } = userCoupon; const { expire_config, expire_type } = coupon; if (expire_type === 'fixed') { // 在设置的时间段内,则允许使用 const start = _.head(_.get(expire_config, 'fixed')); const end = _.last(_.get(expire_config, 'fixed')); if (!start || !end) throw new BusinessError(ErrorCode.DATA_INVALID, '优惠券设置错误'); return moment().isBetween(start, end, null, '[]'); } else if (expire_type === 'days') { let getDate = _.get(meta, 'createdAt'); if (!getDate) return false; // 计算出过期时间, 再看下当前日期是否超过该日期 getDate = moment(getDate).format('YYYY-MM-DD HH:mm:ss'); const days = _.get(expire_config, 'days'); const expire_date = moment(getDate).add(days, 'days'); return moment().isSameOrBefore(expire_date); } else if (expire_type === 'nolimit') return true; // 没有过期日期;可以使用 return false; } /** * 检查该优惠券是否可以在这些商品中使用 * @param {Object} userCoupon 用户领取的优惠券 * @param {Array} shopGoods 按店铺分组的商品列表 */ checkCanUse_use(userCoupon, shopGoods) { const { coupon } = userCoupon; const { use_limit, use_limit_config = {}, shop } = coupon; const { tags = [] } = use_limit_config; // 全类型商品,不需要继续检查 if (use_limit === 'all' || tags.length === 0) return true; // 检查的所有商品中,只要有一个可以使用,就放过 let result = false; for (const s of shopGoods) { // 检查优惠券是否是店铺发行,如果是店铺发行; // 则需要在指定店铺使用,如果不是,则直接检查下一个 if (shop && shop !== s.shop) continue; // 是这个店铺的优惠券,则需要检查这个店铺下的商品是否能用这个优惠券 // 目前只有商品分类(tags标签)来进行设置 // 商品列表(goods)中只要有 标签(tags) 中能找到 和 tags: /** e.g.: const arr = [ ['xxsp', 'jgch', 'htr'], ['xxsp', 'jgch', 'ht'], ]; const arr1 = [ ['xxsp', 'jgch', 'ht1r'], ['xxsp', 'jgch', 'ht'], ]; const r = arr.findIndex((f) => arr1.find((ff) => _.isEqual(f, ff))); console.log(r); */ const r = s.goods.some((g) => g.tags && g.tags.find((f) => tags && tags.find((ff) => _.isEqual(f, ff)))); if (r) { result = true; break; } } return result; } /** * 检查该优惠券是否可以在这些商品中使用 * @param {Object} userCoupon 用户领取的优惠券 * @param {Array} shopGoods 按店铺分组的商品列表 */ checkCanUse_discount(userCoupon, shopGoods) { const { coupon } = userCoupon; const { discount_config = {} } = coupon; const limit = _.get(discount_config, 'limit'); if (limit === 'nolimit') return true; const goods_total = shopGoods.reduce((p, n) => this.ctx.plus(p, this.ctx.plus(n.discount, this.ctx.plus(n.goods_total, n.freight_total))), 0); if (this.ctx.minus(goods_total, limit) >= 0) return true; return false; } /** * 计算订单使用优惠券的明细 * @param {Array} couponIds 订单使用的用户领取的优惠券数据id数组 * @param {Array} goodsSpecs 订单所有商品规格的数据:{id,buy_num,sell_money,price} */ async computedOrderCouponDiscount(couponIds, goodsSpecs) { const result = {}; // 最终结果: 以 couponId: { goodsSpecId: ${money} } 为明细格式 const goodsSpecIds = goodsSpecs.map((i) => i.id); const { populate: gp } = this.ctx.service.shop.goodsSpec.getRefMods(); let goodsSpecList = await this.goodsSpecModel.find({ _id: goodsSpecIds }).populate(gp); // 新价格和新整理的数据结合 goodsSpecList = this.resetGoodsSpecData(goodsSpecList, goodsSpecs); const { populate: cp } = this.getRefMods(); let userCouponList = await this.model.find({ _id: couponIds }, { customer: 0 }).populate(cp); // 先查下有没有不能用的,有不能用的需要返回 let cantUse = userCouponList.find((f) => f.status !== '0'); if (cantUse) throw new BusinessError(ErrorCode.DATA_INVALID, `优惠券:${_.get(cantUse, 'coupon.name')}已使用`); cantUse = userCouponList.find((f) => !this.checkCanUse_expire(f)); if (cantUse) throw new BusinessError(ErrorCode.DATA_INVALID, `优惠券:${_.get(cantUse, 'coupon.name')}不能使用`); userCouponList = this.resetUserCouponData(userCouponList); // 上面整理完数据之后,下面开始用 均分优惠金额 for (const coupon of userCouponList) { // 可以被优惠的商品列表 let goodsList = []; // 1.先找到满足优惠条件的商品 const { issue, shop, use_limit, use_limit_config, _id } = coupon; // 平台券,都可以 if (issue === '0') goodsList = goodsSpecList; // 店铺券,需要过滤店铺 else goodsList = goodsSpecList.filter((f) => ObjectId(shop).equals(_.get(f, 'shop._id'))); // 2.商品已确定,再确定是否是指定分类 if (use_limit !== 'all') { // all的情况不需要再次过滤出不符合的条件: all为全部商品类型 const tags = _.get(use_limit_config, 'tags', []); goodsList = this.getGoodsInTags(goodsSpecList, tags); } // 3.开始分配 const discount_detail = this.divideByCoupon(coupon, goodsList); result[_id] = discount_detail; } // console.log(result); return result; } /** * 分配减免 * @param {Object} coupon 优惠券{_id:用户领取的优惠券id,...} * @param {Array} goodsList 过滤完后,符合条件的 本订单中的规格商品列表 */ divideByCoupon(coupon, goodsList) { const { discount_type, discount_config } = coupon; const { limit, min, max } = discount_config; // 符合条件的商品的总价 const goodsTotal = goodsList.reduce((p, n) => this.ctx.plus(p, n.goods_total), 0); if (limit !== 'nolimit') { // 有最低消费的限制,就需要看看下满足条件的商品够不够这个金额 // 消费下限 大于 满足优惠券使用的商品的消费总额,则不能使用这个券,消费的钱不够 // 且这个地方应该报错 if (this.ctx.minus(limit, goodsTotal) > 0) throw new BusinessError(ErrorCode.DATA_INVALID, `消费券:${coupon.name}不满足使用条件`); } // 减免设置转换成 实际减免总金额: // 满减则直接就是 min(设置的满x减y的y); // 折扣的话,需要计算,是否超过折扣上限(max): // 超过上限则 为上限设置的金额,和满减一样按比例分配即可; // 没超过上限,则为各自去打折即可 let discountRealMoney = 0; let outLimit = true; // 是否超过金额上限 // if (discount_type === 'min') discountRealMoney = this.ctx.toNumber(min); else { // 需要计算是否超过上限 // 没有上限,那就不会超过 if (!max) outLimit = false; else { // 有上限,则需要计算 const percent = this.ctx.minus(1, this.ctx.divide(min, 10)); // 计算实际折扣 减了 多少钱 const discountMoney = this.ctx.multiply(goodsTotal, percent); // 最后看下 不限制减的钱 是不是 超过了 上限 outLimit = this.ctx.minus(discountMoney, max) > 0; // 超过了,就将上限的金额拿去按比例分配 if (outLimit) discountRealMoney = this.ctx.toNumber(max); } } // 根据是否超过上限金额来决定策略: // 超过上限金额, 按 每种货物的总价值 占 这些货物应该交的钱 的比例进行优惠分配 // 没超过上限(目前:只有折扣会有这个情况), 则每个货物都按比例来计算 if (!outLimit) return this.discountPercentForEachGoods(coupon, goodsList); return this.divideProportionForEachGoods(goodsList, discountRealMoney, goodsTotal); } /** * 按比例计算每件商品,这里就不需要优惠券什么事了.需要优惠券的东西参数都带来了 * @param {Array} goodsList 过滤完后,符合条件的 本订单中的规格商品列表 * @param {Number} discountRealMoney 需要按比例分配的优惠金额 * @param {Number} goodsTotal goodsList的商品总金额 */ divideProportionForEachGoods(goodsList, discountRealMoney, goodsTotal) { const result = {}; // 已分配优惠的金额的总数 let allReadyDiscount = 0; for (let i = 0; i < goodsList.length; i++) { const g = goodsList[i]; const { goods_total } = g; const id = _.get(g, 'goodsSpec._id'); let dm = 0; // 除了最后一个商品,其余的商品均按比例分配 if (i < goodsList.length - 1) { const percent = this.ctx.divide(goods_total, goodsTotal); dm = this.ctx.multiply(discountRealMoney, percent); allReadyDiscount += dm; } else { // 最后一个商品是剩余的可分配的钱 dm = this.ctx.minus(discountRealMoney, allReadyDiscount); } result[id] = dm; } return result; } /** * 按折扣计算每件商品 * @param {Object} coupon 优惠券{_id:用户领取的优惠券id,...} * @param {Array} goodsList 过滤完后,符合条件的 本订单中的规格商品列表 */ discountPercentForEachGoods(coupon, goodsList) { const { discount_config } = coupon; const { min } = discount_config; const percent = this.ctx.multiply(min, 10); const result = {}; for (const g of goodsList) { const { goods_total } = g; const id = _.get(g, 'goodsSpec._id'); const realPay = this.ctx.multiply(goods_total, percent); const discountMoney = this.ctx.minus(goods_total, realPay); result[id] = discountMoney; } return result; } /** * 过滤出 订单中商品规格数据 在这些标签中的 商品规格 * @param {Array} goodsSpecList 订单中商品规格列表 * @param {Array} tags 商品标签 */ getGoodsInTags(goodsSpecList, tags) { const arr = goodsSpecList.filter((f) => { const gtags = _.get(f, 'goods.tags'); // 没打标签或者标签里没有内容的直接过滤出去 if (!gtags || gtags.length <= 0) return false; const r = gtags.find((f) => tags.find((ff) => _.isEqual(ff, f))); return r; }); return arr; } /** * 整理商品规格列表数据 * 将goods,shop和goodsSpec重组为3个属性在object同一级上 * @param {Array} list 商品规格列表 * @param {Array} orderInfo 有关订单的信息 {id,buy_num,sell_money,price} */ resetGoodsSpecData(list, orderInfo) { const priceKey = 'price'; const nouse = ['meta', '__v']; const arr = list.map((i) => { const obj = {}; const goodsSpec = _.omit(i, [...nouse, 'goods']); const shop = _.omit(_.get(i, 'goods.shop'), [...nouse]); const goods = _.omit(_.get(i, 'goods'), [...nouse, 'shop']); obj.goodsSpec = goodsSpec; obj.shop = shop; obj.goods = goods; const r = orderInfo.find((f) => ObjectId(f.id).equals(goodsSpec._id)); // 将变化的价格赋上去:传来价格变动就用新的价格;没有就用设置的销售价 if (r) { obj.buy_num = r.buy_num; obj.goodsSpec.price = r.price; } else obj.goodsSpec.price = obj.sell_money; if (obj.buy_num) { // 计算 常规/团购 商品总价 obj.goods_total = this.ctx.multiply(obj.buy_num, _.get(obj.goodsSpec, priceKey)); } return obj; }); return arr; } /** * 整理出下面所需要使用的数据,并将这些数据放在object的第一级上 * @param {Array} list 用户优惠券列表 */ resetUserCouponData(list) { const arr = list.map((i) => { const { _id, meta, coupon } = i; const cd = _.pick(coupon, [ 'issue', 'shop', 'expire_type', 'expire_config', 'discount_type', 'discount_config', 'use_limit', 'use_limit_config', 'get_limit', 'get_limit_config', 'name', ]); const obj = { _id, meta, ...cd }; return obj; }); return arr; } // 转换为显示的格式 async makeShowData(couponList) { const dictDataModel = this.ctx.model.Dev.DictData; // 过期字典 const expList = await dictDataModel.find({ code: 'coupon_expire_type' }); // 减免字典 const disList = await dictDataModel.find({ code: 'coupon_discount_type' }); // 使用字典 const useList = await dictDataModel.find({ code: 'coupon_use_limit' }); // 领取字典 const getList = await dictDataModel.find({ code: 'coupon_get_limit' }); couponList = couponList.map((i) => { const { _id, canUse, coupon, meta, status } = i; let obj = { _id, canUse, name: _.get(coupon, 'name'), status }; // 失效 const expire_type = _.get(coupon, 'expire_type'); const expire_type_label = _.get( expList.find((f) => f.value === expire_type), 'label' ); let expire_time = _.get(coupon, `expire_config.${expire_type}`); if (expire_time) { if (expire_type === 'fixed') expire_time = expire_time.join(' 至 '); else { expire_time = moment(meta.createdAt).add(expire_time, 'days').format('YYYY-MM-DD HH:mm:ss'); } } obj = { ...obj, expire_type, expire_type_label, expire_time }; // 减免 const discount_type = _.get(coupon, 'discount_type'); const discount_type_label = _.get( disList.find((f) => f.value === discount_type), 'label' ); const discount_config = _.get(coupon, 'discount_config'); obj = { ...obj, discount_type, discount_type_label, discount_config }; // 使用 const use_limit = _.get(coupon, 'use_limit'); const use_limit_label = _.get( useList.find((f) => f.value === use_limit), 'label' ); const use_limit_config = _.get(coupon, 'use_limit_config'); obj = { ...obj, use_limit, use_limit_label, use_limit_config }; // 领取 const get_limit = _.get(coupon, 'get_limit'); const get_limit_label = _.get( getList.find((f) => f.value === get_limit), 'label' ); const get_limit_config = _.get(coupon, 'get_limit_config'); obj = { ...obj, get_limit, get_limit_label, get_limit_config }; return obj; }); return couponList; } async afterQuery(filter, data) { data = JSON.parse(JSON.stringify(data)); data = this.makeShowData(data); return data; } /** * 批量发送优惠券 * @param {Object} body 参数体 * @param {Array} body.users 用户列表 * @param {string} body.coupon 要发送的优惠券 * @param {Number} body.num 要发送的优惠券数量 */ async giveCoupon({ users, coupon, num = 1 }) { const couponData = await this.couponModel.findById(coupon).lean(); if (!couponData) throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '未找到优惠券'); if (couponData.status !== '0') throw new BusinessError(ErrorCode.DATA_INVALID, '该优惠券处于禁用状态'); let loop = 0; if (_.get(couponData, 'get_limit') === 'nolimit') loop = num; else { const limit = _.get(couponData, 'get_limit_config.max'); if (this.ctx.minus(num, limit) > 0) loop = limit; else loop = num; } let giveNum = 0; for (const u of users) { try { const obj = { customer: u, shop: _.get(couponData, 'shop'), coupon: couponData }; for (let i = 0; i < loop; i++) { await this.model.create(obj); giveNum++; } } catch (error) { console.log(error); } } const newNum = this.ctx.minus(couponData.num, giveNum); await this.couponModel.updateOne({ _id: couponData._id }, { num: newNum }); } } module.exports = UserCouponService;