Node.js 从零开始:打造自动发卡平台(2)

  1. 技术选型
  2. 项目结构

1. 项目创建

在 CMD 窗口执行以下命令创建项目 share

$ npm install -g koa-generator
$ koa2 -e share
$ cd share && npm install
$ SET DEBUG=koa* & npm start share

然后就可以使用浏览器访问了 http://127.0.0.1:3000

2. 目录结构

执行以上命令后生成的项目目录如下

myapp
│  app.js
│  package-lock.json
│  package.json
│
├─bin
│      www
│
├─node_modules
│
├─public
│  ├─images
│  ├─javascripts
│  └─stylesheets
│          style.css
│
├─routes
│      index.js
│      users.js
│
└─views
        error.ejs
        index.ejs

对项目目录作如下优化

  1. 添加 config 目录,放置系统配置文件,也可以把 数据库、缓存、邮件、支付等工具类文件放于此处,当然也可以放到 utils 目录下,看个人爱好
  2. 添加 controllers 目录,放置控制器文件
  3. 添加 middlewares 目录,放置中间件文件
  4. 创建 models 目录,放置数据库 ORM 文件
  5. 创建 utils 目录,放置工具类文件

安装如下所需模块(可根据具体需求进行选择)

$ npm install koa2-cors --save
$ npm install koa-jwt --save
$ npm install jsonwebtoken --save
$ npm install md5-node --save
$ npm install mongoose --save
$ npm install ioredis --save
$ npm install nodemailer --save
$ npm install alipay-sdk --save

优化后的项目目录如下

myapp
│  app.js
│  package-lock.json
│  package.json
│
├─bin
│      www
│
├─node_modules
│
├─config
│      alipay.js
│      index.js
│      license.txt
│      mail.js
│      mongo.js
│      private-key.pem
│      public-key.pem
│      redis.js
│      store.js
│
├─controllers
├─middlewares
├─models
│      goods.js
│      order.js
│      user.js
│
├─public
│  ├─images
│  │
│  ├─javascripts
│  │      embed.js
│  │      index.html
│  │      pay.js
│  │      qrcode.min.js
│  │
│  └─stylesheets
│          embed.css
│          layer.css
│          pay.css
│          style.css
│
├─routes
│      goods.js
│      index.js
│      order.js
│      user.js
│
├─utils
│      util.js
│
└─views
        error.ejs
        index.ejs
        pay.ejs

3. 样本代码

app.js 入口文件

const Koa = require('koa')
const app = new Koa()

const onerror = require('koa-onerror')
const bodyparser = require('koa-bodyparser')
const json = require('koa-json')
const logger = require('koa-logger')
const views = require('koa-views')

app.keys = require('./config').keys

// error handler
onerror(app)

// cors
const cors = require('koa2-cors')
app.use(cors())

// middlewares
app.use(bodyparser({
    enableTypes: ['json', 'form', 'text']
}))
app.use(json())
app.use(logger())
app.use(require('koa-static')(__dirname + '/public'))

app.use(views(__dirname + '/views', {
    extension: 'ejs'
}))

// logger
app.use(async (ctx, next) => {
    const start = new Date()
    await next()
    const ms = new Date() - start
    console.log(`${ctx.method} ${ctx.url} - ${ms}ms`)
})

// error
app.use(async (ctx, next) => {
    try {
        await next()
    } catch (err) {
        ctx.status = err.statusCode || err.status || 500
        ctx.body = err.message
        ctx.app.emit('error', err, ctx)
    }
})

// jwt
const jwt = require('koa-jwt')
app.use(jwt({
    secret: app.keys.join()
}).unless({
    path: [
        /^\/$/,
        /^\/sell$/,
        /^\/buy$/,
        /^\/pay$/,
        /^\/user\/captcha$/,
        /^\/user\/register$/,
        /^\/user\/login$/,
        /^\/user\/password$/
    ]
}))

// routes
const fs = require('fs')
fs.readdirSync(__dirname + '/routes').forEach(route => {
    let api = require(`./routes/${route}`)
    app.use(api.routes(), api.allowedMethods())
})

// error-handling
app.on('error', (err, ctx) => {
    console.error('server error', err, ctx)
})

// mongodb
const mongo = require('./config/mongo')
mongo.connect()

// redis
const Redis = require('./config/redis')
app.context.$redis = new Redis()

module.exports = app

routes/goods.js 商品路由文件

const router = require('koa-router')()
const Goods = require('../models/goods')
const md5 = require('md5-node')

router.prefix('/goods')

