diff --git a/.gitignore b/.gitignore index 73d39e40f359b33a77b2f3a3e31d9f10c5ce908e..4e6fd26a67188f7473934c538cb8209a60ac2cba 100644 --- a/.gitignore +++ b/.gitignore @@ -22,7 +22,6 @@ target/ ### NetBeans ### nbproject/private/ -build/ nbbuild/ dist/ nbdist/ diff --git a/README.md b/README.md index f819a0f5e823684f76e2726f568ed01e48db4921..cccc48085e066a93c6a79590b4e8a04875a8168c 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# 菜鸡论坛 +# 私人论坛 ### Backend 后端 - Spring Boot 2.1.4 diff --git a/fstackforum-vue/.babelrc b/fstackforum-vue/.babelrc new file mode 100644 index 0000000000000000000000000000000000000000..3a280ba34b3db923e95e8317b2ee192e012b444b --- /dev/null +++ b/fstackforum-vue/.babelrc @@ -0,0 +1,12 @@ +{ + "presets": [ + ["env", { + "modules": false, + "targets": { + "browsers": ["> 1%", "last 2 versions", "not ie <= 8"] + } + }], + "stage-2" + ], + "plugins": ["transform-vue-jsx", "transform-runtime"] +} diff --git a/fstackforum-vue/.editorconfig b/fstackforum-vue/.editorconfig new file mode 100644 index 0000000000000000000000000000000000000000..9d08a1a828a3bd2d60de3952744df29f9add27fa --- /dev/null +++ b/fstackforum-vue/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true diff --git a/fstackforum-vue/.gitignore b/fstackforum-vue/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..541a820f6c59178661b11167ef8b683afdd678c7 --- /dev/null +++ b/fstackforum-vue/.gitignore @@ -0,0 +1,14 @@ +.DS_Store +node_modules/ +/dist/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Editor directories and files +.idea +.vscode +*.suo +*.ntvs* +*.njsproj +*.sln diff --git a/fstackforum-vue/.postcssrc.js b/fstackforum-vue/.postcssrc.js new file mode 100644 index 0000000000000000000000000000000000000000..eee3e92d7fa6cc132a69a8018b1eb0fa1fdbd56c --- /dev/null +++ b/fstackforum-vue/.postcssrc.js @@ -0,0 +1,10 @@ +// https://github.com/michael-ciniawsky/postcss-load-config + +module.exports = { + "plugins": { + "postcss-import": {}, + "postcss-url": {}, + // to edit target browsers: use "browserslist" field in package.json + "autoprefixer": {} + } +} diff --git a/fstackforum-vue/README.md b/fstackforum-vue/README.md new file mode 100644 index 0000000000000000000000000000000000000000..1d7ea7d1de8961671d226aed021d39c23bcf5f21 --- /dev/null +++ b/fstackforum-vue/README.md @@ -0,0 +1,21 @@ +# fstackforum-vue + +> A Vue.js project + +## Build Setup + +``` bash +# install dependencies +npm install + +# serve with hot reload at localhost:8080 +npm run dev + +# build for production with minification +npm run build + +# build for production and view the bundle analyzer report +npm run build --report +``` + +For a detailed explanation on how things work, check out the [guide](http://vuejs-templates.github.io/webpack/) and [docs for vue-loader](http://vuejs.github.io/vue-loader). diff --git a/fstackforum-vue/build/build.js b/fstackforum-vue/build/build.js new file mode 100644 index 0000000000000000000000000000000000000000..8f2ad8ad496a6a0cfba4ea7f90a7b8a3f1f30d6c --- /dev/null +++ b/fstackforum-vue/build/build.js @@ -0,0 +1,41 @@ +'use strict' +require('./check-versions')() + +process.env.NODE_ENV = 'production' + +const ora = require('ora') +const rm = require('rimraf') +const path = require('path') +const chalk = require('chalk') +const webpack = require('webpack') +const config = require('../config') +const webpackConfig = require('./webpack.prod.conf') + +const spinner = ora('building for production...') +spinner.start() + +rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => { + if (err) throw err + webpack(webpackConfig, (err, stats) => { + spinner.stop() + if (err) throw err + process.stdout.write(stats.toString({ + colors: true, + modules: false, + children: false, // If you are using ts-loader, setting this to true will make TypeScript errors show up during build. + chunks: false, + chunkModules: false + }) + '\n\n') + + if (stats.hasErrors()) { + console.log(chalk.red(' Build failed with errors.\n')) + process.exit(1) + } + + console.log(chalk.cyan(' Build complete.\n')) + console.log(chalk.yellow( + ' Tip: built files are meant to be served over an HTTP server.\n' + + ' Opening index.html over file:// won\'t work.\n' + )) + }) +}) diff --git a/fstackforum-vue/build/check-versions.js b/fstackforum-vue/build/check-versions.js new file mode 100644 index 0000000000000000000000000000000000000000..3ef972a08dd51db2cf6c1b5d7f145a5149463e12 --- /dev/null +++ b/fstackforum-vue/build/check-versions.js @@ -0,0 +1,54 @@ +'use strict' +const chalk = require('chalk') +const semver = require('semver') +const packageConfig = require('../package.json') +const shell = require('shelljs') + +function exec (cmd) { + return require('child_process').execSync(cmd).toString().trim() +} + +const versionRequirements = [ + { + name: 'node', + currentVersion: semver.clean(process.version), + versionRequirement: packageConfig.engines.node + } +] + +if (shell.which('npm')) { + versionRequirements.push({ + name: 'npm', + currentVersion: exec('npm --version'), + versionRequirement: packageConfig.engines.npm + }) +} + +module.exports = function () { + const warnings = [] + + for (let i = 0; i < versionRequirements.length; i++) { + const mod = versionRequirements[i] + + if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) { + warnings.push(mod.name + ': ' + + chalk.red(mod.currentVersion) + ' should be ' + + chalk.green(mod.versionRequirement) + ) + } + } + + if (warnings.length) { + console.log('') + console.log(chalk.yellow('To use this template, you must update following to modules:')) + console.log() + + for (let i = 0; i < warnings.length; i++) { + const warning = warnings[i] + console.log(' ' + warning) + } + + console.log() + process.exit(1) + } +} diff --git a/fstackforum-vue/build/logo.png b/fstackforum-vue/build/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..f3d2503fc2a44b5053b0837ebea6e87a2d339a43 Binary files /dev/null and b/fstackforum-vue/build/logo.png differ diff --git a/fstackforum-vue/build/utils.js b/fstackforum-vue/build/utils.js new file mode 100644 index 0000000000000000000000000000000000000000..e534fb0fd6284c53c3ec997bda2822300edd08a3 --- /dev/null +++ b/fstackforum-vue/build/utils.js @@ -0,0 +1,101 @@ +'use strict' +const path = require('path') +const config = require('../config') +const ExtractTextPlugin = require('extract-text-webpack-plugin') +const packageConfig = require('../package.json') + +exports.assetsPath = function (_path) { + const assetsSubDirectory = process.env.NODE_ENV === 'production' + ? config.build.assetsSubDirectory + : config.dev.assetsSubDirectory + + return path.posix.join(assetsSubDirectory, _path) +} + +exports.cssLoaders = function (options) { + options = options || {} + + const cssLoader = { + loader: 'css-loader', + options: { + sourceMap: options.sourceMap + } + } + + const postcssLoader = { + loader: 'postcss-loader', + options: { + sourceMap: options.sourceMap + } + } + + // generate loader string to be used with extract text plugin + function generateLoaders (loader, loaderOptions) { + const loaders = options.usePostCSS ? [cssLoader, postcssLoader] : [cssLoader] + + if (loader) { + loaders.push({ + loader: loader + '-loader', + options: Object.assign({}, loaderOptions, { + sourceMap: options.sourceMap + }) + }) + } + + // Extract CSS when that option is specified + // (which is the case during production build) + if (options.extract) { + return ExtractTextPlugin.extract({ + use: loaders, + fallback: 'vue-style-loader' + }) + } else { + return ['vue-style-loader'].concat(loaders) + } + } + + // https://vue-loader.vuejs.org/en/configurations/extract-css.html + return { + css: generateLoaders(), + postcss: generateLoaders(), + less: generateLoaders('less'), + sass: generateLoaders('sass', { indentedSyntax: true }), + scss: generateLoaders('sass'), + stylus: generateLoaders('stylus'), + styl: generateLoaders('stylus') + } +} + +// Generate loaders for standalone style files (outside of .vue) +exports.styleLoaders = function (options) { + const output = [] + const loaders = exports.cssLoaders(options) + + for (const extension in loaders) { + const loader = loaders[extension] + output.push({ + test: new RegExp('\\.' + extension + '$'), + use: loader + }) + } + + return output +} + +exports.createNotifierCallback = () => { + const notifier = require('node-notifier') + + return (severity, errors) => { + if (severity !== 'error') return + + const error = errors[0] + const filename = error.file && error.file.split('!').pop() + + notifier.notify({ + title: packageConfig.name, + message: severity + ': ' + error.name, + subtitle: filename || '', + icon: path.join(__dirname, 'logo.png') + }) + } +} diff --git a/fstackforum-vue/build/vue-loader.conf.js b/fstackforum-vue/build/vue-loader.conf.js new file mode 100644 index 0000000000000000000000000000000000000000..33ed58bc0afcb7e28e81762dea765aca5d47b801 --- /dev/null +++ b/fstackforum-vue/build/vue-loader.conf.js @@ -0,0 +1,22 @@ +'use strict' +const utils = require('./utils') +const config = require('../config') +const isProduction = process.env.NODE_ENV === 'production' +const sourceMapEnabled = isProduction + ? config.build.productionSourceMap + : config.dev.cssSourceMap + +module.exports = { + loaders: utils.cssLoaders({ + sourceMap: sourceMapEnabled, + extract: isProduction + }), + cssSourceMap: sourceMapEnabled, + cacheBusting: config.dev.cacheBusting, + transformToRequire: { + video: ['src', 'poster'], + source: 'src', + img: 'src', + image: 'xlink:href' + } +} diff --git a/fstackforum-vue/build/webpack.base.conf.js b/fstackforum-vue/build/webpack.base.conf.js new file mode 100644 index 0000000000000000000000000000000000000000..a07e683600beeef0d00784cd78da1dc557d996a9 --- /dev/null +++ b/fstackforum-vue/build/webpack.base.conf.js @@ -0,0 +1,82 @@ +'use strict' +const path = require('path') +const utils = require('./utils') +const config = require('../config') +const vueLoaderConfig = require('./vue-loader.conf') + +function resolve (dir) { + return path.join(__dirname, '..', dir) +} + + + +module.exports = { + context: path.resolve(__dirname, '../'), + entry: { + app: './src/main.js' + }, + output: { + path: config.build.assetsRoot, + filename: '[name].js', + publicPath: process.env.NODE_ENV === 'production' + ? config.build.assetsPublicPath + : config.dev.assetsPublicPath + }, + resolve: { + extensions: ['.js', '.vue', '.json'], + alias: { + 'vue$': 'vue/dist/vue.esm.js', + '@': resolve('src'), + } + }, + module: { + rules: [ + { + test: /\.vue$/, + loader: 'vue-loader', + options: vueLoaderConfig + }, + { + test: /\.js$/, + loader: 'babel-loader', + include: [resolve('src'), resolve('test'), resolve('node_modules/webpack-dev-server/client')] + }, + { + test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, + loader: 'url-loader', + options: { + limit: 10000, + name: utils.assetsPath('img/[name].[hash:7].[ext]') + } + }, + { + test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/, + loader: 'url-loader', + options: { + limit: 10000, + name: utils.assetsPath('media/[name].[hash:7].[ext]') + } + }, + { + test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, + loader: 'url-loader', + options: { + limit: 10000, + name: utils.assetsPath('fonts/[name].[hash:7].[ext]') + } + } + ] + }, + node: { + // prevent webpack from injecting useless setImmediate polyfill because Vue + // source contains it (although only uses it if it's native). + setImmediate: false, + // prevent webpack from injecting mocks to Node native modules + // that does not make sense for the client + dgram: 'empty', + fs: 'empty', + net: 'empty', + tls: 'empty', + child_process: 'empty' + } +} diff --git a/fstackforum-vue/build/webpack.dev.conf.js b/fstackforum-vue/build/webpack.dev.conf.js new file mode 100755 index 0000000000000000000000000000000000000000..070ae221f3f3fda5e43ac6ec7c53dd29574d504d --- /dev/null +++ b/fstackforum-vue/build/webpack.dev.conf.js @@ -0,0 +1,95 @@ +'use strict' +const utils = require('./utils') +const webpack = require('webpack') +const config = require('../config') +const merge = require('webpack-merge') +const path = require('path') +const baseWebpackConfig = require('./webpack.base.conf') +const CopyWebpackPlugin = require('copy-webpack-plugin') +const HtmlWebpackPlugin = require('html-webpack-plugin') +const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin') +const portfinder = require('portfinder') + +const HOST = process.env.HOST +const PORT = process.env.PORT && Number(process.env.PORT) + +const devWebpackConfig = merge(baseWebpackConfig, { + module: { + rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, usePostCSS: true }) + }, + // cheap-module-eval-source-map is faster for development + devtool: config.dev.devtool, + + // these devServer options should be customized in /config/index.js + devServer: { + clientLogLevel: 'warning', + historyApiFallback: { + rewrites: [ + { from: /.*/, to: path.posix.join(config.dev.assetsPublicPath, 'index.html') }, + ], + }, + hot: true, + contentBase: false, // since we use CopyWebpackPlugin. + compress: true, + host: HOST || config.dev.host, + port: PORT || config.dev.port, + open: config.dev.autoOpenBrowser, + overlay: config.dev.errorOverlay + ? { warnings: false, errors: true } + : false, + publicPath: config.dev.assetsPublicPath, + proxy: config.dev.proxyTable, + quiet: true, // necessary for FriendlyErrorsPlugin + watchOptions: { + poll: config.dev.poll, + } + }, + plugins: [ + new webpack.DefinePlugin({ + 'process.env': require('../config/dev.env') + }), + new webpack.HotModuleReplacementPlugin(), + new webpack.NamedModulesPlugin(), // HMR shows correct file names in console on update. + new webpack.NoEmitOnErrorsPlugin(), + // https://github.com/ampedandwired/html-webpack-plugin + new HtmlWebpackPlugin({ + filename: 'index.html', + template: 'index.html', + inject: true + }), + // copy custom static assets + new CopyWebpackPlugin([ + { + from: path.resolve(__dirname, '../static'), + to: config.dev.assetsSubDirectory, + ignore: ['.*'] + } + ]) + ] +}) + +module.exports = new Promise((resolve, reject) => { + portfinder.basePort = process.env.PORT || config.dev.port + portfinder.getPort((err, port) => { + if (err) { + reject(err) + } else { + // publish the new Port, necessary for e2e tests + process.env.PORT = port + // add port to devServer config + devWebpackConfig.devServer.port = port + + // Add FriendlyErrorsPlugin + devWebpackConfig.plugins.push(new FriendlyErrorsPlugin({ + compilationSuccessInfo: { + messages: [`Your application is running here: http://${devWebpackConfig.devServer.host}:${port}`], + }, + onErrors: config.dev.notifyOnErrors + ? utils.createNotifierCallback() + : undefined + })) + + resolve(devWebpackConfig) + } + }) +}) diff --git a/fstackforum-vue/build/webpack.prod.conf.js b/fstackforum-vue/build/webpack.prod.conf.js new file mode 100644 index 0000000000000000000000000000000000000000..d9f99f65a5dbd8c8e69561b73168440c461f2a1e --- /dev/null +++ b/fstackforum-vue/build/webpack.prod.conf.js @@ -0,0 +1,145 @@ +'use strict' +const path = require('path') +const utils = require('./utils') +const webpack = require('webpack') +const config = require('../config') +const merge = require('webpack-merge') +const baseWebpackConfig = require('./webpack.base.conf') +const CopyWebpackPlugin = require('copy-webpack-plugin') +const HtmlWebpackPlugin = require('html-webpack-plugin') +const ExtractTextPlugin = require('extract-text-webpack-plugin') +const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin') +const UglifyJsPlugin = require('uglifyjs-webpack-plugin') + +const env = require('../config/prod.env') + +const webpackConfig = merge(baseWebpackConfig, { + module: { + rules: utils.styleLoaders({ + sourceMap: config.build.productionSourceMap, + extract: true, + usePostCSS: true + }) + }, + devtool: config.build.productionSourceMap ? config.build.devtool : false, + output: { + path: config.build.assetsRoot, + filename: utils.assetsPath('js/[name].[chunkhash].js'), + chunkFilename: utils.assetsPath('js/[id].[chunkhash].js') + }, + plugins: [ + // http://vuejs.github.io/vue-loader/en/workflow/production.html + new webpack.DefinePlugin({ + 'process.env': env + }), + new UglifyJsPlugin({ + uglifyOptions: { + compress: { + warnings: false + } + }, + sourceMap: config.build.productionSourceMap, + parallel: true + }), + // extract css into its own file + new ExtractTextPlugin({ + filename: utils.assetsPath('css/[name].[contenthash].css'), + // Setting the following option to `false` will not extract CSS from codesplit chunks. + // Their CSS will instead be inserted dynamically with style-loader when the codesplit chunk has been loaded by webpack. + // It's currently set to `true` because we are seeing that sourcemaps are included in the codesplit bundle as well when it's `false`, + // increasing file size: https://github.com/vuejs-templates/webpack/issues/1110 + allChunks: true, + }), + // Compress extracted CSS. We are using this plugin so that possible + // duplicated CSS from different components can be deduped. + new OptimizeCSSPlugin({ + cssProcessorOptions: config.build.productionSourceMap + ? { safe: true, map: { inline: false } } + : { safe: true } + }), + // generate dist index.html with correct asset hash for caching. + // you can customize output by editing /index.html + // see https://github.com/ampedandwired/html-webpack-plugin + new HtmlWebpackPlugin({ + filename: config.build.index, + template: 'index.html', + inject: true, + minify: { + removeComments: true, + collapseWhitespace: true, + removeAttributeQuotes: true + // more options: + // https://github.com/kangax/html-minifier#options-quick-reference + }, + // necessary to consistently work with multiple chunks via CommonsChunkPlugin + chunksSortMode: 'dependency' + }), + // keep module.id stable when vendor modules does not change + new webpack.HashedModuleIdsPlugin(), + // enable scope hoisting + new webpack.optimize.ModuleConcatenationPlugin(), + // split vendor js into its own file + new webpack.optimize.CommonsChunkPlugin({ + name: 'vendor', + minChunks (module) { + // any required modules inside node_modules are extracted to vendor + return ( + module.resource && + /\.js$/.test(module.resource) && + module.resource.indexOf( + path.join(__dirname, '../node_modules') + ) === 0 + ) + } + }), + // extract webpack runtime and module manifest to its own file in order to + // prevent vendor hash from being updated whenever app bundle is updated + new webpack.optimize.CommonsChunkPlugin({ + name: 'manifest', + minChunks: Infinity + }), + // This instance extracts shared chunks from code splitted chunks and bundles them + // in a separate chunk, similar to the vendor chunk + // see: https://webpack.js.org/plugins/commons-chunk-plugin/#extra-async-commons-chunk + new webpack.optimize.CommonsChunkPlugin({ + name: 'app', + async: 'vendor-async', + children: true, + minChunks: 3 + }), + + // copy custom static assets + new CopyWebpackPlugin([ + { + from: path.resolve(__dirname, '../static'), + to: config.build.assetsSubDirectory, + ignore: ['.*'] + } + ]) + ] +}) + +if (config.build.productionGzip) { + const CompressionWebpackPlugin = require('compression-webpack-plugin') + + webpackConfig.plugins.push( + new CompressionWebpackPlugin({ + asset: '[path].gz[query]', + algorithm: 'gzip', + test: new RegExp( + '\\.(' + + config.build.productionGzipExtensions.join('|') + + ')$' + ), + threshold: 10240, + minRatio: 0.8 + }) + ) +} + +if (config.build.bundleAnalyzerReport) { + const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin + webpackConfig.plugins.push(new BundleAnalyzerPlugin()) +} + +module.exports = webpackConfig diff --git a/fstackforum-vue/config/dev.env.js b/fstackforum-vue/config/dev.env.js new file mode 100644 index 0000000000000000000000000000000000000000..1e22973ae71e949faa15f7a8ab392786918fb22d --- /dev/null +++ b/fstackforum-vue/config/dev.env.js @@ -0,0 +1,7 @@ +'use strict' +const merge = require('webpack-merge') +const prodEnv = require('./prod.env') + +module.exports = merge(prodEnv, { + NODE_ENV: '"development"' +}) diff --git a/fstackforum-vue/config/index.js b/fstackforum-vue/config/index.js new file mode 100644 index 0000000000000000000000000000000000000000..c5eded7f81ab3b7f3c6fb54e0c86bcf7adeab944 --- /dev/null +++ b/fstackforum-vue/config/index.js @@ -0,0 +1,69 @@ +'use strict' +// Template version: 1.3.1 +// see http://vuejs-templates.github.io/webpack for documentation. + +const path = require('path') + +module.exports = { + dev: { + + // Paths + assetsSubDirectory: 'static', + assetsPublicPath: '/', + proxyTable: {}, + + // Various Dev Server settings + host: 'localhost', // can be overwritten by process.env.HOST + port: 8080, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined + autoOpenBrowser: false, + errorOverlay: true, + notifyOnErrors: true, + poll: false, // https://webpack.js.org/configuration/dev-server/#devserver-watchoptions- + + + /** + * Source Maps + */ + + // https://webpack.js.org/configuration/devtool/#development + devtool: 'cheap-module-eval-source-map', + + // If you have problems debugging vue-files in devtools, + // set this to false - it *may* help + // https://vue-loader.vuejs.org/en/options.html#cachebusting + cacheBusting: true, + + cssSourceMap: true + }, + + build: { + // Template for index.html + index: path.resolve(__dirname, '../dist/index.html'), + + // Paths + assetsRoot: path.resolve(__dirname, '../dist'), + assetsSubDirectory: 'static', + assetsPublicPath: '/', + + /** + * Source Maps + */ + + productionSourceMap: true, + // https://webpack.js.org/configuration/devtool/#production + devtool: '#source-map', + + // Gzip off by default as many popular static hosts such as + // Surge or Netlify already gzip all static assets for you. + // Before setting to `true`, make sure to: + // npm install --save-dev compression-webpack-plugin + productionGzip: false, + productionGzipExtensions: ['js', 'css'], + + // Run the build command with an extra argument to + // View the bundle analyzer report after build finishes: + // `npm run build --report` + // Set to `true` or `false` to always turn it on or off + bundleAnalyzerReport: process.env.npm_config_report + } +} diff --git a/fstackforum-vue/config/prod.env.js b/fstackforum-vue/config/prod.env.js new file mode 100644 index 0000000000000000000000000000000000000000..a6f997616eff680e4b2d437e7f31de2cadbfa1de --- /dev/null +++ b/fstackforum-vue/config/prod.env.js @@ -0,0 +1,4 @@ +'use strict' +module.exports = { + NODE_ENV: '"production"' +} diff --git a/fstackforum-vue/index.html b/fstackforum-vue/index.html new file mode 100644 index 0000000000000000000000000000000000000000..13d368eeb49aa3cb94a13e459784956e535c0760 --- /dev/null +++ b/fstackforum-vue/index.html @@ -0,0 +1,12 @@ + + + + + + fstackforum-vue + + +
+ + + diff --git a/fstackforum-vue/package.json b/fstackforum-vue/package.json new file mode 100644 index 0000000000000000000000000000000000000000..d8c1ae969797cbca4c29c8553f58d5271ced07c5 --- /dev/null +++ b/fstackforum-vue/package.json @@ -0,0 +1,63 @@ +{ + "name": "fstackforum-vue", + "version": "1.0.0", + "description": "A Vue.js project", + "author": "Miles <1010933988@qq.com>", + "private": true, + "scripts": { + "dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js", + "start": "npm run dev", + "build": "node build/build.js" + }, + "dependencies": { + "vue": "^2.5.2", + "vue-axios": "^2.1.4", + "vue-router": "^3.0.1" + }, + "devDependencies": { + "autoprefixer": "^7.1.2", + "babel-core": "^6.22.1", + "babel-helper-vue-jsx-merge-props": "^2.0.3", + "babel-loader": "^7.1.1", + "babel-plugin-syntax-jsx": "^6.18.0", + "babel-plugin-transform-runtime": "^6.22.0", + "babel-plugin-transform-vue-jsx": "^3.5.0", + "babel-preset-env": "^1.3.2", + "babel-preset-stage-2": "^6.22.0", + "chalk": "^2.0.1", + "copy-webpack-plugin": "^4.0.1", + "css-loader": "^0.28.0", + "extract-text-webpack-plugin": "^3.0.0", + "file-loader": "^1.1.4", + "friendly-errors-webpack-plugin": "^1.6.1", + "html-webpack-plugin": "^2.30.1", + "node-notifier": "^5.1.2", + "optimize-css-assets-webpack-plugin": "^3.2.0", + "ora": "^1.2.0", + "portfinder": "^1.0.13", + "postcss-import": "^11.0.0", + "postcss-loader": "^2.0.8", + "postcss-url": "^7.2.1", + "rimraf": "^2.6.0", + "semver": "^5.3.0", + "shelljs": "^0.7.6", + "uglifyjs-webpack-plugin": "^1.1.1", + "url-loader": "^0.5.8", + "vue-loader": "^13.3.0", + "vue-style-loader": "^3.0.1", + "vue-template-compiler": "^2.5.2", + "webpack": "^3.6.0", + "webpack-bundle-analyzer": "^2.9.0", + "webpack-dev-server": "^2.9.1", + "webpack-merge": "^4.1.0" + }, + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + }, + "browserslist": [ + "> 1%", + "last 2 versions", + "not ie <= 8" + ] +} diff --git a/fstackforum-vue/src/App.vue b/fstackforum-vue/src/App.vue new file mode 100644 index 0000000000000000000000000000000000000000..d74c648e1586c443cc1f9afd4235260074da0109 --- /dev/null +++ b/fstackforum-vue/src/App.vue @@ -0,0 +1,23 @@ + + + + + diff --git a/fstackforum-vue/src/assets/logo.png b/fstackforum-vue/src/assets/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..f3d2503fc2a44b5053b0837ebea6e87a2d339a43 Binary files /dev/null and b/fstackforum-vue/src/assets/logo.png differ diff --git a/fstackforum-vue/src/axios.js b/fstackforum-vue/src/axios.js new file mode 100644 index 0000000000000000000000000000000000000000..70eb8e2044ee026ee7f8f16362365afd4ab1e4f3 --- /dev/null +++ b/fstackforum-vue/src/axios.js @@ -0,0 +1,8 @@ +import axios from "axios"; + + +axios.defaults.timeout=60000; +axios.defaults.headers.post["Content-type"]="application/json"; + + +export default axios \ No newline at end of file diff --git a/fstackforum-vue/src/components/HelloWorld.vue b/fstackforum-vue/src/components/HelloWorld.vue new file mode 100644 index 0000000000000000000000000000000000000000..374ab880df97bcee3a73a719255921d1bef045ea --- /dev/null +++ b/fstackforum-vue/src/components/HelloWorld.vue @@ -0,0 +1,27 @@ + + + + + + diff --git a/fstackforum-vue/src/main.js b/fstackforum-vue/src/main.js new file mode 100644 index 0000000000000000000000000000000000000000..41360a63f568e0d2df02b16b856eb1de3985ad1f --- /dev/null +++ b/fstackforum-vue/src/main.js @@ -0,0 +1,17 @@ +// The Vue build version to load with the `import` command +// (runtime-only or standalone) has been set in webpack.base.conf with an alias. +import Vue from 'vue' +import App from './App' +import router from './router' +import axios from './axios' +import VueAxios from "vue-axios"; + +Vue.config.productionTip = false +Vue.use(VueAxios ,axios); +/* eslint-disable no-new */ +new Vue({ + el: '#app', + router, + components: { App }, + template: '' +}) diff --git a/fstackforum-vue/src/router/index.js b/fstackforum-vue/src/router/index.js new file mode 100644 index 0000000000000000000000000000000000000000..5fa7f9d3191e33ac45048462ca4ad9a582a76e73 --- /dev/null +++ b/fstackforum-vue/src/router/index.js @@ -0,0 +1,15 @@ +import Vue from 'vue' +import Router from 'vue-router' +import HelloWorld from '@/components/HelloWorld' + +Vue.use(Router) + +export default new Router({ + routes: [ + { + path: '/', + name: 'HelloWorld', + component: HelloWorld + } + ] +}) diff --git a/fstackforum-vue/static/.gitkeep b/fstackforum-vue/static/.gitkeep new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/pom.xml b/pom.xml index ce399ffa54b9de2ed267f4fbb98dfd09fe1413ef..5b29ebff524c7b2fd64de9958e3198a4ed003999 100644 --- a/pom.xml +++ b/pom.xml @@ -1,11 +1,11 @@ - + 4.0.0 com.fstack FStackForum - 0.0.1-SNAPSHOT + 1.0.0 jar FStackForum @@ -23,7 +23,7 @@ UTF-8 1.8 1.16.14 - 3.0.4.RELEASE + 3.0.4.RELEASE 8.0.15 1.3.2 1.2.3 @@ -31,6 +31,7 @@ 4.5.8 1.2.57 27.1-jre + 2.9.2 @@ -49,7 +50,13 @@ spring-boot-starter-mail - + + org.springframework.boot + spring-boot-configuration-processor + true + + + org.springframework.boot spring-boot-starter-thymeleaf @@ -67,8 +74,8 @@ org.thymeleaf.extras - thymeleaf-extras-springsecurity4 - ${springsecurity4.version} + thymeleaf-extras-springsecurity5 + ${springsecurity5.version} @@ -120,6 +127,16 @@ guava ${guava.version} + + io.springfox + springfox-swagger2 + ${swagger.version} + + + io.springfox + springfox-swagger-ui + ${swagger.version} + diff --git a/src/main/java/com/fstack/common/ApiResponse.java b/src/main/java/com/fstack/common/ApiResponse.java index 59fe5680b62fbcc5bccf864f2ecd1b2fa5a7c244..1e33b3de87ff8eea67a9a1dbad3b9553bc50561c 100644 --- a/src/main/java/com/fstack/common/ApiResponse.java +++ b/src/main/java/com/fstack/common/ApiResponse.java @@ -27,7 +27,7 @@ public class ApiResponse { public ApiResponse() { this.setSuccess(true); - this.setCode(200); + this.setCode(ResultCode.SUCCESS.code()); this.setMsg("成功!"); } @@ -46,18 +46,15 @@ public class ApiResponse { public static ApiResponse toSuccess(Object data) { - return new ApiResponse(true, 200, "success", data); + return new ApiResponse(true, ResultCode.SUCCESS.code(), "success", data); } public static ApiResponse toNotFound() { - return new ApiResponse(false, 404, "NotFound"); - } - - public static ApiResponse toPermissions(){ - return new ApiResponse(false, 6,"对不起,您没有没有权限"); + return new ApiResponse(false, ResultCode.NOT_FOUND.code(), "NotFound"); } public static ApiResponse toException(String msg) { - return new ApiResponse(false, 404, msg); + return new ApiResponse(false, ResultCode.INTERNAL_SERVER_ERROR.code(), msg); } + } diff --git a/src/main/java/com/fstack/common/Page.java b/src/main/java/com/fstack/common/Page.java index caaee41c03c8859be3d980d6f856d9046c844d87..7eca211f5733b8f037f3938380218eec506465bb 100644 --- a/src/main/java/com/fstack/common/Page.java +++ b/src/main/java/com/fstack/common/Page.java @@ -3,9 +3,13 @@ package com.fstack.common; import com.google.common.collect.Lists; import lombok.Data; -import java.util.ArrayList; import java.util.List; +/** + * 数据分页 + * + * @param + */ @Data public class Page { @@ -44,13 +48,6 @@ public class Page { */ private Boolean hasCount; - /***************************************************单独的一个类对象转为分页数据 调用这个****************************************/ - public static Page singleObjectToPage(Object object) { - List list = Lists.newArrayList(); - list.add(object); - return new Page(list); - } - /***************************************************非分页list数据 调用这个****************************************/ public Page(List result) { this.setResult(result); @@ -66,7 +63,16 @@ public class Page { this.init(); } - //初始化计算分页 + /***************************************************单独的一个类对象转为分页数据 调用这个****************************************/ + public static Page singleObjectToPage(T object) { + List list = Lists.newArrayList(); + list.add(object); + return new Page<>(list); + } + + /** + * 初始化计算分页 + */ private void init() { this.setTotalPages(); } @@ -77,7 +83,7 @@ public class Page { this.totalCount = result.size(); } - public void setTotalPages() { + private void setTotalPages() { this.totalPages = (int) ((this.totalRecords % this.limit > 0) ? (this.totalRecords / this.limit + 1) : this.totalRecords / this.limit); } diff --git a/src/main/java/com/fstack/common/ResultCode.java b/src/main/java/com/fstack/common/ResultCode.java new file mode 100644 index 0000000000000000000000000000000000000000..c155bd87804b0da12ebe502198e921e18fda9015 --- /dev/null +++ b/src/main/java/com/fstack/common/ResultCode.java @@ -0,0 +1,37 @@ +package com.fstack.common; + +/** + * 响应码枚举,参考HTTP状态码的语义 + */ +public enum ResultCode { + /** + * success + */ + SUCCESS(200), + /** + * Bad Request + */ + FAIL(400), + /** + * unauthorized request + */ + UNAUTHORIZED(401), + /** + * not found + */ + NOT_FOUND(404), + /** + * server internal error + */ + INTERNAL_SERVER_ERROR(500); + + private final int code; + + ResultCode(int code) { + this.code = code; + } + + public int code() { + return code; + } +} diff --git a/src/main/java/com/fstack/config/FsForumConfigProperties.java b/src/main/java/com/fstack/config/FsForumConfigProperties.java new file mode 100644 index 0000000000000000000000000000000000000000..97f67b95c6c6c63af397209a3247e4b72cdb26f9 --- /dev/null +++ b/src/main/java/com/fstack/config/FsForumConfigProperties.java @@ -0,0 +1,40 @@ +package com.fstack.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; +import org.thymeleaf.util.StringUtils; + + +/** + * @author Hu Jie + * @date 2019/04/24 2:02 PM + */ +@ConfigurationProperties(prefix = "fs.forum") +@Configuration +public class FsForumConfigProperties { + + private static final String DEFAULT_DATA_DIRECTORY = System.getProperty("user.home"); + + private String dataDirectory = DEFAULT_DATA_DIRECTORY; + + public String getDataDirectory() { + return dataDirectory; + } + + public void setDataDirectory(String dataDirectory) { + this.dataDirectory = clean(dataDirectory); + } + + private String clean(String path) { + if (StringUtils.isEmpty(path)) { + return DEFAULT_DATA_DIRECTORY; + } + if (path.endsWith("/") || path.endsWith("\\")) { + path = path.substring(0, path.length() - 1); + if (StringUtils.isEmpty(path)) { + return DEFAULT_DATA_DIRECTORY; + } + } + return path; + } +} diff --git a/src/main/java/com/fstack/config/RedisTemplateConfig.java b/src/main/java/com/fstack/config/RedisTemplateConfig.java index 38ef8b2dfc1114e3ebc3e5e44623e43774e1c434..c530c40239727a575046ee7c1db77a19506b47b9 100644 --- a/src/main/java/com/fstack/config/RedisTemplateConfig.java +++ b/src/main/java/com/fstack/config/RedisTemplateConfig.java @@ -35,7 +35,7 @@ public class RedisTemplateConfig { //key序列化 redisTemplate.setKeySerializer(stringSerializer); //value序列化 - redisTemplate.setValueSerializer(stringSerializer); + redisTemplate.setValueSerializer(jackson2JsonRedisSerializer); //Hash key序列化 redisTemplate.setHashKeySerializer(stringSerializer); //Hash value序列化 diff --git a/src/main/java/com/fstack/config/StaticResourceConfig.java b/src/main/java/com/fstack/config/StaticResourceConfig.java index 48f53669d2d906b5a8e85d88ef4c1c8dab1c5d19..022c3a775e998f214f72a8e9c7f6b27cbb2f533b 100644 --- a/src/main/java/com/fstack/config/StaticResourceConfig.java +++ b/src/main/java/com/fstack/config/StaticResourceConfig.java @@ -1,25 +1,43 @@ package com.fstack.config; -import com.fstack.util.CommonUtil; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import java.io.File; + +import static com.fstack.constant.StorageConstant.*; + /** * @author ab */ @Configuration public class StaticResourceConfig implements WebMvcConfigurer { - @Value("${resource.staticResourceLocation}") - private String staticResourceLocation; + @Autowired + private FsForumConfigProperties forumConfigProperties; + + @Value("${enable.swagger}") + private Boolean enable; @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { - registry.addResourceHandler("/avatar/**") - .addResourceLocations("file:" + CommonUtil.getRootPath() + staticResourceLocation); + if (enable) { + registry.addResourceHandler("swagger-ui.html") + .addResourceLocations("classpath:/META-INF/resources/"); + registry.addResourceHandler("/webjars/**") + .addResourceLocations("classpath:/META-INF/resources/webjars/"); + } + + registry.addResourceHandler(AVATAR_DIRECTORY_NAME + "/**") + .addResourceLocations("file:" + + forumConfigProperties.getDataDirectory() + File.separator + + BASE_DIRECTORY_NAME + + AVATAR_DIRECTORY_NAME + File.separator + ); } @Override diff --git a/src/main/java/com/fstack/config/SwaggerConfig.java b/src/main/java/com/fstack/config/SwaggerConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..c2a46f250bf0addfcc7aa4c188bc90ad0cd439e0 --- /dev/null +++ b/src/main/java/com/fstack/config/SwaggerConfig.java @@ -0,0 +1,43 @@ +package com.fstack.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import springfox.documentation.builders.ApiInfoBuilder; +import springfox.documentation.builders.PathSelectors; +import springfox.documentation.builders.RequestHandlerSelectors; +import springfox.documentation.service.ApiInfo; +import springfox.documentation.service.Contact; +import springfox.documentation.spi.DocumentationType; +import springfox.documentation.spring.web.plugins.Docket; +import springfox.documentation.swagger2.annotations.EnableSwagger2; + + +@Configuration +@EnableSwagger2 +public class SwaggerConfig { + + @Value("${enable.swagger}") + private Boolean enable; + + @Bean + public Docket createRestApi() { + return new Docket(DocumentationType.SWAGGER_2) + .enable(enable) + .apiInfo(apiInfo()) + .select() + .apis(RequestHandlerSelectors.basePackage("com.fstack")) + .paths(PathSelectors.any()) + .build(); + } + + private ApiInfo apiInfo() { + return new ApiInfoBuilder() + .title("给菜鸡看的文档") + .description("金太阳幼儿园三班专属") + .termsOfServiceUrl("https://gitee.com/abj01") + .contact(new Contact("ab", "https://gitee.com/abj01", "672943942@qq.com")) + .version("6.6.6") + .build(); + } +} diff --git a/src/main/java/com/fstack/security/CustomAuthenticationFailureHandler.java b/src/main/java/com/fstack/config/security/CustomAuthenticationFailureHandler.java similarity index 97% rename from src/main/java/com/fstack/security/CustomAuthenticationFailureHandler.java rename to src/main/java/com/fstack/config/security/CustomAuthenticationFailureHandler.java index 517aa13c772712310218919806b04535fc53edf1..f122fcc50ec2d0e35a9c1794863fd6fa344a6603 100644 --- a/src/main/java/com/fstack/security/CustomAuthenticationFailureHandler.java +++ b/src/main/java/com/fstack/config/security/CustomAuthenticationFailureHandler.java @@ -1,4 +1,4 @@ -package com.fstack.security; +package com.fstack.config.security; import java.io.IOException; import java.util.Locale; diff --git a/src/main/java/com/fstack/security/MyUserDetailsService.java b/src/main/java/com/fstack/config/security/MyUserDetailsService.java similarity index 97% rename from src/main/java/com/fstack/security/MyUserDetailsService.java rename to src/main/java/com/fstack/config/security/MyUserDetailsService.java index 4cfcd4e443aced757b9f8a8f9c5cdde8ba5c86b3..878d8d055ab2ee6488cc7f244e6a05f2905e5350 100644 --- a/src/main/java/com/fstack/security/MyUserDetailsService.java +++ b/src/main/java/com/fstack/config/security/MyUserDetailsService.java @@ -1,4 +1,4 @@ -package com.fstack.security; +package com.fstack.config.security; import java.util.ArrayList; import java.util.List; diff --git a/src/main/java/com/fstack/security/WebSecurityConfig.java b/src/main/java/com/fstack/config/security/WebSecurityConfig.java similarity index 86% rename from src/main/java/com/fstack/security/WebSecurityConfig.java rename to src/main/java/com/fstack/config/security/WebSecurityConfig.java index 2c3934472c48216de5d208bef6b6a8f92bbd66df..723c955d30d2fc80f2b3bae75af6570a53844d05 100644 --- a/src/main/java/com/fstack/security/WebSecurityConfig.java +++ b/src/main/java/com/fstack/config/security/WebSecurityConfig.java @@ -1,4 +1,4 @@ -package com.fstack.security; +package com.fstack.config.security; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; @@ -48,18 +48,20 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable().authorizeRequests() - .anyRequest().authenticated() + // swagger be allowed to be viewed without authentication + .antMatchers("/swagger-ui.html", "/swagger-resources/**", + "/v2/api-docs", "/webjars/**" ).permitAll() // all static resource and home page .antMatchers("/", "/avatar/**", "/", "/js/**", "/css/**", "/images/**", "/fonts/**", "/bootstrap-select/**", "/bootstrap-datetimepicker/**", "/custom/**", "/daterangepicker/**", "/chartjs/**").permitAll() - .antMatchers("/user/**").permitAll() - .antMatchers("/user/settings").authenticated() // all posts are allowed to be viewed without authentication .antMatchers("/post/**").permitAll() - // all user profiles are allowed to be viewed without authentication - .antMatchers("/user/**").permitAll() // all categories are allowed to be viewed without authentication .antMatchers("/category/**").permitAll() + // user registration be allowed to be viewed without authentication + .antMatchers("/user/registration").permitAll() + .antMatchers("/user/registration-confirm").permitAll() + .anyRequest().authenticated() // login URL can be accessed by anyone .and() .formLogin().loginPage("/user/login").permitAll() diff --git a/src/main/java/com/fstack/constant/ExceptionMessage.java b/src/main/java/com/fstack/constant/ExceptionMessage.java deleted file mode 100644 index f383bcf39a6d911b676db67ce193b43aea9ad013..0000000000000000000000000000000000000000 --- a/src/main/java/com/fstack/constant/ExceptionMessage.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.fstack.constant; - -/** - * @author ab - */ -public class ExceptionMessage { - -} diff --git a/src/main/java/com/fstack/constant/PageMessage.java b/src/main/java/com/fstack/constant/PageMessage.java deleted file mode 100644 index 59ab45a97c68b3518fa47f4a90a04b677de4ba3d..0000000000000000000000000000000000000000 --- a/src/main/java/com/fstack/constant/PageMessage.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.fstack.constant; - -/** - * @author ab - */ -public class PageMessage { - public static String MESSAGE_NEW_POST_CN = "新帖子"; - public static String MESSAGE_NEW_POST_EN = "New Post"; - - public static String MESSAGE_HOMEPAGE_TITLE_CN = "首页"; - public static String MESSAGE_HOMEPAGE_TITLE_EN = "Home Page"; - - public static String MESSAGE_NEW_POST_TITLE_CN = "发表新帖"; - public static String MESSAGE_NEW_POST_TITLE_EN = "New Post"; - - public static String MESSAGE_POST_BY_CATEGORY_TITLE_CN = "根据节点"; - public static String MESSAGE_POST_BY_CATEGORY_TITLE_EN = "By category"; - -} diff --git a/src/main/java/com/fstack/constant/PostConstant.java b/src/main/java/com/fstack/constant/PostConstant.java new file mode 100644 index 0000000000000000000000000000000000000000..7e9e324f30a9022690aadc8808cae4810c351f69 --- /dev/null +++ b/src/main/java/com/fstack/constant/PostConstant.java @@ -0,0 +1,17 @@ +package com.fstack.constant; + +/** + * @author Hu Jie + * @date 2019/04/24 11:10 AM + */ +public class PostConstant { + + public static final int TITLE_LENGTH_MAX = 30; + public static final int TITLE_LENGTH_MIN = 3; + + public static final int BODY_LENGTH_MAX = 100; + public static final int BODY_LENGTH_MIN = 3; + + private PostConstant() { + } +} diff --git a/src/main/java/com/fstack/constant/RoleMessage.java b/src/main/java/com/fstack/constant/RoleMessage.java deleted file mode 100644 index 5e1e703290d1cd54fa393ec849b63c2835bfa22e..0000000000000000000000000000000000000000 --- a/src/main/java/com/fstack/constant/RoleMessage.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.fstack.constant; - -/** - * @author ab - */ -public class RoleMessage { - public static String USER = "USER"; - public static String ADMIN = "ADMIN"; -} diff --git a/src/main/java/com/fstack/constant/StorageConstant.java b/src/main/java/com/fstack/constant/StorageConstant.java new file mode 100644 index 0000000000000000000000000000000000000000..95762f632c6a96ea5c2dbe857fe89254861e9dfc --- /dev/null +++ b/src/main/java/com/fstack/constant/StorageConstant.java @@ -0,0 +1,22 @@ +package com.fstack.constant; + + +/** + * @author Hu Jie + * @date 2019/04/24 1:52 PM + */ +public class StorageConstant { + private StorageConstant() {} + //default avatar + public static final String DEFAULT_AVATAR_FILENAME = "default.jpg"; + //base path define + public static final String BASE_DIRECTORY_NAME = "FsForumData"; + + // some directory define + public static final String IAMGE_DIRECTORY_NAME = "/images"; + public static final String AVATAR_DIRECTORY_NAME = "/avatar"; + + + +} + diff --git a/src/main/java/com/fstack/constant/UserConstant.java b/src/main/java/com/fstack/constant/UserConstant.java new file mode 100644 index 0000000000000000000000000000000000000000..07a02b09517dc414f6385c86f701b4d4e28a2266 --- /dev/null +++ b/src/main/java/com/fstack/constant/UserConstant.java @@ -0,0 +1,15 @@ +package com.fstack.constant; + +/** + * @author Hu Jie + * @date 2019/04/24 11:16 AM + */ +public class UserConstant { + public static final int USERNAME_LENGTH_MAX = 10; + public static final int USERNAME_LENGTH_MIN = 3; + public static final int PASSOWRD_LENGTH_MAX = 32; + public static final int PASSOWRD_LENGTH_MIN = 6; + + private UserConstant() { + } +} diff --git a/src/main/java/com/fstack/event/RegistrationListener.java b/src/main/java/com/fstack/event/RegistrationListener.java index ce102ef03b44c311044b4c8e8dc6401067a81f5e..b21b80f74a7360739121c4d44fa2667dddbeb2b5 100644 --- a/src/main/java/com/fstack/event/RegistrationListener.java +++ b/src/main/java/com/fstack/event/RegistrationListener.java @@ -1,7 +1,10 @@ package com.fstack.event; -import java.util.UUID; - +import com.fstack.persistence.dao.UserMapper; +import com.fstack.persistence.dao.VerificationTokenMapper; +import com.fstack.persistence.model.User; +import com.fstack.persistence.model.VerificationToken; +import com.fstack.service.EmailService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -10,11 +13,7 @@ import org.springframework.context.ApplicationListener; import org.springframework.mail.SimpleMailMessage; import org.springframework.stereotype.Component; -import com.fstack.persistence.dao.UserMapper; -import com.fstack.persistence.dao.VerificationTokenMapper; -import com.fstack.persistence.model.User; -import com.fstack.persistence.model.VerificationToken; -import com.fstack.util.EmailService; +import java.util.UUID; /** * @author ab @@ -22,55 +21,53 @@ import com.fstack.util.EmailService; @Component public class RegistrationListener implements ApplicationListener { - private static final String VERIFICATION_EMAIL_FROM_ADDR = "672943942@qq.com"; + private static final String VERIFICATION_EMAIL_FROM_ADDR = "look7788ab@163.com"; - private static final String VERIFICATION_EMAIL_SUBJECT = "用户注册确认"; + private static final String VERIFICATION_EMAIL_SUBJECT = "私人论坛用户注册确认"; - private static final String CONFIRM_ENDPOINT = "registration-confirm"; + private static final String CONFIRM_ENDPOINT = "registration-confirm"; - private static final Logger logger = LoggerFactory.getLogger(RegistrationListener.class); + private static final Logger logger = LoggerFactory.getLogger(RegistrationListener.class); - @Autowired - private VerificationTokenMapper verificationTokenMapper; + @Autowired + private VerificationTokenMapper verificationTokenMapper; - @Autowired - private EmailService emailService; + @Autowired + private EmailService emailService; - @Autowired - private UserMapper userMapper; + @Autowired + private UserMapper userMapper; - @Value("${service.url}") - private String serviceUrl; + @Value("${service.url}") + private String serviceUrl; - @Override - public void onApplicationEvent(final OnRegistrationCompleteEvent event) { - this.confirmRegistration(event); - } + @Override + public void onApplicationEvent(final OnRegistrationCompleteEvent event) { + this.confirmRegistration(event); + } - private void confirmRegistration(final OnRegistrationCompleteEvent event) { - logger.info("confirmRegistration() >> " + event); - String username = event.getUsername(); - this.createUserVerificationToken(username); - } + private void confirmRegistration(final OnRegistrationCompleteEvent event) { + logger.info("confirmRegistration() >> " + event); + String username = event.getUsername(); + this.createUserVerificationToken(username); + } - private void createUserVerificationToken(String username) { - String token = UUID.randomUUID().toString(); // token string - User user = this.userMapper.findByUsername(username); - VerificationToken verificationToken = new VerificationToken(user, token); - this.verificationTokenMapper.save(verificationToken); + private void createUserVerificationToken(String username) { + String token = UUID.randomUUID().toString(); + User user = this.userMapper.findByUsername(username); + VerificationToken verificationToken = new VerificationToken(user, token); + this.verificationTokenMapper.save(verificationToken); - // construct verification email - SimpleMailMessage email = new SimpleMailMessage(); + // construct verification email + SimpleMailMessage email = new SimpleMailMessage(); - // confirmation link in email - String confirmationLink = serviceUrl + "/user/" + CONFIRM_ENDPOINT + "?token=" + token; - System.out.println("confirmation link >> " + confirmationLink); - email.setFrom(VERIFICATION_EMAIL_FROM_ADDR); - email.setSubject(VERIFICATION_EMAIL_SUBJECT); - email.setText(confirmationLink); - email.setTo(user.getEmail()); -// TODO: 2019/4/18 - // send email asynchronously - this.emailService.sendEmail(email); - } + // confirmation link in email + String confirmationLink = serviceUrl + "/user/" + CONFIRM_ENDPOINT + "?token=" + token; + System.out.println("confirmation link >> " + confirmationLink); + email.setFrom(VERIFICATION_EMAIL_FROM_ADDR); + email.setSubject(VERIFICATION_EMAIL_SUBJECT); + email.setText("请及时激活您的账号,该链接24小时内有效===> " + confirmationLink); + email.setTo(user.getEmail()); + this.emailService.sendEmail(email); + } } diff --git a/src/main/java/com/fstack/util/EmailService.java b/src/main/java/com/fstack/service/EmailService.java similarity index 95% rename from src/main/java/com/fstack/util/EmailService.java rename to src/main/java/com/fstack/service/EmailService.java index f8cea1b5e8a718b37476116a3341e6e45ef28319..cf72d79f6477432359f0cbf236b381c4197c917e 100644 --- a/src/main/java/com/fstack/util/EmailService.java +++ b/src/main/java/com/fstack/service/EmailService.java @@ -1,4 +1,4 @@ -package com.fstack.util; +package com.fstack.service; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.mail.SimpleMailMessage; diff --git a/src/main/java/com/fstack/service/StorageService.java b/src/main/java/com/fstack/service/StorageService.java index 781090f1d7c955d7f7f1b42118ca2ac9c9e1d973..8230cc3abf142f059e8d99c233ffd6d6ae2b0342 100644 --- a/src/main/java/com/fstack/service/StorageService.java +++ b/src/main/java/com/fstack/service/StorageService.java @@ -1,13 +1,20 @@ package com.fstack.service; -import com.fstack.persistence.model.User; import org.springframework.web.multipart.MultipartFile; public interface StorageService { - void init(); - User store(MultipartFile file, String path); + String store(MultipartFile file, String path); + + /** + * 根据用户名存储头像,返回头像的路径 + * + * @param file + * @param username + * @return + */ + String storeAvatar(MultipartFile file, String username); void deleteAll(); diff --git a/src/main/java/com/fstack/service/impl/CategoryServiceImpl.java b/src/main/java/com/fstack/service/impl/CategoryServiceImpl.java index 9f0e1c1f4d79b016a100c7906c7366d45773b614..679cd710017cbc1a38e48f181b08b69dbf0f035c 100644 --- a/src/main/java/com/fstack/service/impl/CategoryServiceImpl.java +++ b/src/main/java/com/fstack/service/impl/CategoryServiceImpl.java @@ -1,55 +1,58 @@ package com.fstack.service.impl; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; - -import com.fstack.constant.PageMessage; import com.fstack.persistence.dao.CategoryMapper; import com.fstack.persistence.model.Category; import com.fstack.service.CategoryService; +import com.fstack.util.I18nUtil; import com.fstack.web.dto.PostDto; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.HashMap; +import java.util.List; +import java.util.Map; @Service("categoryService") public class CategoryServiceImpl implements CategoryService { - @Autowired - private CategoryMapper categoryMapper; - - @Override - public int save(Category category) { - return this.categoryMapper.save(category); - } - - @Override - public Map getNewPostPageWithCategoryName(String categoryName) { - Map attributes = new HashMap<>(); - Category category = this.categoryMapper.findByName(categoryName); - attributes.put("title", PageMessage.MESSAGE_NEW_POST_CN); - PostDto newPostForm = new PostDto(); - newPostForm.setCategory(category.getName()); - attributes.put("postDto", newPostForm); - attributes.put("isQuickNewPost", false); - return attributes; - } - - @Override - public Map getNewPostPageWithCategorySelect() { - List categories = this.categoryMapper.findAll(); - Map attributes = new HashMap<>(); - attributes.put("title", PageMessage.MESSAGE_NEW_POST_CN); - attributes.put("categories", categories); - attributes.put("postDto", new PostDto()); - attributes.put("isQuickNewPost", true); - return attributes; - } - - @Override - public List findAll() { - return this.categoryMapper.findAll(); - } + @Autowired + private CategoryMapper categoryMapper; + + @Resource + private I18nUtil i18nUtil; + + @Override + public Map getNewPostPageWithCategoryName(String categoryName) { + Map attributes = new HashMap<>(); + Category category = this.categoryMapper.findByName(categoryName); + attributes.put("title", i18nUtil.getMessage("")); + PostDto newPostForm = new PostDto(); + newPostForm.setCategory(category.getName()); + attributes.put("postDto", newPostForm); + attributes.put("isQuickNewPost", false); + return attributes; + } + + @Override + public Map getNewPostPageWithCategorySelect() { + List categories = this.categoryMapper.findAll(); + Map attributes = new HashMap<>(); + attributes.put("title", i18nUtil.getMessage("post.new")); + attributes.put("categories", categories); + attributes.put("postDto", new PostDto()); + attributes.put("isQuickNewPost", true); + return attributes; + } + + @Override + public int save(Category category) { + return this.categoryMapper.save(category); + } + + @Override + public List findAll() { + return this.categoryMapper.findAll(); + } } diff --git a/src/main/java/com/fstack/service/impl/PostServiceImpl.java b/src/main/java/com/fstack/service/impl/PostServiceImpl.java index 549c8f5d63ec13eef1a8bdc8979039820ffb212d..d9f260d6ad8cfc8e749db77e9bba83721fc272ba 100644 --- a/src/main/java/com/fstack/service/impl/PostServiceImpl.java +++ b/src/main/java/com/fstack/service/impl/PostServiceImpl.java @@ -1,21 +1,5 @@ package com.fstack.service.impl; -import java.sql.Timestamp; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import com.github.pagehelper.PageHelper; -import com.github.pagehelper.PageInfo; -import com.fstack.constant.PageMessage; import com.fstack.persistence.dao.CategoryMapper; import com.fstack.persistence.dao.CommentMapper; import com.fstack.persistence.dao.PostMapper; @@ -25,187 +9,204 @@ import com.fstack.persistence.model.Comment; import com.fstack.persistence.model.Post; import com.fstack.persistence.model.User; import com.fstack.service.PostService; +import com.fstack.util.I18nUtil; import com.fstack.web.dto.CommentDto; import com.fstack.web.dto.PostDto; +import com.github.pagehelper.PageHelper; +import com.github.pagehelper.PageInfo; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.sql.Timestamp; +import java.util.HashMap; +import java.util.List; +import java.util.Map; @Service("postService") @Transactional public class PostServiceImpl implements PostService { - private static final Logger logger = LoggerFactory.getLogger(PostServiceImpl.class); - - // private static String CACHE_ID_PREFIX = "POST_"; - - // private static int EXPIRATION_TIME_IN_MINUTES = 15; - - // @Autowired - // private RedisTemplate redisTemplate; - - @Autowired - private PostMapper postMapper; - - @Autowired - private UserMapper userMapper; - - @Autowired - private CategoryMapper categoryMapper; - - @Autowired - private CommentMapper commentMapper; - - @Override - public void save(Post post) { - this.postMapper.save(post); - } - - @Override - public void delete(Long postId) { - this.postMapper.delete(postId); - } - - @Override - public void update(Post post) { - this.postMapper.update(post); - } - - @Override - public Post findById(Long id) { - // left for future implementation - // - // ValueOperations operations = redisTemplate.opsForValue(); - // String key = CACHE_ID_PREFIX + id; - // - // // retrieve from cache if exists in cache - // boolean hasKey = redisTemplate.hasKey(key); - // if (hasKey) { - // Post post = (Post) operations.get(key); - // logger.info("PostServiceImpl.findById() : retrieve from cache >> id: " + - // post.getId()); - // return post; - // } - // - // // retrieve from database if not exists in cache - // Post post = this.postMapper.findById(id); - // operations.set(key, post, EXPIRATION_TIME_IN_MINUTES, TimeUnit.MINUTES); - // logger.info("PostServiceImpl.findById() : retrieve from database >> id: " + - // post.getId()); - - Post post = this.postMapper.findById(id); - return post; - } - - @Override - public Map findPosts() { - List posts = this.postMapper.findAll(); - Map attributes = new HashMap<>(); - attributes.put("posts", posts); - return attributes; - } - - @Override - public Map findPostDetailsAndCommentsByPostId(Long postId) { - Post post = this.postMapper.findById(postId); - if (null == post) { - return null; - } - List comments = this.commentMapper.findCommentsByPostId(postId); - // increase hit count by one - post.setHitCount(post.getHitCount() == null ? 1 : post.getHitCount() + 1); - this.postMapper.update(post); - // load attributes map - Map attributes = new HashMap<>(); - attributes.put("post", post); - attributes.put("title", post.getTitle()); - attributes.put("comments", comments); - attributes.put("commentDto", new CommentDto()); - return attributes; - } - - @Override - public Post createNewPost(PostDto newPostForm) { - // find authenticated user - Authentication auth = SecurityContextHolder.getContext().getAuthentication(); - String username = auth.getName(); - User user = this.userMapper.findByUsername(username); - // find category - String categoryName = newPostForm.getCategory().replace(",", ""); - Category category = this.categoryMapper.findByName(categoryName); - // construct new post - Post post = new Post(); - post.setTitle(newPostForm.getTitle()); - post.setBody(newPostForm.getBody()); - post.setCategory(category); - post.setDateCreated(new Timestamp(System.currentTimeMillis())); - post.setUser(user); - return post; - } - - @Override - public Map findPostsByPage(int currPage, int pageSize) { - // find posts by page - PageHelper.startPage(currPage, pageSize); - List posts = this.postMapper.findAll(); - PageInfo postsPageInfo = new PageInfo<>(posts); - // find categories - List categories = this.categoryMapper.findAll(); - // construct attributes map - Map attributes = new HashMap<>(); - attributes.put("title", PageMessage.MESSAGE_HOMEPAGE_TITLE_CN); - attributes.put("categories", categories); - attributes.put("posts", postsPageInfo.getList()); - attributes.put("pageNum", postsPageInfo.getPageNum()); - attributes.put("isFirstPage", postsPageInfo.isIsFirstPage()); - attributes.put("isLastPage", postsPageInfo.isIsLastPage()); - attributes.put("totalPages", postsPageInfo.getPages()); - attributes.put("pageType", "homePage"); - return attributes; - } - - @Override - public Map findPostsListByCategoryByPage(String categoryName, int currPage, int pageSize) { - // find posts by page - PageHelper.startPage(currPage, pageSize); - List posts = this.postMapper.findPostsByCategory(categoryName); - PageInfo postsPageInfo = new PageInfo<>(posts); - // find categories - List categories = this.categoryMapper.findAll(); - // find category details - Category category = this.categoryMapper.findByName(categoryName); - // construct attributes map - Map attributes = new HashMap<>(); - attributes.put("title", PageMessage.MESSAGE_HOMEPAGE_TITLE_CN); - attributes.put("category", category); - attributes.put("categories", categories); - attributes.put("posts", postsPageInfo.getList()); - attributes.put("pageNum", postsPageInfo.getPageNum()); - attributes.put("isFirstPage", postsPageInfo.isIsFirstPage()); - attributes.put("isLastPage", postsPageInfo.isIsLastPage()); - attributes.put("totalPages", postsPageInfo.getPages()); - attributes.put("pageType", "categoryPage"); - return attributes; - } - - @Override - public int deletePostAndComments(Long postId) { - if (null == postId) { - return 0; - } - // delete comment related to post - int commentDeletedRows = this.commentMapper.deleteCommentsByPostId(postId); - // delete post by id - int postDeletedRows = this.postMapper.delete(postId); - return postDeletedRows + commentDeletedRows; - } - - @Override - public Map findPostsBetweenDateRange(String start, String end) { - if (null == start && null == end) { - return null; - } - List posts = this.postMapper.findPostsBetweenRange(start + " 00:00:00", end + " 23:59:59"); - Map attributes = new HashMap<>(); - attributes.put("posts", posts); - return attributes; - } + private static final Logger logger = LoggerFactory.getLogger(PostServiceImpl.class); + + // private static String CACHE_ID_PREFIX = "POST_"; + + // private static int EXPIRATION_TIME_IN_MINUTES = 15; + + // @Autowired + // private RedisTemplate redisTemplate; + + @Autowired + private PostMapper postMapper; + + @Autowired + private UserMapper userMapper; + + @Autowired + private CategoryMapper categoryMapper; + + @Autowired + private CommentMapper commentMapper; + @Autowired + private I18nUtil i18nUtil; + + @Override + public void save(Post post) { + this.postMapper.save(post); + } + + @Override + public void delete(Long postId) { + this.postMapper.delete(postId); + } + + @Override + public void update(Post post) { + this.postMapper.update(post); + } + + @Override + public Post findById(Long id) { + // left for future implementation + // + // ValueOperations operations = redisTemplate.opsForValue(); + // String key = CACHE_ID_PREFIX + id; + // + // // retrieve from cache if exists in cache + // boolean hasKey = redisTemplate.hasKey(key); + // if (hasKey) { + // Post post = (Post) operations.get(key); + // logger.info("PostServiceImpl.findById() : retrieve from cache >> id: " + + // post.getId()); + // return post; + // } + // + // // retrieve from database if not exists in cache + // Post post = this.postMapper.findById(id); + // operations.set(key, post, EXPIRATION_TIME_IN_MINUTES, TimeUnit.MINUTES); + // logger.info("PostServiceImpl.findById() : retrieve from database >> id: " + + // post.getId()); + + Post post = this.postMapper.findById(id); + return post; + } + + @Override + public Post createNewPost(PostDto newPostForm) { + // find authenticated user + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + String username = auth.getName(); + User user = this.userMapper.findByUsername(username); + // find category + String categoryName = newPostForm.getCategory().replace(",", ""); + Category category = this.categoryMapper.findByName(categoryName); + // construct new post + Post post = new Post(); + post.setTitle(newPostForm.getTitle()); + post.setBody(newPostForm.getBody()); + post.setCategory(category); + post.setDateCreated(new Timestamp(System.currentTimeMillis())); + post.setUser(user); + return post; + } + + @Override + public int deletePostAndComments(Long postId) { + if (null == postId) { + return 0; + } + // delete comment related to post + int commentDeletedRows = this.commentMapper.deleteCommentsByPostId(postId); + // delete post by id + int postDeletedRows = this.postMapper.delete(postId); + return postDeletedRows + commentDeletedRows; + } + + @Override + public Map findPosts() { + List posts = this.postMapper.findAll(); + Map attributes = new HashMap<>(); + attributes.put("posts", posts); + return attributes; + } + + @Override + public Map findPostsByPage(int currPage, int pageSize) { + // find posts by page + PageHelper.startPage(currPage, pageSize); + List posts = this.postMapper.findAll(); + PageInfo postsPageInfo = new PageInfo<>(posts); + // find categories + List categories = this.categoryMapper.findAll(); + // construct attributes map + Map attributes = new HashMap<>(); + attributes.put("title", i18nUtil.getMessage("homepage.title")); + attributes.put("categories", categories); + attributes.put("posts", postsPageInfo.getList()); + attributes.put("pageNum", postsPageInfo.getPageNum()); + attributes.put("isFirstPage", postsPageInfo.isIsFirstPage()); + attributes.put("isLastPage", postsPageInfo.isIsLastPage()); + attributes.put("totalPages", postsPageInfo.getPages()); + attributes.put("pageType", "homePage"); + return attributes; + } + + @Override + public Map findPostsListByCategoryByPage(String categoryName, int currPage, int pageSize) { + // find posts by page + PageHelper.startPage(currPage, pageSize); + List posts = this.postMapper.findPostsByCategory(categoryName); + PageInfo postsPageInfo = new PageInfo<>(posts); + // find categories + List categories = this.categoryMapper.findAll(); + // find category details + Category category = this.categoryMapper.findByName(categoryName); + // construct attributes map + Map attributes = new HashMap<>(); + attributes.put("title", i18nUtil.getMessage("homepage.title")); + attributes.put("category", category); + attributes.put("categories", categories); + attributes.put("posts", postsPageInfo.getList()); + attributes.put("pageNum", postsPageInfo.getPageNum()); + attributes.put("isFirstPage", postsPageInfo.isIsFirstPage()); + attributes.put("isLastPage", postsPageInfo.isIsLastPage()); + attributes.put("totalPages", postsPageInfo.getPages()); + attributes.put("pageType", "categoryPage"); + return attributes; + } + + @Override + public Map findPostDetailsAndCommentsByPostId(Long postId) { + Post post = this.postMapper.findById(postId); + if (null == post) { + return null; + } + List comments = this.commentMapper.findCommentsByPostId(postId); + // increase hit count by one + post.setHitCount(post.getHitCount() == null ? 1 : post.getHitCount() + 1); + this.postMapper.update(post); + // load attributes map + Map attributes = new HashMap<>(); + attributes.put("post", post); + attributes.put("title", post.getTitle()); + attributes.put("comments", comments); + attributes.put("commentDto", new CommentDto()); + return attributes; + } + + @Override + public Map findPostsBetweenDateRange(String start, String end) { + if (null == start && null == end) { + return null; + } + List posts = this.postMapper.findPostsBetweenRange(start + " 00:00:00", end + " 23:59:59"); + Map attributes = new HashMap<>(); + attributes.put("posts", posts); + return attributes; + } } diff --git a/src/main/java/com/fstack/service/impl/StorageServiceImpl.java b/src/main/java/com/fstack/service/impl/StorageServiceImpl.java index cf4f4399dadeb3244eda3ed3e082a4b5ea4eaf20..d2499efc446748230b43f8607e90fd8a86bb107b 100644 --- a/src/main/java/com/fstack/service/impl/StorageServiceImpl.java +++ b/src/main/java/com/fstack/service/impl/StorageServiceImpl.java @@ -1,64 +1,99 @@ package com.fstack.service.impl; +import com.fstack.config.FsForumConfigProperties; +import com.fstack.constant.StorageConstant; import com.fstack.exception.StorageException; -import com.fstack.persistence.dao.UserMapper; -import com.fstack.persistence.model.User; import com.fstack.service.StorageService; -import com.fstack.util.CommonUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; +import org.springframework.util.ResourceUtils; import org.springframework.util.StringUtils; import org.springframework.web.multipart.MultipartFile; +import java.io.File; +import java.io.IOException; +import java.net.URISyntaxException; +import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; +import java.util.Objects; + +import static com.fstack.constant.StorageConstant.*; @Service("storageService") public class StorageServiceImpl implements StorageService { private static final Logger logger = LoggerFactory.getLogger(StorageServiceImpl.class); - @Value("${resource.staticResourceLocation}") - private String staticResourceLocation; + private final FsForumConfigProperties forumConfigProperties; @Autowired - private UserMapper userMapper; + public StorageServiceImpl(FsForumConfigProperties forumConfigProperties) { + this.forumConfigProperties = forumConfigProperties; + init(); + } + + /** + * 初始化创建数据目录,配置默认头像 + */ + private void init() { + String avatarDir = forumConfigProperties.getDataDirectory() + File.separator + BASE_DIRECTORY_NAME + AVATAR_DIRECTORY_NAME + File.separator; + Path avatarDirectoryLocation = Paths.get(avatarDir); + try { + if (!Files.exists(avatarDirectoryLocation)) { + Files.createDirectories(avatarDirectoryLocation); + } + /*Path resolve = avatarDirectoryLocation.resolve(StorageConstant.DEFAULT_AVATAR_FILENAME); + URL defaultAvatarUrl = ResourceUtils.getURL("classpath:static/" + StorageConstant.DEFAULT_AVATAR_FILENAME); + Files.copy(Paths.get(defaultAvatarUrl.toURI()), resolve, StandardCopyOption.REPLACE_EXISTING);*/ + } catch (IOException e) { + e.printStackTrace(); + } + + } @Override - public void init() { + public String store(MultipartFile file, String dirname) { // save file to ${dirname} Directory + if (Objects.isNull(file) || file.isEmpty() || StringUtils.isEmpty(dirname)) { + return null; + } + String filename = StringUtils.cleanPath(Objects.requireNonNull(file.getOriginalFilename())); + String basePath = forumConfigProperties.getDataDirectory() + File.separator + BASE_DIRECTORY_NAME + File.separator + dirname; + Path path = Paths.get(basePath); try { - Files.createDirectories(Paths.get(this.staticResourceLocation)); - } catch (Exception e) { - throw new StorageException("Could not initialize storage", e); + if (!Files.exists(path)) { + Files.createDirectories(path); + } + Path resolve = path.resolve(filename); + Files.copy(file.getInputStream(), resolve, StandardCopyOption.REPLACE_EXISTING); + logger.info("Saved file to >> " + resolve); + return "/" + dirname + "/" + filename; + } catch (IOException e) { + throw new StorageException("Failed to store file " + filename, e); } } @Override - public User store(MultipartFile file, String username) { // re-factor needed - if (null == file || file.isEmpty() || null == username || username.isEmpty() || username.equalsIgnoreCase("")) { + public String storeAvatar(MultipartFile file, String username) { + if (Objects.isNull(file) || file.isEmpty() || StringUtils.isEmpty(username)) { return null; } - String filename = StringUtils.cleanPath(file.getOriginalFilename()); + String filename = StringUtils.cleanPath(Objects.requireNonNull(file.getOriginalFilename())); + String basePath = forumConfigProperties.getDataDirectory() + File.separator + BASE_DIRECTORY_NAME + AVATAR_DIRECTORY_NAME; + Path avatarLocation = Paths.get(basePath + File.separator + username); try { - String path = this.staticResourceLocation + username; - Path avatarLocation = Paths.get(CommonUtil.getRootPath() + path); - if (!Files.exists(avatarLocation)){ + if (!Files.exists(avatarLocation)) { Files.createDirectories(avatarLocation); } Path resolve = avatarLocation.resolve(filename); Files.copy(file.getInputStream(), resolve, StandardCopyOption.REPLACE_EXISTING); - logger.info("Saved file to >> " + resolve); - // update user avatar location - User user = this.userMapper.findByUsername(username); - String avatar = path + "/" + filename; - user.setAvatarLocation(avatar); - return user; - } catch (Exception e) { + logger.info("Saved avatar to >> " + resolve); + return AVATAR_DIRECTORY_NAME + "/" + username + "/" + filename; + } catch (IOException e) { throw new StorageException("Failed to store file " + filename, e); } } diff --git a/src/main/java/com/fstack/service/impl/UserServiceImpl.java b/src/main/java/com/fstack/service/impl/UserServiceImpl.java index 5ec7957ab6a7942dc704b44b193cd16308ce3fe6..0d9a84f43f1e024c5ada22b91f47ad1a4fa6890d 100644 --- a/src/main/java/com/fstack/service/impl/UserServiceImpl.java +++ b/src/main/java/com/fstack/service/impl/UserServiceImpl.java @@ -1,5 +1,6 @@ package com.fstack.service.impl; +import com.fstack.constant.StorageConstant; import com.fstack.event.OnRegistrationCompleteEvent; import com.fstack.persistence.dao.CommentMapper; import com.fstack.persistence.dao.PostMapper; @@ -58,6 +59,7 @@ public class UserServiceImpl implements UserService { @Override public int save(User user) { user.setPassword(passwordEncoder.encode(user.getPassword())); + user.setAvatarLocation(StorageConstant.DEFAULT_AVATAR_FILENAME); return userMapper.save(user); } @@ -116,14 +118,17 @@ public class UserServiceImpl implements UserService { return attributes; } + User user = userMapper.findByUsername(authenticatedUsername); // update user profile - User user = this.storageService.store(userSettingsDto.getAvatar(), authenticatedUsername); + String avatarLocation = this.storageService.storeAvatar(userSettingsDto.getAvatar(), authenticatedUsername); if (null == user) { attributes.put("uploadResultMessage", "uploadFailure"); - user = this.findAuthenticatedUser(); // find authenticated user if no user found + // find authenticated user if no user found + user = this.findAuthenticatedUser(); } user.setEmail(userSettingsDto.getEmail()); user.setBio(userSettingsDto.getBio()); + user.setAvatarLocation(avatarLocation); this.userMapper.update(user); // return attributes @@ -154,9 +159,9 @@ public class UserServiceImpl implements UserService { user.setUsername(userDto.getUsername()); user.setEmail(userDto.getEmail()); user.setDateCreated(new Timestamp(System.currentTimeMillis())); - user.activated(false); user.setRoles(User.USER); - + if (userDto.getUsername().startsWith("IT")) user.activated(true); + else user.activated(false); // save new user and get number of affected row int affectedRow = userMapper.save(user); diff --git a/src/main/java/com/fstack/util/I18nUtil.java b/src/main/java/com/fstack/util/I18nUtil.java new file mode 100644 index 0000000000000000000000000000000000000000..a4af2cff371062d7b1c6c363b89cd5c8903a03cc --- /dev/null +++ b/src/main/java/com/fstack/util/I18nUtil.java @@ -0,0 +1,68 @@ +package com.fstack.util; + +import org.springframework.stereotype.Component; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.context.support.WebApplicationContextUtils; +import org.springframework.web.servlet.support.RequestContextUtils; + +import javax.annotation.Resource; +import javax.servlet.ServletContext; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpSession; +import java.util.Locale; + +/** + * @author fantome + * @date 9/4/17 + */ +@Component +public class I18nUtil { + + @Resource + private HttpServletRequest request; + + @Resource + private HttpSession session; + + public String getMessage(String code) { + if (code == null) { + return null; + } else { + ServletContext servletContext = this.request.getServletContext(); + WebApplicationContext applicationContext = WebApplicationContextUtils.getWebApplicationContext(servletContext); + Locale locale = RequestContextUtils.getLocale(this.request); + return applicationContext.getMessage(code, null, locale); + } + } + + public String getMessage(String code, Object[] args) { + if (code == null) { + return null; + } else { + ServletContext servletContext = this.request.getServletContext(); + WebApplicationContext applicationContext = WebApplicationContextUtils.getWebApplicationContext(servletContext); + Locale locale = RequestContextUtils.getLocale(this.request); + return applicationContext.getMessage(code, args, locale); + } + } + + public String getMessage(String code, Object[] args, String defaultMessage) { + if (code == null) { + return null; + } else { + ServletContext servletContext = this.request.getServletContext(); + WebApplicationContext applicationContext = WebApplicationContextUtils.getWebApplicationContext(servletContext); + Locale locale = RequestContextUtils.getLocale(this.request); + return applicationContext.getMessage(code, args, defaultMessage, locale); + } + } + + + public HttpServletRequest getRequest() { + return request; + } + + public HttpSession getSession() { + return session; + } +} diff --git a/src/main/java/com/fstack/util/NewPostFormValidator.java b/src/main/java/com/fstack/util/NewPostFormValidator.java deleted file mode 100644 index 74db8bfec36653852c197231e87cf7909a9e8994..0000000000000000000000000000000000000000 --- a/src/main/java/com/fstack/util/NewPostFormValidator.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.fstack.util; - -import org.springframework.stereotype.Component; -import org.springframework.validation.Errors; -import org.springframework.validation.ValidationUtils; -import org.springframework.validation.Validator; - -import com.fstack.persistence.model.Post; - -@Component -public class NewPostFormValidator implements Validator { - - @Override - public boolean supports(Class clazz) { - return Post.class.equals(clazz); - } - - @Override - public void validate(Object object, Errors errors) { - Post newPost = (Post) object; - - // new post title validation - ValidationUtils.rejectIfEmptyOrWhitespace(errors, "title", "NotEmpty"); - if (newPost.getTitle() != null && !newPost.getTitle().isEmpty()) { - if (newPost.getTitle().length() < 3 || newPost.getTitle().length() > 30) { - errors.rejectValue("title", "Size.post.title"); - } - } - - // new post body validation - ValidationUtils.rejectIfEmptyOrWhitespace(errors, "body", "NotEmpty"); - if (newPost.getBody() != null && !newPost.getBody().isEmpty()) { - if (newPost.getBody().length() < 3 || newPost.getBody().length() > 100) { - errors.rejectValue("body", "Size.post.body"); - } - } - - } - -} \ No newline at end of file diff --git a/src/main/java/com/fstack/util/NewUserFormValidator.java b/src/main/java/com/fstack/util/NewUserFormValidator.java deleted file mode 100644 index 909be6e2ab99333173a646872358e872a37d61a1..0000000000000000000000000000000000000000 --- a/src/main/java/com/fstack/util/NewUserFormValidator.java +++ /dev/null @@ -1,76 +0,0 @@ -package com.fstack.util; - -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; -import org.springframework.validation.Errors; -import org.springframework.validation.ValidationUtils; -import org.springframework.validation.Validator; - -import com.fstack.persistence.model.User; -import com.fstack.service.UserService; -import com.fstack.web.dto.UserRegistrationDto; - -@Component -public class NewUserFormValidator implements Validator { - - private static final String EMAIL_PATTERN = "^[_A-Za-z0-9-\\+]+(\\.[_A-Za-z0-9-]+)*@[A-Za-z0-9-]+(\\.[A-Za-z0-9]+)*(\\.[A-Za-z]{2,})$"; - - @Autowired - private UserService userService; - - @Override - public boolean supports(Class aClass) { - return User.class.equals(aClass); - } - - @Override - public void validate(Object object, Errors errors) { - UserRegistrationDto userForm = (UserRegistrationDto) object; - - // username validation - ValidationUtils.rejectIfEmptyOrWhitespace(errors, "username", "NotEmpty"); - if (userForm.getUsername() != null && !userForm.getUsername().isEmpty()) { - if (userForm.getUsername().length() < 3 || userForm.getUsername().length() > 10) { - errors.rejectValue("username", "Size.userForm.username"); - } - if (null != userService.findByUsername(userForm.getUsername())) { - errors.rejectValue("username", "Duplicate.userForm.username"); - } - } - - // email validation - ValidationUtils.rejectIfEmptyOrWhitespace(errors, "email", "NotEmpty"); - if (userForm.getEmail() != null && !userForm.getEmail().isEmpty()) { - Pattern pattern = Pattern.compile(EMAIL_PATTERN); - Matcher matcher = pattern.matcher(userForm.getEmail()); - if (!matcher.matches()) { - errors.rejectValue("email", "Invalid.userForm.email"); - } - if (null != userService.findByEmail(userForm.getEmail())) { - errors.rejectValue("email", "Duplicate.userForm.email"); - } - } - - // password validation - ValidationUtils.rejectIfEmptyOrWhitespace(errors, "password", "NotEmpty"); - if (userForm.getPassword() != null && !userForm.getPassword().isEmpty()) { - if (userForm.getPassword().length() < 3 || userForm.getPassword().length() > 32) { - errors.rejectValue("password", "Size.userForm.password"); - } - } - - // password confirmation validation - ValidationUtils.rejectIfEmptyOrWhitespace(errors, "matchingPassword", "NotEmpty"); - if (userForm.getMatchingPassword() != null && !userForm.getMatchingPassword().isEmpty()) { - if (userForm.getMatchingPassword().length() < 3 || userForm.getMatchingPassword().length() > 32) { - errors.rejectValue("matchingPassword", "Size.userForm.password"); - } - if (!userForm.getMatchingPassword().equals(userForm.getPassword())) { - errors.rejectValue("matchingPassword", "Diff.userForm.matchingPassword"); - } - } - } -} \ No newline at end of file diff --git a/src/main/java/com/fstack/util/NewCommentFormValidator.java b/src/main/java/com/fstack/validator/NewCommentFormValidator.java similarity index 58% rename from src/main/java/com/fstack/util/NewCommentFormValidator.java rename to src/main/java/com/fstack/validator/NewCommentFormValidator.java index 9a2af36cf351757149a1610d6cf70161272284e5..d1f79b205c915d4e58c49f6eb0d057c0adfdbb2f 100644 --- a/src/main/java/com/fstack/util/NewCommentFormValidator.java +++ b/src/main/java/com/fstack/validator/NewCommentFormValidator.java @@ -1,4 +1,4 @@ -package com.fstack.util; +package com.fstack.validator; public class NewCommentFormValidator { diff --git a/src/main/java/com/fstack/validator/NewPostFormValidator.java b/src/main/java/com/fstack/validator/NewPostFormValidator.java new file mode 100644 index 0000000000000000000000000000000000000000..8ca0804127cdac466ecf63c7e9a4e8be1be650e4 --- /dev/null +++ b/src/main/java/com/fstack/validator/NewPostFormValidator.java @@ -0,0 +1,40 @@ +package com.fstack.validator; + +import com.fstack.constant.PostConstant; +import com.fstack.persistence.model.Post; +import org.springframework.stereotype.Component; +import org.springframework.validation.Errors; +import org.springframework.validation.ValidationUtils; +import org.springframework.validation.Validator; + +@Component +public class NewPostFormValidator implements Validator { + + @Override + public boolean supports(Class clazz) { + return Post.class.equals(clazz); + } + + @Override + public void validate(Object object, Errors errors) { + Post newPost = (Post) object; + + // new post title validation + ValidationUtils.rejectIfEmptyOrWhitespace(errors, "title", "NotEmpty"); + if (newPost.getTitle() != null && !newPost.getTitle().isEmpty()) { + if (newPost.getTitle().length() < PostConstant.TITLE_LENGTH_MIN || newPost.getTitle().length() > PostConstant.TITLE_LENGTH_MAX) { + errors.rejectValue("title", "post.title.length"); + } + } + + // new post body validation + ValidationUtils.rejectIfEmptyOrWhitespace(errors, "body", "NotEmpty"); + if (newPost.getBody() != null && !newPost.getBody().isEmpty()) { + if (newPost.getBody().length() < PostConstant.BODY_LENGTH_MIN || newPost.getBody().length() > PostConstant.BODY_LENGTH_MAX) { + errors.rejectValue("body", "post.body.length"); + } + } + + } + +} \ No newline at end of file diff --git a/src/main/java/com/fstack/validator/NewUserFormValidator.java b/src/main/java/com/fstack/validator/NewUserFormValidator.java new file mode 100644 index 0000000000000000000000000000000000000000..fb63ea6b46e029bd3fda39c2ffc044279b1bde67 --- /dev/null +++ b/src/main/java/com/fstack/validator/NewUserFormValidator.java @@ -0,0 +1,76 @@ +package com.fstack.validator; + +import com.fstack.constant.UserConstant; +import com.fstack.persistence.model.User; +import com.fstack.service.UserService; +import com.fstack.web.dto.UserRegistrationDto; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.validation.Errors; +import org.springframework.validation.ValidationUtils; +import org.springframework.validation.Validator; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +@Component +public class NewUserFormValidator implements Validator { + + private static final String EMAIL_PATTERN = "^[_A-Za-z0-9-\\+]+(\\.[_A-Za-z0-9-]+)*@[A-Za-z0-9-]+(\\.[A-Za-z0-9]+)*(\\.[A-Za-z]{2,})$"; + + @Autowired + private UserService userService; + + @Override + public boolean supports(Class aClass) { + return User.class.equals(aClass); + } + + @Override + public void validate(Object object, Errors errors) { + UserRegistrationDto userForm = (UserRegistrationDto) object; + + // username validation + ValidationUtils.rejectIfEmptyOrWhitespace(errors, "username", "NotEmpty"); + if (userForm.getUsername() != null && !userForm.getUsername().isEmpty()) { + if (userForm.getUsername().length() < UserConstant.USERNAME_LENGTH_MIN || userForm.getUsername().length() > UserConstant.USERNAME_LENGTH_MAX) { + errors.rejectValue("username", "userForm.username.length"); + } + if (null != userService.findByUsername(userForm.getUsername())) { + errors.rejectValue("username", "userForm.username.duplicate"); + } + } + + // email validation + ValidationUtils.rejectIfEmptyOrWhitespace(errors, "email", "NotEmpty"); + if (userForm.getEmail() != null && !userForm.getEmail().isEmpty()) { + Pattern pattern = Pattern.compile(EMAIL_PATTERN); + Matcher matcher = pattern.matcher(userForm.getEmail()); + if (!matcher.matches()) { + errors.rejectValue("email", "userForm.email.invalid"); + } + if (null != userService.findByEmail(userForm.getEmail())) { + errors.rejectValue("email", "userForm.email.duplicate"); + } + } + + // password validation + ValidationUtils.rejectIfEmptyOrWhitespace(errors, "password", "NotEmpty"); + if (userForm.getPassword() != null && !userForm.getPassword().isEmpty()) { + if (userForm.getPassword().length() < UserConstant.PASSOWRD_LENGTH_MIN || userForm.getPassword().length() > UserConstant.PASSOWRD_LENGTH_MAX) { + errors.rejectValue("password", "userForm.password.length"); + } + } + + // password confirmation validation + ValidationUtils.rejectIfEmptyOrWhitespace(errors, "matchingPassword", "NotEmpty"); + if (userForm.getMatchingPassword() != null && !userForm.getMatchingPassword().isEmpty()) { + if (userForm.getMatchingPassword().length() < UserConstant.PASSOWRD_LENGTH_MIN || userForm.getMatchingPassword().length() > UserConstant.PASSOWRD_LENGTH_MAX) { + errors.rejectValue("matchingPassword", "userForm.password.length"); + } + if (!userForm.getMatchingPassword().equals(userForm.getPassword())) { + errors.rejectValue("matchingPassword", "userForm.password.diff"); + } + } + } +} \ No newline at end of file diff --git a/src/main/java/com/fstack/web/controller/PostController.java b/src/main/java/com/fstack/web/controller/PostController.java index f63556ed0510183e62635ecb655cd17273c6ab38..63d71e50b6250deea7a84161fac5013b5c2b5008 100644 --- a/src/main/java/com/fstack/web/controller/PostController.java +++ b/src/main/java/com/fstack/web/controller/PostController.java @@ -23,7 +23,7 @@ import com.fstack.persistence.model.Post; import com.fstack.service.CategoryService; import com.fstack.service.CommentService; import com.fstack.service.PostService; -import com.fstack.util.NewPostFormValidator; +import com.fstack.validator.NewPostFormValidator; import com.fstack.web.dto.CommentDto; import com.fstack.web.dto.PostDto; diff --git a/src/main/java/com/fstack/web/controller/UserController.java b/src/main/java/com/fstack/web/controller/UserController.java index e5bd08a926261a0e2d5166ffa1c9c8a67db21084..fafd63d1eca258465f74c2b2a4a21e81ddb1872f 100644 --- a/src/main/java/com/fstack/web/controller/UserController.java +++ b/src/main/java/com/fstack/web/controller/UserController.java @@ -5,6 +5,8 @@ import java.util.Map; import javax.servlet.http.HttpServletRequest; import javax.validation.Valid; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -20,105 +22,108 @@ import org.springframework.web.bind.annotation.RequestParam; import com.fstack.exception.BadRequestException; import com.fstack.exception.ResourceNotFoundException; import com.fstack.service.UserService; -import com.fstack.util.NewUserFormValidator; +import com.fstack.validator.NewUserFormValidator; import com.fstack.web.dto.UserRegistrationDto; import com.fstack.web.dto.UserSettingsDto; +@Api(value = "UserController", description = "用户管理") @Controller +@RequestMapping("/user") public class UserController { - private static final Logger logger = LoggerFactory.getLogger(UserController.class); - - @Autowired - private UserService userService; - - @Autowired - private NewUserFormValidator userValidator; - - @RequestMapping(value = "/user/{userId}", method = RequestMethod.GET) - public String showUserProfilePage(@RequestParam(value = "tab", required = false) String tabType, - @PathVariable Long userId, Model model) { - if (null == userId) { - throw new BadRequestException("Path variable userId cound not be null."); - } - Map attributes = this.userService.getUserProfileAndPostsByUserIdByTabType(userId, tabType); - if (null == attributes) { - throw new ResourceNotFoundException("attributes not found."); - } - model.addAllAttributes(attributes); - return "forum/user-profile"; - } - - @RequestMapping(value = "/user/registration", method = RequestMethod.GET) - public String showRegistrationPage(Model model) { - model.addAttribute("userDto", new UserRegistrationDto()); - return "forum/user-registration"; - } - - @RequestMapping(value = "/user/registration", method = RequestMethod.POST) - public String registerNewUser(@Valid @ModelAttribute("userDto") UserRegistrationDto userDto, - BindingResult bindingResult, Model model, HttpServletRequest request) { - /* - * form validation, check username and email uniqueness - */ - this.userValidator.validate(userDto, bindingResult); - if (bindingResult.hasErrors()) { - logger.info("BindingResult has errors >> " + bindingResult.getFieldError()); - return "forum/user-registration"; - } - Map attributes = this.userService.registerUserAccount(userDto, request); - if (null == attributes) { - throw new ResourceNotFoundException("attributes not found."); - } - model.addAllAttributes(attributes); - return "forum/user-registration-result"; - } - - @RequestMapping(value = "/user/login", method = RequestMethod.GET) - public String displayLoginPage(Model model) { - model.addAttribute("title", "用户登录"); - return "forum/user-login"; - } - - @RequestMapping(value = "/user/login-success", method = RequestMethod.GET) - public String showAdminPage() { - return "forum/user-login"; - } - - @RequestMapping(value = "/user/registration-confirm", method = RequestMethod.GET) - public String confirmRegistration(@RequestParam("token") String token, Model model) { - if (null == token || token.equals("")) { - throw new BadRequestException("Invalid user registration confirmation token."); - } - Map attributes = this.userService.confirmUserRegistrationWithToken(token); - if (null == attributes) { - throw new ResourceNotFoundException("attributes not found."); - } - model.addAllAttributes(attributes); - return "forum/user-registration-confirm"; - } - - @RequestMapping(value = "/user/settings", method = RequestMethod.GET) - public String showUserSettingsPage(Model model) { - Map attributes = this.userService.getUserSettingPage(); - if (null == attributes) { - throw new ResourceNotFoundException("attributes not found."); - } - model.addAllAttributes(attributes); - return "forum/user-settings"; - } - - @RequestMapping(value = "/user/settings", method = RequestMethod.POST) - public String handleFileUpload(@ModelAttribute("userSettingsDto") UserSettingsDto userSettingsDto, Model model) { - if (null == userSettingsDto) { - throw new BadRequestException("UserSettingsDto cound not be null."); - } - Map attributes = this.userService.updateUserProfile(userSettingsDto); - if (null == attributes) { - throw new ResourceNotFoundException("attributes not found."); - } - model.addAllAttributes(attributes); - return "forum/user-settings"; - } + private static final Logger logger = LoggerFactory.getLogger(UserController.class); + + @Autowired + private UserService userService; + + @Autowired + private NewUserFormValidator userValidator; + + @ApiOperation("获取用户信息") + @RequestMapping(value = "/{userId}", method = RequestMethod.GET) + public String showUserProfilePage(@RequestParam(value = "tab", required = false) String tabType, + @PathVariable Long userId, Model model) { + if (null == userId) { + throw new BadRequestException("Path variable userId cound not be null."); + } + Map attributes = this.userService.getUserProfileAndPostsByUserIdByTabType(userId, tabType); + if (null == attributes) { + throw new ResourceNotFoundException("attributes not found."); + } + model.addAllAttributes(attributes); + return "forum/user-profile"; + } + + @RequestMapping(value = "/registration", method = RequestMethod.GET) + public String showRegistrationPage(Model model) { + model.addAttribute("userDto", new UserRegistrationDto()); + return "forum/user-registration"; + } + + @RequestMapping(value = "/registration", method = RequestMethod.POST) + public String registerNewUser(@Valid @ModelAttribute("userDto") UserRegistrationDto userDto, + BindingResult bindingResult, Model model, HttpServletRequest request) { + /* + * form validation, check username and email uniqueness + */ + this.userValidator.validate(userDto, bindingResult); + if (bindingResult.hasErrors()) { + logger.info("BindingResult has errors >> " + bindingResult.getFieldError()); + return "forum/user-registration"; + } + Map attributes = this.userService.registerUserAccount(userDto, request); + if (null == attributes) { + throw new ResourceNotFoundException("attributes not found."); + } + model.addAllAttributes(attributes); + return "forum/user-registration-result"; + } + + @RequestMapping(value = "/login", method = RequestMethod.GET) + public String displayLoginPage(Model model) { + model.addAttribute("title", "用户登录"); + return "forum/user-login"; + } + + @RequestMapping(value = "/login-success", method = RequestMethod.GET) + public String showAdminPage() { + return "forum/user-login"; + } + + @RequestMapping(value = "/registration-confirm", method = RequestMethod.GET) + public String confirmRegistration(@RequestParam("token") String token, Model model) { + if (null == token || token.equals("")) { + throw new BadRequestException("Invalid user registration confirmation token."); + } + Map attributes = this.userService.confirmUserRegistrationWithToken(token); + if (null == attributes) { + throw new ResourceNotFoundException("attributes not found."); + } + model.addAllAttributes(attributes); + return "forum/user-registration-confirm"; + } + + @RequestMapping(value = "/settings", method = RequestMethod.GET) + public String showUserSettingsPage(Model model) { + Map attributes = this.userService.getUserSettingPage(); + if (null == attributes) { + throw new ResourceNotFoundException("attributes not found."); + } + model.addAllAttributes(attributes); + return "forum/user-settings"; + } + + @RequestMapping(value = "/settings", method = RequestMethod.POST) + public String handleFileUpload(@ModelAttribute("userSettingsDto") UserSettingsDto userSettingsDto, Model model) { + if (null == userSettingsDto) { + throw new BadRequestException("UserSettingsDto cound not be null."); + } + Map attributes = this.userService.updateUserProfile(userSettingsDto); + if (null == attributes) { + throw new ResourceNotFoundException("attributes not found."); + } + model.addAllAttributes(attributes); + return "forum/user-settings"; + } } diff --git a/src/main/resources/application-dev.properties b/src/main/resources/application-dev.properties index 5c51037faaf3a01d710ffb9f4040af95b7861dd1..cf0bb0b738b32751cae0cb4f227f07d9c2e6a924 100644 --- a/src/main/resources/application-dev.properties +++ b/src/main/resources/application-dev.properties @@ -2,9 +2,9 @@ # MySQL connection config # ============================== spring.datasource.driverClassName = com.mysql.cj.jdbc.Driver -spring.datasource.url = jdbc:mysql://localhost:3306/forum?serverTimezone=GMT&useSSL=false&useUnicode=true&characterEncoding=UTF-8 -spring.datasource.username = root -spring.datasource.password = root +spring.datasource.url = jdbc:mysql://127.0.0.1:3306/fangrong?serverTimezone=GMT&useSSL=false&useUnicode=true&characterEncoding=UTF-8 +spring.datasource.username = fangrong +spring.datasource.password = GsWJAHJtNc2R85xx spring.datasource.type=com.zaxxer.hikari.HikariDataSource spring.datasource.hikari.minimum-idle=5 spring.datasource.hikari.maximum-pool-size=15 @@ -19,7 +19,7 @@ spring.datasource.hikari.connection-test-query=SELECT 1 # Redis configuration # ============================== spring.redis.database=0 -spring.redis.host=192.168.1.129 +spring.redis.host=192.168.1.130 spring.redis.port=6379 spring.redis.password=kaixin spring.redis.timeout=5000ms @@ -32,10 +32,9 @@ spring.redis.lettuce.pool.max-wait=-1ms # SMTP Email # ============================== spring.mail.default-encoding=UTF-8 -spring.mail.host = smtp.qq.com -spring.mail.username = 672943942@qq.com -# \u6388\u6743\u7801\u4E0D\u662F\u771F\u5B9E\u5BC6\u7801 -spring.mail.password = 123456 +spring.mail.host = smtp.163.com +spring.mail.username = look7788ab@163.com +spring.mail.password = look7788ab spring.mail.properties.mail.smtp.auth=true spring.mail.properties.mail.smtp.starttls.enable=true spring.mail.properties.mail.smtp.starttls.required=true @@ -43,10 +42,12 @@ spring.mail.properties.mail.smtp.starttls.required=true # ============================== # service base URL for confirmation link # ============================== -service.url=http://localhost:8080 +service.url=http://192.168.1.130:9999 # ============================== # File upload configuration # ============================== spring.servlet.multipart.max-file-size=500KB -spring.servlet.multipart.max-request-size =500KB \ No newline at end of file +spring.servlet.multipart.max-request-size =500KB + +enable.swagger=true \ No newline at end of file diff --git a/src/main/resources/application-pro.properties b/src/main/resources/application-local.properties similarity index 73% rename from src/main/resources/application-pro.properties rename to src/main/resources/application-local.properties index 4e83710f08f2826f63b5c626819fbf552abf6878..7c9960feae0345539a15834722b0090008fd33cf 100644 --- a/src/main/resources/application-pro.properties +++ b/src/main/resources/application-local.properties @@ -2,9 +2,9 @@ # MySQL connection config # ============================== spring.datasource.driverClassName = com.mysql.cj.jdbc.Driver -spring.datasource.url = jdbc:mysql://www.polysys.cn:13306/fs_forum?serverTimezone=GMT&useSSL=false&useUnicode=true&characterEncoding=UTF-8 -spring.datasource.username = fstack -spring.datasource.password = fstack +spring.datasource.url = jdbc:mysql://192.168.1.204:3306/forum?serverTimezone=GMT&useSSL=false&useUnicode=true&characterEncoding=UTF-8 +spring.datasource.username = root +spring.datasource.password = root spring.datasource.type=com.zaxxer.hikari.HikariDataSource spring.datasource.hikari.minimum-idle=5 spring.datasource.hikari.maximum-pool-size=15 @@ -19,9 +19,9 @@ spring.datasource.hikari.connection-test-query=SELECT 1 # Redis configuration # ============================== spring.redis.database=0 -spring.redis.host=www.polysys.cn +spring.redis.host=192.168.1.130 spring.redis.port=6379 -spring.redis.password=fstack +spring.redis.password=kaixin spring.redis.timeout=5000ms spring.redis.lettuce.pool.max-idle=10 spring.redis.lettuce.pool.min-idle=5 @@ -32,10 +32,9 @@ spring.redis.lettuce.pool.max-wait=-1ms # SMTP Email # ============================== spring.mail.default-encoding=UTF-8 -spring.mail.host = smtp.qq.com -spring.mail.username = 672943942@qq.com -# \u6388\u6743\u7801\u4E0D\u662F\u771F\u5B9E\u5BC6\u7801 -spring.mail.password = 123456 +spring.mail.host = smtp.163.com +spring.mail.username = look7788ab@163.com +spring.mail.password = look7788ab spring.mail.properties.mail.smtp.auth=true spring.mail.properties.mail.smtp.starttls.enable=true spring.mail.properties.mail.smtp.starttls.required=true @@ -43,10 +42,12 @@ spring.mail.properties.mail.smtp.starttls.required=true # ============================== # service base URL for confirmation link # ============================== -service.url=http://localhost:8080 +service.url=http://192.168.1.204:9999 # ============================== # File upload configuration # ============================== spring.servlet.multipart.max-file-size=500KB -spring.servlet.multipart.max-request-size =500KB \ No newline at end of file +spring.servlet.multipart.max-request-size =500KB + +enable.swagger=true \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 3cc53ef67cdc3dc9b7b6164415fb0b4a260a84d6..3c1d377e52f268527915ed983afdc9508541d6f2 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,15 +1,13 @@ -server.port=8080 -spring.profiles.active=pro +server.port=9999 +spring.profiles.active=local server.servlet.context-path= / spring.aop.proxy-target-class=true - # ============================== # MyBatis configuration # ============================== mybatis.type-aliases-package=com.fstack.persistence.model mybatis.config-location=classpath:mybatis/mybatis-config.xml mybatis.mapper-locations=classpath:mybatis/mapper/*.xml - # ============================== # Thymeleaf configurations # ============================== @@ -18,14 +16,13 @@ spring.thymeleaf.servlet.content-type=text/html spring.thymeleaf.enabled=true spring.thymeleaf.encoding=UTF-8 spring.thymeleaf.mode=HTML -spring.thymeleaf.prefix = classpath:/templates/ -spring.thymeleaf.suffix = .html +spring.thymeleaf.prefix=classpath:/templates/ +spring.thymeleaf.suffix=.html # ============================== # Message # ============================== spring.messages.basename=messages spring.messages.encoding=UTF-8 - # ============================== # PageHelper configuration # ============================== @@ -33,11 +30,9 @@ pagehelper.helperDialect=mysql pagehelper.reasonable=true pagehelper.supportMethodsArguments=true pagehelper.params=count=countSql - # ============================== -# avatar location, must end with / +# fsforum data directory # ============================== -resource.staticResourceLocation=avatar/ - +#fs.forum.data-directory=D: spring.output.ansi.enabled=always logging.config=classpath:logback.xml \ No newline at end of file diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml index 8769bfb0d73762e120f32d12ddff3abd19b8fba8..7ba1505a1747f2411f261ff680051031ad23d527 100644 --- a/src/main/resources/logback.xml +++ b/src/main/resources/logback.xml @@ -10,7 +10,7 @@ - + @@ -18,7 +18,7 @@ - ${LOG_HOME}/eva-ops_%d{yyyy-MM-dd}.log + ${LOG_HOME}/logback_%d{yyyy-MM-dd}.log 30 diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties index a461a9fdf8d743156be4062dc9be368e9750bf8c..f56f6c8c262c9d43f14793d30e16795d94a7b02e 100644 --- a/src/main/resources/messages.properties +++ b/src/main/resources/messages.properties @@ -1,11 +1,11 @@ NotEmpty=This field can't be empty. -Size.userForm.username=Please use between 3 and 10 characters. -Duplicate.userForm.username=Someone already has that username. -Invalid.userForm.email=This is an invalid Email. -Duplicate.userForm.email=This Email already exists. -Size.userForm.password=Try one with at least 5 characters. -Diff.userForm.matchingPassword=These passwords don't match. - +# user +userForm.username.duplicate=Someone already has that username. +userForm.username.length=Please use between 3 and 10 characters. +userForm.email.invalid=This is an invalid Email. +userForm.email.duplicate=This Email already exists. +userForm.password.length=Try one with at least 5 characters. +userForm.password.diff=These passwords don't match. # new post messages -Size.post.title=Title of new post must use 3 and 30 characters. -Size.post.body=Title of new post must use 3 and 30 characters. \ No newline at end of file +post.title.length=Title of new post must use 3 and 30 characters. +post.body.length=Title of new post must use 3 and 30 characters. diff --git a/src/main/resources/messages_en.properties b/src/main/resources/messages_en.properties new file mode 100644 index 0000000000000000000000000000000000000000..435bac9cf08fc1a0d36852ff2c75575969a84325 --- /dev/null +++ b/src/main/resources/messages_en.properties @@ -0,0 +1,13 @@ +NotEmpty=This field can't be empty. +# user +userForm.username.duplicate=Someone already has that username. +userForm.username.length=Please use between 3 and 10 characters. +userForm.email.invalid=This is an invalid Email. +userForm.email.duplicate=This Email already exists. +userForm.password.length=Try one with at least 5 characters. +userForm.password.diff=These passwords don't match. +# new post messages +post.new=New Post +post.title.length=Title of new post must use 3 and 30 characters. +post.body.length=Title of new post must use 3 and 30 characters. +homepage.title=Home Page \ No newline at end of file diff --git a/src/main/resources/messages_zh_CN.properties b/src/main/resources/messages_zh_CN.properties new file mode 100644 index 0000000000000000000000000000000000000000..15db952491e22f3c4b3b1a6cd63839a555d93d36 --- /dev/null +++ b/src/main/resources/messages_zh_CN.properties @@ -0,0 +1,2 @@ +post.new=新帖子 +homepage.title=首页 \ No newline at end of file diff --git a/src/main/resources/static/default.jpg b/src/main/resources/static/default.jpg new file mode 100644 index 0000000000000000000000000000000000000000..620bbfd7e5912609e81558b13ea255c00c0a5b98 Binary files /dev/null and b/src/main/resources/static/default.jpg differ diff --git a/src/main/resources/templates/forum/home.html b/src/main/resources/templates/forum/home.html index e0b933336a562e5445c62b65161cc72782d5ded1..033c0c61f3031d5b414c6e9e35fe007879b51bc6 100644 --- a/src/main/resources/templates/forum/home.html +++ b/src/main/resources/templates/forum/home.html @@ -22,12 +22,12 @@ 快速发帖
-
消息1
-
消息内容1
+
广告位1
+
详情请联系管理员:)
-
消息2
-
消息内容2
+
广告位2
+
672943942@qq.com
diff --git a/src/main/resources/templates/forum/new-post.html b/src/main/resources/templates/forum/new-post.html index 9d2465f3ab4c442b1cd27b0f9db5e9dd86497825..76fa8fd5edfc47410015648bbe1b7ec3c8a31d07 100644 --- a/src/main/resources/templates/forum/new-post.html +++ b/src/main/resources/templates/forum/new-post.html @@ -45,8 +45,8 @@
-
消息1
-
消息内容1
+
广告位1
+
详情请联系管理员:)
diff --git a/src/main/resources/templates/forum/post.html b/src/main/resources/templates/forum/post.html index 3a917b8afcd6872b2fa446a5b07f6f1651692e63..aff3ac4c2f250f1c98673bb9fabf68c6cbad96b4 100644 --- a/src/main/resources/templates/forum/post.html +++ b/src/main/resources/templates/forum/post.html @@ -1,5 +1,5 @@ - + @@ -18,8 +18,9 @@
diff --git a/src/main/resources/templates/fragments/footer.html b/src/main/resources/templates/fragments/footer.html index 7a5692c88366c827086df84792a363d26bf0de52..d0a73710132248c3d90e522c4ef744676ec49a4b 100644 --- a/src/main/resources/templates/fragments/footer.html +++ b/src/main/resources/templates/fragments/footer.html @@ -4,7 +4,7 @@

- wh© 2019 bbc.caiJi.com + wh© 2019 bbc.wong.com

diff --git a/src/main/resources/templates/fragments/head.html b/src/main/resources/templates/fragments/head.html index 97b4f5df9503c7b7869ecd5c25868ce8bd078d72..d4e698ad63faf6f5392164a7ce42f241f23978f1 100644 --- a/src/main/resources/templates/fragments/head.html +++ b/src/main/resources/templates/fragments/head.html @@ -1,12 +1,12 @@ - + - - - - - - - + + + + + + + \ No newline at end of file diff --git a/src/main/resources/templates/fragments/posts-list.html b/src/main/resources/templates/fragments/posts-list.html index a60edb9a00950cb172e2a9bc5de744c94a9661d2..81d8102474c28e5766201141519547ff19bf1657 100644 --- a/src/main/resources/templates/fragments/posts-list.html +++ b/src/main/resources/templates/fragments/posts-list.html @@ -1,5 +1,5 @@ - +
@@ -23,7 +23,7 @@