lrf hace 2 años
padre
commit
cc24facd9d

+ 3 - 0
app/controller/config/.zrOrder.js

@@ -39,4 +39,7 @@ module.exports = {
       count: true,
     },
   },
+  checkCanBuy: {
+    requestBody: ['!shop', '!goods', '!num'],
+  },
 };

+ 18 - 0
app/middleware/setUserFromToken.js

@@ -0,0 +1,18 @@
+'use strict';
+const _ = require('lodash');
+module.exports = options => {
+  return async function setuserfromtoken(ctx, next) {
+    const token = _.get(ctx.request, 'header.token');
+    if (token) {
+      const data = ctx.service.util.jwt.decode(token);
+      if (data) ctx.user = data;
+    }
+    // 添加管理员身份
+    const adminToken = _.get(ctx.request, 'header.admin-token');
+    if (adminToken) {
+      const data = ctx.service.util.jwt.decode(adminToken);
+      if (data) ctx.admin = data;
+    }
+    await next();
+  };
+};

+ 29 - 0
app/service/util/email.js

@@ -0,0 +1,29 @@
+'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 EmailService extends CrudService {
+  constructor(ctx) {
+    super(ctx, 'email');
+    this.httpUtil = this.ctx.service.util.httpUtil;
+    this.emailServiceUrl = _.get(this.app, 'config.httpPrefix.email');
+    this.emailServiceConfig = _.get(this.app, 'config.emailConfig.config');
+  }
+
+  async resetPwd(email, password) {
+    const data = { config: this.emailServiceConfig, template: 'resetPwd', receiver: email, params: { password } };
+    const url = `${this.emailServiceUrl}/sendEmail`;
+    await this.httpUtil.cpost(url, data);
+  }
+
+  async errorEmail(error) {
+    const data = { config: this.emailServiceConfig, params: error };
+    const url = `${this.emailServiceUrl}/error`;
+    await this.httpUtil.cpost(url, data);
+  }
+}
+
+module.exports = EmailService;

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

@@ -0,0 +1,83 @@
+'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} `);
+    throw new BusinessError(ErrorCode.SERVICE_FAULT, '请求失败');
+  }
+}
+
+module.exports = HttpUtilService;

+ 106 - 0
app/service/util/install.js

@@ -0,0 +1,106 @@
+'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 path = require('path');
+
+//
+class InstallService extends CrudService {
+  constructor(ctx) {
+    super(ctx, 'install');
+    this.dataIndex = path.resolve('app', 'public', 'defaultData');
+  }
+
+  async init() {
+    // await this.initDict();
+    // await this.initAdmin();
+    // await this.initSelfShop();
+    // await this.initTestCustomer();
+    // await this.initGoodsTags();
+    await this.initConfig();
+  }
+
+  async initConfig() {
+    const model = this.ctx.model.System.Config;
+    const num = await model.count({});
+    if (num > 0) return;
+    const p = path.resolve(this.dataIndex, 'config.js');
+    const data = require(p);
+    await model.create(data);
+  }
+
+  async initGoodsTags() {
+    const model = this.ctx.model.System.GoodsTags;
+    const p = path.resolve(this.dataIndex, 'goodsTags.js');
+    const data = require(p);
+    const loop = async (list, pid, level = 1) => {
+      for (const i of list) {
+        const { children = [], label, code } = i;
+        let pdata;
+        pdata = await model.findOne({ code });
+        if (!pdata) pdata = await model.create({ label, code, pid, level });
+        if (children.length > 0) loop(children, pdata._id, level + 1);
+      }
+    };
+    await loop(data);
+  }
+
+  /**
+   * 初始化测试顾客
+   */
+  async initTestCustomer() {
+    const model = this.ctx.model.User.User;
+    await model.deleteMany({ name: '测试用户', phone: '11111111111' });
+    // const num = await model.count({ code: 'self' });
+    // if (num > 0) return;
+    // const p = path.resolve(this.dataIndex, 'user.js');
+    // const data = require(p);
+    // await model.create(data);
+  }
+
+
+  /**
+   * 初始化自营店
+   */
+  async initSelfShop() {
+    const model = this.ctx.model.Shop.Shop;
+    const num = await model.count({ code: 'self' });
+    if (num > 0) return;
+    const p = path.resolve(this.dataIndex, 'selfShop.js');
+    const data = require(p);
+    await model.create(data);
+  }
+
+  /**
+   * 初始化超级管理员
+   */
+  async initAdmin() {
+    const model = this.ctx.model.User.Admin;
+    const num = await model.count();
+    if (num > 0) return;
+    const p = path.resolve(this.dataIndex, 'admin.js');
+    const data = require(p);
+    await model.create(data);
+  }
+
+  /**
+   * 初始化字典
+   */
+  async initDict() {
+    const indexModel = this.ctx.model.Dev.DictIndex;
+    const dataModel = this.ctx.model.Dev.DictData;
+    const p = path.resolve(this.dataIndex, 'dict.js');
+    const data = require(p);
+    for (const i of data) {
+      const { children, ...others } = i;
+      const num = await indexModel.count({ code: others.code });
+      if (num > 0) continue;
+      await indexModel.create(others);
+      const newChildren = children.map(i => ({ ...i, code: others.code }));
+      await dataModel.insertMany(newChildren);
+    }
+  }
+}
+
+module.exports = InstallService;

