|
@@ -0,0 +1,388 @@
|
|
|
+import { Inject, Provide } from '@midwayjs/decorator';
|
|
|
+import { InjectEntityModel } from '@midwayjs/typegoose';
|
|
|
+import { ReturnModelType } from '@typegoose/typegoose';
|
|
|
+import { BaseService, FrameworkErrorEnum, ServiceError } from 'free-midway-component';
|
|
|
+import { GroupOrder } from '../entity/group/groupOrder';
|
|
|
+import { Shop } from '../entity/base/shop';
|
|
|
+import { toOrderPageDTO } from '../interface/groupOrder.interface';
|
|
|
+import { Goods } from '../entity/base/goods';
|
|
|
+import { GoodsSpec } from '../entity/base/goodsSpec';
|
|
|
+import { Group } from '../entity/group/group';
|
|
|
+import { GoodsConfig } from '../entity/group/goodsConfig';
|
|
|
+import _ = require('lodash');
|
|
|
+import moment = require('moment');
|
|
|
+import { RedisService } from '@midwayjs/redis';
|
|
|
+import * as computedUtil from '../util/computed';
|
|
|
+import { PopulateOptions, Types } from 'mongoose';
|
|
|
+import { CashBack } from '../entity/base/cashBack';
|
|
|
+import { User } from '../entity/base/user';
|
|
|
+import { HttpServiceFactory } from '@midwayjs/axios';
|
|
|
+import { randomStr } from '../util/util';
|
|
|
+import { GroupAfterSale } from '../entity/group/groupAfterSale';
|
|
|
+import { lookUp } from '../util/pipeline.util';
|
|
|
+import { GoodsConfigService } from './goodsConfig.service';
|
|
|
+const ObjectId = Types.ObjectId;
|
|
|
+type modelType = ReturnModelType<typeof GroupOrder>;
|
|
|
+@Provide()
|
|
|
+export class GroupOrderService extends BaseService<modelType> {
|
|
|
+ @Inject()
|
|
|
+ httpServiceFactory: HttpServiceFactory;
|
|
|
+ @InjectEntityModel(GroupOrder)
|
|
|
+ model: modelType;
|
|
|
+
|
|
|
+ @Inject()
|
|
|
+ goodsConfigService: GoodsConfigService;
|
|
|
+
|
|
|
+ @InjectEntityModel(Shop)
|
|
|
+ shopModel: ReturnModelType<typeof Shop>;
|
|
|
+
|
|
|
+ @InjectEntityModel(Goods)
|
|
|
+ goodsModel: ReturnModelType<typeof Goods>;
|
|
|
+ @InjectEntityModel(GoodsSpec)
|
|
|
+ goodsSpecModel: ReturnModelType<typeof GoodsSpec>;
|
|
|
+ @InjectEntityModel(Group)
|
|
|
+ groupModel: ReturnModelType<typeof Group>;
|
|
|
+ @InjectEntityModel(GoodsConfig)
|
|
|
+ goodsConfigModel: ReturnModelType<typeof GoodsConfig>;
|
|
|
+ @InjectEntityModel(CashBack)
|
|
|
+ cashBackModel: ReturnModelType<typeof CashBack>;
|
|
|
+ @InjectEntityModel(User)
|
|
|
+ userModel: ReturnModelType<typeof User>;
|
|
|
+ @InjectEntityModel(GroupAfterSale)
|
|
|
+ afterSaleModel: ReturnModelType<typeof GroupAfterSale>;
|
|
|
+
|
|
|
+ @Inject()
|
|
|
+ redis: RedisService;
|
|
|
+
|
|
|
+ async checkGroupSuccess(id) {
|
|
|
+ const data = await this.model.findById(id, { group: 1 });
|
|
|
+ const group = await this.groupModel.findById(_.get(data, 'group')).lean();
|
|
|
+ return _.get(group, 'status') === '1';
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 给团长提成
|
|
|
+ * @param id 订单id
|
|
|
+ * @param tran 事务实例
|
|
|
+ */
|
|
|
+ async leaderCommission(id, tran) {
|
|
|
+ const order = await this.model.findById(id);
|
|
|
+ if (!order) throw new ServiceError('未找到订单数据', FrameworkErrorEnum.NOT_FOUND_DATA);
|
|
|
+ const { num, config, group } = order;
|
|
|
+ const money = computedUtil.multiply(num, _.get(config, 'leader_get'));
|
|
|
+ const time = moment().format('YYYY-MM-DD HH:mm:ss');
|
|
|
+ const source = '2';
|
|
|
+ const source_id = id;
|
|
|
+ const gd = await this.groupModel.findById(group);
|
|
|
+ if (!gd) throw new ServiceError('未找到团信息', FrameworkErrorEnum.NOT_FOUND_DATA);
|
|
|
+ const inviter = _.get(gd, 'leader');
|
|
|
+ if (!inviter) return;
|
|
|
+ const cashBackData = { inviter, money, time, source, source_id };
|
|
|
+ // 检查是否入账,不要重复入账
|
|
|
+ const cbn = await this.cashBackModel.count({ source, source_id });
|
|
|
+ if (cbn > 0) throw new ServiceError('改订单已经入账', FrameworkErrorEnum.SERVICE_FAULT);
|
|
|
+ // 0元不入账
|
|
|
+ if (money > 0) tran.insert('CashBack', cashBackData);
|
|
|
+ return money;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 使用事务存储数据
|
|
|
+ * @param data 数据
|
|
|
+ * @param tran 事务
|
|
|
+ */
|
|
|
+ tranCreate(data, tran) {
|
|
|
+ return tran.insert('goodsOrder', data);
|
|
|
+ }
|
|
|
+ /**
|
|
|
+ * 组织创建订单数据
|
|
|
+ * @param data 接口数据
|
|
|
+ * @param customer 用户id
|
|
|
+ */
|
|
|
+ async toMakeOrderData(data, customer) {
|
|
|
+ data.customer = customer;
|
|
|
+ const result = _.cloneDeep(data);
|
|
|
+ const goods = await this.goodsModel.findById(data.goods).lean();
|
|
|
+ if (!goods) throw new ServiceError('未找到指定商品,订单创建失败', FrameworkErrorEnum.SERVICE_FAULT);
|
|
|
+ result.goods = goods;
|
|
|
+ const goodsSpec = await this.goodsSpecModel.findById(data.goodsSpec).lean();
|
|
|
+ if (!goodsSpec) throw new ServiceError('未找到指定商品规格,订单创建失败', FrameworkErrorEnum.SERVICE_FAULT);
|
|
|
+ result.goodsSpec = goodsSpec;
|
|
|
+ // 找到当前团设置的商品规格
|
|
|
+ const gpd = await this.groupModel.findById(data.group).lean();
|
|
|
+ if (!gpd) throw new ServiceError('未找到团信息');
|
|
|
+ const goodsConfigs = _.get(gpd, 'group_config', []);
|
|
|
+ const goodsConfig = goodsConfigs.find(f => _.isEqual(_.get(f, 'spec._id'), data.goodsSpec));
|
|
|
+ result.config = goodsConfig;
|
|
|
+ result.buy_time = moment().format('YYYY-MM-DD HH:mm:ss');
|
|
|
+ result.no = this.getNo();
|
|
|
+ const moneyDetail = await this.moneyDetail(result);
|
|
|
+ const user = await this.userModel.findById(customer, { openid: 1 }).lean();
|
|
|
+ const pay_money = _.get(moneyDetail, 'total');
|
|
|
+ const pay = {
|
|
|
+ pay_money,
|
|
|
+ openid: _.get(user, 'openid'),
|
|
|
+ };
|
|
|
+ result.pay = pay;
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 减库存
|
|
|
+ * @param data 订单数据
|
|
|
+ * @param tran 事务实例
|
|
|
+ */
|
|
|
+ async dealGoods(data, tran) {
|
|
|
+ const { goodsSpec, num } = data;
|
|
|
+ const newNum = computedUtil.minus(_.get(goodsSpec, 'num'), num);
|
|
|
+ tran.update('GoodsSpec', { _id: _.get(goodsSpec, '_id') }, { num: newNum });
|
|
|
+ }
|
|
|
+ /**
|
|
|
+ * 还原库存
|
|
|
+ * @param data 订单数据
|
|
|
+ * @param tran 事务实例
|
|
|
+ */
|
|
|
+ async returnGoods(data, tran) {
|
|
|
+ const { goodsSpec, num } = data;
|
|
|
+ const newNum = computedUtil.plus(_.get(goodsSpec, 'num'), num);
|
|
|
+ tran.update('GoodsSpec', { _id: _.get(goodsSpec, '_id') }, { num: newNum });
|
|
|
+ }
|
|
|
+ /**
|
|
|
+ * 加销量
|
|
|
+ * @param goods 商品id
|
|
|
+ * @param num 数量
|
|
|
+ * @param tran 事务实例
|
|
|
+ */
|
|
|
+ async addSell(goods: string, num: number, tran) {
|
|
|
+ const gd = await this.goodsModel.findById(goods, { sell_num: 1 }).lean();
|
|
|
+ if (!gd) throw new ServiceError('未找到商品信息', FrameworkErrorEnum.NOT_FOUND_DATA);
|
|
|
+ const { sell_num } = gd;
|
|
|
+ const newSellNum = computedUtil.plus(sell_num, num);
|
|
|
+ tran.update('Goods', { _id: _.get(goods, '_id') }, { sell_num: newSellNum });
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 组织成页面数据
|
|
|
+ ** 因为要新写页面,所以这个地方的数据变得简单点
|
|
|
+ ** 团购规定是只能买一种商品且是1个规格.
|
|
|
+ ** 所以只有数量 和 优惠有变化(优惠取决于是否可以使用优惠券,先不做TODO标记下)
|
|
|
+ ** 数据返回简单点
|
|
|
+ * @param data 缓存数据(检查是否可以购买的数据)
|
|
|
+ */
|
|
|
+ async getOrderPageData(data: toOrderPageDTO) {
|
|
|
+ const { shop, goods, goodsSpec, num, group } = data;
|
|
|
+ const sd = await this.shopModel.findById(shop, { name: 1 }).lean();
|
|
|
+ const gd = await this.goodsModel.findById(goods, { name: 1, file: 1 }).lean();
|
|
|
+ const gsd = await this.goodsSpecModel.findById(goodsSpec, { name: 1, freight: 1 }).lean();
|
|
|
+ Object.assign(gsd, { freight: computedUtil.toNumber(gsd.freight) });
|
|
|
+ const gpd = await this.groupModel.findById(group).lean();
|
|
|
+ const goodsConfigs = _.get(gpd, 'group_config', []);
|
|
|
+ const goodsConfig = goodsConfigs.find(f => _.isEqual(_.get(f, 'spec._id'), goodsSpec));
|
|
|
+ // 团购中,团长也按团员价格走
|
|
|
+ // const leader = _.get(this.ctx, 'user.is_leader', '1');
|
|
|
+ const customer = _.get(this.ctx, 'user._id');
|
|
|
+ let price = 0;
|
|
|
+ // if (leader === '0') price = _.get(goodsConfig, 'leader_price');
|
|
|
+ // else
|
|
|
+ price = _.get(goodsConfig, 'price');
|
|
|
+ const freight = _.get(goodsConfig, 'freight');
|
|
|
+ Object.assign(gsd, { price, freight });
|
|
|
+ // 获取默认地址
|
|
|
+ const axios = this.httpServiceFactory.get('base');
|
|
|
+ const result = await axios.get(`/address?customer=${customer}&is_default=1`);
|
|
|
+ const address = _.head(result.data);
|
|
|
+ // 计算total_detail
|
|
|
+ const total_detail = [];
|
|
|
+ const goods_total = computedUtil.multiply(num, price);
|
|
|
+ total_detail.push({ key: 'goods_total', zh: '商品总价', money: goods_total });
|
|
|
+ const freight_total = computedUtil.multiply(num, freight);
|
|
|
+ total_detail.push({ key: 'freight_total', zh: '运费总价', money: freight_total });
|
|
|
+ return { shop: sd, goods: gd, goodsSpec: gsd, group, num, address, total_detail };
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 1.检查是否可以购买商品-检查店铺
|
|
|
+ * @param data 前端数据
|
|
|
+ */
|
|
|
+ async checkCanBuy_shop(data: toOrderPageDTO) {
|
|
|
+ const { shop } = data;
|
|
|
+ const num = await this.shopModel.count({ _id: shop, status: '1' });
|
|
|
+ if (!num) throw new ServiceError('该店铺不处于营业状态', FrameworkErrorEnum.SERVICE_FAULT);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 2.检查是否可以购买商品-检查商品
|
|
|
+ * @param data 前端数据
|
|
|
+ */
|
|
|
+ async checkCanBuy_goods(data: toOrderPageDTO) {
|
|
|
+ const { goods } = data;
|
|
|
+ const num = await this.goodsModel.count({ _id: goods, status: '1' }); // , status: '0'
|
|
|
+ if (!num) throw new ServiceError('该商品已下架', FrameworkErrorEnum.SERVICE_FAULT);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 3.检查是否可以购买商品-检查商品规格
|
|
|
+ * @param data 前端数据
|
|
|
+ */
|
|
|
+ async checkCanBuy_goodsSpec(data: toOrderPageDTO) {
|
|
|
+ const { goodsSpec } = data;
|
|
|
+ const gs = await this.goodsSpecModel.findById(goodsSpec).lean();
|
|
|
+ if (!gs) throw new ServiceError('未找到指定规格', FrameworkErrorEnum.SERVICE_FAULT);
|
|
|
+ const num = _.get(gs, 'num', 0);
|
|
|
+ const buy_num = _.get(data, 'num', 0);
|
|
|
+ if (num < buy_num) throw new ServiceError('规格库存不足', FrameworkErrorEnum.SERVICE_FAULT);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 4.检查是否可以购买商品-检查团
|
|
|
+ * @param data 前端数据
|
|
|
+ */
|
|
|
+ async checkCanBuy_group(data: toOrderPageDTO) {
|
|
|
+ const { group } = data;
|
|
|
+ const g = await this.groupModel.findById(group);
|
|
|
+ // 找这个团
|
|
|
+ if (!g) throw new ServiceError('未找到指定团', FrameworkErrorEnum.SERVICE_FAULT);
|
|
|
+ const { status, start_time, end_time, person_limit } = g;
|
|
|
+ // 看这个团的状态
|
|
|
+ if (status !== '0') throw new ServiceError('团已结束', FrameworkErrorEnum.SERVICE_FAULT);
|
|
|
+ const r = moment().isBetween(start_time, end_time, null, '[]');
|
|
|
+ // 再看当前时间
|
|
|
+ if (!r) throw new ServiceError('当前时间不在该团开团时间内', FrameworkErrorEnum.SERVICE_FAULT);
|
|
|
+ // 再看已经有几单了
|
|
|
+ const num = await this.model.count({
|
|
|
+ group,
|
|
|
+ 'pay.result': { $exists: true },
|
|
|
+ });
|
|
|
+ if (num > person_limit) throw new ServiceError('团已满人', FrameworkErrorEnum.SERVICE_FAULT);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 5.检查指定规格是否在团购设置中存在
|
|
|
+ * @param data 前端数据
|
|
|
+ */
|
|
|
+ async checkCanBuy_specInGroupConfig(data: toOrderPageDTO) {
|
|
|
+ const { group, goods, goodsSpec, num } = data;
|
|
|
+ const refs = this.goodsConfigService.getRefs();
|
|
|
+ const gc = await this.goodsConfigModel
|
|
|
+ .find({ group, goods })
|
|
|
+ .populate(refs as PopulateOptions[])
|
|
|
+ .lean();
|
|
|
+ if (gc.length <= 0) throw new ServiceError('未找到商品参加团购设置', FrameworkErrorEnum.SERVICE_FAULT);
|
|
|
+ const config = gc.find(f => new ObjectId(_.get(f, 'spec._id')).toString() === goodsSpec);
|
|
|
+ if (!config) throw new ServiceError('未找到规格参加团购设置', FrameworkErrorEnum.SERVICE_FAULT);
|
|
|
+ // 检查是否有购买限制
|
|
|
+ const buy_limit = _.get(config, 'buy_limit', _.get(config, 'spec.buy_limit'));
|
|
|
+ // 没有购买限制或者购买限制为无限制,则不进行后续检查
|
|
|
+ if (!buy_limit || buy_limit === '0') return;
|
|
|
+ const limit_num = _.get(config, 'limit_num', _.get(config, 'spec.limit_num', 0));
|
|
|
+ if (buy_limit === '1') {
|
|
|
+ if (num < limit_num) throw new ServiceError(`该规格至少购买 ${limit_num} 份, 购买数量不足`, FrameworkErrorEnum.SERVICE_FAULT);
|
|
|
+ } else if (buy_limit === '2') {
|
|
|
+ if (num > limit_num) throw new ServiceError(`该规格至多购买 ${limit_num} 份, 购买数量超过上限`, FrameworkErrorEnum.SERVICE_FAULT);
|
|
|
+ } else if (buy_limit === '3') {
|
|
|
+ if (num !== limit_num) throw new ServiceError(`该规格只能购买 ${limit_num} 份`, FrameworkErrorEnum.SERVICE_FAULT);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 缓存进redis
|
|
|
+ * @param data 缓存数据
|
|
|
+ */
|
|
|
+ async makeCache(data) {
|
|
|
+ const str = randomStr();
|
|
|
+ const orderKey = this.app.getConfig('redisKey.orderKeyPrefix');
|
|
|
+ const timeout = this.app.getConfig('redisTimeout');
|
|
|
+ const key = `${orderKey}${str}`;
|
|
|
+ const value = JSON.stringify(data);
|
|
|
+ await this.redis.set(key, value, 'EX', timeout);
|
|
|
+ return str;
|
|
|
+ }
|
|
|
+ /**
|
|
|
+ * 获取缓存内容
|
|
|
+ * @param key redis的key
|
|
|
+ * @returns 缓存数据
|
|
|
+ */
|
|
|
+ async getCache(key) {
|
|
|
+ const orderKey = this.app.getConfig('redisKey.orderKeyPrefix');
|
|
|
+ key = `${orderKey}${key}`;
|
|
|
+ const data = await this.redis.get(key);
|
|
|
+ await this.redis.del(key);
|
|
|
+ if (data) return JSON.parse(data);
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 获取订单号 */
|
|
|
+ getNo(): string {
|
|
|
+ const str = randomStr();
|
|
|
+ const no = `TEHQG${moment().format('YYYYMMDDHHmmss')}-${str}`;
|
|
|
+ return no;
|
|
|
+ }
|
|
|
+ /**
|
|
|
+ * 计算价格明细,因为只有一个商品,所以直接就是一个数
|
|
|
+ * @param data 含有规格及其团购设置的对象,购买数量
|
|
|
+ * @property {object} goodsSpec 商品规格
|
|
|
+ * @property {object} config 规格的团购设置
|
|
|
+ * @property {number} num 购买数量
|
|
|
+ */
|
|
|
+ async moneyDetail(data): Promise<object> {
|
|
|
+ const { goodsSpec, config, num, customer } = data;
|
|
|
+ const { _id } = goodsSpec;
|
|
|
+ const { spec = {} } = config;
|
|
|
+ if (!new Types.ObjectId(_id).equals(spec._id)) throw new ServiceError('价格明细处理报错:不是同一个规格', FrameworkErrorEnum.SERVICE_FAULT);
|
|
|
+ // 如果当前用户是团长身份,则以团长价格购买,反之以团员价格购买
|
|
|
+ // const leader = _.get(this.ctx, 'user.is_leader', '1');
|
|
|
+ // const user = await this.userModel.findById(customer);
|
|
|
+ // 团购中,团长也按照团员价格走
|
|
|
+ // const leader = _.get(user, 'is_leader', '1');
|
|
|
+ let price = 0;
|
|
|
+ // if (leader === '0') price = _.get(config, 'leader_price');
|
|
|
+ // else
|
|
|
+ price = _.get(config, 'price');
|
|
|
+ const gt = computedUtil.multiply(price, num);
|
|
|
+ const ft = computedUtil.multiply(_.get(config, 'freight'), num);
|
|
|
+ const obj = { price, gt, ft, num, total: computedUtil.plus(gt, ft) };
|
|
|
+ return obj;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 非id单查询
|
|
|
+ * @param query 查询条件
|
|
|
+ */
|
|
|
+ async findOne(query: object) {
|
|
|
+ const data = await this.model.findOne(query).lean();
|
|
|
+ return data;
|
|
|
+ }
|
|
|
+ /**
|
|
|
+ * 为数据填充内容:
|
|
|
+ ** is_afterSale:是否存在售后
|
|
|
+ ** is_rate:是否评价(暂时不加)
|
|
|
+ * @param data 数据
|
|
|
+ */
|
|
|
+ async addInfo(data) {
|
|
|
+ const asn = await this.afterSaleModel.count({ order: data._id, status: { $nin: ['-1', '!1', '-2', '!2', '-3', '!3', '-4', '!4', '-5', '!5'] } });
|
|
|
+ data.is_afterSale = asn > 0;
|
|
|
+
|
|
|
+ return data;
|
|
|
+ }
|
|
|
+
|
|
|
+ async userListView(filter, pageOptions = {}) {
|
|
|
+ const dup = _.cloneDeep(filter.getFilter());
|
|
|
+ const refs = this.getRefs();
|
|
|
+ const pipeline = [];
|
|
|
+ for (const ref of refs) {
|
|
|
+ const path = _.get(ref, 'path');
|
|
|
+ const modelName = _.lowerFirst(_.get(ref, 'model.modelName'));
|
|
|
+ if (!path) continue;
|
|
|
+ const arr = lookUp(path, modelName);
|
|
|
+ pipeline.push(...arr);
|
|
|
+ }
|
|
|
+ pipeline.push({ $match: dup });
|
|
|
+ if (_.get(pageOptions, 'sort')) pipeline.push({ $sort: _.get(pageOptions, 'sort') });
|
|
|
+ const qp = _.cloneDeep(pipeline);
|
|
|
+ if (_.get(pageOptions, 'skip')) qp.push({ $skip: _.get(pageOptions, 'skip') });
|
|
|
+ if (_.get(pageOptions, 'limit')) qp.push({ $limit: _.get(pageOptions, 'limit') });
|
|
|
+ const data = await this.model.aggregate(qp);
|
|
|
+ const totalResult = await this.model.aggregate([...pipeline, { $count: 'total' }]);
|
|
|
+ const total = _.get(_.head(totalResult), 'total', 0);
|
|
|
+ return { data, total };
|
|
|
+ }
|
|
|
+}
|