正在显示
11 个修改的文件
包含
199 行增加
和
159 行删除
| @@ -24,19 +24,6 @@ eskimo --help | @@ -24,19 +24,6 @@ eskimo --help | ||
| 24 | ``` | 24 | ``` |
| 25 | 25 | ||
| 26 | 26 | ||
| 27 | -## Components | ||
| 28 | - | ||
| 29 | -Using `electrolyte`, you an inject the following dependencies into your app with `eskimo`: | ||
| 30 | - | ||
| 31 | -* `app` - returns an instance of `express()` | ||
| 32 | -* `error-handler` - returns an error handler that can be used via `app.use` | ||
| 33 | -* `db` - returns connection to MongoDB | ||
| 34 | -* `logger` - returns a Winston logger instance | ||
| 35 | -* `model-common-plugin` - returns a Mongoose plugin for common schema paths | ||
| 36 | -* `sessions` - returns connection to Redis | ||
| 37 | -* `settings` - returns config | ||
| 38 | - | ||
| 39 | - | ||
| 40 | ## Contributors | 27 | ## Contributors |
| 41 | 28 | ||
| 42 | * Nick Baugh <niftylettuce@gmail.com> | 29 | * Nick Baugh <niftylettuce@gmail.com> |
| 1 | 1 | ||
| 2 | -// # db | 2 | +// # boot - db |
| 3 | 3 | ||
| 4 | var mongoose = require('mongoose') | 4 | var mongoose = require('mongoose') |
| 5 | var _ = require('underscore') | 5 | var _ = require('underscore') |
| @@ -30,4 +30,4 @@ exports = module.exports = function(logger, settings) { | @@ -30,4 +30,4 @@ exports = module.exports = function(logger, settings) { | ||
| 30 | } | 30 | } |
| 31 | 31 | ||
| 32 | exports['@singleton'] = true | 32 | exports['@singleton'] = true |
| 33 | -exports['@require'] = [ 'logger', 'settings' ] | 33 | +exports['@require'] = [ 'igloo/logger', 'igloo/settings' ] |
| 1 | 1 | ||
| 2 | -// # error handler | 2 | +// # boot - error handler |
| 3 | 3 | ||
| 4 | var _ = require('underscore') | 4 | var _ = require('underscore') |
| 5 | var util = require('util') | 5 | var util = require('util') |
| 6 | 6 | ||
| 7 | exports = module.exports = function(logger, settings) { | 7 | exports = module.exports = function(logger, settings) { |
| 8 | 8 | ||
| 9 | - return function(err, req, res, next) { | ||
| 10 | - | ||
| 11 | - // set default error status code | ||
| 12 | - res.statusCode = (_.isNumber(err.status)) ? err.status : 500 | ||
| 13 | - | ||
| 14 | - if (!_.isString(err.message)) | ||
| 15 | - err.message = 'An unknown error has occured, please try again' | ||
| 16 | - | ||
| 17 | - if (_.isObject(err) && _.isNumber(err.code) && err.code === 11000) { | ||
| 18 | - // <https://github.com/LearnBoost/mongoose/issues/2129> | ||
| 19 | - var field = err.message.split('index: test.')[1].split('.$')[1] | ||
| 20 | - // now we have `email_1 dup key` | ||
| 21 | - field = field.split(' dup key')[0] | ||
| 22 | - field = field.substring(0, field.lastIndexOf('_')) | ||
| 23 | - err.message = util.format('Duplicate %s already exists in database, try making a more unique value', field) | ||
| 24 | - err.param = field | ||
| 25 | - } | ||
| 26 | - | ||
| 27 | - // if we pass an error object, then we want to simply return the message... | ||
| 28 | - // if we pass an object, then we want to do a stack trace, and then return the object + stack | ||
| 29 | - var error = {} | ||
| 30 | - | ||
| 31 | - // set error type | ||
| 32 | - error.type = _.isString(err.param) ? 'invalid_request_error' : 'api_error' | ||
| 33 | - | ||
| 34 | - if (error.type === 'invalid_request_error' && res.statusCode === 500) | ||
| 35 | - res.statusCode = 400 | ||
| 36 | - | ||
| 37 | - // set error message and stack trace | ||
| 38 | - if (util.isError(err)) { | ||
| 39 | - error.message = err.message | ||
| 40 | - } else { | ||
| 41 | - _.extend(error, err) | ||
| 42 | - } | ||
| 43 | - | ||
| 44 | - // set status code for BadRequestError | ||
| 45 | - if (_.isString(error.name) && error.name === 'BadRequestError') { | ||
| 46 | - error.type = 'invalid_request_error' | ||
| 47 | - res.statusCode = 400 | ||
| 48 | - delete error.name | ||
| 49 | - } | ||
| 50 | - | ||
| 51 | - if (settings.showStack) | ||
| 52 | - error.stack = _.isUndefined(err.stack) ? new Error(err.message).stack : err.stack | ||
| 53 | - | ||
| 54 | - // set error level | ||
| 55 | - var level = (res.statusCode < 500) ? 'warn' : 'error' | ||
| 56 | - logger[level](error) | ||
| 57 | - | ||
| 58 | - // set error back to warning if it was warn | ||
| 59 | - // logger level type = "warn" | ||
| 60 | - // req.flash messages type = "warning" | ||
| 61 | - if (level === 'warn') | ||
| 62 | - level = 'warning' | ||
| 63 | - | ||
| 64 | - // if we have a mongoose validation err | ||
| 65 | - // then we know to output all the errors | ||
| 66 | - if (_.isObject(error.errors) && !_.isEmpty(error.errors)) { | ||
| 67 | - var messages = [] | ||
| 68 | - _.each(error.errors, function(errMsg) { | ||
| 69 | - if (_.isString(errMsg.message)) | ||
| 70 | - messages.push(errMsg.message) | ||
| 71 | - }) | ||
| 72 | - if (!_.isEmpty(messages)) | ||
| 73 | - error.message = messages.join(' ') | ||
| 74 | - } | ||
| 75 | - | ||
| 76 | - res.format({ | ||
| 77 | - text: function() { | ||
| 78 | - res.send(error.message) | ||
| 79 | - }, | ||
| 80 | - html: function() { | ||
| 81 | - req.flash(level, error.message) | ||
| 82 | - res.redirect('back') | ||
| 83 | - }, | ||
| 84 | - json: function() { | ||
| 85 | - res.json({ error: error }) | 9 | + return function() { |
| 10 | + | ||
| 11 | + var app = this | ||
| 12 | + | ||
| 13 | + app.use(function(err, req, res, next) { | ||
| 14 | + | ||
| 15 | + // set default error status code | ||
| 16 | + res.statusCode = (_.isNumber(err.status)) ? err.status : 500 | ||
| 17 | + | ||
| 18 | + if (!_.isString(err.message)) | ||
| 19 | + err.message = 'An unknown error has occured, please try again' | ||
| 20 | + | ||
| 21 | + if (_.isObject(err) && _.isNumber(err.code) && err.code === 11000) { | ||
| 22 | + // <https://github.com/LearnBoost/mongoose/issues/2129> | ||
| 23 | + var field = err.message.split('index: test.')[1].split('.$')[1] | ||
| 24 | + // now we have `email_1 dup key` | ||
| 25 | + field = field.split(' dup key')[0] | ||
| 26 | + field = field.substring(0, field.lastIndexOf('_')) | ||
| 27 | + err.message = util.format('Duplicate %s already exists in database, try making a more unique value', field) | ||
| 28 | + err.param = field | ||
| 29 | + } | ||
| 30 | + | ||
| 31 | + // if we pass an error object, then we want to simply return the message... | ||
| 32 | + // if we pass an object, then we want to do a stack trace, and then return the object + stack | ||
| 33 | + var error = {} | ||
| 34 | + | ||
| 35 | + // set error type | ||
| 36 | + error.type = _.isString(err.param) ? 'invalid_request_error' : 'api_error' | ||
| 37 | + | ||
| 38 | + if (error.type === 'invalid_request_error' && res.statusCode === 500) | ||
| 39 | + res.statusCode = 400 | ||
| 40 | + | ||
| 41 | + // set error message and stack trace | ||
| 42 | + if (util.isError(err)) { | ||
| 43 | + error.message = err.message | ||
| 44 | + } else { | ||
| 45 | + _.extend(error, err) | ||
| 86 | } | 46 | } |
| 47 | + | ||
| 48 | + // set status code for BadRequestError | ||
| 49 | + if (_.isString(error.name) && error.name === 'BadRequestError') { | ||
| 50 | + error.type = 'invalid_request_error' | ||
| 51 | + res.statusCode = 400 | ||
| 52 | + delete error.name | ||
| 53 | + } | ||
| 54 | + | ||
| 55 | + if (settings.showStack) | ||
| 56 | + error.stack = _.isUndefined(err.stack) ? new Error(err.message).stack : err.stack | ||
| 57 | + | ||
| 58 | + // set error level | ||
| 59 | + var level = (res.statusCode < 500) ? 'warn' : 'error' | ||
| 60 | + logger[level](error) | ||
| 61 | + | ||
| 62 | + // set error back to warning if it was warn | ||
| 63 | + // logger level type = "warn" | ||
| 64 | + // req.flash messages type = "warning" | ||
| 65 | + if (level === 'warn') | ||
| 66 | + level = 'warning' | ||
| 67 | + | ||
| 68 | + // if we have a mongoose validation err | ||
| 69 | + // then we know to output all the errors | ||
| 70 | + if (_.isObject(error.errors) && !_.isEmpty(error.errors)) { | ||
| 71 | + var messages = [] | ||
| 72 | + _.each(error.errors, function(errMsg) { | ||
| 73 | + if (_.isString(errMsg.message)) | ||
| 74 | + messages.push(errMsg.message) | ||
| 75 | + }) | ||
| 76 | + if (!_.isEmpty(messages)) | ||
| 77 | + error.message = messages.join(' ') | ||
| 78 | + } | ||
| 79 | + | ||
| 80 | + res.format({ | ||
| 81 | + text: function() { | ||
| 82 | + res.send(error.message) | ||
| 83 | + }, | ||
| 84 | + html: function() { | ||
| 85 | + req.flash(level, error.message) | ||
| 86 | + res.redirect('back') | ||
| 87 | + }, | ||
| 88 | + json: function() { | ||
| 89 | + res.json({ error: error }) | ||
| 90 | + } | ||
| 91 | + }) | ||
| 92 | + | ||
| 87 | }) | 93 | }) |
| 88 | 94 | ||
| 89 | } | 95 | } |
| @@ -91,4 +97,4 @@ exports = module.exports = function(logger, settings) { | @@ -91,4 +97,4 @@ exports = module.exports = function(logger, settings) { | ||
| 91 | } | 97 | } |
| 92 | 98 | ||
| 93 | exports['@singleton'] = true | 99 | exports['@singleton'] = true |
| 94 | -exports['@require'] = [ 'logger', 'settings' ] | 100 | +exports['@require'] = [ 'igloo/logger', 'igloo/settings' ] |
| 1 | 1 | ||
| 2 | -// # logger | 2 | +// # boot - logger |
| 3 | 3 | ||
| 4 | var mergeDefaults = require('merge-defaults') | 4 | var mergeDefaults = require('merge-defaults') |
| 5 | var winston = require('winston') | 5 | var winston = require('winston') |
| @@ -46,4 +46,4 @@ exports = module.exports = function(settings) { | @@ -46,4 +46,4 @@ exports = module.exports = function(settings) { | ||
| 46 | } | 46 | } |
| 47 | 47 | ||
| 48 | exports['@singleton'] = true | 48 | exports['@singleton'] = true |
| 49 | -exports['@require'] = [ 'settings' ] | 49 | +exports['@require'] = [ 'igloo/settings' ] |
| 1 | 1 | ||
| 2 | -// # boot - model common plugin | 2 | +// # boot - mongoose plugin |
| 3 | 3 | ||
| 4 | var _ = require('underscore') | 4 | var _ = require('underscore') |
| 5 | var jsonSelect = require('mongoose-json-select') | 5 | var jsonSelect = require('mongoose-json-select') |
| @@ -58,4 +58,4 @@ exports = module.exports = function(settings) { | @@ -58,4 +58,4 @@ exports = module.exports = function(settings) { | ||
| 58 | } | 58 | } |
| 59 | 59 | ||
| 60 | exports['@singleton'] = true | 60 | exports['@singleton'] = true |
| 61 | -exports['@require'] = [ 'settings' ] | 61 | +exports['@require'] = [ 'igloo/settings' ] |
lib/boot/server.js
0 → 100644
| 1 | + | ||
| 2 | +// # boot - server | ||
| 3 | + | ||
| 4 | +// Inspired by `bixby-server` by Jared Hanson | ||
| 5 | + | ||
| 6 | +var https = require('https') | ||
| 7 | +var http = require('http') | ||
| 8 | +var cluster = require('cluster') | ||
| 9 | +var os = require('os') | ||
| 10 | +var path = require('path') | ||
| 11 | + | ||
| 12 | +exports = module.exports = function(logger, settings) { | ||
| 13 | + | ||
| 14 | + return function(done) { | ||
| 15 | + | ||
| 16 | + if (cluster.isMaster && settings.server.cluster) { | ||
| 17 | + | ||
| 18 | + var size = settings.server.cluster.size || os.cpus().length | ||
| 19 | + | ||
| 20 | + logger.info('creating cluster with %d workers', size) | ||
| 21 | + | ||
| 22 | + for (var i=0; i<size; i++) { | ||
| 23 | + logger.info('spawning worker #%d', i + 1) | ||
| 24 | + cluster.fork() | ||
| 25 | + } | ||
| 26 | + | ||
| 27 | + cluster.on('fork', function(worker) { | ||
| 28 | + logger.info('worker #%s with pid %d spawned', worker.id, worker.process.pid) | ||
| 29 | + }) | ||
| 30 | + | ||
| 31 | + cluster.on('online', function(worker) { | ||
| 32 | + logger.info('worker #%s with pid %d online', worker.id, worker.process.pid) | ||
| 33 | + }) | ||
| 34 | + | ||
| 35 | + cluster.on('listening', function(worker, addr) { | ||
| 36 | + logger.info('worker #%s with pid %d listening on %s:%d', worker.id, worker.process.pid, addr.address, addr.port) | ||
| 37 | + }) | ||
| 38 | + | ||
| 39 | + cluster.on('disconnect', function(worker) { | ||
| 40 | + logger.info('worker #%s with pid %d disconnected', worker.id, worker.process.pid) | ||
| 41 | + }) | ||
| 42 | + | ||
| 43 | + cluster.on('exit', function(worker, code, signal) { | ||
| 44 | + logger.error('worker #%s with pid %d exited with code/signal', worker.id, worker.process.pid, signal, code) | ||
| 45 | + if (worker.suicide) return | ||
| 46 | + logger.info('worker #%s restarting', worker.id) | ||
| 47 | + cluster.fork() | ||
| 48 | + }) | ||
| 49 | + | ||
| 50 | + } else { | ||
| 51 | + | ||
| 52 | + if (settings.server.ssl.enabled) | ||
| 53 | + this.server = https.createServer(settings.server.ssl.options, this) | ||
| 54 | + else | ||
| 55 | + this.server = http.createServer(this) | ||
| 56 | + | ||
| 57 | + this.server.listen(settings.server.port, settings.server.host, function() { | ||
| 58 | + var addr = this.address() | ||
| 59 | + logger.info('app listening on %s:%d', addr.address, addr.port) | ||
| 60 | + done() | ||
| 61 | + }) | ||
| 62 | + | ||
| 63 | + } | ||
| 64 | + | ||
| 65 | + } | ||
| 66 | + | ||
| 67 | +} | ||
| 68 | + | ||
| 69 | +exports['@singleton'] = true | ||
| 70 | +exports['@require'] = [ 'igloo/logger', 'igloo/settings' ] |
| 1 | 1 | ||
| 2 | -// # sessions | 2 | +// # boot - sessions |
| 3 | 3 | ||
| 4 | var session = require('express-session') | 4 | var session = require('express-session') |
| 5 | var RedisStore = require('connect-redis')(session) | 5 | var RedisStore = require('connect-redis')(session) |
| @@ -19,4 +19,4 @@ exports = module.exports = function(logger, settings) { | @@ -19,4 +19,4 @@ exports = module.exports = function(logger, settings) { | ||
| 19 | } | 19 | } |
| 20 | 20 | ||
| 21 | exports['@singleton'] = true | 21 | exports['@singleton'] = true |
| 22 | -exports['@require'] = [ 'logger', 'settings' ] | 22 | +exports['@require'] = [ 'igloo/logger', 'igloo/settings' ] |
| 1 | 1 | ||
| 2 | -// # settings | 2 | +// # boot - settings |
| 3 | 3 | ||
| 4 | +var _ = require('underscore') | ||
| 5 | +var util = require('util') | ||
| 4 | var mergeDefaults = require('merge-defaults') | 6 | var mergeDefaults = require('merge-defaults') |
| 5 | 7 | ||
| 6 | exports = module.exports = function(config) { | 8 | exports = module.exports = function(config) { |
| @@ -9,6 +11,9 @@ exports = module.exports = function(config) { | @@ -9,6 +11,9 @@ exports = module.exports = function(config) { | ||
| 9 | 11 | ||
| 10 | var env = process.env.NODE_ENV || 'development' | 12 | var env = process.env.NODE_ENV || 'development' |
| 11 | 13 | ||
| 14 | + if (!_.isObject(config[env])) | ||
| 15 | + throw new Error(util.format('Unknown environment %s', env)) | ||
| 16 | + | ||
| 12 | mergeDefaults(settings, config[env], config.defaults) | 17 | mergeDefaults(settings, config[env], config.defaults) |
| 13 | 18 | ||
| 14 | return settings | 19 | return settings |
| 1 | 1 | ||
| 2 | -// # app | 2 | +// # boot - update notifier |
| 3 | 3 | ||
| 4 | -var express = require('express') | ||
| 5 | -var winstonRequestLogger = require('winston-request-logger') | ||
| 6 | -var bootable = require('bootable') | ||
| 7 | var _ = require('underscore') | 4 | var _ = require('underscore') |
| 8 | var updateNotifier = require('update-notifier') | 5 | var updateNotifier = require('update-notifier') |
| 9 | -var path = require('path') | ||
| 10 | -var expressResource = require('express-resource') | ||
| 11 | 6 | ||
| 12 | exports = module.exports = function(logger, settings) { | 7 | exports = module.exports = function(logger, settings) { |
| 13 | 8 | ||
| 14 | - // check for updates to all packages when not in production | ||
| 15 | - if (settings.updateNotifier.enabled) | ||
| 16 | - _.each(settings.pkg.dependencies, function(version, name) { | ||
| 17 | - var notifier = updateNotifier({ | ||
| 18 | - packageName: name, | ||
| 19 | - packageVersion: version, | ||
| 20 | - optOut: settings.updateNotifier.dependencies[name] || false, | ||
| 21 | - updateCheckInterval: settings.updateNotifier.updateCheckInterval | 1000 * 60 * 60, // hourly | ||
| 22 | - updateCheckTimeout: settings.updateNotifier.updateCheckTimeout | 1000 * 20 // 20 seconds | 9 | + return function() { |
| 10 | + | ||
| 11 | + // check for updates to all packages when not in production | ||
| 12 | + if (settings.updateNotifier.enabled) | ||
| 13 | + _.each(settings.pkg.dependencies, function(version, name) { | ||
| 14 | + var notifier = updateNotifier({ | ||
| 15 | + packageName: name, | ||
| 16 | + packageVersion: version, | ||
| 17 | + optOut: settings.updateNotifier.dependencies[name] || false, | ||
| 18 | + updateCheckInterval: settings.updateNotifier.updateCheckInterval | 1000 * 60 * 60, // hourly | ||
| 19 | + updateCheckTimeout: settings.updateNotifier.updateCheckTimeout | 1000 * 20 // 20 seconds | ||
| 20 | + }) | ||
| 21 | + if (_.isUndefined(notifier.update) || !_.isString(notifier.update.latest)) return | ||
| 22 | + logger.warn( | ||
| 23 | + '%s of %s released (current: %s), run `npm install -S %s@%s` to upgrade', | ||
| 24 | + notifier.update.latest, | ||
| 25 | + name, | ||
| 26 | + version, | ||
| 27 | + name, | ||
| 28 | + notifier.update.latest | ||
| 29 | + ) | ||
| 23 | }) | 30 | }) |
| 24 | - if (_.isUndefined(notifier.update) || !_.isString(notifier.update.latest)) return | ||
| 25 | - logger.warn( | ||
| 26 | - '%s of %s released (current: %s), run `npm install -S %s@%s` to upgrade', | ||
| 27 | - notifier.update.latest, | ||
| 28 | - name, | ||
| 29 | - version, | ||
| 30 | - name, | ||
| 31 | - notifier.update.latest | ||
| 32 | - ) | ||
| 33 | - }) | ||
| 34 | - | ||
| 35 | - // create the app | ||
| 36 | - var app = bootable(express()) | ||
| 37 | - | ||
| 38 | - // winston request logger before everything else | ||
| 39 | - // but only if it was enabled in settings | ||
| 40 | - if (settings.logger.requests) | ||
| 41 | - app.use(winstonRequestLogger.create(logger)) | ||
| 42 | - | ||
| 43 | - // integrate express-resource | ||
| 44 | - app = expressResource(app) | ||
| 45 | - | ||
| 46 | - return app | 31 | + |
| 32 | + } | ||
| 47 | 33 | ||
| 48 | } | 34 | } |
| 49 | 35 | ||
| 50 | exports['@singleton'] = true | 36 | exports['@singleton'] = true |
| 51 | -exports['@require'] = [ 'logger', 'settings' ] | 37 | +exports['@require'] = [ 'igloo/logger', 'igloo/settings' ] |
| @@ -15,18 +15,7 @@ | @@ -15,18 +15,7 @@ | ||
| 15 | // # igloo | 15 | // # igloo |
| 16 | 16 | ||
| 17 | var path = require('path') | 17 | var path = require('path') |
| 18 | -var os = require('os') | ||
| 19 | -var cluster = require('cluster') | ||
| 20 | 18 | ||
| 21 | -var bootDir = path.join(__dirname, 'boot') | ||
| 22 | - | ||
| 23 | -module.exports = { | ||
| 24 | - loader: function(id) { | ||
| 25 | - return require(path.join(bootDir, id)) | ||
| 26 | - }, | ||
| 27 | - app: function(IoC) { | ||
| 28 | - IoC.loader(IoC.node(bootDir)) | ||
| 29 | - var app = IoC.create('app') | ||
| 30 | - return app | ||
| 31 | - } | 19 | +module.exports = function(id) { |
| 20 | + return require(path.join(__dirname, 'boot', id)) | ||
| 32 | } | 21 | } |
| 1 | { | 1 | { |
| 2 | "name": "igloo", | 2 | "name": "igloo", |
| 3 | - "version": "0.0.4", | 3 | + "version": "0.0.4-patch", |
| 4 | "description": "Igloo is a lightweight, fast, and minimal framework for rapid development", | 4 | "description": "Igloo is a lightweight, fast, and minimal framework for rapid development", |
| 5 | - "main": "./index.js", | 5 | + "main": "./lib", |
| 6 | "repository": { | 6 | "repository": { |
| 7 | "type": "git", | 7 | "type": "git", |
| 8 | "url": "git://github.com/niftylettuce/igloo.git" | 8 | "url": "git://github.com/niftylettuce/igloo.git" |
| @@ -18,8 +18,6 @@ | @@ -18,8 +18,6 @@ | ||
| 18 | "chalk": "^0.4.0", | 18 | "chalk": "^0.4.0", |
| 19 | "commander": "^2.2.0", | 19 | "commander": "^2.2.0", |
| 20 | "connect-redis": "~2.0.0", | 20 | "connect-redis": "~2.0.0", |
| 21 | - "express": "~4.2.0", | ||
| 22 | - "express-resource": "git://github.com/niftylettuce/express-resource", | ||
| 23 | "express-session": "^1.2.1", | 21 | "express-session": "^1.2.1", |
| 24 | "merge-defaults": "^0.1.0", | 22 | "merge-defaults": "^0.1.0", |
| 25 | "mongoose": "~3.8.7", | 23 | "mongoose": "~3.8.7", |
| @@ -28,7 +26,6 @@ | @@ -28,7 +26,6 @@ | ||
| 28 | "underscore": "~1.6.0", | 26 | "underscore": "~1.6.0", |
| 29 | "update-notifier": "git://github.com/niftylettuce/update-notifier", | 27 | "update-notifier": "git://github.com/niftylettuce/update-notifier", |
| 30 | "winston": "git://github.com/niftylettuce/winston", | 28 | "winston": "git://github.com/niftylettuce/winston", |
| 31 | - "winston-mongodb": "~0.4.3", | ||
| 32 | - "winston-request-logger": "^1.0.5" | 29 | + "winston-mongodb": "~0.4.3" |
| 33 | } | 30 | } |
| 34 | } | 31 | } |
请
注册
或
登录
后发表评论