+ 22 - 0
app/service/util/jwt.js

@@ -0,0 +1,22 @@
+'use strict';
+const { CrudService } = require('naf-framework-mongoose-free/lib/service');
+const jwt = require('jsonwebtoken');
+
+// jsonWebToken处理
+class JwtService extends CrudService {
+  constructor(ctx) {
+    super(ctx, 'jwt');
+  }
+
+  encrypt(data) {
+    const { secret } = this.config.jwt;
+    const token = jwt.sign(JSON.stringify(data), secret);
+    return token;
+  }
+  decode(token) {
+    const data = jwt.decode(token);
+    return data;
+  }
+}
+
+module.exports = JwtService;

+ 137 - 0
app/service/util/rabbitMq.js

@@ -0,0 +1,137 @@
+'use strict';
+
+const Service = require('egg').Service;
+const _ = require('lodash');
+
+class RabbitmqService extends Service {
+  constructor(ctx) {
+    super(ctx);
+    this.exType = 'topic';
+    this.durable = true;
+    // 定时任务队列
+    this.task = this.app.config.taskMqConfig;
+  }
+
+  // 初始化死信机制
+  async initDeadProcess() {
+    try {
+      await this.initDeadQueue();
+      await this.initTaskQueue();
+    } catch (error) {
+      console.error('初始化死信机制失败');
+      return;
+    }
+    console.log('初始化:死信机制----成功');
+
+  }
+
+  /**
+   * 初始化定时任务队列并设置死信机制
+   */
+  async initTaskQueue() {
+    const { mq } = this.ctx;
+    try {
+      const ch = await mq.conn.createChannel();
+      // 声明正常交换器
+      await ch.assertExchange(this.task.ex, 'direct', { durable: true });
+      // 声明正常队列(配置死信队列设置)
+      const q = await ch.assertQueue(this.task.queue, { exclusive: false, deadLetterExchange: this.task.deadEx, deadLetterRoutingKey: this.task.deadLetterRoutingKey });
+      // 正常队里绑定至正常交换器
+      await ch.bindQueue(q.queue, this.task.ex);
+    } catch (error) {
+      console.error('mq---- 死信机制-任务队列生成失败');
+      console.error(error);
+    }
+  }
+  /**
+   * 初始化定时任务死信机制队列
+   */
+  async initDeadQueue() {
+    const { mq } = this.ctx;
+    try {
+      const ch = await mq.conn.createChannel();
+      await ch.assertExchange(this.task.deadEx, 'direct', { durable: true });
+      const qr = await ch.assertQueue(this.task.deadQueue, {
+        exclusive: false,
+      });
+      await ch.bindQueue(qr.queue, this.task.deadEx, this.task.deadLetterRoutingKey);
+      await ch.consume(
+        qr.queue,
+        async msg => {
+          console.log('in dead');
+          await this.dealTask(msg);
+        },
+        { noAck: true }
+      );
+    } catch (error) {
+      console.error('mq---- 死信机制-死信队列生成失败');
+      console.error(error);
+    }
+  }
+  async dealTask(msg) {
+    try {
+      const str = msg.content.toString();
+      const obj = JSON.parse(str);
+      const { service, method, params } = obj;
+      const arr = service.split('.');
+      let ser = this.ctx.service;
+      for (const s of arr) {
+        ser = ser[s];
+      }
+      ser[method](params);
+    } catch (error) {
+      console.log(error);
+    }
+  }
+
+
+  /**
+   * 发送定时消息
+   * @param {String} queue 队列名
+   * @param {Any} data 数据
+   * @param {Number} time 时间:分(函数内需要转为毫秒) 默认 6秒
+   */
+  async makeTask(queue, data, time = '0.1') {
+    time = this.ctx.multiply(time, 60 * 1000); // 转换为毫秒
+    const ch = await this.ctx.mq.conn.createChannel();
+    await ch.sendToQueue(queue, Buffer.from(JSON.stringify(data)), { expiration: time });
+    await ch.close();
+  }
+
+  // mission队列处理
+  async mission() {
+    const { mq } = this.ctx;
+    if (mq) {
+      const ch = await mq.conn.createChannel();
+      const queue = 'mission/market';
+      try {
+        // 创建队列:在没有队列的情况,直接获取会导致程序无法启动
+        await ch.assertQueue(queue, { durable: false });
+        await ch.consume(queue, msg => this.dealMission(msg), { noAck: true });
+      } catch (error) {
+        this.ctx.logger.error('未找到订阅的队列');
+      }
+    } else {
+      this.ctx.logger.error('!!!!!!没有配置MQ插件!!!!!!');
+    }
+  }
+  // 执行任务
+  async dealMission(bdata) {
+    if (!bdata) this.ctx.logger.error('mission队列中信息不存在');
+    let data = bdata.content.toString();
+    try {
+      data = JSON.parse(data);
+    } catch (error) {
+      this.ctx.logger.error('数据不是object');
+    }
+    const { service, method, project, ...others } = data;
+    const arr = service.split('.');
+    let s = this.ctx.service;
+    for (const key of arr) {
+      s = s[key];
+    }
+    s[method](others);
+  }
+}
+
+module.exports = RabbitmqService;

