userCoupon.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453
  1. 'use strict';
  2. const { CrudService } = require('naf-framework-mongoose-free/lib/service');
  3. const { BusinessError, ErrorCode } = require('naf-core').Error;
  4. const _ = require('lodash');
  5. const assert = require('assert');
  6. const moment = require('moment');
  7. const { ObjectId } = require('mongoose').Types;
  8. const Transaction = require('mongoose-transactions');
  9. //
  10. class UserCouponService extends CrudService {
  11. constructor(ctx) {
  12. super(ctx, 'usercoupon');
  13. this.model = this.ctx.model.User.UserCoupon;
  14. this.couponModel = this.ctx.model.Trade.Coupon;
  15. this.goodsSpecModel = this.ctx.model.Shop.GoodsSpec;
  16. this.tran = new Transaction();
  17. }
  18. /**
  19. * 修改优惠券状态
  20. * @param {Array} ids 用户领取的优惠券id列表
  21. * @param {Transaction} tran 数据库事务实例
  22. */
  23. async useCoupon(ids, tran) {
  24. for (const id of ids) {
  25. tran.update('UserCoupon', id, { status: '1' });
  26. }
  27. }
  28. /**
  29. * 领取优惠券
  30. * @param {Object} params 地址参数
  31. * @param params.coupon_id 优惠券id
  32. */
  33. async getCoupon({ coupon_id }) {
  34. const coupon = await this.couponModel.findById(coupon_id);
  35. if (!coupon) throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '未找到优惠券');
  36. const canGet = this.ctx.minus(coupon.num, 1) < 0;
  37. if (canGet) throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '您手慢了,没抢到');
  38. const res = await this.checkCouponCanGet(coupon);
  39. if (!res.result) return res;
  40. const customer = _.get(this.ctx, 'user._id');
  41. if (!customer) throw new BusinessError(ErrorCode.NOT_LOGIN, '未找到用户信息');
  42. const data = { coupon: coupon_id, customer, shop: _.get(coupon, 'shop') };
  43. try {
  44. // 领券=>减券库存
  45. this.tran.insert('UserCoupon', data);
  46. this.tran.update('Coupon', coupon_id, { num: this.ctx.minus(coupon.num, 1) });
  47. await this.tran.run();
  48. } catch (error) {
  49. console.log(error);
  50. await this.tran.rollback();
  51. throw new BusinessError(ErrorCode.SERVICE_FAULT, '领取优惠券失败');
  52. } finally {
  53. this.tran.clean();
  54. }
  55. }
  56. /**
  57. * 检查优惠券是否满足可领取的条件
  58. * 可使用,就可以领取;否则只是垃圾数据,领了也用不了
  59. * @param {Object} coupon 优惠券
  60. */
  61. async checkCouponCanGet(coupon) {
  62. const { expire_type, expire_config, get_limit_config, get_limit, _id } = coupon;
  63. // 1.检查优惠券是否生效: expire
  64. const tr = this.checkCanGet_expire(expire_type, expire_config);
  65. if (!tr.result) return tr;
  66. // 2.不需要检查减免配置和使用配置,这两个配置是在使用的时候才检查
  67. // 检查领取配置
  68. const gr = this.checkCanGet_get(get_limit, get_limit_config, _id);
  69. if (!gr.result) return gr;
  70. return { result: true };
  71. }
  72. /**
  73. * 检查优惠券的过期时间是否符合领取条件
  74. * 是否过了 失效时间
  75. * 只有时间段的情况才会有这个问题.领取后开始计算失效问题不需要检查
  76. * @param {String} type 过期类型
  77. * @param {Object} config 过期设置
  78. */
  79. checkCanGet_expire(type, config) {
  80. const res = { result: true };
  81. if (type === 'fixed') {
  82. const end = _.last(_.get(config, 'fixed'));
  83. if (!end) throw new BusinessError(ErrorCode.DATA_INVALID, '优惠券设置错误');
  84. const r = moment().isBefore(end);
  85. if (!r) {
  86. res.result = false;
  87. res.msg = '已超过优惠券可使用日期';
  88. }
  89. }
  90. return res;
  91. }
  92. /**
  93. * 检查优惠券的领取设置是否符合领取条件
  94. * nolimit直接放过,max需要查看confing里面的设置
  95. * @param {String} type 领取类型
  96. * @param {Object} config 领取设置
  97. * @param {String} coupon 优惠券id
  98. */
  99. async checkCanGet_get(type, config, coupon) {
  100. const res = { result: true };
  101. if (type === 'nolimit') return res;
  102. const max = _.get(config, type);
  103. // 需要找到用户
  104. const customer = _.get(this.ctx, 'user._id');
  105. if (!customer) throw new BusinessError(ErrorCode.NOT_LOGIN, '未找到用户信息');
  106. const num = await this.model.count({ coupon, user: customer });
  107. if (num >= max) {
  108. res.result = false;
  109. res.msg = '该优惠券已到达领取上限';
  110. }
  111. return res;
  112. }
  113. /**
  114. * 根据用户和下单页的商品,对用户的优惠券进行检查,整理数据
  115. * @param {Array} goods 商品列表
  116. */
  117. async toMakeOrder_getList(goods) {
  118. const customer = _.get(this.ctx, 'user._id');
  119. if (!customer) throw new BusinessError(ErrorCode.NOT_LOGIN, '未找到用户信息');
  120. const { populate } = this.getRefMods();
  121. let couponList = await this.model.find({ customer, status: '0' }, { customer: 0 }).populate(populate);
  122. couponList = JSON.parse(JSON.stringify(couponList));
  123. // 接下来检查哪些优惠券符合条件
  124. for (const uc of couponList) {
  125. // 1.检查失效问题
  126. let r = this.checkCanUse_expire(uc);
  127. if (!r) {
  128. uc.canUse = false;
  129. continue;
  130. }
  131. // 2.检查使用问题
  132. r = this.checkCanUse_use(uc, goods);
  133. if (!r) {
  134. uc.canUse = false;
  135. continue;
  136. }
  137. // 3.检查是否满足优惠券是用条件
  138. r = this.checkCanUse_discount(uc, goods);
  139. if (!r) {
  140. uc.canUse = false;
  141. continue;
  142. }
  143. uc.canUse = true;
  144. }
  145. return couponList;
  146. }
  147. /**
  148. * 检查优惠券的过期时间是否符合使用条件
  149. * @param {Object} userCoupon 用户领取的优惠券
  150. */
  151. checkCanUse_expire(userCoupon) {
  152. const { coupon, meta } = userCoupon;
  153. const { expire_config, expire_type } = coupon;
  154. if (expire_type === 'fixed') {
  155. // 在设置的时间段内,则允许使用
  156. const start = _.head(_.get(expire_config, 'fixed'));
  157. const end = _.last(_.get(expire_config, 'fixed'));
  158. if (!start || !end) throw new BusinessError(ErrorCode.DATA_INVALID, '优惠券设置错误');
  159. return moment().isBetween(start, end, null, '[]');
  160. } else if (expire_type === 'days') {
  161. let getDate = _.get(meta, 'createdAt');
  162. if (!getDate) return false;
  163. // 计算出过期时间, 再看下当前日期是否超过该日期
  164. getDate = moment(getDate).format('YYYY-MM-DD HH:mm:ss');
  165. const days = _.get(expire_config, 'days');
  166. const expire_date = moment(getDate).add(days, 'days');
  167. return moment().isSameOrBefore(expire_date);
  168. } else if (expire_type === 'nolimit') return true; // 没有过期日期;可以使用
  169. return false;
  170. }
  171. /**
  172. * 检查该优惠券是否可以在这些商品中使用
  173. * @param {Object} userCoupon 用户领取的优惠券
  174. * @param {Array} shopGoods 按店铺分组的商品列表
  175. */
  176. checkCanUse_use(userCoupon, shopGoods) {
  177. const { coupon } = userCoupon;
  178. const { use_limit, use_limit_config = {}, shop } = coupon;
  179. const { tags = [] } = use_limit_config;
  180. // 全类型商品,不需要继续检查
  181. if (use_limit === 'all' || tags.length === 0) return true;
  182. // 检查的所有商品中,只要有一个可以使用,就放过
  183. let result = false;
  184. for (const s of shopGoods) {
  185. // 检查优惠券是否是店铺发行,如果是店铺发行;
  186. // 则需要在指定店铺使用,如果不是,则直接检查下一个
  187. if (shop && shop !== s.shop) continue;
  188. // 是这个店铺的优惠券,则需要检查这个店铺下的商品是否能用这个优惠券
  189. // 目前只有商品分类(tags标签)来进行设置
  190. // 商品列表(goods)中只要有 标签(tags) 中能找到 和 tags:
  191. /** e.g.:
  192. const arr = [
  193. ['xxsp', 'jgch', 'htr'],
  194. ['xxsp', 'jgch', 'ht'],
  195. ];
  196. const arr1 = [
  197. ['xxsp', 'jgch', 'ht1r'],
  198. ['xxsp', 'jgch', 'ht'],
  199. ];
  200. const r = arr.findIndex((f) => arr1.find((ff) => _.isEqual(f, ff)));
  201. console.log(r);
  202. */
  203. const r = s.goods.some(g => g.tags.find(f => tags.find(ff => _.isEqual(f, ff))));
  204. if (r) {
  205. result = true;
  206. break;
  207. }
  208. }
  209. return result;
  210. }
  211. /**
  212. * 检查该优惠券是否可以在这些商品中使用
  213. * @param {Object} userCoupon 用户领取的优惠券
  214. * @param {Array} shopGoods 按店铺分组的商品列表
  215. */
  216. checkCanUse_discount(userCoupon, shopGoods) {
  217. const { coupon } = userCoupon;
  218. const { discount_config = {} } = coupon;
  219. const limit = _.get(discount_config, 'limit');
  220. if (limit === 'nolimit') return true;
  221. const goods_total = shopGoods.reduce((p, n) =>
  222. this.ctx.plus(
  223. p,
  224. n.goods.reduce((gp, gn) =>
  225. this.ctx.plus(gp, this.ctx.multiply(gn.sell_money, gn.buy_num)), 0)
  226. )
  227. , 0);
  228. if (this.ctx.minus(goods_total, limit) > 0) return true;
  229. return false;
  230. }
  231. /**
  232. * 计算订单使用优惠券的明细
  233. * @param {Array} couponIds 订单使用的用户领取的优惠券数据id数组
  234. * @param {Array} goodsSpecs 订单所有商品规格的数据:{id,buy_num}
  235. */
  236. async computedOrderCouponDiscount(couponIds, goodsSpecs) {
  237. const result = {}; // 最终结果: 以 couponId: { goodsSpecId: ${money} } 为明细格式
  238. const goodsSpecIds = goodsSpecs.map(i => i.id);
  239. const { populate: gp } = this.ctx.service.shop.goodsSpec.getRefMods();
  240. let goodsSpecList = await this.goodsSpecModel.find({ _id: goodsSpecIds }).populate(gp);
  241. goodsSpecList = this.resetGoodsSpecData(goodsSpecList, goodsSpecs);
  242. const { populate: cp } = this.getRefMods();
  243. let userCouponList = await this.model.find({ _id: couponIds }, { customer: 0 }).populate(cp);
  244. // 先查下有没有不能用的,有不能用的需要返回
  245. let cantUse = userCouponList.find(f => f.status !== '0');
  246. if (cantUse) throw new BusinessError(ErrorCode.DATA_INVALID, `优惠券:${_.get(cantUse, 'coupon.name')}已使用`);
  247. cantUse = userCouponList.find(f => !this.checkCanUse_expire(f));
  248. if (cantUse) throw new BusinessError(ErrorCode.DATA_INVALID, `优惠券:${_.get(cantUse, 'coupon.name')}不能使用`);
  249. userCouponList = this.resetUserCouponData(userCouponList);
  250. // 上面整理完数据之后,下面开始用 均分优惠金额
  251. for (const coupon of userCouponList) {
  252. // 可以被优惠的商品列表
  253. let goodsList = [];
  254. // 1.先找到满足优惠条件的商品
  255. const { issue, shop, use_limit, use_limit_config, _id } = coupon;
  256. // 平台券,都可以
  257. if (issue === '0') goodsList = goodsSpecList;
  258. // 店铺券,需要过滤店铺
  259. else goodsList = goodsSpecList.filter(f => ObjectId(shop).equals(_.get(f, 'shop._id')));
  260. // 2.商品已确定,再确定是否是指定分类
  261. if (use_limit !== 'all') {
  262. // all的情况不需要再次过滤出不符合的条件: all为全部商品类型
  263. const tags = _.get(use_limit_config, 'tags', []);
  264. goodsList = this.getGoodsInTags(goodsSpecList, tags);
  265. }
  266. // 3.开始分配
  267. const discount_detail = this.divideByCoupon(coupon, goodsList);
  268. result[_id] = discount_detail;
  269. }
  270. // console.log(result);
  271. return result;
  272. }
  273. /**
  274. * 分配减免
  275. * @param {Object} coupon 优惠券{_id:用户领取的优惠券id,...}
  276. * @param {Array} goodsList 过滤完后,符合条件的 本订单中的规格商品列表
  277. */
  278. divideByCoupon(coupon, goodsList) {
  279. const { discount_type, discount_config } = coupon;
  280. const { limit, min, max } = discount_config;
  281. // 符合条件的商品的总价
  282. const goodsTotal = goodsList.reduce((p, n) => this.ctx.plus(p, n.goods_total), 0);
  283. if (limit !== 'nolimit') {
  284. // 有最低消费的限制,就需要看看下满足条件的商品够不够这个金额
  285. // 消费下限 大于 满足优惠券使用的商品的消费总额,则不能使用这个券,消费的钱不够
  286. // 且这个地方应该报错
  287. if (parseFloat(limit) > goodsTotal) throw new BusinessError(ErrorCode.DATA_INVALID, `消费券:${coupon.name}不满足使用条件`);
  288. }
  289. // 减免设置转换成 实际减免总金额:
  290. // 满减则直接就是 min(设置的满x减y的y);
  291. // 折扣的话,需要计算,是否超过折扣上限(max):
  292. // 超过上限则 为上限设置的金额,和满减一样按比例分配即可;
  293. // 没超过上限,则为各自去打折即可
  294. let discountRealMoney = 0;
  295. let outLimit = true; // 是否超过金额上限
  296. //
  297. if (discount_type === 'min') discountRealMoney = parseFloat(min);
  298. else {
  299. // 需要计算是否超过上限
  300. // 没有上限,那就不会超过
  301. if (!max) outLimit = false;
  302. else {
  303. // 有上限,则需要计算
  304. // const percent = 1 - parseFloat(min) / 10;
  305. const percent = this.ctx.minus(1, this.ctx.divide(min, 10));
  306. // 计算实际折扣 减了 多少钱
  307. const discountMoney = this.ctx.multiply(goodsTotal, percent);
  308. // 最后看下 不限制减的钱 是不是 超过了 上限
  309. outLimit = this.ctx.minus(discountMoney, max) > 0;
  310. // 超过了,就将上限的金额拿去按比例分配
  311. if (outLimit) discountRealMoney = parseFloat(max);
  312. }
  313. }
  314. // 根据是否超过上限金额来决定策略:
  315. // 超过上限金额, 按 每种货物的总价值 占 这些货物应该交的钱 的比例进行优惠分配
  316. // 没超过上限(目前:只有折扣会有这个情况), 则每个货物都按比例来计算
  317. if (!outLimit) return this.discountPercentForEachGoods(coupon, goodsList);
  318. return this.divideProportionForEachGoods(goodsList, discountRealMoney, goodsTotal);
  319. }
  320. /**
  321. * 按比例计算每件商品,这里就不需要优惠券什么事了.需要优惠券的东西参数都带来了
  322. * @param {Array} goodsList 过滤完后,符合条件的 本订单中的规格商品列表
  323. * @param {Number} discountRealMoney 需要按比例分配的优惠金额
  324. * @param {Number} goodsTotal goodsList的商品总金额
  325. */
  326. divideProportionForEachGoods(goodsList, discountRealMoney, goodsTotal) {
  327. const result = {};
  328. // 已分配优惠的金额的总数
  329. let allReadyDiscount = 0;
  330. for (let i = 0; i < goodsList.length; i++) {
  331. const g = goodsList[i];
  332. const { goods_total } = g;
  333. const id = _.get(g, 'goodsSpec._id');
  334. let dm = 0;
  335. console.log(goods_total, goodsTotal);
  336. // 除了最后一个商品,其余的商品均按比例分配
  337. if (i < goodsList.length - 1) {
  338. const percent = this.ctx.divide(goods_total, goodsTotal);
  339. dm = this.ctx.multiply(discountRealMoney, percent);
  340. allReadyDiscount += dm;
  341. } else {
  342. // 最后一个商品是剩余的可分配的钱
  343. dm = this.ctx.minus(discountRealMoney, allReadyDiscount);
  344. }
  345. console.log(dm);
  346. result[id] = dm;
  347. }
  348. return result;
  349. }
  350. /**
  351. * 按折扣计算每件商品
  352. * @param {Object} coupon 优惠券{_id:用户领取的优惠券id,...}
  353. * @param {Array} goodsList 过滤完后,符合条件的 本订单中的规格商品列表
  354. */
  355. discountPercentForEachGoods(coupon, goodsList) {
  356. const { discount_config } = coupon;
  357. const { min } = discount_config;
  358. const percent = this.ctx.multiply(min, 10);
  359. const result = {};
  360. for (const g of goodsList) {
  361. const { goods_total } = g;
  362. const id = _.get(g, 'goodsSpec._id');
  363. const realPay = this.ctx.multiply(goods_total, percent);
  364. const discountMoney = this.ctx.minus(goods_total, realPay);
  365. console.log(discountMoney);
  366. result[id] = discountMoney;
  367. }
  368. return result;
  369. }
  370. /**
  371. * 过滤出 订单中商品规格数据 在这些标签中的 商品规格
  372. * @param {Array} goodsSpecList 订单中商品规格列表
  373. * @param {Array} tags 商品标签
  374. */
  375. getGoodsInTags(goodsSpecList, tags) {
  376. const arr = goodsSpecList.filter(f => {
  377. const gtags = _.get(f, 'goods.tags');
  378. // 没打标签或者标签里没有内容的直接过滤出去
  379. if (!gtags || gtags.length <= 0) return false;
  380. const r = gtags.find(f => tags.find(ff => _.isEqual(ff, f)));
  381. return r;
  382. });
  383. return arr;
  384. }
  385. /**
  386. * 整理商品规格列表数据
  387. * 将goods,shop和goodsSpec重组为3个属性在object同一级上
  388. * @param {Array} list 商品规格列表
  389. * @param {Array} orderInfo 有关订单的信息 {id,buy_num}
  390. */
  391. resetGoodsSpecData(list, orderInfo) {
  392. const nouse = [ 'meta', '__v' ];
  393. const arr = list.map(i => {
  394. const obj = {};
  395. const goodsSpec = _.omit(i, [ ...nouse, 'goods' ]);
  396. const shop = _.omit(_.get(i, 'goods.shop'), [ ...nouse ]);
  397. const goods = _.omit(_.get(i, 'goods'), [ ...nouse, 'shop' ]);
  398. obj.goodsSpec = goodsSpec;
  399. obj.shop = shop;
  400. obj.goods = goods;
  401. const r = orderInfo.find(f => ObjectId(f.id).equals(goodsSpec._id));
  402. if (r) obj.buy_num = r.buy_num;
  403. if (obj.buy_num) {
  404. obj.goods_total = this.ctx.multiply(obj.buy_num, _.get(obj.goodsSpec, 'sell_money'));
  405. }
  406. return obj;
  407. });
  408. return arr;
  409. }
  410. /**
  411. * 整理出下面所需要使用的数据,并将这些数据放在object的第一级上
  412. * @param {Array} list 用户优惠券列表
  413. */
  414. resetUserCouponData(list) {
  415. const arr = list.map(i => {
  416. const { _id, meta, coupon } = i;
  417. const cd = _.pick(coupon, [
  418. 'issue',
  419. 'shop',
  420. 'expire_type',
  421. 'expire_config',
  422. 'discount_type',
  423. 'discount_config',
  424. 'use_limit',
  425. 'use_limit_config',
  426. 'get_limit',
  427. 'get_limit_config',
  428. 'name',
  429. ]);
  430. const obj = { _id, meta, ...cd };
  431. return obj;
  432. });
  433. return arr;
  434. }
  435. }
  436. module.exports = UserCouponService;