/* 添加商品 */
router.post('/', async (ctx, next) => {
    let token = ctx.request.header.authorization
    let user = await ctx.$redis.get(token)
    if (user && user._id) {
        let data = ctx.request.body
        data.code = md5(Date.now())
        data.user = user.email
        data.date = Date.now()
        data.state = 0
        let goods = await Goods.create(data)
        if (goods && goods._id) {
            return ctx.body = {
                code: 0,
                msg: 'success',
                data: null
            }
        }
    }
    ctx.body = {
        code: 1,
        msg: 'failure',
        data: null
    }
})

/* 删除商品 */
router.delete('/:_id', async (ctx, next) => {
    let token = ctx.request.header.authorization
    let user = await ctx.$redis.get(token)
    if (user && user._id) {
        let _id = ctx.params._id
        let result = await Goods.deleteOne({
            _id: _id,
            user: user.email
        })
        if (result && result.ok > 0 && result.deleteCount > 0) {
            return ctx.body = {
                code: 0,
                msg: 'success',
                data: null
            }
        }
    }
    ctx.body = {
        code: 1,
        msg: 'failure',
        data: null
    }
})

/* 修改商品 */
router.put('/:_id', async (ctx, next) => {
    let token = ctx.request.header.authorization
    let user = await ctx.$redis.get(token)
    if (user && user._id) {
        let _id = ctx.params._id
        let data = ctx.request.body
        let result = await User.updateOne({
            _id: _id,
            user: user.email
        }, {
            $set: data
        }, {
            runValidators: true
        })
        if (result && result.ok > 0 && result.nModified > 0) {
            return ctx.body = {
                code: 0,
                msg: 'success',
                data: null
            }
        }
    }
    ctx.body = {
        code: 1,
        msg: 'failure',
        data: null
    }
})

router.get('/', async (ctx, next) => {
    let token = ctx.request.header.authorization
    let user = await ctx.$redis.get(token)
    if (user && user._id) {
        let skip = parseInt(ctx.query.skip || 0)
        let limit = parseInt(ctx.query.limit || 50)
        let goods = await Goods.find({
            user: user.email
        }).skip(skip).limit(limit)
        if (goods && goods.length > 0) {
            return ctx.body = {
                code: 0,
                msg: 'success',
                data: goods
            }
        }
    }
    ctx.body = {
        code: 1,
        msg: 'failure',
        data: null
    }
})

router.get('/:_id', async (ctx, next) => {
    let token = ctx.request.header.authorization
    let user = await ctx.$redis.get(token)
    if (user && user._id) {
        let _id = ctx.params._id
        let goods = await Goods.findOne({
            _id: _id,
            user: user.email
        })
        if (goods && goods._id) {
            return ctx.body = {
                code: 0,
                msg: 'success',
                data: goods
            }
        }
    }
    ctx.body = {
        code: 1,
        msg: 'failure',
        data: null
    }
})

module.exports = router

models/goods.js 商品模型文件

const mongoose = require('mongoose')

const GoodsSchema = mongoose.Schema({
    code: {
        type: String,
        required: [true, 'code cannot be null']
    }, // 商品编号
    name: {
        type: String,
        required: [true, 'name cannot be null']
    }, // 名称
    description: { type: String }, // 描述
    type: {
        type: String,
        enum: ['one', 'many'],
        default: 'one'
    }, // 类型
    content: {
        type: [String],
        required: [true, 'content cannot be null']
    }, // 内容
    price: {
        type: Number,
        required: [true, 'price cannot be null']
    }, // 价格
    count: {
        type: Number,
        default: 0
    }, // 数量
    date: {
        type: Date,
        default: Date.now
    }, // 日期
    state: {
        type: Number,
        default: 0
    }, // 状态
    user: { type: String, required: [true, 'user cannot be null'] } // 用户
})

const Goods = mongoose.model('Goods', GoodsSchema)

module.exports = Goods

config/index.js 系统配置文件

const fs = require('fs')

const config = {
    license: fs.readFileSync(__dirname + '/license.txt'),
    keys: ['I Love U'],
    mongo: {
        debug: true,
        autoReconnect: true,
        url: 'mongodb://localhost:27017/myapp'
    },
    mail: {
        host: 'smtp.126.com',
        port: 465,
        secure: true,
        secureConnection: true,
        auth: {
            user: '[email protected]',
            pass: 'xxxxxx'
        },
        from: 'myapp'
    },
    alipay: {
        appId: 'xxxxxx', // 必填
        privateKey: fs.readFileSync(__dirname + '/private-key.pem', 'ascii'), // 必填
        encryptKey: 'xxxxxx', // 必填
        signType: 'RSA2', // 选填 默认
        alipayPublicKey: fs.readFileSync(__dirname + '/public-key.pem', 'ascii'), // 选填
        gateway: 'https://openapi.alipay.com/gateway.do', // 选填 默认
        timeout: 5000, // 选填 默认
        camelcase: true, // 选填 默认
        notifyUrl: 'xxxxxx',
        qrCodeTimeoutExpress: '5m'
    }
}

