123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550 |
- '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;
|