order.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396
  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 Transaction = require('mongoose-transactions');
  8. const { ObjectId } = require('mongoose').Types;
  9. //
  10. class OrderService extends CrudService {
  11. constructor(ctx) {
  12. super(ctx, 'order');
  13. this.redis = this.app.redis;
  14. this.redisKey = this.app.config.redisKey;
  15. this.model = this.ctx.model.Trade.Order;
  16. this.goodsModel = this.ctx.model.Shop.Goods;
  17. this.goodsSpecModel = this.ctx.model.Shop.GoodsSpec;
  18. this.addressModel = this.ctx.model.User.Address;
  19. this.cartModel = this.ctx.model.Trade.Cart;
  20. this.userCouponModel = this.ctx.model.User.UserCoupon;
  21. this.platformActModel = this.ctx.model.System.PlatformAct;
  22. this.gjaModel = this.ctx.model.Shop.GoodsJoinAct;
  23. this.tran = new Transaction();
  24. }
  25. /**
  26. * 创建订单
  27. * 1.检测商品是否可以购买
  28. * 2.数据做快照处理
  29. * @param {Object} body
  30. */
  31. async create(body) {
  32. // 声明事务
  33. try {
  34. const user = this.ctx.user;
  35. const customer = _.get(user, '_id');
  36. if (!customer) throw new BusinessError(ErrorCode.NOT_LOGIN, '未找到用户信息');
  37. const { address, goods, total_detail, coupon = [], type = '0', group, inviter } = body;
  38. if (coupon.length > 1) throw new BusinessError(ErrorCode.DATA_INVALID, '目前只允许使用1张优惠券');
  39. // 检测商品是否可以下单
  40. for (const i of goods) {
  41. const { shop } = i;
  42. for (const g of i.goods) {
  43. const { goods_id: goods, goodsSpec_id: goodsSpec, num } = g;
  44. const { result, msg } = await this.ctx.service.util.trade.checkCanBuy({ shop, goods, goodsSpec, num }, false);
  45. if (!result) throw new BusinessError(ErrorCode.DATA_INVALID, msg);
  46. }
  47. }
  48. const orderData = {};
  49. const goodsSpecs = [];
  50. // 数据做快照处理
  51. // 1.地址快照
  52. const addressData = await this.addressModel.findById(address._id);
  53. if (!addressData) throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '未找到邮寄地址数据');
  54. // 2.商品快照
  55. const goodsData = [];
  56. // 商店不做快照,但是商品和商品对应的规格做快照
  57. const { populate } = this.ctx.service.shop.goodsSpec.getRefMods();
  58. for (const i of goods) {
  59. const { goods: goodsList, ...others } = i;
  60. const qp = [];
  61. for (const g of goodsList) {
  62. const { goodsSpec_id, num, cart_id } = g;
  63. let goodsSpec = await this.goodsSpecModel.findById(goodsSpec_id).populate(populate);
  64. if (!goodsSpec) continue;
  65. goodsSpec = JSON.parse(JSON.stringify(goodsSpec));
  66. // 将商店内容剔除
  67. const { goods: gd, ...dOthers } = goodsSpec;
  68. const gdOthers = _.omit(gd, [ 'shop' ]);
  69. const obj = { ...dOthers, goods: gdOthers, buy_num: num, tags: _.get(gdOthers, 'tags') };
  70. if (cart_id) obj.cart_id = cart_id;
  71. qp.push(obj);
  72. goodsSpecs.push({ id: goodsSpec_id, buy_num: num, sell_money: obj.sell_money, group_sell_money: _.get(obj, 'group_config.money'), type });
  73. }
  74. goodsData.push({ ...others, goods: qp });
  75. }
  76. // 3.商品总计明细.
  77. // 计算优惠券的明细
  78. const discountDetail = await this.ctx.service.user.userCoupon.computedOrderCouponDiscount(coupon, goodsSpecs);
  79. const totalDetailData = { ...total_detail, discount_detail: JSON.parse(JSON.stringify(discountDetail)) };
  80. // 接下来组织订单数据
  81. orderData.address = addressData;
  82. orderData.goods = goodsData;
  83. orderData.total_detail = totalDetailData;
  84. // 1.用户数据
  85. orderData.customer = customer;
  86. // 2.下单时间
  87. orderData.buy_time = moment().format('YYYY-MM-DD HH:mm:ss');
  88. // 3.订单号
  89. const str = this.ctx.service.util.trade.createNonceStr();
  90. orderData.no = `${moment().format('YYYYMMDDHHmmss')}-${str}`;
  91. // 4.状态
  92. orderData.status = '0';
  93. // #region 是否是团购
  94. orderData.type = type;
  95. if (type === '1' || group) {
  96. orderData.group = group;
  97. }
  98. // #endregion
  99. // 5.返现部分:邀请人: 自己发链接自己买不行
  100. if (customer !== inviter && ObjectId.isValid(inviter)) orderData.inviter = inviter;
  101. // 生成数据
  102. const order_id = this.tran.insert('Order', orderData);
  103. // 处理库存,删除购物车
  104. await this.dealGoodsNum(goodsData);
  105. // 处理优惠券,改为使用过
  106. if (coupon.length > 0) await this.ctx.service.user.userCoupon.useCoupon(coupon, this.tran);
  107. await this.tran.run();
  108. // 创建定时任务(mq死信机制任务)
  109. await this.toMakeTask(order_id);
  110. return order_id;
  111. } catch (error) {
  112. await this.tran.rollback();
  113. console.error(error);
  114. throw new BusinessError(ErrorCode.SERVICE_FAULT, '订单发生错误,下单失败');
  115. } finally {
  116. // 清空事务
  117. this.tran.clean();
  118. }
  119. }
  120. /**
  121. * 取消订单(支付前)
  122. * @param {Object} body 参数体
  123. * @param {String} body.order_id 订单id
  124. */
  125. async cancel({ order_id }) {
  126. try {
  127. assert(order_id, '缺少订单信息');
  128. const order = await this.model.findById(order_id);
  129. if (!order) throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '未找到订单信息');
  130. if (_.get(order, 'pay.result')) {
  131. throw new BusinessError(ErrorCode.DATA_INVALID, '该订单已支付完成,无法使用此接口');
  132. }
  133. if (_.get(order, 'status') !== '0') {
  134. throw new BusinessError(ErrorCode.DATA_INVALID, '该订单不处于可以退单的状态');
  135. }
  136. // 退单分为 2 部分, 涉及价格不需要管.因为这是交钱之前的的操作,不涉及退款
  137. // 1.货物的库存
  138. const { goods: shopGoods, total_detail } = order;
  139. // 需要归还库存的商品规格列表:{_id:商品规格id,buy_num:购买的数量}
  140. for (const sg of shopGoods) {
  141. const { goods: goodsList } = sg;
  142. const list = goodsList.map(i => _.pick(i, [ '_id', 'buy_num' ]));
  143. for (const i of list) {
  144. const goodsSpec = await this.goodsSpecModel.findById(i._id, { num: 1 });
  145. if (!goodsSpec) continue;
  146. const newNum = this.ctx.plus(goodsSpec.num, i.buy_num);
  147. this.tran.update('GoodsSpec', i._id, { num: newNum });
  148. }
  149. }
  150. // 2.优惠券
  151. const { discount_detail } = total_detail;
  152. if (discount_detail) {
  153. // 第一层key值全都是使用的优惠券id.拿去改了就好了
  154. const couponIds = Object.keys(discount_detail);
  155. for (const uc_id of couponIds) {
  156. this.tran.update('UserCoupon', uc_id, { status: '0' });
  157. }
  158. }
  159. // 3.订单修改为关闭
  160. this.tran.update('Order', order_id, { status: '-1' });
  161. await this.tran.run();
  162. } catch (error) {
  163. await this.tran.rollback();
  164. console.error(error);
  165. throw new BusinessError(ErrorCode.SERVICE_FAULT, '订单取消失败');
  166. } finally {
  167. this.tran.clean();
  168. }
  169. }
  170. /**
  171. * 减库存,删除购物车
  172. * @param {Array} list 商品
  173. */
  174. async dealGoodsNum(list) {
  175. for (const i of list) {
  176. for (const g of i.goods) {
  177. const { _id, buy_num, cart_id } = g;
  178. const goodsSpec = await this.goodsSpecModel.findById(_id);
  179. const newNum = this.ctx.minus(goodsSpec.num, buy_num);
  180. this.tran.update('GoodsSpec', _id, { num: newNum });
  181. if (cart_id) this.tran.remove('Cart', cart_id);
  182. }
  183. }
  184. }
  185. /**
  186. * 进入下单页面
  187. * @param {Object} body 请求参数
  188. * @param body.key 缓存key
  189. * @property {Array} data key中的数据,可能是 [string],[object]; [string]:数据是购物车id; [object] :是直接购买的数据
  190. */
  191. async toMakeOrder({ key }) {
  192. key = `${this.redisKey.orderKeyPrefix}${key}`;
  193. let data = await this.redis.get(key);
  194. if (!data) throw new BusinessError(ErrorCode.SERVICE_FAULT, '请求超时,请重新进入下单页');
  195. data = JSON.parse(data);
  196. let specsData = [];
  197. // 根据缓存,整理商品数据
  198. if (!_.isArray(data)) throw new BusinessError(ErrorCode.DATA_INVALID, '数据不正确,请重新下单');
  199. const head = _.head(data);
  200. if (_.isString(head)) {
  201. // 购物车来的,将购物车中的数据拿出来转换下
  202. const carts = await this.cartModel.find({ _id: data });
  203. data = carts;
  204. }
  205. const { result, msg } = await this.ctx.service.util.trade.checkCanBuy(data, false);
  206. if (!result) throw new BusinessError(ErrorCode.DATA_INVALID, msg);
  207. // 正常整理商品的内容
  208. specsData = await this.getPageData(data);
  209. // 组装页面的数据
  210. const user = this.ctx.user;
  211. const customer = _.get(user, '_id');
  212. if (!customer) throw new BusinessError(ErrorCode.NOT_LOGIN, '未找到用户信息');
  213. const pageData = {};
  214. // 找到默认地址
  215. const address = await this.addressModel.findOne({ customer, is_default: '1' });
  216. pageData.address = address;
  217. // 商品总价,各店铺的价格明细
  218. specsData = this.ctx.service.util.order.makeOrder_computedShopTotal(specsData);
  219. const shopTotalDetail = this.ctx.service.util.order.makerOrder_computedOrderTotal(specsData);
  220. pageData.goodsData = specsData;
  221. pageData.orderTotal = shopTotalDetail;
  222. // 优惠券列表
  223. // 查询优惠券列表
  224. const couponList = await this.ctx.service.user.userCoupon.toMakeOrder_getList(specsData);
  225. pageData.couponList = couponList;
  226. // 返现部分:添加推荐人信息
  227. const inviter = data.find(f => ObjectId.isValid(f.inviter));
  228. pageData.inviter = inviter;
  229. // 活动部分
  230. // 每个数据中都有act字段.这个字段表明商品参加了哪些活动
  231. // await this.dealAct();
  232. return pageData;
  233. }
  234. /**
  235. * 处理该订单活动部分
  236. * * 该商品可能会参加多个活动,活动之间有叠加问题
  237. * * 买赠:没关系,只要去找赠品就行
  238. * * 特价:需要找到特价,将价格更改为特价
  239. * * 加价购: 满足下限金额时,组织数据,允许前端进行加价购
  240. * * 满减/折: 针对整个订单而言. 满足就处理,不满足就是不处理
  241. * * 套装:暂不处理
  242. * * ps: 特价与满减/折叠加, 按特价计算总价再看满减/折; 加价购不在满减/折的金额下限计算范围内
  243. * @param {Array} data 购物车数据(直接购买也会组织成购物车数据)
  244. */
  245. async dealAct(data) {
  246. const actList = [];
  247. for (const i of data) {
  248. const { act = [], spec } = i;
  249. if (act.length <= 0) continue;
  250. for (const a of act) {
  251. const platformAct = await this.platformActModel.findById(a);
  252. // 没有找到活动,略过
  253. if (!platformAct) continue;
  254. // 活动未开启,略过
  255. if (_.get(platformAct, 'is_use') !== '0') continue;
  256. // 活动类型为 0&1不需要处理
  257. const type = _.get(platformAct, 'type');
  258. if (type === '1' || type === '0') continue;
  259. // 先找下是否处于活动设置的程序时间
  260. const start = _.get(platformAct, 'config.time_start');
  261. const end = _.get(platformAct, 'config.time_end');
  262. const r = moment().isBetween(start, end, null, '[]');
  263. // 不在程序设定的活动时间内,下一个
  264. if (!r) continue;
  265. // 有关最后活动总结数据问题:
  266. // 1.买赠:需要具体到规格
  267. // 2.特价:需要具体到规格
  268. // 3.加价购:需要和商品一起进行判断
  269. // 4&5:满减/折:需要和商品一起判断
  270. // 6.套装:先不考虑
  271. if (type === '2') {
  272. // 买赠,直接去到活动中找到赠品
  273. const gja = await this.gjaModel.findOne({ platform_act: platformAct._id, 'spec._id': spec }, { config: 1 });
  274. const gift = _.get(gja, 'config.gift', []);
  275. actList.push({ platform_act_type: type, spec, gift });
  276. } else if (type === '3') {
  277. // 特价,找出特价
  278. const gja = await this.gjaModel.findOne({ platform_act: platformAct._id, 'spec._id': spec }, { config: 1 });
  279. const sp_price = _.get(gja, 'config.sp_price');
  280. } else if (type === '4') {
  281. // 加价购,找出加价购下限;如果判断下限够了,那就可以让前端去加价购
  282. const plus_money = _.get(platformAct, 'config.plus_money', 0);
  283. } else if (type === '5') {
  284. // 满减
  285. const discount = _.get(platformAct, 'discount', []);
  286. } else if (type === '6') {
  287. // 满折
  288. const discount = _.get(platformAct, 'discount', []);
  289. } else if (type === '7') {
  290. // 套装先不考虑
  291. }
  292. }
  293. }
  294. }
  295. // 直接购买&购物车,这俩字段基本没差, 组织订单页商品数据
  296. async getPageData(data) {
  297. let arr = [];
  298. for (const i of data) {
  299. const { goodsSpec, num } = i;
  300. const d = await this.goodsSpecModel.aggregate([
  301. { $match: { _id: ObjectId(goodsSpec) } },
  302. // #region 处理店铺与商品部分
  303. { $addFields: { goods_id: { $toObjectId: '$goods' } } },
  304. {
  305. $lookup: {
  306. from: 'goods',
  307. localField: 'goods_id',
  308. foreignField: '_id',
  309. pipeline: [
  310. { $addFields: { shop_id: { $toObjectId: '$shop' } } },
  311. {
  312. $lookup: {
  313. from: 'shop',
  314. localField: 'shop_id',
  315. foreignField: '_id',
  316. pipeline: [{ $project: { name: 1 } }],
  317. as: 'shop',
  318. },
  319. },
  320. { $project: { name: 1, file: 1, tag: 1, act_tag: 1, shop: { $first: '$shop' } } },
  321. ],
  322. as: 'goods',
  323. },
  324. },
  325. { $unwind: '$goods' },
  326. // #endregion
  327. {
  328. $project: {
  329. _id: 0,
  330. shop: '$goods.shop._id',
  331. shop_name: '$goods.shop.name',
  332. goods_id: '$goods._id',
  333. goods_name: '$goods.name',
  334. goodsSpec_id: '$_id',
  335. goodsSpec_name: '$name',
  336. freight: { $toString: '$freight' },
  337. sell_money: { $toString: '$sell_money' },
  338. num: { $toDouble: num },
  339. file: '$goods.file',
  340. tags: '$goods.tags',
  341. act_tags: '$goods.act_tags',
  342. },
  343. },
  344. ]);
  345. arr.push(...d);
  346. }
  347. arr = Object.values(_.groupBy(arr, 'shop'));
  348. const result = [];
  349. // 按店铺分组
  350. for (const i of arr) {
  351. const head = _.head(i);
  352. const obj = { shop: _.get(head, 'shop'), shop_name: _.get(head, 'shop_name') };
  353. const goods = i.map(e => _.omit(e, [ 'shop', 'shop_name' ]));
  354. obj.goods = goods;
  355. result.push(obj);
  356. }
  357. return result;
  358. }
  359. async afterQuery(filter, data) {
  360. data = JSON.parse(JSON.stringify(data));
  361. for (const i of data) {
  362. const { goods } = i;
  363. const buy_num_total = goods.reduce(
  364. (p, n) =>
  365. this.ctx.plus(
  366. p,
  367. n.goods.reduce((np, ng) => this.ctx.plus(np, ng.buy_num), 0)
  368. ),
  369. 0
  370. );
  371. i.buy_num_total = buy_num_total;
  372. i.real_pay = _.get(i, 'pay.pay_money');
  373. }
  374. return data;
  375. }
  376. async toMakeTask(order_id) {
  377. const { taskMqConfig } = this.app.config;
  378. const data = { service: 'trade.order', method: 'cancel', params: { order_id } };
  379. const config = await this.ctx.service.system.config.query();
  380. const setting = _.get(config, 'config.autoCloseOrder', -1);
  381. // 设置为小于等于0时,不进行自动关闭
  382. if (setting <= 0) return;
  383. await this.ctx.service.util.rabbitMq.makeTask(taskMqConfig.queue, data, 15);
  384. }
  385. }
  386. module.exports = OrderService;