'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'); // 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.tran = new Transaction(); } /** * 创建订单 * 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, total_detail, coupon = [], type = '0', group, inviter } = body; if (coupon.length > 1) throw new BusinessError(ErrorCode.DATA_INVALID, '目前只允许使用1张优惠券'); // 检测商品是否可以下单 for (const i of goods) { const { shop } = i; 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); } } const orderData = {}; const goodsSpecs = []; // 数据做快照处理 // 1.地址快照 const addressData = await this.addressModel.findById(address._id); if (!addressData) throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '未找到邮寄地址数据'); // 2.商品快照 const goodsData = []; // 商店不做快照,但是商品和商品对应的规格做快照 const { populate } = this.ctx.service.shop.goodsSpec.getRefMods(); for (const i of goods) { const { goods: goodsList, ...others } = i; const qp = []; for (const g of goodsList) { const { goodsSpec_id, num, cart_id } = g; let goodsSpec = await this.goodsSpecModel.findById(goodsSpec_id).populate(populate); if (!goodsSpec) continue; goodsSpec = JSON.parse(JSON.stringify(goodsSpec)); // 将商店内容剔除 const { goods: gd, ...dOthers } = goodsSpec; const gdOthers = _.omit(gd, [ 'shop' ]); const obj = { ...dOthers, goods: gdOthers, buy_num: num, tags: _.get(gdOthers, 'tags') }; if (cart_id) obj.cart_id = cart_id; qp.push(obj); goodsSpecs.push({ id: goodsSpec_id, buy_num: num, sell_money: obj.sell_money, group_sell_money: _.get(obj, 'group_config.money'), type }); } goodsData.push({ ...others, goods: qp }); } // 3.商品总计明细. // 计算优惠券的明细 const discountDetail = await this.ctx.service.user.userCoupon.computedOrderCouponDiscount(coupon, goodsSpecs); 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'; // #region 是否是团购 orderData.type = type; if (type === '1' || group) { orderData.group = group; } // #endregion // 5.返现部分:邀请人: 自己发链接自己买不行 if (customer !== inviter) orderData.inviter = inviter; // 生成数据 const order_id = this.tran.insert('Order', orderData); // 处理库存,删除购物车 await this.dealGoodsNum(goodsData); // 处理优惠券,改为使用过 if (coupon.length > 1) 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, '该订单已支付完成,无法使用此接口'); } // 退单分为 2 部分, 涉及价格不需要管.因为这是交钱之前的的操作,不涉及退款 // 1.货物的库存 const { goods: shopGoods, total_detail } = order; // 需要归还库存的商品规格列表:{_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 }); } } // 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) { 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) this.tran.remove('Cart', cart_id); } } } /** * 进入下单页面 * @param {Object} body 请求参数 * @param body.key 缓存key */ 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)) { // 购物车来的: 1.循环校验 规格商品; 2.按店铺分组 const { populate } = this.ctx.service.trade.cart.getRefMods(); const carts = await this.cartModel.find({ _id: data }).populate(populate); for (const cart of carts) { const { result, msg } = await this.ctx.service.util.trade.checkCanBuy(cart, false); if (!result) throw new BusinessError(ErrorCode.DATA_INVALID, msg); } specsData = this.setCartsGoodsToPageData(carts); } else if (_.isObject(data)) { // 商品页单独买: 1.校验规格商品; 2:按店铺分组 const { result, msg } = await this.ctx.service.util.trade.checkCanBuy(data, false); if (!result) throw new BusinessError(ErrorCode.DATA_INVALID, msg); const { populate } = this.ctx.service.shop.goodsSpec.getRefMods(); const list = await this.goodsSpecModel.find({ _id: data.goodsSpec }).populate(populate); specsData = this.setGoodsToPageData(list, data.num); } else throw new BusinessError(ErrorCode.DATA_INVALID, '数据不正确,请重新下单'); // 组装页面的数据 const user = this.ctx.user; const customer = _.get(user, '_id'); if (!customer) throw new BusinessError(ErrorCode.NOT_LOGIN, '未找到用户信息'); const pageData = {}; // 找到默认地址 const address = await this.addressModel.findOne({ customer, is_default: '1' }); pageData.address = address; // #region 团购部分 const { type, group } = data; pageData.type = type; // 新添字段,订单类型 pageData.group = group; // 新添字段,团购的团id // #endregion // 商品总价,各店铺的价格明细 specsData = this.ctx.service.util.order.makeOrder_computedShopTotal(specsData, type); const shopTotalDetail = this.ctx.service.util.order.makerOrder_computedOrderTotal(specsData); pageData.goodsData = specsData; pageData.orderTotal = shopTotalDetail; // 优惠券列表 // 查询优惠券列表 const couponList = await this.ctx.service.user.userCoupon.toMakeOrder_getList(specsData); pageData.couponList = couponList; // 返现部分:添加推荐人信息 pageData.inviter = _.get(data, 'inviter'); return pageData; } /** * 单商品整理数据,剩下的可以简略 * @param {Array} list 规格数组 * @param num 购买数量 */ setGoodsToPageData(list, num) { // 按店铺分组,精简字段 const arr = []; for (const i of list) { const obj = {}; obj.shop_name = _.get(i.goods, 'shop.name'); obj.shop = _.get(i.goods, 'shop._id'); const goods = {}; goods.goods_id = _.get(i.goods, '_id'); goods.goods_name = _.get(i.goods, 'name'); goods.goodsSpec_id = _.get(i, '_id'); goods.goodsSpec_name = _.get(i, 'name'); goods.freight = _.get(i, 'freight'); goods.money = _.get(i, 'sell_money'); goods.num = num; goods.file = _.get(i.goods, 'file'); goods.tags = _.get(i.goods, 'tags'); // #region 团购部分 // 团购价 goods.group_sell_money = _.get(i, 'group_config.money'); // #endregion obj.goods = [ goods ]; arr.push(obj); } return arr; } /** *购物车数据整理 * @param {Array} list 规格数组 */ setCartsGoodsToPageData(list) { const arr = []; list = _.groupBy(list, 'shop._id'); for (const key in list) { const data = list[key]; const shopData = _.get(_.head(data), 'shop'); const obj = {}; obj.shop = _.get(shopData, '_id'); obj.shop_name = _.get(shopData, 'name'); const goodsList = []; for (const i of data) { const goods = {}; goods.cart_id = _.get(i, '_id'); goods.goods_id = _.get(i.goods, '_id'); goods.goods_name = _.get(i.goods, 'name'); goods.goodsSpec_id = _.get(i.goodsSpec, '_id'); goods.goodsSpec_name = _.get(i.goodsSpec, 'name'); goods.freight = _.get(i.goodsSpec, 'freight'); goods.money = _.get(i.goodsSpec, 'sell_money'); goods.num = _.get(i, 'num'); goods.file = _.get(i.goods, 'file', []); goods.tags = _.get(i.goods, 'tags'); // #region 团购部分 // 团购价 goods.group_sell_money = _.get(i.goodsSpec, 'group_config.money'); // #endregion goodsList.push(goods); } obj.goods = goodsList; arr.push(obj); } return arr; } async afterQuery(filter, data) { data = JSON.parse(JSON.stringify(data)); for (const i of data) { const { goods } = i; const buy_num_total = goods.reduce( (p, n) => this.ctx.plus( p, n.goods.reduce((np, ng) => this.ctx.plus(np, ng.buy_num), 0) ), 0 ); i.buy_num_total = buy_num_total; i.real_pay = _.get(i, 'pay.pay_money'); } 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;