+ 85 - 0
app/service/util/token.js

@@ -0,0 +1,85 @@
+'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 { ObjectId } = require('mongoose').Types;
+
+//
+class TokenService extends CrudService {
+  constructor(ctx) {
+    super(ctx, 'token');
+    this.redis = this.app.redis;
+    this.tokenTimes = 2; // token的使用次数
+    this.tokenTimeOut = 5 * 60; // x * 60s;token超时时间
+  }
+
+  /**
+   * 生成token
+   * token用于检测是否允许请求接口
+   * 每个用户的每个token有使用次数(上面设置的),
+   * @param {String} id 用户信息id
+   * @property {String} oid 随机ObjectId作为key,在redis中也是使用这个作为key
+   */
+  async initToken(id) {
+    // 生成混合id
+    const oid = ObjectId().toString();
+    const arr = [];
+    // 将用户id翻转
+    const idArr = _.reverse(oid.split(''));
+    const oidArr = id.split('');
+    // 将用户id和混合id进行穿插组合
+    for (let i = 0; i < idArr.length; i++) {
+      const v = idArr[i];
+      const s = oidArr[i];
+      arr.push(v, s);
+    }
+    // 拼接成要返回的token的键
+    const tokenKey = arr.join('');
+    // 设置该用户的token次数
+    await this.redis.set(`token:${oid}`, this.tokenTimes, 'ex', this.tokenTimeOut);
+    return tokenKey;
+  }
+  /**
+   * 从token中取出用户id
+   * @param {String} str token
+   * @return {String} uid 用户id/oid redis的key
+   */
+  getIdFromToken(str) {
+    const idArr = str.split('');
+    const arr = [];
+    const uArr = [];
+    for (let i = 0; i < idArr.length; i += 2) {
+      arr.push(idArr[i]);
+      if (idArr[i + 1]) uArr.push(idArr[i + 1]);
+    }
+    const oid = _.reverse(arr).join('');
+    const uid = uArr.join('');
+    return { oid, uid };
+  }
+
+  /**
+   * 检查并使用token
+   * @param {String} token token
+   */
+  async useToken(token) {
+    const { oid, uid } = this.getIdFromToken(token);
+    let tvalue = await this.redis.get(`token:${oid}`);
+    if (!(tvalue && parseInt(tvalue))) return { token, check: false };
+    tvalue = parseInt(tvalue);
+    if (tvalue - 1 <= 0) {
+      // 把上个token删了
+      await this.redis.del(`token:${oid}`);
+      // 该token用完了,换下一个
+      const newToken = await this.initToken(uid);
+      return { token: newToken, check: true, fresh: true };
+    }
+    // token使用次数-1
+    tvalue--;
+    await this.redis.set(`token:${oid}`, tvalue);
+    return { token, check: true };
+  }
+
+}
+
+module.exports = TokenService;

+ 108 - 0
app/service/zrOrder.js

