'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 Transaction = require('mongoose-transactions'); const { ObjectId } = require('mongoose').Types; // class OrderService extends CrudService { constructor(ctx) { super(ctx, 'order'); this.redis = this.app.redis; this.redisKey = this.app.config.redisKey; this.model = this.ctx.model.Trade.Order; this.goodsModel = this.ctx.model.Shop.Goods; this.goodsSpecModel = this.ctx.model.Shop.GoodsSpec; this.addressModel = this.ctx.model.User.Address; this.cartModel = this.ctx.model.Trade.Cart; this.userCouponModel = this.ctx.model.User.UserCoupon; this.platformActModel = this.ctx.model.System.PlatformAct; this.gjaModel = this.ctx.model.Shop.GoodsJoinAct; this.orderUtil = this.ctx.service.util.order; this.goodsConfigModel = this.ctx.model.Shop.GoodsConfig; this.tran = new Transaction(); this.setModel = this.ctx.model.Shop.GoodsSet; } /** * 创建订单 * 1.检测商品是否可以购买 * 2.数据做快照处理 * @param {Object} body */ async create(body) { // 声明事务 try { const user = this.ctx.user; const customer = _.get(user, '_id'); if (!customer) throw new BusinessError(ErrorCode.NOT_LOGIN, '未找到用户信息'); const { address, goods, coupon = [], plus_goods = [], inviter } = body; if (coupon.length > 1) throw new BusinessError(ErrorCode.DATA_INVALID, '目前只允许使用1张优惠券'); // 检测商品是否可以下单 const resetGoodsList = []; for (const i of goods) { const { shop, is_set = '1' } = i; if (is_set === '1') { // 非套装 for (const g of i.goods) { const { goods_id: goods, goodsSpec_id: goodsSpec, num } = g; const { result, msg } = await this.ctx.service.util.trade.checkCanBuy({ shop, goods, goodsSpec, num }, false); if (!result) throw new BusinessError(ErrorCode.DATA_INVALID, msg); } resetGoodsList.push(i); } else { const { result, msg } = await this.ctx.service.util.trade.checkCanBuy(i, false); if (!result) throw new BusinessError(ErrorCode.DATA_INVALID, msg); } } const orderData = {}; // 数据做快照处理 // 1.地址快照 const addressData = await this.addressModel.findById(address._id); if (!addressData) throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '未找到邮寄地址数据'); // 2.商品快照 const res = await this.orderUtil.makeOrderGoodsData(goods); // 活动和购物券计算数据不需要更改,而重组的商品列表需要进行数据的更改 const { actList, goodsSpecs } = res; let { goodsData } = res; // 3.商品总计明细. // 加购不算在优惠内,所以先计算优惠券的明细 const discountDetail = await this.ctx.service.user.userCoupon.computedOrderCouponDiscount(coupon, goodsSpecs); // 处理加购商品 goodsData = await this.orderUtil.addPlusGoods(goodsData, plus_goods); // 计算价格 goodsData = this.orderUtil.computedShopDetail(goodsData); const total_detail = { goods_total: goodsData.reduce((p, n) => this.ctx.plus(p, n.goods_total), 0), freight_total: goodsData.reduce((p, n) => this.ctx.plus(p, n.freight_total), 0), act: actList, }; const totalDetailData = { ...total_detail, discount_detail: JSON.parse(JSON.stringify(discountDetail)) }; // 接下来组织订单数据 orderData.address = addressData; orderData.goods = goodsData; orderData.total_detail = totalDetailData; // 1.用户数据 orderData.customer = customer; // 2.下单时间 orderData.buy_time = moment().format('YYYY-MM-DD HH:mm:ss'); // 3.订单号 const str = this.ctx.service.util.trade.createNonceStr(); orderData.no = `${moment().format('YYYYMMDDHHmmss')}-${str}`; // 4.状态 orderData.status = '0'; // 5.返现部分:邀请人: 自己发链接自己买不行 if (customer !== inviter && ObjectId.isValid(inviter)) orderData.inviter = inviter; // 生成数据 const order_id = this.tran.insert('Order', orderData); // 处理库存,删除购物车 await this.dealGoodsNum(goodsData); // 处理优惠券,改为使用过 if (coupon.length > 0) await this.ctx.service.user.userCoupon.useCoupon(coupon, this.tran); await this.tran.run(); // 创建定时任务(mq死信机制任务) await this.toMakeTask(order_id); return order_id; } catch (error) { await this.tran.rollback(); console.error(error); throw new BusinessError(ErrorCode.SERVICE_FAULT, '订单发生错误,下单失败'); } finally { // 清空事务 this.tran.clean(); } } /** * 取消订单(支付前) * @param {Object} body 参数体 * @param {String} body.order_id 订单id */ async cancel({ order_id }) { try { assert(order_id, '缺少订单信息'); const order = await this.model.findById(order_id); if (!order) throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '未找到订单信息'); if (_.get(order, 'pay.result')) { throw new BusinessError(ErrorCode.DATA_INVALID, '该订单已支付完成,无法使用此接口'); } if (_.get(order, 'status') !== '0') { throw new BusinessError(ErrorCode.DATA_INVALID, '该订单不处于可以退单的状态'); } // 退单分为 2 部分, 涉及价格不需要管.因为这是交钱之前的的操作,不涉及退款 // 1.货物的库存 const { goods: shopGoods, total_detail, is_set = '1' } = order; if (is_set === '1') { // 需要归还库存的商品规格列表:{_id:商品规格id,buy_num:购买的数量} for (const sg of shopGoods) { const { goods: goodsList } = sg; const list = goodsList.map(i => _.pick(i, [ '_id', 'buy_num' ])); for (const i of list) { const goodsSpec = await this.goodsSpecModel.findById(i._id, { num: 1 }); if (!goodsSpec) continue; const newNum = this.ctx.plus(goodsSpec.num, i.buy_num); this.tran.update('GoodsSpec', i._id, { num: newNum }); } } } else { // 套装退库存 } // 2.优惠券 const { discount_detail } = total_detail; if (discount_detail) { // 第一层key值全都是使用的优惠券id.拿去改了就好了 const couponIds = Object.keys(discount_detail); for (const uc_id of couponIds) { this.tran.update('UserCoupon', uc_id, { status: '0' }); } } // 3.订单修改为关闭 this.tran.update('Order', order_id, { status: '-1' }); await this.tran.run(); } catch (error) { await this.tran.rollback(); console.error(error); throw new BusinessError(ErrorCode.SERVICE_FAULT, '订单取消失败'); } finally { this.tran.clean(); } } /** * 减库存,删除购物车 * @param {Array} list 商品 */ async dealGoodsNum(list) { for (const i of list) { const { is_set = '1' } = i; if (is_set === '1') { for (const g of i.goods) { const { _id, buy_num, cart_id } = g; const goodsSpec = await this.goodsSpecModel.findById(_id); const newNum = this.ctx.minus(goodsSpec.num, buy_num); this.tran.update('GoodsSpec', _id, { num: newNum }); if (cart_id) { const num = await this.cartModel.count({ _id: cart_id }); if (num > 0) { this.tran.remove('Cart', cart_id); } } // 活动相关: // 买赠:不一定非要处理赠品,赠品可能不在库中; // 特价,满减/折:没关系 // 加价购,作为另一个商品出现,一样会减少库存,不需要处理 // 先不处理活动库存问题.先有逻辑可以做到 } } else { // 套装减库存 const { goods, buy_num, cart_id } = i; for (const i of goods) { const { spec, set_num } = i; const specData = await this.goodsSpecModel.findById(spec._id, { num: 1 }).lean(); const newNum = this.ctx.minus(specData.num, this.ctx.multiply(set_num, buy_num)); this.tran.update('GoodsSpec', spec._id, { num: newNum }); } if (cart_id) { const num = await this.cartModel.count({ _id: cart_id }); if (num > 0) { this.tran.remove('Cart', cart_id); } } } } } /** * 进入下单页面 * @param {Object} body 请求参数 * @param body.key 缓存key * @property {Array} data key中的数据,可能是 [string],[object]; [string]:数据是购物车id; [object] :是直接购买的数据 */ async toMakeOrder({ key }) { key = `${this.redisKey.orderKeyPrefix}${key}`; let data = await this.redis.get(key); if (!data) throw new BusinessError(ErrorCode.SERVICE_FAULT, '请求超时,请重新进入下单页'); data = JSON.parse(data); let specsData = []; // 根据缓存,整理商品数据 if (!_.isArray(data)) throw new BusinessError(ErrorCode.DATA_INVALID, '数据不正确,请重新下单'); const head = _.head(data); if (_.isString(head)) { // 购物车来的,将购物车中的数据拿出来转换下 let carts = await this.cartModel.find({ _id: data }); if (carts.length > 0) { carts = JSON.parse(JSON.stringify(carts)); carts = carts.map(i => ({ ...i, cart_id: i._id })); } carts = await this.ctx.service.trade.cart.getCartGoodsAct(carts); data = carts; } else { data = await this.ctx.service.trade.cart.getCartGoodsAct(data); } const { result, msg } = await this.ctx.service.util.trade.checkCanBuy(data, false); if (!result) throw new BusinessError(ErrorCode.DATA_INVALID, msg); // 本次订单 有关活动的数据 const actList = await this.getActList(data, true); // 正常整理商品的内容,与活动结合 specsData = await this.getPageData(data, actList); // 组装页面的数据 const user = this.ctx.user; const customer = _.get(user, '_id'); if (!customer) throw new BusinessError(ErrorCode.NOT_LOGIN, '未找到用户信息'); const pageData = {}; // 商品总价,各店铺的价格明细 specsData = this.ctx.service.util.order.makeOrder_computedShopTotal(specsData); const shopTotalDetail = this.ctx.service.util.order.makerOrder_computedOrderTotal(specsData); // 找到默认地址 const address = await this.addressModel.findOne({ customer, is_default: '1' }); pageData.address = address; // 优惠券列表 // 查询优惠券列表 const couponList = await this.ctx.service.user.userCoupon.toMakeOrder_getList(specsData); pageData.couponList = couponList; // 返现部分:添加推荐人信息 const inviter = _.get(_.head(data), 'inviter'); // const inviter = data.find((f) => ObjectId.isValid(f.inviter)); if (inviter) pageData.inviter = inviter; // 活动部分 // 将加价购拿出来,给前端,需要特殊处理; pageData.actList = actList.filter(f => f.platform_act_type === '4' && f.activity); // 满减/折 直接放在orderTotal中,作为明细,反正后面也不用; 只有discount为数字(金额)的情况,说明满足该满减/折的优惠,数组是不满足的 const daList = actList.filter(f => (f.platform_act_type === '5' || f.platform_act_type === '6') && _.isNumber(f.discount)); for (const da of daList) { const obj = { zh: da.title, key: da.platform_act, money: da.discount }; shopTotalDetail.push(obj); } pageData.goodsData = specsData; pageData.orderTotal = shopTotalDetail; return pageData; } /** * 活动相关第一步: 处理规格和活动的问题 * @param {Array} goodsList 商店商品列表 * @param {Array} actList 活动列表 */ async dealAct(goodsList, actList) { // 套装没活动,直接略过 if (actList.length <= 0) return; // 活动根据类型有优先级设置,需要按优先级进行处理 // 特价(3)>满减(5)>满折(6)>加价购(4); 买赠(2)无所谓 const spActs = actList.filter(f => f.platform_act_type === '3'); this.orderUtil.dealAct_sp(goodsList, spActs); const dmActs = actList.filter(f => f.platform_act_type === '5'); await this.orderUtil.dealAct_discount(goodsList, dmActs); const dpActs = actList.filter(f => f.platform_act_type === '6'); await this.orderUtil.dealAct_discount(goodsList, dpActs); let plusActs = actList.filter(f => f.platform_act_type === '4'); await this.orderUtil.dealAct_plus(goodsList, plusActs); plusActs = plusActs.filter(f => f.activity); const giftActs = actList.filter(f => f.platform_act_type === '2'); await this.orderUtil.dealAct_gift(goodsList, giftActs); } /** * 处理该订单活动部分 * * 该商品可能会参加多个活动,活动之间有叠加问题 * * 买赠:没关系,只要去找赠品就行 * * 特价:需要找到特价,将价格更改为特价 * * 加价购: 满足下限金额时,组织数据,允许前端进行加价购 * * 满减/折: 针对整个订单而言. 满足就处理,不满足就是不处理 * * 套装:暂不处理 * * ps1: 特价与满减/折叠加, 按特价计算总价再看满减/折; 加价购不在满减/折的金额下限计算范围内 * * ps2: 满减与满折的优先级为:先满减,后满折(理论上不应该同时出现在一个商品上) * @param {Array} data 购物车数据(直接购买也会组织成购物车数据) * @param {Boolean} getAll 获取所有活动 */ async getActList(data) { const actList = []; for (const i of data) { const { act = [], goodsSpec: spec, is_set = '1' } = i; if (is_set !== '1') continue; if (act.length <= 0) continue; for (const a of act) { let platformAct = await this.platformActModel.findById(a); // 没有找到活动,略过 if (!platformAct) continue; platformAct = JSON.parse(JSON.stringify(platformAct)); // 活动未开启,略过 if (_.get(platformAct, 'is_use') !== '0') continue; // 活动类型为 0&1不需要处理 const type = _.get(platformAct, 'type'); if (type === '1' || type === '0') continue; // 先找下是否处于活动设置的程序时间 const start = _.get(platformAct, 'config.time_start'); const end = _.get(platformAct, 'config.time_end'); const r = moment().isBetween(start, end, null, '[]'); // 不在程序设定的活动时间内,下一个 if (!r) continue; // 有关最后活动总结数据问题: // 1.买赠:需要具体到规格 // 2.特价:需要具体到规格 // 3.加价购:需要和商品一起进行判断 // 4&5:满减/折:需要和商品一起判断 // 6.套装:先不考虑 if (type === '2') { // 买赠,直接去到活动中找到赠品 const gja = await this.gjaModel.findOne({ platform_act: platformAct._id, 'spec._id': spec }, { config: 1 }); if (!gja) continue; const gift = _.get(gja, 'config.gift', []); actList.push({ platform_act: platformAct._id, platform_act_type: type, spec, gift }); } else if (type === '3') { // 特价,找出特价 const gja = await this.gjaModel.findOne({ platform_act: platformAct._id, 'spec._id': spec }, { config: 1 }); if (!gja) continue; const sp_price = _.get(gja, 'config.sp_price'); actList.push({ platform_act: platformAct._id, platform_act_type: type, spec, sp_price }); } else if (type === '4') { // 加价购,找出加价购下限;如果判断下限够了,那就可以让前端去加价购 const plus_money = _.get(platformAct, 'config.plus_money', 0); const obj = { platform_act: platformAct._id, platform_act_type: type, plus_money }; const r = actList.find(f => _.isEqual(f, obj)); if (!r) actList.push(obj); } else if (type === '5' || type === '6') { // 满减/折 const discount = _.get(platformAct, 'config.discount', []); const obj = { platform_act: platformAct._id, platform_act_type: type, discount }; const r = actList.find(f => _.isEqual(f, obj)); if (!r) actList.push(obj); } } } return actList; } // 直接购买&购物车,这俩字段基本没差, 组织订单页商品数据 async getPageData(data, actList) { const arr = []; for (const i of data) { const { goodsSpec, num, cart_id, is_set = '1', set_id, shop } = i; if (is_set === '1' || !set_id) { const d = await this.goodsSpecModel.aggregate([ { $match: { _id: ObjectId(goodsSpec) } }, // #region 处理店铺与商品部分 { $addFields: { goods_id: { $toObjectId: '$goods' } } }, { $lookup: { from: 'goods', localField: 'goods_id', foreignField: '_id', pipeline: [ { $addFields: { shop_id: { $toObjectId: '$shop' } } }, { $lookup: { from: 'shop', localField: 'shop_id', foreignField: '_id', pipeline: [{ $project: { name: 1 } }], as: 'shop', }, }, { $project: { name: 1, file: 1, tag: 1, act_tag: 1, shop: { $first: '$shop' } } }, ], as: 'goods', }, }, { $unwind: '$goods' }, // #endregion { $project: { _id: 0, shop: '$goods.shop._id', shop_name: '$goods.shop.name', goods_id: '$goods._id', goods_name: '$goods.name', goodsSpec_id: '$_id', goodsSpec_name: '$name', freight: { $toString: '$freight' }, sell_money: { $toString: '$sell_money' }, leader_price: { $toString: '$leader_price' }, num: { $toDouble: num }, file: '$goods.file', tags: '$goods.tags', act_tags: '$goods.act_tags', price: { $toDouble: '$sell_money' }, }, }, ]); let gs = _.head(d); if (gs) gs = JSON.parse(JSON.stringify(gs)); if (cart_id) gs.cart_id = cart_id; arr.push(gs); } else { // 套装信息 const setData = await this.setModel.findById(set_id).lean(); const { _id, set = [], sell_money, name, freight } = setData; const newSet = []; const obj = { _id, set_id: _id, name, sell_money, is_set: '0', num, cart_id, freight, shop }; for (const s of set) { const { goods, goods_name, spec, spec_name, set_num } = s; const goodsData = await this.goodsModel.findById(goods, { file: 1 }).lean(); const specData = await this.goodsSpecModel.findById(spec, { file: 1 }).lean(); const file = [ ..._.get(specData, 'file', []), ..._.get(goodsData, 'file', []) ]; const newSetData = { goods_name, goods_id: goods, spec_name, file, set_num }; newSet.push(newSetData); } obj.goods = newSet; arr.push(obj); } } // 检测是否是团长,使用团长价格 const user = _.get(this.ctx, 'user'); if (!user) throw new BusinessError(ErrorCode.NOT_LOGIN); const is_leader = _.get(user, 'is_leader', '1'); if (is_leader === '0') { for (const i of arr) { const { is_set = '1' } = i; // 套装不需要团长价格 if (is_set !== '1') continue; if (i.leader_price) { i.leader_price = this.ctx.toNumber(i.leader_price); i.price = i.leader_price; } } } // 平铺数据后,需要处理活动相关部分 // 经过处理后的数据,会添加act字段,表明与活动有关的信息 await this.dealAct(arr, actList); const result = await this.toMakeGroupData(arr); return result; } /** * 将平铺的数据按店铺分组形成 * * [ { shop:${value}, shop_name:${value}, goods:${value} } ] * 的形式 * @param {Array} cartList 平铺的数据集合 */ async toMakeGroupData(cartList) { const notSetList = cartList.filter(f => _.get(f, 'is_set', '1') === '1'); const setList = cartList.filter(f => _.get(f, 'is_set', '1') === '0'); const list = Object.values(_.groupBy(notSetList, 'shop')); const result = [ ...setList ]; // 按店铺分组 for (const i of list) { const head = _.head(i); const obj = { shop: _.get(head, 'shop'), shop_name: _.get(head, 'shop_name') }; const goods = i.map(e => _.omit(e, [ 'shop', 'shop_name' ])); obj.goods = goods; result.push(obj); } return result; } async afterQuery(filter, data) { data = JSON.parse(JSON.stringify(data)); for (const i of data) { const { goods } = i; let buy_num_total = 0; for (const g of goods) { const { is_set = '1' } = g; if (is_set === '1') { const bn = g.goods.reduce((p, n) => this.ctx.plus(p, n.buy_num), 0); buy_num_total = this.ctx.plus(buy_num_total, bn); } else { buy_num_total = this.ctx.plus(buy_num_total, g.buy_num); } } i.buy_num_total = buy_num_total; i.real_pay = _.get(i, 'pay.pay_money'); } return data; } async afterFetch(filter, data) { data = JSON.parse(JSON.stringify(data)); const total_detail = _.get(data, 'total_detail', {}); data.total_detail = await this.ctx.service.trade.orderDetail.fetchResetTotalDetail(total_detail); return data; } async toMakeTask(order_id) { const { taskMqConfig } = this.app.config; const data = { service: 'trade.order', method: 'cancel', params: { order_id } }; const config = await this.ctx.service.system.config.query(); const setting = _.get(config, 'config.autoCloseOrder', -1); // 设置为小于等于0时,不进行自动关闭 if (setting <= 0) return; await this.ctx.service.util.rabbitMq.makeTask(taskMqConfig.queue, data, 15); } } module.exports = OrderService;