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