module.exports = config

config/mongo.js 操作 MongoDB

// config/mongo.js
const config = require('./index').mongo
const mongoose = require('mongoose').set('debug', config.debug)

const url = config.url

const options = {
    // autoReconnect: config.autoReconnect,
    useNewUrlParser: true,
    useUnifiedTopology: true
}

module.exports = {
    connect: () => {
        mongoose.connect(url, options)
        let db = mongoose.connection
        db.on('error', () => {
            console.log('mongodb: error')
        })
        db.once('open', () => {
            console.log('mongodb: open success')
        })
    }
}

config/redis.js 操作 Redis

const IORedis = require('ioredis')

class Redis extends Object {
    constructor() {
        super()
        this.redis = new IORedis()
    }

    async get(cid) {
        let data = await this.redis.get(`CACHE:${cid}`)
        return JSON.parse(data)
    }

    async set(cid, cache, maxAge = 60 * 60 * 1000) {
        try {
            // Use redis set EX to automatically drop expired caches
            await this.redis.set(`CACHE:${cid}`, JSON.stringify(cache), 'EX', maxAge / 1000)
        } catch (e) {
            console.log(e)
        }
        return cid
    }

    async destroy(cid) {
        return await this.redis.del(`CACHE:${cid}`)
    }
}

module.exports = Redis

config/mail.js 操作邮件

const config = require('./index').mail
const nodemailer = require('nodemailer')

const transporter = nodemailer.createTransport({
    host: config.host,
    port: config.port,
    secure: config.secure,
    secureConnection: config.secureConnection,
    auth: {
        user: config.auth.user,
        pass: config.auth.pass
    }
})

const test = (email) => {
    let mail = {
        from: config.from,
        to: email,
        subject: 'trace',
        text: 'trace success'
    }
    return send(mail)
}

const send = (mail) => {
    return new Promise((resolve, reject) => {
        mail.from = config.from
        transporter.sendMail(mail, (error, info) => {
            if (error) {
                return reject(error)
            } else {
                return resolve(info)
            }
        })
    })
}

module.exports = {
    test: test,
    send: send
}

config/alipay.js 支付宝当面付

const config = require('./index').alipay
const AlipaySdk = require('alipay-sdk').default

const alipaySdk = new AlipaySdk({
    appId: config.appId,
    privateKey: config.privateKey,
    encryptKey: config.encryptKey,
    signType: config.signType,
    alipayPublicKey: config.alipayPublicKey,
    gateway: config.gateway,
    timeout: config.timeout,
    camelcase: config.camelcase
})

const pay = (outTradeNo, goods, count = 1) => {
    return alipaySdk.exec('alipay.trade.precreate', {
        notifyUrl: config.notifyUrl,
        // app_auth_token: '',
        bizContent: {
            outTradeNo: outTradeNo,
            // sellerId: '',
            totalAmount: goods.price * count,
            // discountableAmount: 0.01,
            subject: goods.name,
            body: goods.description,
            storeId: goods.user,
            qrCodeTimeoutExpress: config.qrCodeTimeoutExpress
        }
    })
}

const cancel = (outTradeNo, tradeNo) => {
    return alipaySdk.exec('alipay.trade.cancel', {
        bizContent: {
            outTradeNo: outTradeNo,
            tradeNo: tradeNo
        }
    })
}

const close = (outTradeNo, tradeNo, operatorId) => {
    return alipaySdk.exec('alipay.trade.close', {
        notifyUrl: '',
        bizContent: {
            tradeNo: tradeNo,
            outTradeNo: outTradeNo,
            operatorId: operatorId
        }
    })
}

const query = (outTradeNo, tradeNo, queryOptions) => {
    return alipaySdk.exec('alipay.trade.query', {
        bizContent: {
            outTradeNo: outTradeNo,
            tradeNo: tradeNo,
            queryOptions: queryOptions
        }
    })
}

module.exports = {
    pay: pay,
    cancel: cancel,
    close: close,
    query: query
}

4. 小结

以上就是 node.js + koa + mongodb 项目的基本创建过程和项目核心代码了,并且使用了一些中间件来完成项目中所必须的一些操作,剩下的主要就是根据需求进行业务逻辑的开发。

koa2-cors // 解决跨域问题
koa-jwt // 进行 JWT 验证
jsonwebtoken // 进行 JWT 验证
md5-node // MD5 加密
mongoose // 操作 MongoDB 的 ORM 库
ioredis // 操作 Redis
nodemailer // 发送邮件
alipay-sdk // 支付宝官方 SDK

你可能感兴趣的:(Node.js 从零开始:打造自动发卡平台(2))