lrf 2 years ago
parent
commit
bae441d1fd

+ 36 - 4
app/controller/trade/config/.coupon.js

@@ -1,6 +1,20 @@
 module.exports = {
   create: {
-    requestBody: ['money', 'issue', 'shop', 'can_plus', 'use_time', 'time_start', 'time_end', 'num', 'status', 'is_use', 'tags'],
+    requestBody: [
+      'issue',
+      'shop',
+      'name',
+      'expire_type',
+      'expire_config',
+      'discount_type',
+      'discount_config',
+      'use_limit',
+      'use_limit_config',
+      'get_limit',
+      'get_limit_config',
+      'num',
+      'status',
+    ],
   },
   destroy: {
     params: ['!id'],
@@ -8,7 +22,21 @@ module.exports = {
   },
   update: {
     params: ['!id'],
-    requestBody: ['money', 'issue', 'shop', 'can_plus', 'use_time', 'time_start', 'time_end', 'num', 'status', 'is_use', 'tags'],
+    requestBody: [
+      'issue',
+      'shop',
+      'name',
+      'expire_type',
+      'expire_config',
+      'discount_type',
+      'discount_config',
+      'use_limit',
+      'use_limit_config',
+      'get_limit',
+      'get_limit_config',
+      'num',
+      'status',
+    ],
   },
   show: {
     parameters: {
@@ -22,8 +50,12 @@ module.exports = {
         'meta.createdAt@start': 'meta.createdAt@start',
         'meta.createdAt@end': 'meta.createdAt@end',
         shop: 'shop',
-        'time_start@start': 'time_start@start',
-        'time_end@end': 'time_end@end',
+        name: 'name',
+        expire_type: 'expire_type',
+        discount_type: 'discount_type',
+        use_limit: 'use_limit',
+        get_limit: 'get_limit',
+        status: 'status',
       },
       // options: {
       //   "meta.state": 0 // 默认条件

+ 4 - 1
app/controller/trade/config/.order.js

@@ -1,6 +1,6 @@
 module.exports = {
   create: {
-    requestBody: ['customer', 'address', 'goods', 'total_detail', 'buy_time', 'pay', 'no', 'status'],
+    requestBody: ['customer', 'address', 'goods', 'total_detail', 'buy_time', 'pay', 'no', 'status', 'coupon'],
   },
   destroy: {
     params: ['!id'],
@@ -48,4 +48,7 @@ module.exports = {
   fromGoodsToOrder: {
     requestBody: ['!shop', '!goods', '!goodsSpec', '!num'],
   },
+  cancel: {
+    requestBody: ['!order_id'],
+  },
 };

+ 43 - 0
app/controller/user/config/.userCoupon.js

@@ -0,0 +1,43 @@
+module.exports = {
+  create: {
+    requestBody: ['customer', 'coupon', 'status'],
+  },
+  destroy: {
+    params: ['!id'],
+    service: 'delete',
+  },
+  update: {
+    params: ['!id'],
+    requestBody: ['customer', 'coupon', 'status'],
+  },
+  show: {
+    parameters: {
+      params: ['!id'],
+    },
+    service: 'fetch',
+  },
+  index: {
+    parameters: {
+      query: {
+        'meta.createdAt@start': 'meta.createdAt@start',
+        'meta.createdAt@end': 'meta.createdAt@end',
+        customer: 'customer',
+        coupon: 'coupon',
+        status: 'status',
+      },
+      // options: {
+      //   "meta.state": 0 // 默认条件
+      // },
+    },
+    service: 'query',
+    options: {
+      query: ['skip', 'limit'],
+      sort: ['meta.createdAt'],
+      desc: true,
+      count: true,
+    },
+  },
+  getCoupon: {
+    params: ['!coupon_id'],
+  },
+};

+ 13 - 0
app/controller/user/userCoupon.js

@@ -0,0 +1,13 @@
+'use strict';
+const meta = require('./config/.userCoupon.js');
+const Controller = require('egg').Controller;
+const { CrudController } = require('naf-framework-mongoose-free/lib/controller');
+
+// 
+class UserCouponController extends Controller {
+  constructor(ctx) {
+    super(ctx);
+    this.service = this.ctx.service.user.userCoupon;
+  }
+}
+module.exports = CrudController(UserCouponController, meta);

+ 37 - 12
app/model/trade/coupon.js

@@ -1,31 +1,56 @@
 'use strict';
 const Schema = require('mongoose').Schema;
 const metaPlugin = require('naf-framework-mongoose-free/lib/model/meta-plugin');
+// 失效配置: expire_type nolimit 不需要设置
+const expire_config = {
+  fixed: [{ start: '生效开始日期', end: '生效结束日期' }], // 存数组
+  days: '生效${days}天后过期', // Number
+};
+// 减免配置: 在limit中, 都有一个选项,是无限制(nolimit)
+// min(减免)/dicount(折扣)
+const discount_config = {
+  limit: '使用券的金额下限',
+  min: '满减:减多少 / 折扣:9折 = 90%;8.5折=85%; 输入前面 x折的 x',
+  max: '上限,折扣有上限,满减不取上限',
+};
+// 使用配置: use_limit为all时,不需要这个设置
+const use_limit_config = {
+  tags: '指定商品类型',
+};
+// 领取配置: get_limit为nolimit时,不需要这个设置
+const get_limit_config = {
+  max: '限制每人做多领取多少张该优惠券',
+};
 
-const MoneyPlugin = require('naf-framework-mongoose-free/lib/model/type-money-plugin');
 
 // 优惠券
 const coupon = {
   issue: { type: String, required: false, zh: '发行方' }, // 字典:coupon_issue 平台/店铺
   shop: { type: String, required: false, zh: '店铺', ref: 'Shop.Shop' }, // 店铺发行的需要有店铺关联
-  can_plus: { type: String, required: false, zh: '可叠加' }, // 字典:use
-  use_time: { type: String, required: false, zh: '时间限制' }, // 字典:use
-  time_start: { type: String, required: false, zh: '开始时间' }, //
-  time_end: { type: String, required: false, zh: '结束时间' }, //
-  num: { type: String, required: false, zh: '数量' }, // 没有就是不限制
-  status: { type: String, required: false, default: '0', zh: '状态' }, // 字典:coupon_status
-  is_use: { type: String, required: false, zh: '是否使用' }, // 字典:use
-  tags: { type: Array, required: false, zh: '商品类型' }, // 优惠券适用于的商品标签.没有就是全都适用
+  name: { type: String, required: false, zh: '优惠券名称' }, //
+  expire_type: { type: String, required: false, zh: '失效方式' }, // 字典:coupon_expire_type
+  expire_config: { type: Object, required: false, zh: '失效配置' }, //
+  discount_type: { type: String, required: false, zh: '减免方式' }, // 字典:coupon_discount_type
+  discount_config: { type: Object, required: false, zh: '减免配置' }, //
+  use_limit: { type: String, required: false, zh: '使用限制' }, // 字典:coupon_use_limit
+  use_limit_config: { type: Object, required: false, zh: '使用配置' }, //
+  get_limit: { type: String, required: false, zh: '领取限制' }, // 字典:coupon_get_limit
+  get_limit_config: { type: String, required: false, zh: '领取配置' }, //
+  num: { type: Number, required: false, zh: '数量' }, //
+  status: { type: String, required: false, zh: '使用状态' }, // 字典:use
 };
 const schema = new Schema(coupon, { toJSON: { getters: true, virtuals: true } });
 schema.index({ id: 1 });
 schema.index({ 'meta.createdAt': 1 });
 schema.index({ shop: 1 });
-schema.index({ time_start: 1 });
-schema.index({ time_end: 1 });
+schema.index({ name: 1 });
+schema.index({ expire_type: 1 });
+schema.index({ discount_type: 1 });
+schema.index({ use_limit: 1 });
+schema.index({ get_limit: 1 });
+schema.index({ status: 1 });
 
 schema.plugin(metaPlugin);
-schema.plugin(MoneyPlugin({ zh: '适用价格', required: false, key: 'money' }));
 
 module.exports = app => {
   const { mongoose } = app;

+ 23 - 0
app/model/user/userCoupon.js

@@ -0,0 +1,23 @@
+'use strict';
+const Schema = require('mongoose').Schema;
+const metaPlugin = require('naf-framework-mongoose-free/lib/model/meta-plugin');
+
+// 领劵记录
+const userCoupon = {
+  customer: { type: String, required: false, zh: '顾客', ref: 'User.User' }, //
+  coupon: { type: String, required: false, zh: '优惠券', ref: 'Trade.Coupon' }, //
+  status: { type: String, required: false, default: '0', zh: '是否使用' }, //
+};
+const schema = new Schema(userCoupon, { toJSON: { getters: true, virtuals: true } });
+schema.index({ id: 1 });
+schema.index({ 'meta.createdAt': 1 });
+schema.index({ customer: 1 });
+schema.index({ coupon: 1 });
+schema.index({ status: 1 });
+
+schema.plugin(metaPlugin);
+
+module.exports = app => {
+  const { mongoose } = app;
+  return mongoose.model('UserCoupon', schema, 'userCoupon');
+};

+ 4 - 1
app/service/trade/coupon.js

@@ -3,13 +3,16 @@ 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');
 
-// 
+//
 class CouponService extends CrudService {
   constructor(ctx) {
     super(ctx, 'coupon');
     this.model = this.ctx.model.Trade.Coupon;
   }
+
+
 }
 
 module.exports = CouponService;

+ 65 - 6
app/service/trade/order.js

@@ -16,6 +16,7 @@ class OrderService extends CrudService {
     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();
   }
 
@@ -31,7 +32,7 @@ class OrderService extends CrudService {
       const user = this.ctx.user;
       const customer = _.get(user, '_id');
       if (!customer) throw new BusinessError(ErrorCode.NOT_LOGIN, '未找到用户信息');
-      const { address, goods, total_detail } = body;
+      const { address, goods, total_detail, coupon = [] } = body;
       // 检测商品是否可以下单
       for (const i of goods) {
         const { shop } = i;
@@ -42,6 +43,7 @@ class OrderService extends CrudService {
         }
       }
       const orderData = {};
+      const goodsSpecs = [];
       // 数据做快照处理
       // 1.地址快照
       const addressData = await this.addressModel.findById(address._id);
@@ -61,14 +63,18 @@ class OrderService extends CrudService {
           // 将商店内容剔除
           const { goods: gd, ...dOthers } = d;
           const gdOthers = _.omit(gd, [ 'shop' ]);
-          const obj = { ...dOthers, goods: gdOthers, buy_num: num };
+          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 });
         }
         goodsData.push({ ...others, goods: qp });
       }
-      // TODO: 3.商品总计明细.这地方没加优惠券,所以先直接复制即可
-      const totalDetailData = total_detail;
+      // 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;
@@ -85,9 +91,10 @@ class OrderService extends CrudService {
       // 生成数据
       // const order = await this.model.create(orderData);
       const order_id = this.tran.insert('Order', orderData);
-      // 处理库存问题
       // 处理库存,删除购物车
       await this.dealGoodsNum(goodsData);
+      // 处理优惠券,改为使用过
+      await this.ctx.service.user.userCoupon.useCoupon(coupon, this.tran);
       await this.tran.run();
       return order_id;
     } catch (error) {
@@ -99,6 +106,52 @@ class OrderService extends CrudService {
       this.tran.clean();
     }
   }
+
+  /**
+   * 取消订单(支付前)
+   * @param {Object} body 参数体
+   * @param {String} body.order_id 订单id
+   */
+  async cancel({ order_id }) {
+    try {
+      const order = await this.model.findById(order_id);
+      if (!order) throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '未找到订单信息');
+      // 退单分为 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 = (parseInt(goodsSpec.num) || 0) + (parseInt(i.buy_num) || 0);
+          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.log(error);
+      throw new BusinessError(ErrorCode.SERVICE_FAULT, '订单取消失败');
+    } finally {
+      this.tran.clean();
+    }
+  }
+
+
   /**
    * 减库存,删除购物车
    * @param {Array} list 商品
@@ -151,11 +204,15 @@ class OrderService extends CrudService {
     // 找到默认地址
     const address = await this.addressModel.findOne({ customer, is_default: '1' });
     pageData.address = address;
-    // // 商品总价,各店铺的价格明细
+    // 商品总价,各店铺的价格明细
     specsData = this.computedShopTotal(specsData);
     const shopTotalDetail = this.computedAllTotal(specsData);
     pageData.goodsData = specsData;
     pageData.orderTotal = shopTotalDetail;
+    // 优惠券列表
+    const couponList = await this.ctx.service.user.userCoupon.toMakeOrder_getList(specsData);
+    pageData.couponList = couponList;
+    // 查询优惠券列表
     return pageData;
   }
   /**
@@ -215,6 +272,7 @@ class OrderService extends CrudService {
       goods.money = _.get(i, 'sell_money');
       goods.num = num;
       goods.file = _.get(i.goods, 'file');
+      goods.tags = _.get(i.goods, 'tags');
       obj.goods = [ goods ];
       arr.push(obj);
     }
@@ -245,6 +303,7 @@ class OrderService extends CrudService {
         goods.money = _.get(i.goodsSpec, 'sell_money');
         goods.num = _.get(i, 'num');
         goods.file = _.get(i.goods, 'file', []);
+        goods.tags = _.get(i.goods, 'tags');
         goodsList.push(goods);
       }
       obj.goods = goodsList;

+ 31 - 3
app/service/trade/orderDetail.js

@@ -23,17 +23,26 @@ class OrderDetailService extends CrudService {
     // if (!tran) throw new BusinessError(ErrorCode.DATA_INVALID, '支付成功添加积分:缺少事务参数');
     const order = await this.orderModel.findById(order_id);
     if (!order) throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '未找到支付订单的数据');
-    const { customer, address, goods, no, status, buy_time, pay } = order;
+    const { customer, address, goods, no, status, buy_time, pay, total_detail: otd } = order;
     if (status === '0') throw new BusinessError(ErrorCode.DATA_INVALID, '订单未支付');
     const orderDetailData = { customer, address, order: order_id, buy_time, pay_time: _.get(pay, 'pay_time'), status };
     // 补全 shop, goods, total_detail TODO: 补全 discount;
     // const arr = [];
+    // 分订单计数器
     let noTimes = 1;
     for (const s of goods) {
       const shop = _.get(s, 'shop');
       const goodsList = _.get(s, 'goods', []);
       const detailNo = `${no}-${noTimes}`;
       const total_detail = this.getTotalDetail(goodsList);
+      // 优惠部分分割
+      console.log(otd);
+      if (_.get(otd, 'discount_detail')) {
+        // 如果有优惠部分,那就得找,优惠里面有没有对应的商品规格
+        const discount_detail = this.getGoodsListDiscountDetail(goodsList, _.get(otd, 'discount_detail'));
+        console.log(discount_detail);
+        total_detail.discount_detail = discount_detail;
+      }
       noTimes++;
       const obj = { ...orderDetailData, shop, goods: goodsList, no: detailNo, total_detail };
       tran.insert('OrderDetail', obj);
@@ -41,14 +50,33 @@ class OrderDetailService extends CrudService {
     }
     // await this.model.insertMany(arr);
   }
+  /**
+   * 将商品规格列表中,优惠的部分提取出来,分单用
+   * @param {Array} goodsList 某店的商品列表
+   * @param {Object} odd discount_detail 支付订单的优惠券设置
+   */
+  getGoodsListDiscountDetail(goodsList, odd) {
+    const result = {};
+    for (const uc_id in odd) {
+      const detail = odd[uc_id];
+      const obj = {};
+      for (const g of goodsList) {
+        const { _id } = g;
+        const gdd = detail[_id];
+        if (gdd) obj[_id] = gdd;
+      }
+      result[uc_id] = obj;
+    }
+    return result;
+  }
+
   /**
    * 计算某店铺的金额总计
-   * TODO: 缺少优惠计算部分
    * @param {Array} goodsList 商品规格列表
    */
   getTotalDetail(goodsList) {
     const goods_total = _.floor(
-      goodsList.reduce((p, n) => p + (parseFloat(n.sell_money) || 0), 0),
+      goodsList.reduce((p, n) => p + (parseFloat(n.sell_money) || 0) * (parseInt(n.buy_num) || 0), 0),
       2
     );
     const freight_total = _.floor(

+ 18 - 3
app/service/trade/pay.js

@@ -116,7 +116,6 @@ class PayService extends CrudService {
       await this.ctx.service.trade.orderDetail.create({ order_id: orderData._id }, this.tran);
       // 加积分
       await this.ctx.service.user.point.afterPayOrder({ order_id: orderData._id, openid }, this.tran);
-      console.log(this.tran.getOperations());
       await this.tran.run();
     } catch (error) {
       console.error(error);
@@ -179,9 +178,26 @@ class PayService extends CrudService {
   getOrderNeedPay(order) {
     let total = 0;
     const { total_detail = {} } = order;
-    for (const key in total_detail) {
+    const { discount_detail, ...others } = total_detail;
+    for (const key in others) {
       total = _.floor(total_detail[key] + total, 2);
     }
+    // 优惠券部分
+    if (discount_detail) {
+      for (const uc in discount_detail) {
+        // uc:用户领取的优惠券id
+        // detail: 该优惠券下的明细
+        const detail = discount_detail[uc];
+        if (!detail) continue;
+        for (const gsid in detail) {
+          // gsid:商品规格的id
+          // gs:该商品的价格明细,只取出discountMoney就行
+          const gs = detail[gsid];
+          const dm = _.get(gs, 'discountMoney', 0);
+          total = _.floor(total - dm, 2);
+        }
+      }
+    }
     return total;
   }
 
@@ -206,7 +222,6 @@ class PayService extends CrudService {
     const wxOrderData = { config: this.appConfig, money, openid, order_no, desc, notice_url };
     const url = `${this.wxDomain}/pay/payOrder`;
     const res = await this.httpUtil.cpost(url, wxOrderData);
-    console.log(data);
     if (res) return res;
     throw new BusinessError(ErrorCode.SERVICE_FAULT, '微信下单失败!');
   }

+ 433 - 0
app/service/user/userCoupon.js

@@ -0,0 +1,433 @@
+'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 { ObjectId } = require('mongoose').Types;
+const Transaction = require('mongoose-transactions');
+
+//
+class UserCouponService extends CrudService {
+  constructor(ctx) {
+    super(ctx, 'usercoupon');
+    this.model = this.ctx.model.User.UserCoupon;
+    this.couponModel = this.ctx.model.Trade.Coupon;
+    this.goodsSpecModel = this.ctx.model.Shop.GoodsSpec;
+    this.tran = new Transaction();
+  }
+
+  /**
+   * 修改优惠券状态
+   * @param {Array} ids 用户领取的优惠券id列表
+   * @param {Transaction} tran 数据库事务实例
+   */
+  async useCoupon(ids, tran) {
+    for (const id of ids) {
+      tran.update('UserCoupon', id, { status: '1' });
+    }
+  }
+
+  /**
+   * 领取优惠券
+   * @param {Object} params 地址参数
+   * @param params.coupon_id 优惠券id
+   */
+  async getCoupon({ coupon_id }) {
+    const coupon = await this.couponModel.findById(coupon_id);
+    if (!coupon) throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '未找到优惠券');
+    const canGet = parseInt(coupon.num) - 1 < 0;
+    if (canGet) throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '您手慢了,没抢到');
+    const res = await this.checkCouponCanGet(coupon);
+    if (!res.result) return res;
+    const customer = _.get(this.ctx, 'user._id');
+    if (!customer) throw new BusinessError(ErrorCode.NOT_LOGIN, '未找到用户信息');
+    const data = { coupon: coupon_id, customer };
+    try {
+      // 领券=>减券库存
+      this.tran.insert('UserCoupon', data);
+      this.tran.update('Coupon', coupon_id, { num: parseInt(coupon.num) - 1 });
+      await this.tran.run();
+    } catch (error) {
+      console.log(error);
+      await this.tran.rollback();
+      throw new BusinessError(ErrorCode.SERVICE_FAULT, '领取优惠券失败');
+    } finally {
+      this.tran.clean();
+    }
+  }
+
+  /**
+   * 检查优惠券是否满足可领取的条件
+   * 可使用,就可以领取;否则只是垃圾数据,领了也用不了
+   * @param {Object} coupon 优惠券
+   */
+  async checkCouponCanGet(coupon) {
+    const { expire_type, expire_config, get_limit_config, get_limit, _id } = coupon;
+    // 1.检查优惠券是否生效: expire
+    const tr = this.checkCanGet_expire(expire_type, expire_config);
+    if (!tr.result) return tr;
+    // 2.不需要检查减免配置和使用配置,这两个配置是在使用的时候才检查
+    // 检查领取配置
+    const gr = this.checkCanGet_get(get_limit, get_limit_config, _id);
+    if (!gr.result) return gr;
+    return { result: true };
+  }
+
+  /**
+   * 检查优惠券的过期时间是否符合领取条件
+   * 是否过了 失效时间
+   * 只有时间段的情况才会有这个问题.领取后开始计算失效问题不需要检查
+   * @param {String} type 过期类型
+   * @param {Object} config 过期设置
+   */
+  checkCanGet_expire(type, config) {
+    const res = { result: true };
+    if (type === 'fixed') {
+      const end = _.last(_.get(config, 'fixed'));
+      if (!end) throw new BusinessError(ErrorCode.DATA_INVALID, '优惠券设置错误');
+      const r = moment().isBefore(end);
+      if (!r) {
+        res.result = false;
+        res.msg = '已超过优惠券可使用日期';
+      }
+    }
+    return res;
+  }
+  /**
+   * 检查优惠券的领取设置是否符合领取条件
+   * nolimit直接放过,max需要查看confing里面的设置
+   * @param {String} type 领取类型
+   * @param {Object} config 领取设置
+   * @param {String} coupon 优惠券id
+   */
+  async checkCanGet_get(type, config, coupon) {
+    const res = { result: true };
+    if (type === 'nolimit') return res;
+    const max = _.get(config, type);
+    // 需要找到用户
+    const customer = _.get(this.ctx, 'user._id');
+    if (!customer) throw new BusinessError(ErrorCode.NOT_LOGIN, '未找到用户信息');
+    const num = await this.model.count({ coupon, user: customer });
+    if (num >= max) {
+      res.result = false;
+      res.msg = '该优惠券已到达领取上限';
+    }
+    return res;
+  }
+
+  /**
+   * 根据用户和下单页的商品,对用户的优惠券进行检查,整理数据
+   * @param {Array} goods 商品列表
+   */
+  async toMakeOrder_getList(goods) {
+    const customer = _.get(this.ctx, 'user._id');
+    if (!customer) throw new BusinessError(ErrorCode.NOT_LOGIN, '未找到用户信息');
+    const { populate } = this.getRefMods();
+    let couponList = await this.model.find({ customer, status: '0' }, { customer: 0 }).populate(populate);
+    couponList = JSON.parse(JSON.stringify(couponList));
+    // 接下来检查哪些优惠券符合条件
+    for (const uc of couponList) {
+      // 1.检查失效问题
+      let r = this.checkCanUse_expire(uc);
+      if (!r) {
+        uc.canUse = false;
+        continue;
+      }
+      // 2.检查使用问题
+      r = this.checkCanUse_use(uc, goods);
+      if (!r) {
+        uc.canUse = false;
+        continue;
+      }
+      uc.canUse = true;
+    }
+    return couponList;
+  }
+
+  /**
+   * 检查优惠券的过期时间是否符合使用条件
+   * @param {Object} userCoupon 用户领取的优惠券
+   */
+  checkCanUse_expire(userCoupon) {
+    const { coupon, meta } = userCoupon;
+    const { expire_config, expire_type } = coupon;
+    if (expire_type === 'fixed') {
+      // 在设置的时间段内,则允许使用
+      const start = _.head(_.get(expire_config, 'fixed'));
+      const end = _.last(_.get(expire_config, 'fixed'));
+      if (!start || !end) throw new BusinessError(ErrorCode.DATA_INVALID, '优惠券设置错误');
+      return moment().isBetween(start, end, null, '[]');
+    } else if (expire_type === 'days') {
+      let getDate = _.get(meta, 'createdAt');
+      if (!getDate) return false;
+      // 计算出过期时间, 再看下当前日期是否超过该日期
+      getDate = moment(getDate).format('YYYY-MM-DD HH:mm:ss');
+      const days = _.get(expire_config, 'days');
+      const expire_date = moment(getDate).add(days, 'days');
+      return moment().isSameOrBefore(expire_date);
+    } else if (expire_type === 'nolimit') return true; // 没有过期日期;可以使用
+    return false;
+  }
+
+  /**
+   * 检查该优惠券是否可以在这些商品中使用
+   * @param {Object} userCoupon 用户领取的优惠券
+   * @param {Array} shopGoods 按店铺分组的商品列表
+   */
+  checkCanUse_use(userCoupon, shopGoods) {
+    const { coupon } = userCoupon;
+    const { use_limit, use_limit_config = {}, shop } = coupon;
+    const { tags = [] } = use_limit_config;
+    // 全类型商品,不需要继续检查
+    if (use_limit === 'all' || tags.length === 0) return true;
+    // 检查的所有商品中,只要有一个可以使用,就放过
+    let result = false;
+    for (const s of shopGoods) {
+      // 检查优惠券是否是店铺发行,如果是店铺发行;
+      // 则需要在指定店铺使用,如果不是,则直接检查下一个
+      if (shop && shop !== s.shop) continue;
+      // 是这个店铺的优惠券,则需要检查这个店铺下的商品是否能用这个优惠券
+      // 目前只有商品分类(tags标签)来进行设置
+      // 商品列表(goods)中只要有 标签(tags) 中能找到 和 tags:
+      /** e.g.:
+        const arr = [
+          ['xxsp', 'jgch', 'htr'],
+          ['xxsp', 'jgch', 'ht'],
+        ];
+        const arr1 = [
+          ['xxsp', 'jgch', 'ht1r'],
+          ['xxsp', 'jgch', 'ht'],
+        ];
+        const r = arr.findIndex((f) => arr1.find((ff) => _.isEqual(f, ff)));
+        console.log(r);
+       */
+      const r = s.goods.some((g) => g.tags.find((f) => tags.find((ff) => _.isEqual(f, ff))));
+      if (r) {
+        result = true;
+        break;
+      }
+    }
+    return result;
+  }
+
+  /**
+   * 计算订单使用优惠券的明细
+   * @param {Array} couponIds 订单使用的用户领取的优惠券数据id数组
+   * @param {Array} goodsSpecs 订单所有商品规格的数据:{id,buy_num}
+   */
+  async computedOrderCouponDiscount(couponIds, goodsSpecs) {
+    const result = {}; // 最终结果: 以 couponId: { goodsSpecId: ${money} } 为明细格式
+    const goodsSpecIds = goodsSpecs.map((i) => i.id);
+    const { populate: gp } = this.ctx.service.shop.goodsSpec.getRefMods();
+    let goodsSpecList = await this.goodsSpecModel.find({ _id: goodsSpecIds }).populate(gp);
+    goodsSpecList = this.resetGoodsSpecData(goodsSpecList, goodsSpecs);
+    const { populate: cp } = this.getRefMods();
+    let userCouponList = await this.model.find({ _id: couponIds }, { customer: 0 }).populate(cp);
+    // 先查下有没有不能用的,有不能用的需要返回
+    let cantUse = userCouponList.find((f) => f.status !== '0');
+    if (cantUse) throw new BusinessError(ErrorCode.DATA_INVALID, `优惠券:${_.get(cantUse, 'coupon.name')}已使用`);
+    cantUse = userCouponList.find((f) => !this.checkCanUse_expire(f));
+    if (cantUse) throw new BusinessError(ErrorCode.DATA_INVALID, `优惠券:${_.get(cantUse, 'coupon.name')}不能使用`);
+    userCouponList = this.resetUserCouponData(userCouponList);
+    // 上面整理完数据之后,下面开始用 均分优惠金额
+    for (const coupon of userCouponList) {
+      // 可以被优惠的商品列表
+      let goodsList = [];
+      // 1.先找到满足优惠条件的商品
+      const { issue, shop, use_limit, use_limit_config, _id } = coupon;
+      // 平台券,都可以
+      if (issue === '0') goodsList = goodsSpecList;
+      // 店铺券,需要过滤店铺
+      else goodsList = goodsSpecList.filter((f) => ObjectId(shop).equals(_.get(f, 'shop._id')));
+      // 2.商品已确定,再确定是否是指定分类
+      if (use_limit !== 'all') {
+        // all的情况不需要再次过滤出不符合的条件: all为全部商品类型
+        const tags = _.get(use_limit_config, 'tags', []);
+        goodsList = this.getGoodsInTags(goodsSpecList, tags);
+      }
+      // 3.开始分配
+      const discount_detail = this.divideByCoupon(coupon, goodsList);
+      result[_id] = discount_detail;
+    }
+    // console.log(result);
+    return result;
+  }
+
+  /**
+   * 分配减免
+   * @param {Object} coupon 优惠券{_id:用户领取的优惠券id,...}
+   * @param {Array} goodsList 过滤完后,符合条件的 本订单中的规格商品列表
+   */
+  divideByCoupon(coupon, goodsList) {
+    const { discount_type, discount_config } = coupon;
+    const { limit, min, max } = discount_config;
+    // 符合条件的商品的总价
+    const goodsTotal = goodsList.reduce((p, n) => p + (parseFloat(n.goods_total) || 0), 0);
+    if (limit !== 'nolimit') {
+      // 有最低消费的限制,就需要看看下满足条件的商品够不够这个金额
+      // 消费下限 大于 满足优惠券使用的商品的消费总额,则不能使用这个券,消费的钱不够
+      // 且这个地方应该报错
+      if (parseFloat(limit) > goodsTotal) throw new BusinessError(ErrorCode.DATA_INVALID, `消费券:${coupon.name}不满足使用条件`);
+    }
+    // 减免设置转换成 实际减免总金额:
+    // 满减则直接就是 min(设置的满x减y的y);
+    // 折扣的话,需要计算,是否超过折扣上限(max):
+    // 超过上限则 为上限设置的金额,和满减一样按比例分配即可;
+    // 没超过上限,则为各自去打折即可
+    let discountRealMoney = 0;
+    let outLimit = true; // 是否超过金额上限
+    //
+    if (discount_type === 'min') discountRealMoney = parseFloat(min);
+    else {
+      // 需要计算是否超过上限
+      // 没有上限,那就不会超过
+      if (!max) outLimit = false;
+      else {
+        // 有上限,则需要计算
+        const percent = 1 - parseFloat(min) / 10;
+        // 计算实际折扣 减了 多少钱
+        const discountMoney = _.floor(goodsTotal * percent, 2);
+        // 最后看下 不限制减的钱 是不是 超过了 上限
+        outLimit = discountMoney > parseFloat(max);
+        // 超过了,就将上限的金额拿去按比例分配
+        if (outLimit) discountRealMoney = parseFloat(max);
+      }
+    }
+
+    // 根据是否超过上限金额来决定策略:
+    // 超过上限金额, 按 每种货物的总价值 占 这些货物应该交的钱 的比例进行优惠分配
+    // 没超过上限(目前:只有折扣会有这个情况), 则每个货物都按比例来计算
+    if (!outLimit) return this.discountPercentForEachGoods(coupon, goodsList);
+    return this.divideProportionForEachGoods(goodsList, discountRealMoney, goodsTotal);
+  }
+
+  /**
+   * 按比例计算每件商品,这里就不需要优惠券什么事了.需要优惠券的东西参数都带来了
+   * @param {Array} goodsList 过滤完后,符合条件的 本订单中的规格商品列表
+   * @param {Number} discountRealMoney 需要按比例分配的优惠金额
+   * @param {Number} goodsTotal goodsList的商品总金额
+   */
+  divideProportionForEachGoods(goodsList, discountRealMoney, goodsTotal) {
+    const result = {};
+    // 已分配优惠的金额的总数
+    let allReadyDiscount = 0;
+    for (let i = 0; i < goodsList.length; i++) {
+      const g = goodsList[i];
+      const { goods_total } = g;
+      const id = _.get(g, 'goodsSpec._id');
+      const obj = { original: goods_total };
+      // 除了最后一个商品,其余的商品均按比例分配
+      if (i < goodsList.length - 1) {
+        const percent = _.floor(goods_total / goodsTotal);
+        const discountMoney = _.floor(discountRealMoney * percent, 2);
+        const realPay = _.floor(goods_total - discountMoney, 2);
+        obj.realPay = realPay;
+        obj.discountMoney = discountMoney;
+        allReadyDiscount += discountMoney;
+      } else {
+        // 最后一个商品是剩余的可分配的钱
+        const discountMoney = _.floor(discountRealMoney - allReadyDiscount, 2);
+        const realPay = _.floor(goods_total - discountMoney, 2);
+        obj.realPay = realPay;
+        obj.discountMoney = discountMoney;
+      }
+      result[id] = obj;
+    }
+    return result;
+  }
+
+  /**
+   * 按折扣计算每件商品
+   * @param {Object} coupon 优惠券{_id:用户领取的优惠券id,...}
+   * @param {Array} goodsList 过滤完后,符合条件的 本订单中的规格商品列表
+   */
+  discountPercentForEachGoods(coupon, goodsList) {
+    const { discount_config } = coupon;
+    const { min } = discount_config;
+    const percent = parseFloat(min) / 10;
+    const result = {};
+    for (const g of goodsList) {
+      const { goods_total } = g;
+      const id = _.get(g, 'goodsSpec._id');
+      const realPay = _.floor(goods_total * percent, 2);
+      const discountMoney = goods_total - realPay;
+      const obj = {
+        original: goods_total,
+        realPay,
+        discountMoney,
+      };
+      result[id] = obj;
+    }
+    return result;
+  }
+
+  /**
+   * 过滤出 订单中商品规格数据 在这些标签中的 商品规格
+   * @param {Array} goodsSpecList 订单中商品规格列表
+   * @param {Array} tags 商品标签
+   */
+  getGoodsInTags(goodsSpecList, tags) {
+    const arr = goodsSpecList.filter((f) => {
+      const gtags = _.get(f, 'goods.tags');
+      // 没打标签或者标签里没有内容的直接过滤出去
+      if (!gtags || gtags.length <= 0) return false;
+      const r = gtags.find((f) => tags.find((ff) => _.isEqual(ff, f)));
+      return r;
+    });
+    return arr;
+  }
+
+  /**
+   * 整理商品规格列表数据
+   * 将goods,shop和goodsSpec重组为3个属性在object同一级上
+   * @param {Array} list 商品规格列表
+   * @param {Array} orderInfo 有关订单的信息 {id,buy_num}
+   */
+  resetGoodsSpecData(list, orderInfo) {
+    const nouse = ['meta', '__v'];
+    const arr = list.map((i) => {
+      const obj = {};
+      const goodsSpec = _.omit(i, [...nouse, 'goods']);
+      const shop = _.omit(_.get(i, 'goods.shop'), [...nouse]);
+      const goods = _.omit(_.get(i, 'goods'), [...nouse, 'shop']);
+      obj.goodsSpec = goodsSpec;
+      obj.shop = shop;
+      obj.goods = goods;
+      const r = orderInfo.find((f) => ObjectId(f.id).equals(goodsSpec._id));
+      if (r) obj.buy_num = r.buy_num;
+      if (obj.buy_num) {
+        obj.goods_total = parseInt(obj.buy_num) * parseFloat(_.get(obj.goodsSpec, 'sell_money'));
+      }
+      return obj;
+    });
+    return arr;
+  }
+  /**
+   * 整理出下面所需要使用的数据,并将这些数据放在object的第一级上
+   * @param {Array} list 用户优惠券列表
+   */
+  resetUserCouponData(list) {
+    const arr = list.map((i) => {
+      const { _id, meta, coupon } = i;
+      const cd = _.pick(coupon, [
+        'issue',
+        'shop',
+        'expire_type',
+        'expire_config',
+        'discount_type',
+        'discount_config',
+        'use_limit',
+        'use_limit_config',
+        'get_limit',
+        'get_limit_config',
+        'name',
+      ]);
+      const obj = { _id, meta, ...cd };
+      return obj;
+    });
+    return arr;
+  }
+}
+
+module.exports = UserCouponService;

+ 1 - 0
app/z_router/trade/order.js

@@ -7,6 +7,7 @@ const rkey = 'order';
 const ckey = 'trade.order';
 const keyZh = '总订单';
 const routes = [
+  { method: 'post', path: `${rkey}/cancel`, controller: `${ckey}.cancel`, name: `${ckey}cancel`, zh: '取消订单' },
   { method: 'post', path: `${rkey}/toMakeOrder`, controller: `${ckey}.toMakeOrder`, name: `${ckey}toMakeOrder`, zh: '组织下单页数据' },
   { method: 'post', path: `${rkey}/fromGoodsToOrder`, controller: `${ckey}.fromGoodsToOrder`, name: `${ckey}fromGoodsToOrder`, zh: '直接购买' },
   { method: 'get', path: `${rkey}`, controller: `${ckey}.index`, name: `${ckey}Query`, zh: `${keyZh}列表查询` },

+ 1 - 0
app/z_router/user/index.js

@@ -6,4 +6,5 @@ module.exports = app => {
   require('./user')(app); // 用户
   require('./address')(app); // 收货地址
   require('./point')(app); // 积分
+  require('./userCoupon')(app); // 用户优惠券
 };

+ 20 - 0
app/z_router/user/userCoupon.js

@@ -0,0 +1,20 @@
+'use strict';
+// 路由配置
+const path = require('path');
+const regPath = path.resolve('app', 'public', 'routerRegister');
+const routerRegister = require(regPath);
+const rkey = 'userCoupon';
+const ckey = 'user.userCoupon';
+const keyZh = '用户优惠券';
+const routes = [
+  { method: 'post', path: `${rkey}/getCoupon/:coupon_id`, controller: `${ckey}.getCoupon`, name: `${ckey}getCoupon`, zh: `${keyZh}-领取` },
+  { method: 'get', path: `${rkey}`, controller: `${ckey}.index`, name: `${ckey}Query`, zh: `${keyZh}列表查询` },
+  { method: 'get', path: `${rkey}/:id`, controller: `${ckey}.show`, name: `${ckey}Show`, zh: `${keyZh}查询` },
+  // { method: 'post', path: `${rkey}`, controller: `${ckey}.create`, name: `${ckey}Create`, zh: `创建${keyZh}` },
+  { method: 'post', path: `${rkey}/:id`, controller: `${ckey}.update`, name: `${ckey}Update`, zh: `修改${keyZh}` },
+  { method: 'delete', path: `${rkey}/:id`, controller: `${ckey}.destroy`, name: `${ckey}Delete`, zh: `删除${keyZh}` },
+];
+
+module.exports = app => {
+  routerRegister(app, routes, keyZh, rkey, ckey);
+};