@@ -7,7 +7,115 @@ const assert = require('assert');
 class ZrOrderService extends CrudService {
   constructor(ctx) {
     super(ctx, 'zrorder');
+    this.redis = this.app.redis;
+    this.redisKey = this.app.config.redisKey;
+    this.redisTimeout = this.app.config.redisTimeout;
     this.model = this.ctx.model.ZrOrder;
+    this.goodsModel = this.ctx.model.ZrGoods;
+    this.pointModel = this.ctx.model.Base.Point;
+    this.shopModel = this.ctx.model.Base.Shop;
+  }
+  // 进入订单页前,通过函数判断,存储是否可以购买 指定商品 的 指定数量
+  // 下单,走同样的函数判断.
+  // 直接生成订单,扣除积分
+
+
+  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);
+    const { shop, goods, num: buy_num } = data;
+    const shopInfo = await this.shopModel.findById(shop);
+    // const goodsInfo = await this.
+  }
+
+
+  /**
+   * 检查是否可以购买商品,并生成缓存
+   * @param {Object} body 参数体
+   * @param body.shop 店铺id
+   * @param body.goods 尊荣商品id
+   * @param body.num 购买数量
+   * @param {Boolean} makeCache 是否生成缓存
+   */
+  async checkCanBuy({ shop, goods, num }, makeCache = true) {
+    const result = { result: true };
+    const shopInfo = await this.shopModel.findById(shop);
+    if (!shopInfo || _.get(shopInfo, '1')) {
+      result.result = false;
+      result.msg = '店铺当前不处于营业状态';
+      return result;
+    }
+    const goodsInfo = await this.goodsModel.findById(goods);
+    if (!goodsInfo) {
+      result.result = false;
+      result.msg = '未找到尊荣商品';
+      return result;
+    }
+    const user = this.ctx.user;
+    const customer = _.get(user, '_id');
+    if (!customer) {
+      result.result = false;
+      result.msg = '未找到用户信息';
+    }
+    // #region 库存判断
+    const knum = _.get(goodsInfo, 'num');
+    const res = this.ctx.minus(knum, num);
+    if (res <= 0) {
+      result.result = false;
+      result.msg = '库存不足';
+      return result;
+    }
+    // #endregion
+
+    // #region 积分判断
+    const pointTotal = await this.pointComputedTotal({ customer });
+    // 计算购买所需积分
+    const cost = _.get(goodsInfo, 'cost', 0);
+    if (cost === 0) {
+      result.result = false;
+      result.msg = '商品设置的尊荣有误,无法进行购买';
+      return result;
+    }
+    const costTotal = this.ctx.multiply(cost, num);
+    const afterCost = this.ctx.minus(pointTotal, costTotal);
+    if (afterCost < 0) {
+      result.result = false;
+      result.msg = '您的积分不足';
+      return result;
+    }
+    // #endregion
+
+    if (makeCache) {
+      const key = await this.makeOrderKey({ shop, goods, num });
+      result.key = key;
+    }
+    return result;
+  }
+
+  // 生成key
+  async makeOrderKey(data) {
+    const str = this.createNonceStr();
+    const key = `${this.redisKey.orderKeyPrefix}${str}`;
+    const value = JSON.stringify(data);
+    await this.redis.set(key, value, 'EX', this.redisTimeout);
+    return str;
+  }
+  /**
+   * 计算用户的积分
+   * @param {Object} data 数据对象
+   * @param data.customer 用户数据id
+   */
+  async pointComputedTotal({ customer }) {
+    assert(customer, '缺少用户信息');
+    const res = await this.pointModel.find({ customer });
+    const total = res.reduce((p, n) => {
+      let point = n.point;
+      if (!(n.source === '0' || n.source === '1')) point = -point;
+      return this.ctx.plus(p, point);
+    }, 0);
+    return total;
   }
 }
 

+ 1 - 0
app/z_router/zrOrder.js

@@ -7,6 +7,7 @@ const rkey = 'zrOrder';
 const ckey = 'zrOrder';
 const keyZh = '尊荣订单';
 const routes = [
+  { method: 'post', path: `${rkey}/checkCanBuy`, controller: `${ckey}.checkCanBuy`, name: `${ckey}checkCanBuy`, 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}` },

+ 2 - 2
config/config.default.js

@@ -18,7 +18,7 @@ module.exports = appInfo => {
   config.appName = '天恩活泉商城-服务';
 
   // add your middleware config here
-  config.middleware = [];
+  config.middleware = [ 'setUserFromToken' ];
   config.checkToken = {
     enable: false,
   };
@@ -62,7 +62,7 @@ module.exports = appInfo => {
   };
 
   config.redisKey = {
-    orderKeyPrefix: 'orderKey:',
+    orderKeyPrefix: 'zrOrderKey:',
   };
   config.redisTimeout = 3600;
   config.wxPayConfig = 'pointApp';