Просмотр исходного кода

支付的情况讨论注释写完了.但是代码还没写.使用了事务:在下单逻辑中

lrf 2 лет назад
Родитель
Сommit
5cd8d5ff94

+ 0 - 1
app/controller/home.js

@@ -1,7 +1,6 @@
 'use strict';
 'use strict';
 
 
 const Controller = require('egg').Controller;
 const Controller = require('egg').Controller;
-
 class HomeController extends Controller {
 class HomeController extends Controller {
   async index() {
   async index() {
     const { ctx } = this;
     const { ctx } = this;

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

@@ -1,6 +1,6 @@
 module.exports = {
 module.exports = {
   create: {
   create: {
-    requestBody: ['customer', 'address', 'goods', 'total_detail', 'buy_time', 'pay_time', 'pay_id', 'no', 'status'],
+    requestBody: ['customer', 'address', 'goods', 'total_detail', 'buy_time', 'pay', 'no', 'status'],
   },
   },
   destroy: {
   destroy: {
     params: ['!id'],
     params: ['!id'],
@@ -8,7 +8,7 @@ module.exports = {
   },
   },
   update: {
   update: {
     params: ['!id'],
     params: ['!id'],
-    requestBody: ['customer', 'address', 'goods', 'total_detail', 'buy_time', 'pay_time', 'pay_id', 'no', 'status'],
+    requestBody: ['customer', 'address', 'goods', 'total_detail', 'buy_time', 'pay', 'no', 'status'],
   },
   },
   show: {
   show: {
     parameters: {
     parameters: {
@@ -23,7 +23,8 @@ module.exports = {
         'meta.createdAt@end': 'meta.createdAt@end',
         'meta.createdAt@end': 'meta.createdAt@end',
         customer: 'customer',
         customer: 'customer',
         buy_time: 'buy_time',
         buy_time: 'buy_time',
-        pay_id: 'pay_id',
+        'pay_time@start': 'pay.pay_time@start',
+        'pay_time@end': 'pay.pay_time@end',
         no: 'no',
         no: 'no',
       },
       },
       // options: {
       // options: {

+ 2 - 3
app/model/trade/order.js

@@ -9,14 +9,13 @@ const metaPlugin = require('naf-framework-mongoose-free/lib/model/meta-plugin');
  */
  */
 const order = {
 const order = {
   customer: { type: String, required: false, zh: '顾客', ref: 'User.Customer' }, //
   customer: { type: String, required: false, zh: '顾客', ref: 'User.Customer' }, //
-  address: { type: String, required: false, zh: '邮寄地址' }, //
+  address: { type: Object, required: false, zh: '邮寄地址' }, //
   goods: { type: Array, required: false, zh: '商品' }, // 按店铺分组,快照过来
   goods: { type: Array, required: false, zh: '商品' }, // 按店铺分组,快照过来
   total_detail: { type: Object, required: false, zh: '总金额明细' }, // 优惠,活动;商品总额和运费由商品而来
   total_detail: { type: Object, required: false, zh: '总金额明细' }, // 优惠,活动;商品总额和运费由商品而来
   buy_time: { type: String, required: false, zh: '下单时间' }, //
   buy_time: { type: String, required: false, zh: '下单时间' }, //
-  pay_time: { type: String, required: false, zh: '支付时间' }, //
-  pay_id: { type: String, required: false, zh: '支付id' }, //
   no: { type: String, required: false, zh: '订单号' }, //
   no: { type: String, required: false, zh: '订单号' }, //
   status: { type: String, required: false, zh: '订单状态' }, // 字典:order_process
   status: { type: String, required: false, zh: '订单状态' }, // 字典:order_process
+  pay: { type: Object, required: false, zh: '支付数据' }, // 有关支付时间,支付方式,支付订单号内容全都写在这里面
 };
 };
 const schema = new Schema(order, { toJSON: { getters: true, virtuals: true } });
 const schema = new Schema(order, { toJSON: { getters: true, virtuals: true } });
 schema.index({ id: 1 });
 schema.index({ id: 1 });

+ 1 - 1
app/public/routerRegister.js

@@ -11,7 +11,7 @@ const _ = require('lodash');
 module.exports = (app, routes, keyZh, rkey, ckey) => {
 module.exports = (app, routes, keyZh, rkey, ckey) => {
   const { router, config } = app;
   const { router, config } = app;
   const mwares = app.middleware;
   const mwares = app.middleware;
-  if (process.env.NODE_ENV === 'development') console.log(`${keyZh}:  ${rkey}`);
+  // if (process.env.NODE_ENV === 'development') console.log(`${keyZh}:  ${rkey}`);
   for (const route of routes) {
   for (const route of routes) {
     const { method, path, controller: ctl, zh } = route;
     const { method, path, controller: ctl, zh } = route;
     let { middleware = [] } = route;
     let { middleware = [] } = route;

+ 141 - 0
app/service/pay.js

@@ -0,0 +1,141 @@
+'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');
+
+//
+class PayService extends CrudService {
+  constructor(ctx) {
+    super(ctx, 'pay');
+    this.httpUtil = this.ctx.service.util.httpUtil;
+    this.appConfig = 'pointApp';
+    this.orderModel = this.ctx.model.Trade.Order;
+    this.dictDataModel = this.ctx.model.Dev.DictData;
+    this.payOrderReturnUrl = this.app.config.payReturn.order;
+    this.wxDomain = _.get(this.app, 'config.httpPrefix.wechat');
+  }
+
+  /**
+   * 去支付订单
+   * 1.有支付方式之分; 微信/支付宝
+   * 2.根据不同方式去请求
+   * @param {Object} body 请求体
+   * @param body.order_id 订单id
+   * @param body.type 支付方式
+   */
+  async toPayOrder({ order_id, type }) {
+    const payWay = await this.dictDataModel.findOne({ value: type, status: '0' });
+    if (!payWay) throw new BusinessError(ErrorCode.DATA_INVALID, '该支付方式暂时无法使用');
+    const order = await this.orderModel.findById(order_id);
+    if (!order) throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '未找到订单数据');
+    const { no, pay } = order;
+    // 检查是否有支付信息如果有的话,支付方式是否一致
+    if (Object.keys(pay).length > 0) {
+      const pay_type = _.get(pay, 'pay_type');
+      // 支付方式不同,则将支付内容刷掉,重新进行数据生成
+      if (pay_type === type) {
+        // 支付方式相同,则再找有没有支付订单
+        const pay_no = _.get(pay, 'pay_no');
+        if (pay_no) {
+          // 有支付订单.则先查支付订单是否可以继续支付
+          // TODO:
+          // 1.当前用户和之前下单支付的用户的openid是不是一个人.是一个人=>2;不是一个人=>3
+          // 2.需要关闭之前的支付订单并且重新进行支付: 关闭订单.再放到下面
+          // 3.查询订单是否过期 没过期=>4; 过期=>5
+          // 4.继续支付:取出数据,return;
+          // 5.放到下面.重新支付
+        }
+        // 没有的话,就说明数据有问题.按照没有支付信息处理即可
+      }
+    }
+    // 没有支付信息(要是有支付信息,上面直接return了.漏下来的都是没有的处理方案)
+    const str = this.ctx.service.util.trade.createNonceStr();
+    const arr = no.split('-');
+    // 订单中pay的信息
+    const payObject = { pay_type: type, pay_no: `${_.last(arr)}-${str}` };
+    const totalMoney = this.getOrderNeedPay(order);
+    // 找到当前用户的openid:这里涉及问题是: 如果自己下单,自己付款.那没有问题;
+    // 如果是自己下单.如果使用账号密码登录再付款.还用下单的人找到的openid就不能在这个微信号上进行支付了;
+    // 所以此处是需要用当前用户的openid进行支付,如果之前生成单子.还需要检查当前用户的openid和之前的openid是否一致
+    // 如果不一致.则需要将之前的订单关闭,重新生成
+
+    // 请求微信支付接口的数据
+    if (type === '0') {
+      const toPayData = { config: this.appConfig, money: totalMoney, openid: '', order_no: payObject.pay_no, desc: '购物', notice_url: this.payOrderReturnUrl('order_id') };
+    }
+  }
+
+  /**
+   * 支付订单回调函数
+   * @param {Object} param 请求地址参数
+   * @param param.order 订单id
+   */
+  async callBackPayOrder({ order }) {}
+
+
+  /**
+   * 计算订单需支付的金额
+   * @param {Object} order 要支付的订单数据
+   */
+  getOrderNeedPay(order) {
+    let total = 0;
+    const { total_detail = {} } = order;
+    for (const key in total_detail) {
+      total = _.floor(total_detail[key] + total);
+    }
+    return total;
+  }
+
+  /**
+   * 查询订单
+   * @param {String} order_no 订单号
+   */
+  async search(order_no) {
+    assert(order_no, '缺少订单号,无法查询订单信息');
+    const params = { config: this.appConfig, order_no };
+    const url = `${this.wxDomain}/pay/searchOrderByOrderNo`;
+    const wxOrderReq = await this.httpUtil.cpost(url, params);
+    return wxOrderReq;
+  }
+
+  /**
+   * 创建订单,获取微信支付签名
+   * @param {Object} data 数据
+   */
+  async create(data) {
+    const { money, openid, order_no, desc } = data;
+    const wxOrderData = { config: this.appConfig, money, openid, order_no, desc };
+    const url = `${this.wxDomain}/pay/payOrder`;
+    const res = await this.httpUtil.cpost(url, wxOrderData);
+    if (res) return res;
+    throw new BusinessError(ErrorCode.SERVICE_FAULT, '微信下单失败!');
+  }
+
+  /**
+   * 关闭订单
+   * @param {String} order_no 订单号
+   */
+  async close(order_no) {
+    assert(order_no, '缺少订单号,无法查询订单信息');
+    const params = { config: this.appConfig, order_no };
+    const url = `${this.wxDomain}/pay/closeOrder`;
+    const res = await this.httpUtil.cpost(url, params);
+    return res || 'ok';
+  }
+
+  /**
+   * TODO 退款,金额需要指定.可能是部分退款,也可能是全额退款
+   * @param {String} order_no 订单号
+   * @param {String} reason 原因
+   */
+  async refund(order_no, reason) {
+    assert(order_no, '缺少订单号,无法查询订单信息');
+    const url = `${this.wxDomain}/pay/refundOrder`;
+    const params = { config: this.appConfig, order_no, reason };
+    const wxRefundReq = await this.httpUtil.cpost(url, params);
+    return wxRefundReq || 'ok';
+  }
+}
+
+module.exports = PayService;

+ 108 - 9
app/service/trade/order.js

@@ -3,7 +3,8 @@ const { CrudService } = require('naf-framework-mongoose-free/lib/service');
 const { BusinessError, ErrorCode } = require('naf-core').Error;
 const { BusinessError, ErrorCode } = require('naf-core').Error;
 const _ = require('lodash');
 const _ = require('lodash');
 const assert = require('assert');
 const assert = require('assert');
-
+const moment = require('moment');
+const Transaction = require('mongoose-transactions');
 //
 //
 class OrderService extends CrudService {
 class OrderService extends CrudService {
   constructor(ctx) {
   constructor(ctx) {
@@ -14,17 +15,103 @@ class OrderService extends CrudService {
     this.goodsModel = this.ctx.model.Shop.Goods;
     this.goodsModel = this.ctx.model.Shop.Goods;
     this.goodsSpecModel = this.ctx.model.Shop.GoodsSpec;
     this.goodsSpecModel = this.ctx.model.Shop.GoodsSpec;
     this.addressModel = this.ctx.model.User.Address;
     this.addressModel = this.ctx.model.User.Address;
-
     this.cartModel = this.ctx.model.Trade.Cart;
     this.cartModel = this.ctx.model.Trade.Cart;
+    this.tran = new Transaction();
   }
   }
-
   /**
   /**
    * 创建订单
    * 创建订单
    * 1.检测商品是否可以购买
    * 1.检测商品是否可以购买
-   * 2.
+   * 2.数据做快照处理
    * @param {Object} body
    * @param {Object} body
    */
    */
-  async create(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 } = body;
+      // 检测商品是否可以下单
+      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 = {};
+      // 数据做快照处理
+      // 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 d = await this.goodsSpecModel.findById(goodsSpec_id).populate(populate);
+          if (!d) continue;
+          d = JSON.parse(JSON.stringify(d));
+          // 将商店内容剔除
+          const { goods: gd, ...dOthers } = d;
+          const gdOthers = _.omit(gd, [ 'shop' ]);
+          const obj = { ...dOthers, goods: gdOthers, buy_num: num };
+          if (cart_id) obj.cart_id = cart_id;
+          qp.push(obj);
+        }
+        goodsData.push({ ...others, goods: qp });
+      }
+      // TODO: 3.商品总计明细.这地方没加优惠券,所以先直接复制即可
+      const totalDetailData = total_detail;
+      // 接下来组织订单数据
+      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';
+      // 生成数据
+      // const order = await this.model.create(orderData);
+      this.tran.insert('Order', orderData);
+      // 处理库存问题
+      // 处理库存,删除购物车
+      await this.dealGoodsNum(goodsData);
+      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 = parseInt(goodsSpec.num - buy_num);
+        this.tran.update('GoodsSpec', _id, { num: newNum });
+        if (cart_id) this.tran.remove('Cart', cart_id);
+      }
+    }
+  }
 
 
   /**
   /**
    * 进入下单页面
    * 进入下单页面
@@ -75,8 +162,14 @@ class OrderService extends CrudService {
    */
    */
   computedShopTotal(list) {
   computedShopTotal(list) {
     for (const i of list) {
     for (const i of list) {
-      i.goods_total = _.floor(i.goods.reduce((p, n) => p + (n.money || 0) * (n.num || 0), 0), 2);
-      i.freight_total = _.floor(i.goods.reduce((p, n) => p + (n.freight || 0) * (n.num || 0), 0), 2);
+      i.goods_total = _.floor(
+        i.goods.reduce((p, n) => p + (n.money || 0) * (n.num || 0), 0),
+        2
+      );
+      i.freight_total = _.floor(
+        i.goods.reduce((p, n) => p + (n.freight || 0) * (n.num || 0), 0),
+        2
+      );
       console.log(i);
       console.log(i);
     }
     }
     return list;
     return list;
@@ -88,8 +181,14 @@ class OrderService extends CrudService {
    */
    */
   computedAllTotal(list) {
   computedAllTotal(list) {
     const obj = {
     const obj = {
-      goods_total: _.floor(list.reduce((p, n) => p + (n.goods_total || 0), 0), 2),
-      freight_total: _.floor(list.reduce((p, n) => p + (n.freight_total || 0), 0), 2),
+      goods_total: _.floor(
+        list.reduce((p, n) => p + (n.goods_total || 0), 0),
+        2
+      ),
+      freight_total: _.floor(
+        list.reduce((p, n) => p + (n.freight_total || 0), 0),
+        2
+      ),
     };
     };
     return obj;
     return obj;
   }
   }

+ 82 - 0
app/service/util/http-util.js

@@ -0,0 +1,82 @@
+'use strict';
+const { AxiosService } = require('naf-framework-mongoose-free/lib/service');
+const { BusinessError, ErrorCode } = require('naf-core').Error;
+const { isNullOrUndefined } = require('naf-core').Util;
+const _ = require('lodash');
+
+//
+class HttpUtilService extends AxiosService {
+  constructor(ctx) {
+    super(ctx, {}, {});
+  }
+
+  // 替换uri中的参数变量
+  merge(uri, query = {}) {
+    const keys = Object.keys(query);
+    const arr = [];
+    for (const k of keys) {
+      arr.push(`${k}=${query[k]}`);
+    }
+    if (arr.length > 0) {
+      uri = `${uri}?${arr.join('&')}`;
+    }
+    return uri;
+  }
+
+  /**
+   * curl-get请求
+   * @param {String} uri 接口地址
+   * @param {Object} query 地址栏参数
+   * @param {Object} options 额外参数
+   */
+  async cget(uri, query, options) {
+    return this.toRequest(uri, null, query, options);
+  }
+
+  /**
+   * curl-post请求
+   * @param {String} uri 接口地址
+   * @param {Object} data post的body
+   * @param {Object} query 地址栏参数
+   * @param {Object} options 额外参数
+   */
+  async cpost(uri, data = {}, query, options) {
+    return this.toRequest(uri, data, query, options);
+  }
+
+  async toRequest(uri, data, query, options) {
+    query = _.pickBy(query, val => val !== '' && val !== 'undefined' && val !== 'null');
+    if (!uri) console.error('uri不能为空');
+    if (_.isObject(query) && _.isObject(options)) {
+      const params = query.params ? query.params : query;
+      options = { ...options, params };
+    } else if (_.isObject(query) && !query.params) {
+      options = { params: query };
+    } else if (_.isObject(query) && query.params) {
+      options = query;
+    }
+    const headers = { 'content-type': 'application/json' };
+    const url = this.merge(`${uri}`, options.params);
+    let res = await this.ctx.curl(url, {
+      method: isNullOrUndefined(data) ? 'get' : 'post',
+      url,
+      data,
+      dataType: 'json',
+      headers,
+      ...options,
+    });
+    if (res.status === 200) {
+      res = res.data || {};
+      const { errcode, errmsg, details } = res;
+      if (errcode) {
+        console.warn(`[${uri}] fail: ${errcode}-${errmsg} ${details}`);
+        return { errcode, errmsg };
+      }
+      return res.data;
+    }
+    const { status } = res;
+    console.warn(`[${uri}] fail: ${status}-${res.data.message} `);
+  }
+}
+
+module.exports = HttpUtilService;

+ 11 - 0
config/config.default.js

@@ -42,6 +42,7 @@ module.exports = appInfo => {
       authSource: 'admin',
       authSource: 'admin',
       useNewUrlParser: true,
       useNewUrlParser: true,
       useCreateIndex: true,
       useCreateIndex: true,
+      useFindAndModify: true,
     },
     },
   };
   };
   // jwt设置
   // jwt设置
@@ -62,6 +63,16 @@ module.exports = appInfo => {
   };
   };
   // 路由设置
   // 路由设置
   config.routePrefix = '/point/v1/api';
   config.routePrefix = '/point/v1/api';
+  // 支付路由回调设置
+  config.payReturn = {
+    order: id => `/point/v1/api/pay/order/${id}`,
+  };
+
+  // http请求前缀
+  config.httpPrefix = {
+    // wechat: 'http://127.0.0.1:14001/wechat/api',
+    wechat: 'https://broadcast.waityou24.cn/wechat/api',
+  };
   // 中间件
   // 中间件
   config.requestLog = {
   config.requestLog = {
     toMongoDB: true,
     toMongoDB: true,

+ 1 - 0
package.json

@@ -13,6 +13,7 @@
     "jsonwebtoken": "^8.5.1",
     "jsonwebtoken": "^8.5.1",
     "lodash": "^4.17.21",
     "lodash": "^4.17.21",
     "moment": "^2.29.4",
     "moment": "^2.29.4",
+    "mongoose-transactions": "^1.1.4",
     "naf-framework-mongoose-free": "^0.0.36"
     "naf-framework-mongoose-free": "^0.0.36"
   },
   },
   "devDependencies": {
   "devDependencies": {