├── screenshots ├── pc_1.png ├── pc_2.png ├── mobile_1.png └── mobile_2.png ├── plan.md ├── static ├── images │ ├── logo.png │ ├── favicon.ico │ ├── logo_short.png │ └── friendly │ │ ├── gocn.png │ │ ├── ruby.png │ │ └── cnode.png ├── styles │ ├── fonts │ │ ├── FontAwesome.otf │ │ ├── fontawesome-webfont.eot │ │ ├── fontawesome-webfont.ttf │ │ ├── fontawesome-webfont.woff │ │ └── fontawesome-webfont.woff2 │ ├── scss │ │ ├── base │ │ │ ├── _extends.scss │ │ │ ├── _config.scss │ │ │ ├── _reset.scss │ │ │ └── _mixin.scss │ │ ├── override.scss │ │ └── mobile.scss │ ├── override.css.map │ ├── mobile.css.map │ ├── override.css │ └── mobile.css └── scripts │ ├── lib │ ├── editor │ │ └── fonts │ │ │ ├── icomoon.eot │ │ │ ├── icomoon.ttf │ │ │ └── icomoon.woff │ ├── at-who │ │ └── jquery.atwho.min.css │ ├── high-light │ │ └── styles │ │ │ ├── mono-blue.css │ │ │ ├── github-gist.css │ │ │ ├── monokai.css │ │ │ ├── monokai-sublime.css │ │ │ └── github.css │ └── jquery.caret.min.js │ └── app │ ├── login.js │ ├── register.js │ ├── reset-password.js │ ├── bind-user.js │ ├── mount-editor.js │ ├── new-password.js │ ├── comment-editor.js │ ├── user.js │ ├── frame.js │ ├── topic-editor.js │ ├── validator.js │ └── uploader.js ├── src ├── models │ ├── mod.rs │ ├── category.rs │ ├── message.rs │ ├── comment.rs │ ├── topic.rs │ └── user.rs ├── common │ ├── mod.rs │ ├── macros.rs │ ├── config.rs │ ├── middlewares.rs │ ├── lazy_static.rs │ ├── db.rs │ └── http.rs ├── services │ ├── mod.rs │ ├── category.rs │ ├── collection.rs │ ├── message.rs │ ├── topic_vote.rs │ ├── comment_vote.rs │ └── comment.rs ├── controllers │ ├── logout.rs │ ├── mod.rs │ ├── simple_render.rs │ ├── error.rs │ ├── rss.rs │ ├── message.rs │ ├── register.rs │ ├── login.rs │ ├── user.rs │ ├── reset_password.rs │ ├── comment.rs │ ├── topic_list.rs │ └── upload.rs ├── main.rs └── routes.rs ├── .travis.yml ├── .gitignore ├── views ├── common │ ├── crumbs.hbs │ ├── pagination.hbs │ ├── footer.hbs │ ├── header.hbs │ ├── aside.hbs │ └── nav.hbs ├── config │ └── global-config.hbs ├── error.hbs ├── about-site.hbs ├── comment-editor.hbs ├── resource.hbs ├── reset-password.hbs ├── bind-user.hbs ├── message.hbs ├── topic-editor.hbs ├── login.hbs ├── new-password.hbs ├── register.hbs └── topic-list.hbs ├── log4rs.yaml ├── config.toml ├── Cargo.toml ├── LICENSE ├── README.md └── tables.sql /screenshots/pc_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yinyanlv/runner/HEAD/screenshots/pc_1.png -------------------------------------------------------------------------------- /screenshots/pc_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yinyanlv/runner/HEAD/screenshots/pc_2.png -------------------------------------------------------------------------------- /plan.md: -------------------------------------------------------------------------------- 1 | ## 2018-7-1开始,将着手优化本项目代码并引入ORM框架diesel 2 | ## 由于iron框架已停止维护,因此计划将后端框架替换为actix-web,开始时间待定 -------------------------------------------------------------------------------- /static/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yinyanlv/runner/HEAD/static/images/logo.png -------------------------------------------------------------------------------- /screenshots/mobile_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yinyanlv/runner/HEAD/screenshots/mobile_1.png -------------------------------------------------------------------------------- /screenshots/mobile_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yinyanlv/runner/HEAD/screenshots/mobile_2.png -------------------------------------------------------------------------------- /static/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yinyanlv/runner/HEAD/static/images/favicon.ico -------------------------------------------------------------------------------- /static/images/logo_short.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yinyanlv/runner/HEAD/static/images/logo_short.png -------------------------------------------------------------------------------- /src/models/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod user; 2 | pub mod topic; 3 | pub mod comment; 4 | pub mod message; 5 | pub mod category; -------------------------------------------------------------------------------- /static/images/friendly/gocn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yinyanlv/runner/HEAD/static/images/friendly/gocn.png -------------------------------------------------------------------------------- /static/images/friendly/ruby.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yinyanlv/runner/HEAD/static/images/friendly/ruby.png -------------------------------------------------------------------------------- /static/images/friendly/cnode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yinyanlv/runner/HEAD/static/images/friendly/cnode.png -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: rust 3 | rust: 4 | - stable 5 | before_script: 6 | script: 7 | - cargo test 8 | -------------------------------------------------------------------------------- /static/styles/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yinyanlv/runner/HEAD/static/styles/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /static/scripts/lib/editor/fonts/icomoon.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yinyanlv/runner/HEAD/static/scripts/lib/editor/fonts/icomoon.eot -------------------------------------------------------------------------------- /static/scripts/lib/editor/fonts/icomoon.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yinyanlv/runner/HEAD/static/scripts/lib/editor/fonts/icomoon.ttf -------------------------------------------------------------------------------- /static/styles/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yinyanlv/runner/HEAD/static/styles/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /static/styles/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yinyanlv/runner/HEAD/static/styles/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .DS_Store 3 | .vscode 4 | 5 | target/ 6 | 7 | npm-debug.log 8 | node_modules/ 9 | 10 | upload/ 11 | log/ 12 | -------------------------------------------------------------------------------- /static/scripts/lib/editor/fonts/icomoon.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yinyanlv/runner/HEAD/static/scripts/lib/editor/fonts/icomoon.woff -------------------------------------------------------------------------------- /static/styles/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yinyanlv/runner/HEAD/static/styles/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /static/styles/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yinyanlv/runner/HEAD/static/styles/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /src/models/category.rs: -------------------------------------------------------------------------------- 1 | #[derive(Serialize, Deserialize, Clone, Debug)] 2 | pub struct Category { 3 | pub id: u8, 4 | pub name: String 5 | } 6 | -------------------------------------------------------------------------------- /src/common/mod.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | pub mod macros; 3 | pub mod config; 4 | pub mod db; 5 | pub mod utils; 6 | pub mod http; 7 | pub mod middlewares; 8 | pub mod lazy_static; -------------------------------------------------------------------------------- /views/common/crumbs.hbs: -------------------------------------------------------------------------------- 1 |
2 | 首页 3 | 4 | {{title}} 5 |
-------------------------------------------------------------------------------- /src/services/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod user; 2 | pub mod topic; 3 | pub mod comment; 4 | pub mod message; 5 | pub mod topic_vote; 6 | pub mod comment_vote; 7 | pub mod category; 8 | pub mod collection; -------------------------------------------------------------------------------- /src/controllers/logout.rs: -------------------------------------------------------------------------------- 1 | use iron::prelude::*; 2 | use iron_sessionstorage::traits::SessionRequestExt; 3 | 4 | use common::http::*; 5 | 6 | pub fn logout(req: &mut Request) -> IronResult { 7 | 8 | req.session().clear().unwrap(); 9 | 10 | redirect_to("/") 11 | } -------------------------------------------------------------------------------- /static/styles/scss/base/_extends.scss: -------------------------------------------------------------------------------- 1 | %textOverflowEllipsis { 2 | overflow: hidden; 3 | white-space: nowrap; 4 | text-overflow: ellipsis; 5 | } 6 | 7 | %borderBox { 8 | -webkit-box-sizing: border-box; 9 | -moz-box-sizing: border-box; 10 | box-sizing: border-box; 11 | } -------------------------------------------------------------------------------- /views/config/global-config.hbs: -------------------------------------------------------------------------------- 1 | 11 | -------------------------------------------------------------------------------- /src/controllers/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod login; 2 | pub mod register; 3 | pub mod logout; 4 | pub mod reset_password; 5 | pub mod user; 6 | pub mod topic; 7 | pub mod topic_list; 8 | pub mod comment; 9 | pub mod message; 10 | pub mod error; 11 | pub mod simple_render; 12 | pub mod upload; 13 | pub mod rss; 14 | 15 | -------------------------------------------------------------------------------- /src/models/message.rs: -------------------------------------------------------------------------------- 1 | use chrono::{NaiveDateTime}; 2 | 3 | #[derive(Serialize, Deserialize, Clone, Debug)] 4 | pub struct Message { 5 | pub id: String, 6 | pub from_user_id: u16, 7 | pub to_user_id: u16, 8 | pub topic_id: String, 9 | pub content: String, 10 | pub status: u8, 11 | pub create_time: NaiveDateTime 12 | } 13 | -------------------------------------------------------------------------------- /src/controllers/simple_render.rs: -------------------------------------------------------------------------------- 1 | use iron::prelude::*; 2 | 3 | use common::http::*; 4 | 5 | pub fn render_resource(req: &mut Request) -> IronResult { 6 | 7 | respond_view("resource", &ViewData::new(req)) 8 | } 9 | 10 | pub fn render_about_site(req: &mut Request) -> IronResult { 11 | 12 | respond_view("about-site", &ViewData::new(req)) 13 | } -------------------------------------------------------------------------------- /views/error.hbs: -------------------------------------------------------------------------------- 1 | {{var "styles" 2 | [ 3 | "/styles/frame.css", 4 | "/styles/override.css", 5 | "/styles/mobile.css" 6 | ] 7 | }} 8 | 9 | {{~> common/header ~}} 10 | 11 |
12 |
{{title}} :(
13 | 14 |
15 | 16 | 17 | -------------------------------------------------------------------------------- /src/models/comment.rs: -------------------------------------------------------------------------------- 1 | use chrono::{NaiveDateTime}; 2 | 3 | #[derive(Serialize, Deserialize, Clone, Debug)] 4 | pub struct Comment { 5 | pub id: String, 6 | pub user_id: u16, 7 | pub username: String, 8 | pub avatar_url: String, 9 | pub topic_id: String, 10 | pub content: String, 11 | pub agree_count: u16, 12 | pub disagree_count: u16, 13 | pub status: u8, 14 | pub create_time: NaiveDateTime, 15 | pub update_time: NaiveDateTime 16 | } 17 | -------------------------------------------------------------------------------- /log4rs.yaml: -------------------------------------------------------------------------------- 1 | refresh_rate: 30 seconds 2 | appenders: 3 | stdout: 4 | kind: console 5 | requests: 6 | kind: file 7 | path: "log/requests.log" 8 | encoder: 9 | pattern: "{d} - {m}{n}" 10 | root: 11 | level: warn 12 | appenders: 13 | - requests 14 | loggers: 15 | app::backend::db: 16 | level: warn 17 | appenders: 18 | - requests 19 | additive: false 20 | app::requests: 21 | level: warn 22 | appenders: 23 | - requests 24 | additive: false -------------------------------------------------------------------------------- /src/controllers/error.rs: -------------------------------------------------------------------------------- 1 | use iron::prelude::*; 2 | 3 | use common::http::*; 4 | 5 | pub fn render_not_found(req: &mut Request) -> IronResult { 6 | 7 | let mut data = ViewData::new(req); 8 | 9 | data.insert("title", json!("此页面不存在")); 10 | 11 | respond_view("error", &data) 12 | } 13 | 14 | pub fn render_forbidden(req: &mut Request) -> IronResult { 15 | 16 | let mut data = ViewData::new(req); 17 | 18 | data.insert("title", json!("禁止访问")); 19 | 20 | respond_view("error", &data) 21 | } -------------------------------------------------------------------------------- /src/services/category.rs: -------------------------------------------------------------------------------- 1 | use common::lazy_static::SQL_POOL; 2 | use models::category::Category; 3 | 4 | pub fn get_categories() -> Vec { 5 | 6 | let result = SQL_POOL.prep_exec("SELECT id, name FROM category", ()).unwrap(); 7 | 8 | result.map(|row_wrapper| row_wrapper.unwrap()) 9 | .map(|mut row| { 10 | 11 | Category { 12 | id: row.get::(0).unwrap(), 13 | name: row.get::(1).unwrap() 14 | } 15 | }) 16 | .collect() 17 | } 18 | 19 | -------------------------------------------------------------------------------- /src/models/topic.rs: -------------------------------------------------------------------------------- 1 | use chrono::{NaiveDateTime}; 2 | 3 | #[derive(Serialize, Deserialize, Clone, Debug)] 4 | pub struct Topic { 5 | pub id: String, 6 | pub user_id: u16, 7 | pub category_id: u8, 8 | pub category_name: String, 9 | pub title: String, 10 | pub content: String, 11 | pub status: u8, 12 | pub sticky: u8, 13 | pub essence: u8, 14 | pub view_count: u32, 15 | pub agree_count: u16, 16 | pub disagree_count: u16, 17 | pub create_time: NaiveDateTime, 18 | pub update_time: NaiveDateTime 19 | } 20 | -------------------------------------------------------------------------------- /src/models/user.rs: -------------------------------------------------------------------------------- 1 | use chrono::{NaiveDateTime}; 2 | 3 | #[derive(Serialize, Deserialize, Clone, Debug)] 4 | pub struct User { 5 | pub id: u16, 6 | pub username: String, 7 | pub nickname: String, 8 | pub user_role: u8, 9 | pub register_source: u8, 10 | pub gender: u8, 11 | pub signature: String, 12 | pub email: String, 13 | pub avatar_url: String, 14 | pub qq: String, 15 | pub location: String, 16 | pub site: String, 17 | pub github_account: String, 18 | pub create_time: NaiveDateTime, 19 | pub update_time: NaiveDateTime 20 | } 21 | -------------------------------------------------------------------------------- /src/common/macros.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused_macros)] 2 | 3 | macro_rules! gen_hashmap { 4 | 5 | (@single $($e: tt)*) => (()); 6 | 7 | (@count $($e: expr)*) => (<[()]>::len(&[$(gen_hashmap!(@single $e)),*])); 8 | 9 | ($($key: expr => $value: expr,)+) => (gen_hashmap!($($key => $value),+)); 10 | 11 | {$($key: expr => $value: expr),*} => { 12 | 13 | { 14 | let count = gen_hashmap!(@count $($key)*); 15 | let mut hashmap = ::std::collections::HashMap::with_capacity(count); 16 | 17 | $( 18 | hashmap.insert($key, $value); 19 | )* 20 | 21 | hashmap 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /static/styles/scss/base/_config.scss: -------------------------------------------------------------------------------- 1 | $theme-color: #1e90ff; 2 | $font-color: #333; 3 | $lighter-font-color: #666; 4 | $lightest-font-color: #999; 5 | $bd-color: #ccc; 6 | $lighter-bd-color:#f0f0f0; 7 | $bg-color: #eee; 8 | $body-bg-color: #e4e4e4; 9 | $panel-header-bg-color: #f6f6f6; 10 | $topic-hover-bg-color: #f5f5f5; 11 | $box-shadow-color: #ddd; 12 | $pagination-btn-bd-color:#ddd; 13 | $disabled-btn-bg-color: #ccc; 14 | $ask-user-color: #ff4500; 15 | $answer-user-color:#ff4500; 16 | $error-color: red; 17 | $required-color: red; 18 | $reply-count-color: #20b2aa; 19 | $view-count-color: $lightest-font-color; 20 | $tag-category-bg-color: #eee; 21 | $comment-highlight-bg-color: #e4f1ff; 22 | -------------------------------------------------------------------------------- /views/common/pagination.hbs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/controllers/rss.rs: -------------------------------------------------------------------------------- 1 | use iron::prelude::*; 2 | use iron::status; 3 | use rss::{Channel}; 4 | 5 | use common::lazy_static::PATH; 6 | use services::topic::get_rss_topic_list; 7 | 8 | pub fn render_rss(_: &mut Request) -> IronResult { 9 | 10 | let items = get_rss_topic_list(); 11 | 12 | let channel = Channel { 13 | title: "Rust社区".to_owned(), 14 | description: "Rust社区最新话题".to_owned(), 15 | link: PATH.to_owned(), 16 | items: items, 17 | language: Some("zh-cn".to_owned()), 18 | ..Default::default() 19 | }; 20 | 21 | let mut res = Response::new(); 22 | 23 | res.set_mut(status::Ok) 24 | .set_mut(mime!(Application/Xml)) 25 | .set_mut(channel.to_string()); 26 | 27 | Ok(res) 28 | } 29 | -------------------------------------------------------------------------------- /views/common/footer.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 14 | 15 |
16 | 回到顶部 17 |
18 | 19 | {{#each scripts}} 20 | 21 | {{/each}} 22 | 23 | -------------------------------------------------------------------------------- /views/common/header.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | {{title}} 11 | 12 | 13 | {{#each styles}} 14 | 15 | {{/each}} 16 | {{~> config/global-config ~}} 17 | 18 | -------------------------------------------------------------------------------- /static/scripts/lib/at-who/jquery.atwho.min.css: -------------------------------------------------------------------------------- 1 | .atwho-view{position:absolute;top:0;left:0;display:none;margin-top:18px;background:#fff;color:#000;border:1px solid #DDD;border-radius:3px;box-shadow:0 0 5px rgba(0,0,0,.1);min-width:120px;z-index:11110!important}.atwho-view .atwho-header{padding:5px;margin:5px;cursor:pointer;border-bottom:solid 1px #eaeff1;color:#6f8092;font-size:11px;font-weight:700}.atwho-view .atwho-header .small{color:#6f8092;float:right;padding-top:2px;margin-right:-5px;font-size:12px;font-weight:400}.atwho-view .atwho-header:hover{cursor:default}.atwho-view .cur{background:#36F;color:#fff}.atwho-view .cur small{color:#fff}.atwho-view strong{color:#36F}.atwho-view .cur strong{color:#fff;font:700}.atwho-view ul{list-style:none;padding:0;margin:auto;max-height:200px;overflow-y:auto}.atwho-view ul li{display:block;padding:5px 10px;border-bottom:1px solid #DDD;cursor:pointer}.atwho-view small{font-size:smaller;color:#777;font-weight:400} -------------------------------------------------------------------------------- /config.toml: -------------------------------------------------------------------------------- 1 | host = "localhost" 2 | port = 3000 3 | path = "http://localhost:3000" 4 | static_path = "http://localhost:3000/static" 5 | upload_path = "http://localhost:3000" # 文件服务器地址 6 | 7 | # 管理员账号列表 8 | admins = ["admin"] 9 | 10 | [github] 11 | client_id = "c28f7718a19ee09c4a71" 12 | client_secret = "df1380ad7e8bbee69bff50d9671aac2a7f1a31b8" 13 | 14 | [mysql] 15 | host = "localhost" 16 | port = 3306 17 | username = "root" 18 | password = "111111" 19 | db_name = "runner" # 数据库名称 20 | 21 | [redis] 22 | protocol = "redis" 23 | host = "localhost" 24 | port = 6379 25 | username = "" 26 | password = "" 27 | session_key = "runner" 28 | max_age = 2592000 # 单位s,session过期时间,默认30天 29 | 30 | [smtp] 31 | host = "smtp.163.com" 32 | port = 25 33 | username = "rustchina@163.com" 34 | password = "runner111111" 35 | 36 | [upload] 37 | temp_path = "upload/temp" # 上传的临时文件夹 38 | assets_path = "upload/assets" # 保存后,文件存放的文件夹 39 | clean_temp_dir_ttl = 86400000 # 单位ms,定时清理用户上传但并未保存的文件,默认24小时 -------------------------------------------------------------------------------- /src/common/config.rs: -------------------------------------------------------------------------------- 1 | use std::io::Read; 2 | use std::path::Path; 3 | use std::fs::File; 4 | use std::cmp::Ord; 5 | use std::borrow::Borrow; 6 | 7 | use iron::typemap::Key; 8 | use toml::from_str; 9 | use toml::value::{Table, Value}; 10 | 11 | #[derive(Clone)] 12 | pub struct Config(Table); 13 | 14 | impl Config { 15 | 16 | pub fn new(path: &str) -> Config { 17 | 18 | let path = Path::new(path); 19 | let mut file = File::open(&path).unwrap(); 20 | let mut temp = String::new(); 21 | 22 | file.read_to_string(&mut temp).unwrap(); 23 | 24 | let table = from_str(&temp).unwrap(); 25 | 26 | Config(table) 27 | } 28 | 29 | pub fn get(&self, key: &T) -> &Value where String: Borrow, T: Ord { 30 | 31 | self.0.get(key).unwrap() 32 | } 33 | 34 | pub fn value(&self) -> Table { 35 | 36 | self.0.clone() 37 | } 38 | } 39 | 40 | impl Key for Config { 41 | 42 | type Value = Config; 43 | } -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["yinyanlv "] 3 | name = "runner" 4 | version = "0.0.0" 5 | 6 | [dependencies] 7 | handlebars-iron = "^0.25.2" 8 | hyper = "^0.10.13" 9 | hyper-native-tls = "^0.2.4" 10 | iron = "^0.5.1" 11 | lazy_static = "^0.2.8" 12 | lettre = "^0.6.2" 13 | log4rs = "^0.7.0" 14 | mime = "^0.2.6" 15 | mount = "^0.3.0" 16 | multipart = "^0.12.0" 17 | mysql = "^12.0.1" 18 | pulldown-cmark = "^0.1.0" 19 | rand = "^0.3.16" 20 | regex = "^0.2.2" 21 | router = "^0.5.1" 22 | rss = "^0.4.0" 23 | rust-crypto = "^0.2.36" 24 | schedule = "^0.1.0" 25 | serde = "^1.0.14" 26 | serde_derive = "^1.0.14" 27 | serde_json = "^1.0.2" 28 | staticfile = "^0.4.0" 29 | toml = "^0.4.5" 30 | url = "^1.5.1" 31 | urlencoded = "^0.5.0" 32 | 33 | [dependencies.chrono] 34 | features = ["serde"] 35 | version = "^0.4.0" 36 | 37 | [dependencies.iron-sessionstorage2] 38 | features = ["redis-backend"] 39 | version = "^0.7.1" 40 | 41 | [dependencies.uuid] 42 | features = ["v4"] 43 | version = "^0.5.1" 44 | -------------------------------------------------------------------------------- /static/scripts/lib/high-light/styles/mono-blue.css: -------------------------------------------------------------------------------- 1 | /* 2 | Five-color theme from a single blue hue. 3 | */ 4 | .hljs { 5 | display: block; 6 | overflow-x: auto; 7 | padding: 0.5em; 8 | background: #eaeef3; 9 | } 10 | 11 | .hljs { 12 | color: #00193a; 13 | } 14 | 15 | .hljs-keyword, 16 | .hljs-selector-tag, 17 | .hljs-title, 18 | .hljs-section, 19 | .hljs-doctag, 20 | .hljs-name, 21 | .hljs-strong { 22 | font-weight: bold; 23 | } 24 | 25 | .hljs-comment { 26 | color: #738191; 27 | } 28 | 29 | .hljs-string, 30 | .hljs-title, 31 | .hljs-section, 32 | .hljs-built_in, 33 | .hljs-literal, 34 | .hljs-type, 35 | .hljs-addition, 36 | .hljs-tag, 37 | .hljs-quote, 38 | .hljs-name, 39 | .hljs-selector-id, 40 | .hljs-selector-class { 41 | color: #0048ab; 42 | } 43 | 44 | .hljs-meta, 45 | .hljs-subst, 46 | .hljs-symbol, 47 | .hljs-regexp, 48 | .hljs-attribute, 49 | .hljs-deletion, 50 | .hljs-variable, 51 | .hljs-template-variable, 52 | .hljs-link, 53 | .hljs-bullet { 54 | color: #4c81c9; 55 | } 56 | 57 | .hljs-emphasis { 58 | font-style: italic; 59 | } 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2017-present by yinyanlv 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /static/styles/scss/base/_reset.scss: -------------------------------------------------------------------------------- 1 | @import "./mixin"; 2 | 3 | body, div, ul, dl, ol, dt, dd, li, p, a, label, span, i, img, form, input, textarea, header, footer, article, section, nav, aside, figure, h1, h2, h3, h4, h5, h6, table, thead, tbody, tr, th, td, pre { 4 | padding: 0; 5 | margin: 0; 6 | border: 0; 7 | @include boxSizing(border-box); 8 | } 9 | 10 | ul, dl, ol { 11 | list-style: none; 12 | } 13 | 14 | header, footer, article, section, nav, aside, figure { 15 | display: block 16 | } 17 | 18 | input, textarea{ 19 | 20 | @include placeholderColor(red); 21 | } 22 | 23 | input, textarea, a, button, select { 24 | outline: none; 25 | } 26 | 27 | input[type="button"], input[type="submit"], input[type="reset"], textarea, button { 28 | -webkit-appearance: none; 29 | @include borderRadius(0); 30 | border:0; 31 | } 32 | 33 | input:focus, textarea:focus, a:focus, button:focus{ 34 | -webkit-tap-highlight-color: rgba(255,255,255,0); 35 | } 36 | 37 | input{ 38 | 39 | &::-ms-clear { 40 | display: none; 41 | } 42 | 43 | &::-ms-reveal{ 44 | display:none; 45 | } 46 | 47 | &::-webkit-search-cancel-button{ 48 | display: none; 49 | } 50 | } 51 | 52 | a { 53 | text-decoration: none; 54 | } 55 | 56 | -------------------------------------------------------------------------------- /views/about-site.hbs: -------------------------------------------------------------------------------- 1 | {{var "title" "关于本站"}} 2 | {{var "styles" 3 | [ 4 | "/styles/font-awesome.min.css", 5 | "/styles/frame.css", 6 | "/styles/override.css", 7 | "/styles/mobile.css" 8 | ] 9 | }} 10 | {{var "scripts" 11 | [ 12 | "/scripts/lib/jquery-3.2.1.min.js", 13 | "/scripts/app/frame.js" 14 | ] 15 | }} 16 | 17 | {{~> common/header ~}} 18 | {{~> common/nav ~}} 19 | 20 |
21 |
22 |
23 | {{~> common/crumbs ~}} 24 |
25 |
26 |
27 |

关于本站

28 |

本站希望发展成为一个开放、活跃、健康的Rust社区,致力于 Rust 的技术研究与发展。

29 |

本站完全兼容PC端和移动端,后端由 Rust 语言编写,本站源码地址

30 |

目前该项目由本人维护,由于本人水平有限,因此,本站尚有许多不足之处,希望各位多提宝贵意见或者直接参与建设!

31 |

本人联系方式:1761869346@qq.com

32 |
33 |
34 |
35 |
36 | 37 | {{~> common/aside ~}} 38 | {{~> common/footer ~}} -------------------------------------------------------------------------------- /static/scripts/lib/high-light/styles/github-gist.css: -------------------------------------------------------------------------------- 1 | /** 2 | * GitHub Gist Theme 3 | * Author : Louis Barranqueiro - https://github.com/LouisBarranqueiro 4 | */ 5 | 6 | .hljs { 7 | display: block; 8 | background: white; 9 | padding: 0.5em; 10 | color: #333333; 11 | overflow-x: auto; 12 | } 13 | 14 | .hljs-comment, 15 | .hljs-meta { 16 | color: #969896; 17 | } 18 | 19 | .hljs-string, 20 | .hljs-variable, 21 | .hljs-template-variable, 22 | .hljs-strong, 23 | .hljs-emphasis, 24 | .hljs-quote { 25 | color: #df5000; 26 | } 27 | 28 | .hljs-keyword, 29 | .hljs-selector-tag, 30 | .hljs-type { 31 | color: #a71d5d; 32 | } 33 | 34 | .hljs-literal, 35 | .hljs-symbol, 36 | .hljs-bullet, 37 | .hljs-attribute { 38 | color: #0086b3; 39 | } 40 | 41 | .hljs-section, 42 | .hljs-name { 43 | color: #63a35c; 44 | } 45 | 46 | .hljs-tag { 47 | color: #333333; 48 | } 49 | 50 | .hljs-title, 51 | .hljs-attr, 52 | .hljs-selector-id, 53 | .hljs-selector-class, 54 | .hljs-selector-attr, 55 | .hljs-selector-pseudo { 56 | color: #795da3; 57 | } 58 | 59 | .hljs-addition { 60 | color: #55a532; 61 | background-color: #eaffea; 62 | } 63 | 64 | .hljs-deletion { 65 | color: #bd2c00; 66 | background-color: #ffecec; 67 | } 68 | 69 | .hljs-link { 70 | text-decoration: underline; 71 | } 72 | -------------------------------------------------------------------------------- /static/scripts/lib/high-light/styles/monokai.css: -------------------------------------------------------------------------------- 1 | /* 2 | Monokai style - ported by Luigi Maselli - http://grigio.org 3 | */ 4 | 5 | .hljs { 6 | display: block; 7 | overflow-x: auto; 8 | padding: 0.5em; 9 | background: #272822; color: #ddd; 10 | } 11 | 12 | .hljs-tag, 13 | .hljs-keyword, 14 | .hljs-selector-tag, 15 | .hljs-literal, 16 | .hljs-strong, 17 | .hljs-name { 18 | color: #f92672; 19 | } 20 | 21 | .hljs-code { 22 | color: #66d9ef; 23 | } 24 | 25 | .hljs-class .hljs-title { 26 | color: white; 27 | } 28 | 29 | .hljs-attribute, 30 | .hljs-symbol, 31 | .hljs-regexp, 32 | .hljs-link { 33 | color: #bf79db; 34 | } 35 | 36 | .hljs-string, 37 | .hljs-bullet, 38 | .hljs-subst, 39 | .hljs-title, 40 | .hljs-section, 41 | .hljs-emphasis, 42 | .hljs-type, 43 | .hljs-built_in, 44 | .hljs-builtin-name, 45 | .hljs-selector-attr, 46 | .hljs-selector-pseudo, 47 | .hljs-addition, 48 | .hljs-variable, 49 | .hljs-template-tag, 50 | .hljs-template-variable { 51 | color: #a6e22e; 52 | } 53 | 54 | .hljs-comment, 55 | .hljs-quote, 56 | .hljs-deletion, 57 | .hljs-meta { 58 | color: #75715e; 59 | } 60 | 61 | .hljs-keyword, 62 | .hljs-selector-tag, 63 | .hljs-literal, 64 | .hljs-doctag, 65 | .hljs-title, 66 | .hljs-section, 67 | .hljs-type, 68 | .hljs-selector-id { 69 | font-weight: bold; 70 | } 71 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # runner 2 | [![build status](https://www.travis-ci.org/yinyanlv/runner.svg?branch=master)](https://www.travis-ci.org/yinyanlv/runner) 3 | [![license](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE) 4 | 5 | ## 介绍 6 | runner 是一个使用 **Rust** 语言开发的社区论坛系统,界面优雅,功能完整,响应式布局,完全兼容PC(IE9+)和手机端。该项目完全开源,且将持续优化和完善,欢迎提issue, star, fork等 7 | 8 | ## 主要功能 9 | * 登录(支持github oauth登录) 10 | * 注册 11 | * 发送邮件(通过邮箱重置密码) 12 | * 图片上传 13 | * 创建、编辑、删除话题 14 | * 创建、编辑、删除评论(支持@某用户) 15 | * 收藏话题 16 | * 点赞、点踩话题及评论 17 | * 置顶、加精 18 | * 消息提醒 19 | * 用户中心 20 | * 全站话题搜索 21 | * RSS 22 | 23 | ## 依赖 24 | * mysql 25 | * redis 26 | * open-ssl [[教程]](https://github.com/sfackler/rust-openssl) [[windows open-ssl下载地址]](http://slproweb.com/products/Win32OpenSSL.html) 27 | * sass **该项不是必须的**,目前网站样式表源码采用scss编写,如果你不打算使用scss编写样式表源码,可忽略该项 28 | 29 | ## 启动 30 | ``` 31 | 1. clone项目到本地 32 | 2. 启动mysql和redis 33 | 3. 将项目根目录下的tables.sql导入到mysql对应的数据库中(默认runner) 34 | 4. 配置项目根目录下的config.toml,主要是更改mysql节点下的用户名、密码、数据库名(默认runner),其他项可暂时不改,不影响启动 35 | 5. cargo run 36 | 6. 访问http://localhost:3000 37 | ``` 38 | 39 | ## 截图 40 | 41 | **PC** 42 | 43 | ![pc_1 主页](screenshots/pc_1.png) 44 | 45 | ![pc_2 话题页](screenshots/pc_2.png) 46 | 47 | **mobile** 48 | 49 | ![mobile_1 主页](screenshots/mobile_1.png) 50 | 51 | ![mobile_2 话题页](screenshots/mobile_2.png) 52 | 53 | 54 | ## License 55 | MIT -------------------------------------------------------------------------------- /static/scripts/lib/high-light/styles/monokai-sublime.css: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Monokai Sublime style. Derived from Monokai by noformnocontent http://nn.mit-license.org/ 4 | 5 | */ 6 | 7 | .hljs { 8 | display: block; 9 | overflow-x: auto; 10 | padding: 0.5em; 11 | background: #23241f; 12 | } 13 | 14 | .hljs, 15 | .hljs-tag, 16 | .hljs-subst { 17 | color: #f8f8f2; 18 | } 19 | 20 | .hljs-strong, 21 | .hljs-emphasis { 22 | color: #a8a8a2; 23 | } 24 | 25 | .hljs-bullet, 26 | .hljs-quote, 27 | .hljs-number, 28 | .hljs-regexp, 29 | .hljs-literal, 30 | .hljs-link { 31 | color: #ae81ff; 32 | } 33 | 34 | .hljs-code, 35 | .hljs-title, 36 | .hljs-section, 37 | .hljs-selector-class { 38 | color: #a6e22e; 39 | } 40 | 41 | .hljs-strong { 42 | font-weight: bold; 43 | } 44 | 45 | .hljs-emphasis { 46 | font-style: italic; 47 | } 48 | 49 | .hljs-keyword, 50 | .hljs-selector-tag, 51 | .hljs-name, 52 | .hljs-attr { 53 | color: #f92672; 54 | } 55 | 56 | .hljs-symbol, 57 | .hljs-attribute { 58 | color: #66d9ef; 59 | } 60 | 61 | .hljs-params, 62 | .hljs-class .hljs-title { 63 | color: #f8f8f2; 64 | } 65 | 66 | .hljs-string, 67 | .hljs-type, 68 | .hljs-built_in, 69 | .hljs-builtin-name, 70 | .hljs-selector-id, 71 | .hljs-selector-attr, 72 | .hljs-selector-pseudo, 73 | .hljs-addition, 74 | .hljs-variable, 75 | .hljs-template-variable { 76 | color: #e6db74; 77 | } 78 | 79 | .hljs-comment, 80 | .hljs-deletion, 81 | .hljs-meta { 82 | color: #75715e; 83 | } 84 | -------------------------------------------------------------------------------- /views/comment-editor.hbs: -------------------------------------------------------------------------------- 1 | {{var "title" "编辑回复"}} 2 | {{var "styles" 3 | [ 4 | "/styles/font-awesome.min.css", 5 | "/scripts/lib/editor/editor.css", 6 | "/styles/frame.css", 7 | "/styles/override.css", 8 | "/styles/mobile.css" 9 | ] 10 | }} 11 | {{var "scripts" 12 | [ 13 | "/scripts/lib/jquery-3.2.1.min.js", 14 | "/scripts/lib/editor/editor.js", 15 | "/scripts/lib/markdown-it.min.js", 16 | "/scripts/app/uploader.js", 17 | "/scripts/app/mount-editor.js", 18 | "/scripts/app/frame.js", 19 | "/scripts/app/comment-editor.js" 20 | ] 21 | }} 22 | 23 | {{~> common/header ~}} 24 | {{~> common/nav ~}} 25 | 26 |
27 |
28 |
29 | {{~> common/crumbs ~}} 30 |
31 |
32 |
33 |
34 |
35 | 36 |
37 |
38 |
39 | 提交 40 |
41 | 42 |
43 |
44 |
45 |
46 | 47 | {{~> common/aside ~}} 48 | {{~> common/footer ~}} -------------------------------------------------------------------------------- /static/scripts/lib/high-light/styles/github.css: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | github.com style (c) Vasily Polovnyov 4 | 5 | */ 6 | 7 | .hljs { 8 | display: block; 9 | overflow-x: auto; 10 | padding: 0.5em; 11 | color: #333; 12 | background: #f8f8f8; 13 | } 14 | 15 | .hljs-comment, 16 | .hljs-quote { 17 | color: #998; 18 | font-style: italic; 19 | } 20 | 21 | .hljs-keyword, 22 | .hljs-selector-tag, 23 | .hljs-subst { 24 | color: #333; 25 | font-weight: bold; 26 | } 27 | 28 | .hljs-number, 29 | .hljs-literal, 30 | .hljs-variable, 31 | .hljs-template-variable, 32 | .hljs-tag .hljs-attr { 33 | color: #008080; 34 | } 35 | 36 | .hljs-string, 37 | .hljs-doctag { 38 | color: #d14; 39 | } 40 | 41 | .hljs-title, 42 | .hljs-section, 43 | .hljs-selector-id { 44 | color: #900; 45 | font-weight: bold; 46 | } 47 | 48 | .hljs-subst { 49 | font-weight: normal; 50 | } 51 | 52 | .hljs-type, 53 | .hljs-class .hljs-title { 54 | color: #458; 55 | font-weight: bold; 56 | } 57 | 58 | .hljs-tag, 59 | .hljs-name, 60 | .hljs-attribute { 61 | color: #000080; 62 | font-weight: normal; 63 | } 64 | 65 | .hljs-regexp, 66 | .hljs-link { 67 | color: #009926; 68 | } 69 | 70 | .hljs-symbol, 71 | .hljs-bullet { 72 | color: #990073; 73 | } 74 | 75 | .hljs-built_in, 76 | .hljs-builtin-name { 77 | color: #0086b3; 78 | } 79 | 80 | .hljs-meta { 81 | color: #999; 82 | font-weight: bold; 83 | } 84 | 85 | .hljs-deletion { 86 | background: #fdd; 87 | } 88 | 89 | .hljs-addition { 90 | background: #dfd; 91 | } 92 | 93 | .hljs-emphasis { 94 | font-style: italic; 95 | } 96 | 97 | .hljs-strong { 98 | font-weight: bold; 99 | } 100 | -------------------------------------------------------------------------------- /views/resource.hbs: -------------------------------------------------------------------------------- 1 | {{var "title" "资源"}} 2 | {{var "styles" 3 | [ 4 | "/styles/font-awesome.min.css", 5 | "/styles/frame.css", 6 | "/styles/override.css", 7 | "/styles/mobile.css" 8 | ] 9 | }} 10 | {{var "scripts" 11 | [ 12 | "/scripts/lib/jquery-3.2.1.min.js", 13 | "/scripts/app/frame.js" 14 | ] 15 | }} 16 | 17 | {{~> common/header ~}} 18 | {{~> common/nav ~}} 19 | 20 |
21 |
22 |
23 | {{~> common/crumbs ~}} 24 |
25 | 37 |
38 |
39 | 40 | {{~> common/aside ~}} 41 | {{~> common/footer ~}} -------------------------------------------------------------------------------- /src/controllers/message.rs: -------------------------------------------------------------------------------- 1 | use iron::prelude::*; 2 | 3 | use common::http::*; 4 | use common::utils::*; 5 | use services::message::*; 6 | use services::user::get_user_id; 7 | 8 | pub fn render_unread_message(req: &mut Request) -> IronResult { 9 | 10 | let session = get_session_obj(req); 11 | let username = session["username"].as_str().unwrap(); 12 | let user_id = get_user_id(username); 13 | let page: u32 = get_query_page(req); 14 | let base_url = "/".to_string() + username + "/message/unread?page="; 15 | 16 | let list = get_user_message_list(user_id, page); 17 | let list_count = get_user_message_list_count(user_id); 18 | 19 | let pagination = build_pagination(page, list_count, &*base_url); 20 | 21 | let mut data = ViewData::new(req); 22 | 23 | data.insert("has_message_list", json!(list.len())); 24 | data.insert("message_list", json!(list)); 25 | data.insert("pagination", json!(pagination)); 26 | 27 | respond_view("message", &data) 28 | } 29 | 30 | pub fn read_message(req: &mut Request) -> IronResult { 31 | 32 | let params = get_router_params(req); 33 | let message_id = params.find("message_id").unwrap(); 34 | let query = get_request_query(req); 35 | let topic_id = &*query.get("topic-id").unwrap()[0]; 36 | let comment_id = &*query.get("comment-id").unwrap()[0]; 37 | 38 | delete_message(message_id); 39 | 40 | let url = "/topic/".to_string() + topic_id + "#" + comment_id; 41 | redirect_to(&*url) 42 | } 43 | 44 | pub fn read_all_message(req: &mut Request) -> IronResult { 45 | 46 | let session = get_session_obj(req); 47 | let username = session["username"].as_str().unwrap(); 48 | let user_id = get_user_id(username); 49 | 50 | delete_all_message_by_user_id(user_id); 51 | 52 | redirect_to(&*format!("/{}/message/unread", username)) 53 | } 54 | -------------------------------------------------------------------------------- /views/reset-password.hbs: -------------------------------------------------------------------------------- 1 | {{var "title" "重置密码"}} 2 | {{var "styles" 3 | [ 4 | "/styles/font-awesome.min.css", 5 | "/styles/frame.css", 6 | "/styles/override.css", 7 | "/styles/mobile.css" 8 | ] 9 | }} 10 | {{var "scripts" 11 | [ 12 | "/scripts/lib/jquery-3.2.1.min.js", 13 | "/scripts/app/frame.js", 14 | "/scripts/app/validator.js", 15 | "/scripts/app/reset-password.js" 16 | ] 17 | }} 18 | 19 | {{~> common/header ~}} 20 | {{~> common/nav ~}} 21 | 22 |
23 |
24 |
25 | {{~> common/crumbs ~}} 26 |
27 |
28 |
29 |
30 | 31 |
32 |
33 |
34 |
35 | {{message}} 36 |
37 |
38 |
39 |
40 | 发送验证邮件 41 |
42 |
43 |
44 |
45 |
46 |
47 | 48 | {{~> common/aside ~}} 49 | {{~> common/footer ~}} -------------------------------------------------------------------------------- /static/scripts/app/login.js: -------------------------------------------------------------------------------- 1 | $(function () { 2 | 3 | var login = { 4 | 5 | init: function () { 6 | var self = this; 7 | 8 | self.initElements(); 9 | self.initPlugins(); 10 | self.initEvents(); 11 | }, 12 | 13 | initElements: function () { 14 | var self = this; 15 | 16 | self.$btnLogin = $('#btn-login'); 17 | }, 18 | 19 | initPlugins: function () { 20 | var self = this; 21 | 22 | self.validator = Validator ? new Validator({ 23 | form: '#form-login', 24 | submit: self.login.bind(this) 25 | }) : null; 26 | }, 27 | 28 | initEvents: function () { 29 | var self = this; 30 | 31 | self.$btnLogin.on('click', function () { 32 | 33 | self.login(); 34 | }); 35 | }, 36 | 37 | login: function () { 38 | var self = this; 39 | 40 | if (!self.validator.isValid()) return; 41 | if (self.$btnLogin.is('.disabled')) return; 42 | 43 | self.$btnLogin.addClass('disabled'); 44 | 45 | var params = self.validator.getValues(); 46 | 47 | $.ajax({ 48 | url: globalConfig.path + '/login', 49 | type: 'POST', 50 | data: params, 51 | success: function (res) { 52 | 53 | if (res.success) { 54 | 55 | window.location.href = res.data; 56 | } else { 57 | 58 | self.validator.showError(null, res.message); 59 | } 60 | }, 61 | complete: function () { 62 | self.$btnLogin.removeClass('disabled'); 63 | } 64 | }); 65 | } 66 | }; 67 | 68 | login.init(); 69 | }); -------------------------------------------------------------------------------- /src/services/collection.rs: -------------------------------------------------------------------------------- 1 | use mysql::from_row; 2 | use mysql::error::Error::MySqlError; 3 | 4 | use common::utils::*; 5 | use common::lazy_static::SQL_POOL; 6 | 7 | pub fn create_collection(user_id: &str, topic_id: &str) -> Option { 8 | 9 | let create_time = gen_datetime().to_string(); 10 | 11 | let mut stmt = SQL_POOL.prepare(r#" 12 | INSERT INTO collection 13 | (user_id, topic_id, create_time) 14 | VALUES 15 | (?, ?, ?) 16 | "#).unwrap(); 17 | let result = stmt.execute(( 18 | user_id, 19 | topic_id, 20 | &*create_time, 21 | )); 22 | 23 | if let Err(MySqlError(ref err)) = result { 24 | println!("{:?}", err.message); 25 | return None; 26 | } 27 | 28 | Some(1) 29 | } 30 | 31 | pub fn delete_collection(user_id: &str, topic_id: &str) -> Option { 32 | 33 | let mut stmt = SQL_POOL.prepare(r#" 34 | DELETE FROM collection 35 | WHERE 36 | user_id = ? AND topic_id = ? 37 | "#).unwrap(); 38 | 39 | let result = stmt.execute((user_id, topic_id)); 40 | 41 | if let Err(MySqlError(ref err)) = result { 42 | println!("{:?}", err.message); 43 | return None; 44 | } 45 | 46 | Some(1) 47 | } 48 | 49 | pub fn is_collected(user_id: &str, topic_id: &str) -> bool { 50 | 51 | let mut result = SQL_POOL.prep_exec("SELECT count(id) FROM collection WHERE user_id = ? AND topic_id = ?", (user_id, topic_id)).unwrap(); 52 | let row_wrapper = result.next(); 53 | 54 | if row_wrapper.is_none() { 55 | return false; 56 | } 57 | 58 | let row = row_wrapper.unwrap().unwrap(); 59 | let (count, ) = from_row::<(u8, )>(row); 60 | 61 | if count == 0 { 62 | false 63 | } else { 64 | true 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /views/bind-user.hbs: -------------------------------------------------------------------------------- 1 | {{var "title" "绑定用户"}} 2 | {{var "styles" 3 | [ 4 | "/styles/font-awesome.min.css", 5 | "/styles/frame.css", 6 | "/styles/override.css", 7 | "/styles/mobile.css" 8 | ] 9 | }} 10 | {{var "scripts" 11 | [ 12 | "/scripts/lib/jquery-3.2.1.min.js", 13 | "/scripts/app/frame.js", 14 | "/scripts/app/validator.js", 15 | "/scripts/app/bind-user.js" 16 | ] 17 | }} 18 | 19 | {{~> common/header ~}} 20 | {{~> common/nav ~}} 21 | 22 |
23 |
24 |
25 | {{~> common/crumbs ~}} 26 |
27 |
28 |
29 |
30 | 31 |
32 |
33 |
34 |
35 | {{message}} 36 |
37 |
38 |
39 |
40 | 绑定 41 |
42 |
43 | 44 |
45 |
46 |
47 |
48 | 49 | {{~> common/aside ~}} 50 | {{~> common/footer ~}} -------------------------------------------------------------------------------- /static/scripts/app/register.js: -------------------------------------------------------------------------------- 1 | $(function () { 2 | 3 | var register = { 4 | 5 | init: function () { 6 | var self = this; 7 | 8 | self.initElements(); 9 | self.initPlugins(); 10 | self.initEvents(); 11 | }, 12 | 13 | initElements: function () { 14 | var self = this; 15 | 16 | self.$btnRegister = $('#btn-register'); 17 | }, 18 | 19 | initPlugins: function () { 20 | var self = this; 21 | 22 | self.validator = Validator ? new Validator({ 23 | form: '#form-register', 24 | submit: self.register.bind(this) 25 | }) : null; 26 | }, 27 | 28 | initEvents: function () { 29 | var self = this; 30 | 31 | self.$btnRegister.on('click', function () { 32 | 33 | self.register(); 34 | }); 35 | }, 36 | 37 | register: function () { 38 | var self = this; 39 | 40 | if (!self.validator.isValid()) return; 41 | if (self.$btnRegister.is('.disabled')) return; 42 | 43 | self.$btnRegister.addClass('disabled'); 44 | 45 | var params = self.validator.getValues(); 46 | 47 | $.ajax({ 48 | url: globalConfig.path + '/register', 49 | type: 'POST', 50 | data: params, 51 | success: function (res) { 52 | 53 | if (res.success) { 54 | 55 | window.location.href = res.data; 56 | } else { 57 | 58 | self.validator.showError(null, res.message); 59 | } 60 | }, 61 | complete: function () { 62 | self.$btnRegister.removeClass('disabled'); 63 | } 64 | }); 65 | } 66 | }; 67 | 68 | register.init(); 69 | }); -------------------------------------------------------------------------------- /static/scripts/app/reset-password.js: -------------------------------------------------------------------------------- 1 | $(function () { 2 | 3 | var reset = { 4 | 5 | init: function () { 6 | var self = this; 7 | 8 | self.initElements(); 9 | self.initPlugins(); 10 | self.initEvents(); 11 | }, 12 | 13 | initElements: function () { 14 | var self = this; 15 | 16 | self.$btnResetPassword = $('#btn-reset-password'); 17 | }, 18 | 19 | initPlugins: function () { 20 | var self = this; 21 | 22 | self.validator = Validator ? new Validator({ 23 | form: '#form-reset-password', 24 | submit: self.reset.bind(this) 25 | }) : null; 26 | }, 27 | 28 | initEvents: function () { 29 | var self = this; 30 | 31 | self.$btnResetPassword.on('click', function () { 32 | 33 | self.reset(); 34 | }); 35 | }, 36 | 37 | reset: function () { 38 | var self = this; 39 | 40 | if (!self.validator.isValid()) return; 41 | if (self.$btnResetPassword.is('.disabled')) return; 42 | 43 | self.$btnResetPassword.addClass('disabled'); 44 | 45 | var params = self.validator.getValues(); 46 | 47 | $.ajax({ 48 | url: globalConfig.path + '/reset-password', 49 | type: 'POST', 50 | data: params, 51 | success: function (res) { 52 | 53 | if (res.success) { 54 | 55 | alert(res.message); 56 | window.location.href = res.data; 57 | } else { 58 | 59 | self.validator.showError(null, res.message); 60 | } 61 | }, 62 | complete: function () { 63 | self.$btnResetPassword.removeClass('disabled'); 64 | } 65 | }); 66 | } 67 | }; 68 | 69 | reset.init(); 70 | }); -------------------------------------------------------------------------------- /static/scripts/app/bind-user.js: -------------------------------------------------------------------------------- 1 | $(function () { 2 | 3 | var bind = { 4 | 5 | init: function () { 6 | var self = this; 7 | 8 | self.initElements(); 9 | self.initPlugins(); 10 | self.initEvents(); 11 | }, 12 | 13 | initElements: function () { 14 | var self = this; 15 | 16 | self.$btnBind = $('#btn-bind'); 17 | self.$inputUserInfo = $('#user-info'); 18 | }, 19 | 20 | initPlugins: function () { 21 | var self = this; 22 | 23 | self.validator = Validator ? new Validator({ 24 | form: '#form-bind', 25 | submit: self.bind.bind(this) 26 | }) : null; 27 | }, 28 | 29 | initEvents: function () { 30 | var self = this; 31 | 32 | self.$btnBind.on('click', function () { 33 | 34 | self.bind(); 35 | }); 36 | }, 37 | 38 | bind: function () { 39 | var self = this; 40 | 41 | if (!self.validator.isValid()) return; 42 | if (self.$btnBind.is('.disabled')) return; 43 | 44 | self.$btnBind.addClass('disabled'); 45 | 46 | var params = self.validator.getValues(); 47 | 48 | params.userInfo = $.trim(self.$inputUserInfo.val()); 49 | 50 | $.ajax({ 51 | url: globalConfig.path + '/bind-user', 52 | type: 'POST', 53 | data: params, 54 | success: function (res) { 55 | 56 | if (res.success) { 57 | 58 | window.location.href = res.data; 59 | } else { 60 | 61 | self.validator.showError(null, res.message); 62 | } 63 | }, 64 | complete: function () { 65 | self.$btnBind.removeClass('disabled'); 66 | } 67 | }); 68 | } 69 | }; 70 | 71 | bind.init(); 72 | }); -------------------------------------------------------------------------------- /static/scripts/app/mount-editor.js: -------------------------------------------------------------------------------- 1 | (function (Editor, markdownit) { 2 | 3 | // Set default options 4 | var md = new markdownit(); 5 | var toolbar = Editor.toolbar; 6 | 7 | md.set({ 8 | html: false, // Enable HTML tags in source 9 | xhtmlOut: false, // Use '/' to close single tags (
) 10 | breaks: true, // Convert '\n' in paragraphs into
11 | langPrefix: 'lang-', // CSS language prefix for fenced blocks 12 | linkify: false, // Autoconvert URL-like text to links 13 | typographer: false // Enable smartypants and other sweet transforms 14 | }); 15 | 16 | window.markdowniter = md; 17 | 18 | // 追加内容 19 | Editor.prototype.push = function (txt) { 20 | var cm = this.codemirror; 21 | var line = cm.lastLine(); 22 | cm.setLine(line, cm.getLine(line) + txt); 23 | }; 24 | 25 | var replaceTool = function (name, callback) { 26 | for (var i = 0, len = toolbar.length; i < len; i++) { 27 | var v = toolbar[i]; 28 | if (typeof(v) !== 'string' && v.name === name) { 29 | v.action = callback; 30 | break; 31 | } 32 | } 33 | }; 34 | 35 | replaceTool('image', function (editor) { 36 | 37 | if (Uploader) { 38 | var uploader = new Uploader({ 39 | success: function (res) { 40 | var result = res[0]; 41 | var str = ''; 42 | 43 | for (var i = 0; i < result.data.length; i++) { 44 | var item = result.data[i]; 45 | 46 | str += '![' + item.filename + '](' + globalConfig.uploadPath + '/' + item.path + ') '; 47 | } 48 | editor.push(str); 49 | }, 50 | error: function () { 51 | 52 | console.log('upload error'); 53 | } 54 | }); 55 | 56 | uploader.show(); 57 | } 58 | }); 59 | 60 | })(window.Editor, window.markdownit); 61 | -------------------------------------------------------------------------------- /views/message.hbs: -------------------------------------------------------------------------------- 1 | {{var "title" "未读消息"}} 2 | {{var "styles" 3 | [ 4 | "/styles/font-awesome.min.css", 5 | "/styles/frame.css", 6 | "/styles/override.css", 7 | "/styles/mobile.css" 8 | ] 9 | }} 10 | {{var "scripts" 11 | [ 12 | "/scripts/lib/jquery-3.2.1.min.js", 13 | "/scripts/app/frame.js" 14 | ] 15 | }} 16 | 17 | {{~> common/header ~}} 18 | {{~> common/nav ~}} 19 | 20 |
21 |
22 |
23 | {{#if has_message_list}} 24 | 全部设为已读 25 | {{/if}} 26 | {{~> common/crumbs ~}} 27 |
28 |
29 | {{#if has_message_list}} 30 |
31 | {{#each message_list}} 32 |
33 | {{#if type}} 34 | {{username}} 在话题 {{title}} 中@了你 35 | {{else}} 36 | {{username}} 回复了你的话题 {{title}} 37 | {{/if}} 38 |
39 | {{/each}} 40 |
41 | {{else}} 42 |
43 | 暂无数据 44 |
45 | {{/if}} 46 | 47 |
48 | {{#if has_message_list}} 49 | 52 | {{/if}} 53 |
54 |
55 | 56 | {{~> common/aside ~}} 57 | {{~> common/footer ~}} -------------------------------------------------------------------------------- /static/scripts/app/new-password.js: -------------------------------------------------------------------------------- 1 | $(function () { 2 | 3 | var reset = { 4 | 5 | init: function () { 6 | var self = this; 7 | 8 | self.initElements(); 9 | self.initPlugins(); 10 | self.initEvents(); 11 | }, 12 | 13 | initElements: function () { 14 | var self = this; 15 | 16 | self.$btnResetPassword = $('#btn-reset-password'); 17 | self.$inputUsername = $('#username'); 18 | }, 19 | 20 | initPlugins: function () { 21 | var self = this; 22 | 23 | self.validator = Validator ? new Validator({ 24 | form: '#form-reset-password', 25 | submit: self.reset.bind(this) 26 | }) : null; 27 | }, 28 | 29 | initEvents: function () { 30 | var self = this; 31 | 32 | self.$btnResetPassword.on('click', function () { 33 | 34 | self.reset(); 35 | }); 36 | }, 37 | 38 | reset: function () { 39 | var self = this; 40 | 41 | if (!self.validator.isValid()) return; 42 | if (self.$btnResetPassword.is('.disabled')) return; 43 | 44 | self.$btnResetPassword.addClass('disabled'); 45 | 46 | var params = self.validator.getValues(); 47 | 48 | params.username = $.trim(self.$inputUsername.val()); 49 | 50 | $.ajax({ 51 | url: globalConfig.path + '/set-new-password', 52 | type: 'POST', 53 | data: params, 54 | success: function (res) { 55 | 56 | if (res.success) { 57 | 58 | alert(res.message); 59 | window.location.href = res.data; 60 | } else { 61 | 62 | self.validator.showError(null, res.message); 63 | } 64 | }, 65 | complete: function () { 66 | self.$btnResetPassword.removeClass('disabled'); 67 | } 68 | }); 69 | } 70 | }; 71 | 72 | reset.init(); 73 | }); -------------------------------------------------------------------------------- /views/topic-editor.hbs: -------------------------------------------------------------------------------- 1 | {{var "styles" 2 | [ 3 | "/styles/font-awesome.min.css", 4 | "/scripts/lib/editor/editor.css", 5 | "/styles/frame.css", 6 | "/styles/override.css", 7 | "/styles/mobile.css" 8 | ] 9 | }} 10 | {{var "scripts" 11 | [ 12 | "/scripts/lib/jquery-3.2.1.min.js", 13 | "/scripts/lib/editor/editor.js", 14 | "/scripts/lib/markdown-it.min.js", 15 | "/scripts/app/uploader.js", 16 | "/scripts/app/mount-editor.js", 17 | "/scripts/app/frame.js", 18 | "/scripts/app/topic-editor.js" 19 | ] 20 | }} 21 | 22 | {{~> common/header ~}} 23 | {{~> common/nav ~}} 24 | 25 |
26 |
27 |
28 | {{~> common/crumbs ~}} 29 |
30 |
31 |
32 |
33 | 34 | 40 |
41 |
42 |
43 |
44 |
45 |
46 | 47 |
48 |
49 |
50 | 提交 51 |
52 | 53 |
54 |
55 |
56 |
57 | 58 | {{~> common/aside ~}} 59 | {{~> common/footer ~}} -------------------------------------------------------------------------------- /src/common/middlewares.rs: -------------------------------------------------------------------------------- 1 | use iron::prelude::*; 2 | use iron::{BeforeMiddleware, AfterMiddleware, AroundMiddleware, Handler}; 3 | 4 | use common::http::*; 5 | use common::utils::{get_session_obj, is_login, is_admin}; 6 | 7 | pub struct FlowControl; 8 | 9 | impl BeforeMiddleware for FlowControl { 10 | 11 | fn before(&self, _req: &mut Request) -> IronResult<()> { 12 | 13 | Ok(()) 14 | } 15 | } 16 | 17 | impl AfterMiddleware for FlowControl { 18 | 19 | fn after(&self, _req: &mut Request, res: Response) -> IronResult { 20 | 21 | Ok(res) 22 | } 23 | } 24 | 25 | impl AroundMiddleware for FlowControl { 26 | 27 | fn around(self, handler: Box) -> Box { 28 | 29 | Box::new(move |req: &mut Request| -> IronResult { 30 | 31 | handler.handle(req) 32 | }) 33 | } 34 | } 35 | 36 | pub fn authorize(handler: F, check_login: bool, check_admin: bool) -> Box 37 | where F: Send + Sync + 'static + Fn(&mut Request) -> IronResult { 38 | 39 | Box::new(move |req: &mut Request| -> IronResult { 40 | 41 | if check_login { 42 | 43 | if !is_login(req) { // 未登录 44 | 45 | if req.headers.get_raw("X-Requested-With").is_some() { // ajax 46 | 47 | let mut data = JsonData::new(); 48 | 49 | data.success = false; 50 | data.message = "当前用户尚未登录".to_string(); 51 | 52 | return respond_unauthorized_json(&data); 53 | } else { 54 | 55 | return redirect_to("/login"); 56 | } 57 | } 58 | } 59 | 60 | if check_admin { 61 | let session = get_session_obj(req); 62 | let username = session["username"].as_str().unwrap(); 63 | 64 | if !is_admin(username) { // 非管理员 65 | 66 | if req.headers.get_raw("X-Requested-With").is_some() { // ajax 67 | 68 | let mut data = JsonData::new(); 69 | 70 | data.success = false; 71 | data.message = "禁止访问".to_string(); 72 | 73 | return respond_forbidden_json(&data); 74 | } else { 75 | 76 | return redirect_to("/forbidden"); 77 | } 78 | } 79 | } 80 | 81 | handler(req) 82 | }) 83 | } -------------------------------------------------------------------------------- /src/common/lazy_static.rs: -------------------------------------------------------------------------------- 1 | use hyper::Client; 2 | use hyper::net::HttpsConnector; 3 | use hyper_native_tls::NativeTlsClient; 4 | use mysql::Pool; 5 | use toml::value::{Table, Array}; 6 | 7 | use common::config::Config; 8 | use common::db::MySqlPool; 9 | 10 | pub static RECORDS_COUNT_PER_PAGE: u32 = 15; 11 | 12 | lazy_static! { 13 | pub static ref HTTPS_CLIENT: Client = { 14 | 15 | let ssl = NativeTlsClient::new().unwrap(); 16 | let connector = HttpsConnector::new(ssl); 17 | 18 | Client::with_connector(connector) 19 | }; 20 | } 21 | 22 | lazy_static! { 23 | 24 | pub static ref CONFIG: Config = { 25 | 26 | Config::new("config.toml") 27 | }; 28 | 29 | pub static ref CONFIG_TABLE: Table = { 30 | 31 | CONFIG.value() 32 | }; 33 | 34 | pub static ref PATH: &'static str = { 35 | CONFIG_TABLE.get("path").unwrap().as_str().unwrap() 36 | }; 37 | 38 | pub static ref STATIC_PATH: &'static str = { 39 | CONFIG_TABLE.get("static_path").unwrap().as_str().unwrap() 40 | }; 41 | 42 | pub static ref UPLOAD_PATH: &'static str = { 43 | CONFIG_TABLE.get("upload_path").unwrap().as_str().unwrap() 44 | }; 45 | 46 | pub static ref GITHUB_LOGIN_PATH: String = { 47 | 48 | let github_config = CONFIG_TABLE.get("github").unwrap().as_table().unwrap(); 49 | let client_id = github_config.get("client_id").unwrap().as_str().unwrap(); 50 | 51 | "https://github.com/login/oauth/authorize?client_id=".to_string() + client_id 52 | }; 53 | 54 | pub static ref ADMINS: &'static Array = { 55 | 56 | CONFIG_TABLE.get("admins").unwrap().as_array().unwrap() 57 | }; 58 | 59 | pub static ref SESSION_KEY: &'static str = { 60 | 61 | let redis_config = CONFIG_TABLE.get("redis").unwrap().as_table().unwrap(); 62 | redis_config.get("session_key").unwrap().as_str().unwrap() 63 | }; 64 | 65 | pub static ref UPLOAD_TEMP_PATH: &'static str = { 66 | 67 | let upload_config = CONFIG_TABLE.get("upload").unwrap().as_table().unwrap(); 68 | upload_config.get("temp_path").unwrap().as_str().unwrap() 69 | }; 70 | 71 | pub static ref UPLOAD_ASSETS_PATH: &'static str = { 72 | 73 | let upload_config = CONFIG_TABLE.get("upload").unwrap().as_table().unwrap(); 74 | upload_config.get("assets_path").unwrap().as_str().unwrap() 75 | }; 76 | } 77 | 78 | lazy_static! { 79 | 80 | pub static ref SQL_POOL: Pool = { 81 | 82 | MySqlPool::new(&CONFIG).value() 83 | }; 84 | } -------------------------------------------------------------------------------- /src/common/db.rs: -------------------------------------------------------------------------------- 1 | use mysql; 2 | 3 | use iron::typemap::Key; 4 | 5 | use common::config::Config; 6 | 7 | pub struct MySqlPool(mysql::Pool); 8 | 9 | impl MySqlPool { 10 | 11 | pub fn new(config: &Config) -> MySqlPool { 12 | 13 | let table = config.value(); 14 | let mysql_config = table.get("mysql").unwrap().as_table().unwrap(); 15 | let username = mysql_config.get("username").unwrap().as_str().unwrap(); 16 | let password = mysql_config.get("password").unwrap().as_str().unwrap(); 17 | let host = mysql_config.get("host").unwrap().as_str().unwrap(); 18 | let port = mysql_config.get("port").unwrap().as_integer().unwrap(); 19 | let db_name = mysql_config.get("db_name").unwrap().as_str().unwrap(); 20 | 21 | let mut builder = mysql::OptsBuilder::default(); 22 | 23 | builder.user(Some(username)) 24 | .pass(Some(password)) 25 | .ip_or_hostname(Some(host)) 26 | .tcp_port(port as u16) 27 | .db_name(Some(db_name)) 28 | .prefer_socket(false); // 默认为true,为true时win10报错 29 | 30 | let pool = mysql::Pool::new(builder).unwrap(); 31 | 32 | MySqlPool(pool) 33 | } 34 | 35 | pub fn value(&self) -> mysql::Pool { 36 | 37 | self.0.clone() 38 | } 39 | } 40 | 41 | impl Key for MySqlPool { 42 | 43 | type Value = MySqlPool; 44 | } 45 | 46 | pub struct RedisConfig { 47 | pub connect_string: String, 48 | pub expire: u64 49 | } 50 | 51 | pub fn get_redis_config(config: &Config) -> RedisConfig { 52 | 53 | let table = config.value(); 54 | let redis_config = table.get("redis").unwrap().as_table().unwrap(); 55 | let protocol = redis_config.get("protocol").unwrap().as_str().unwrap(); 56 | let host = redis_config.get("host").unwrap().as_str().unwrap(); 57 | let port = redis_config.get("port").unwrap().as_integer().unwrap().to_string(); 58 | let username = redis_config.get("username").unwrap().as_str().unwrap(); 59 | let password = redis_config.get("password").unwrap().as_str().unwrap(); 60 | let max_age = redis_config.get("max_age").unwrap().as_integer().unwrap(); 61 | let connect_string; 62 | 63 | if password == "" { 64 | 65 | connect_string = format!("{}://{}:{}", protocol, host, &*port) 66 | } else { 67 | 68 | connect_string = format!("{}://{}:{}@{}:{}", protocol, username, password, host, &*port) 69 | } 70 | 71 | RedisConfig { 72 | connect_string: connect_string, 73 | expire: max_age as u64 74 | } 75 | } -------------------------------------------------------------------------------- /views/login.hbs: -------------------------------------------------------------------------------- 1 | {{var "title" "登录"}} 2 | {{var "styles" 3 | [ 4 | "/styles/font-awesome.min.css", 5 | "/styles/frame.css", 6 | "/styles/override.css", 7 | "/styles/mobile.css" 8 | ] 9 | }} 10 | {{var "scripts" 11 | [ 12 | "/scripts/lib/jquery-3.2.1.min.js", 13 | "/scripts/app/frame.js", 14 | "/scripts/app/validator.js", 15 | "/scripts/app/login.js" 16 | ] 17 | }} 18 | 19 | {{~> common/header ~}} 20 | {{~> common/nav ~}} 21 | 22 |
23 |
24 |
25 | {{~> common/crumbs ~}} 26 |
27 |
28 |
29 |
30 | 31 |
32 |
33 |
34 | 35 |
36 |
37 |
38 |
39 | 忘记密码? 40 |
41 |
42 |
43 |
44 | 您输入的用户名或密码有误! 45 |
46 |
47 |
48 | 51 |
52 |
53 |
54 |
55 |
56 | 57 | {{~> common/aside ~}} 58 | {{~> common/footer ~}} -------------------------------------------------------------------------------- /static/styles/override.css.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "mappings": "AAEA,uMAAwM;EACvM,OAAO,EAAE,CAAC;EACV,MAAM,EAAE,CAAC;EACT,MAAM,EAAE,CAAC;ECOR,kBAAkB,EDNA,UAAU;ECO5B,eAAe,EDPG,UAAU;ECQ5B,UAAU,EDRQ,UAAU;;;AAG9B,UAAW;EACV,UAAU,EAAE,IAAI;;;AAGjB,oDAAqD;EACpD,OAAO,EAAE,KAAK;;;AC4Bb,iFAAiC;EAC/B,KAAK,EAAE,cAAiB;EACxB,SAAS,EAH+B,IAAI;;AAK9C,+DAAwB;EACtB,KAAK,EAAE,cAAiB;EACxB,SAAS,EAP+B,IAAI;;AAS9C,uEAA4B;EAC1B,KAAK,EAAE,cAAiB;EACxB,SAAS,EAX+B,IAAI;;;ADnBhD,kCAAmC;EAClC,OAAO,EAAE,IAAI;;;AAGd,iFAAkF;EACjF,kBAAkB,EAAE,IAAI;ECTvB,qBAAqB,EDUA,CAAC;ECTtB,kBAAkB,EDSG,CAAC;ECRtB,aAAa,EDQQ,CAAC;EACvB,MAAM,EAAC,CAAC;;;AAGT,kDAAkD;EACjD,2BAA2B,EAAE,sBAAmB;;;AAKhD,gBAAa;EACZ,OAAO,EAAE,IAAI;;AAGd,iBAAa;EACZ,OAAO,EAAC,IAAI;;AAGb,mCAA+B;EAC9B,OAAO,EAAE,IAAI;;;AAIf,CAAE;EACD,eAAe,EAAE,IAAI;;;AE9CtB,eAAgB;EACd,SAAS,EAAE,MAAM;;AAEjB,6CAAkB;EAEhB,UAAU,ECPH,IAAI;;ADUb,2BAAY;EACV,WAAW,EAAE,KAAK;;AAGpB,iCAAkB;EAChB,YAAY,EAAE,KAAK;;;AAIvB,eAAgB;EACd,OAAO,EAAE,aAAa;EACtB,UAAU,EAAC,IAAI;;;AAGjB,iBAAkB;EAChB,OAAO,EAAE,IAAI;;;AAKb,sCAAO;EACL,OAAO,EAAE,CAAC;EACV,MAAM,EAAE,aAAa;;AAGvB,kBAAG;EACD,UAAU,EAAE,IAAI;;AAGlB,kBAAG;EACD,UAAU,EAAE,OAAO;;AAGrB,kBAAG;EACD,MAAM,EAAE,MAAM;EACd,MAAM,EAAE,CAAC;EACT,UAAU,EAAE,iBAAiB;EAC7B,aAAa,EAAE,iBAAiB;;AAGlC;yCAC0B;EACxB,MAAM,EAAE,IAAI;EACZ,aAAa,EAAE,kBAAkB;;AAGnC,oBAAK;EACH,SAAS,EAAE,GAAG;EACd,cAAc,EAAE,SAAS;;AAG3B,0BAAW;EACT,OAAO,EAAE,UAAU;EACnB,MAAM,EAAE,QAAQ;EAChB,WAAW,EAAE,iBAAiB;;AAGhC,4BAAa;EACX,aAAa,EAAE,CAAC;EAChB,SAAS,EAAE,MAAM;EACjB,WAAW,EAAE,GAAG;EAChB,WAAW,EAAE,IAAI;;AAGnB,gCAAiB;EACf,OAAO,EAAE,KAAK;EACd,WAAW,EAAE,IAAI;EACjB,KAAK,EAAE,OAAO;;AAGhB,uCAAwB;EACtB,OAAO,EAAE,aAAa;;AAGxB;;;gCAGiB;EACf,OAAO,EAAE,EAAE;;AAGb,uBAAQ;EACN,OAAO,EAAE,KAAK;EACd,aAAa,EAAE,IAAI;EACnB,UAAU,EAAE,MAAM;EAClB,WAAW,EAAE,IAAI;;AAGnB;mBACI;EACF,OAAO,EAAE,SAAS;EAClB,WAAW,EAAE,iDAAiD;EAC9D,SAAS,EAAE,IAAI;EACf,KAAK,EAAE,OAAO;EACd,qBAAqB,EAAE,GAAG;EAC1B,kBAAkB,EAAE,GAAG;EACvB,aAAa,EAAE,GAAG;;AAGpB,oBAAK;EACH,OAAO,EAAE,OAAO;EAChB,KAAK,EAAE,IAAI;EACX,WAAW,EAAE,MAAM;EACnB,gBAAgB,EAAE,OAAO;EACzB,MAAM,EAAE,iBAAiB;;AAG3B,mBAAI;EACF,OAAO,EAAE,KAAK;EACd,OAAO,EAAE,KAAK;EACd,MAAM,EAAE,QAAQ;EAChB,SAAS,EAAE,IAAI;EACf,WAAW,EAAE,IAAI;EACjB,UAAU,EAAE,SAAS;EACrB,SAAS,EAAE,UAAU;EACrB,WAAW,EAAE,QAAQ;EACrB,gBAAgB,EAAE,OAAO;EACzB,MAAM,EAAE,6BAA6B;EACrC,qBAAqB,EAAE,GAAG;EAC1B,kBAAkB,EAAE,GAAG;EACvB,aAAa,EAAE,GAAG;;AAGpB,wBAAS;EACP,OAAO,EAAE,CAAC;EACV,KAAK,EAAE,OAAO;EACd,WAAW,EAAE,QAAQ;EACrB,gBAAgB,EAAE,WAAW;EAC7B,MAAM,EAAE,CAAC;;;AAKb,gBAAgB;EACd,UAAU,EAAE,kBAAuB;;;AAMnC,iBAAE;EACA,UAAU,EAAE,IAAI;;AAGlB,iBAAE;EACA,UAAU,EAAE,OAAO", 4 | "sources": ["scss/base/_reset.scss","scss/base/_mixin.scss","scss/override.scss","scss/base/_config.scss"], 5 | "names": [], 6 | "file": "override.css" 7 | } -------------------------------------------------------------------------------- /views/new-password.hbs: -------------------------------------------------------------------------------- 1 | {{var "title" "设置新密码"}} 2 | {{var "styles" 3 | [ 4 | "/styles/font-awesome.min.css", 5 | "/styles/frame.css", 6 | "/styles/override.css", 7 | "/styles/mobile.css" 8 | ] 9 | }} 10 | {{var "scripts" 11 | [ 12 | "/scripts/lib/jquery-3.2.1.min.js", 13 | "/scripts/app/frame.js", 14 | "/scripts/app/validator.js", 15 | "/scripts/app/new-password.js" 16 | ] 17 | }} 18 | 19 | {{~> common/header ~}} 20 | {{~> common/nav ~}} 21 | 22 |
23 |
24 |
25 | {{~> common/crumbs ~}} 26 |
27 |
28 | {{#if username}} 29 |
30 |
31 | 32 |
33 |
34 |
35 | 36 |
37 |
38 |
39 |
40 | {{message}} 41 |
42 |
43 |
44 |
45 | 设置 46 |
47 |
48 | 49 |
50 | {{else}} 51 |
{{retrieve_message}}
52 | {{/if}} 53 |
54 |
55 |
56 | 57 | {{~> common/aside ~}} 58 | {{~> common/footer ~}} -------------------------------------------------------------------------------- /src/controllers/register.rs: -------------------------------------------------------------------------------- 1 | use iron::prelude::*; 2 | use iron_sessionstorage::traits::SessionRequestExt; 3 | 4 | use common::http::*; 5 | use common::utils::*; 6 | use common::lazy_static::GITHUB_LOGIN_PATH; 7 | 8 | use services::user::*; 9 | 10 | pub fn render_register(req: &mut Request) -> IronResult { 11 | 12 | let mut data = ViewData::new(req); 13 | 14 | data.insert("github_login_path", json!(GITHUB_LOGIN_PATH.to_string())); 15 | 16 | respond_view("register", &data) 17 | } 18 | 19 | pub fn register(req: &mut Request) -> IronResult { 20 | 21 | let params = get_request_body(req); 22 | let username = ¶ms.get("username").unwrap()[0]; 23 | let email = ¶ms.get("email").unwrap()[0]; 24 | let password = ¶ms.get("password").unwrap()[0]; 25 | let avatar_url = gen_gravatar_url(email); 26 | let salt = gen_salt(); 27 | let password_with_salt = password.to_string() + &*salt; 28 | let password_hashed = gen_md5(&password_with_salt); 29 | let create_time = gen_datetime().to_string(); 30 | let obj = json!({ 31 | "username": username.to_owned(), 32 | "email": email.to_owned(), 33 | "avatar_url": avatar_url, 34 | "github_account": "".to_owned(), 35 | "password_hashed": password_hashed, 36 | "salt": salt, 37 | "register_source": 0, 38 | "create_time": create_time 39 | }); 40 | 41 | let result = create_user(&obj); 42 | 43 | let mut data = JsonData::new(); 44 | 45 | if result.is_none() { 46 | 47 | data.success = false; 48 | data.message = "该用户名或邮箱已被注册!".to_owned(); 49 | 50 | return respond_json(&data); 51 | } 52 | 53 | data.data = json!("/login"); 54 | 55 | respond_json(&data) 56 | } 57 | 58 | /// 绑定github用户 59 | pub fn bind_user(req: &mut Request) -> IronResult { 60 | 61 | let params = get_request_body(req); 62 | let username_str = ¶ms.get("username").unwrap()[0]; 63 | let user_info_str = ¶ms.get("userInfo").unwrap()[0]; 64 | 65 | let mut data = JsonData::new(); 66 | 67 | if is_user_created(username_str) { 68 | 69 | data.success = false; 70 | data.message = "该用户名已被注册!".to_owned(); 71 | 72 | return respond_json(&data); 73 | } 74 | 75 | let mut user_info_obj = json_parse(user_info_str); 76 | 77 | user_info_obj["login"] = json!(username_str); 78 | 79 | let username_wrapper = bind_github_user(&user_info_obj); 80 | 81 | let username = username_wrapper.unwrap(); 82 | 83 | let user = get_user(&*username).unwrap(); 84 | 85 | req.session().set(SessionData { 86 | user: json_stringify(&user) 87 | }).unwrap(); 88 | 89 | data.data = json!("/"); 90 | 91 | respond_json(&data) 92 | } -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | extern crate iron; 2 | extern crate router; 3 | extern crate mount; 4 | extern crate staticfile; 5 | extern crate handlebars_iron as hbs; 6 | extern crate iron_sessionstorage2 as iron_sessionstorage; 7 | extern crate serde; 8 | #[macro_use] 9 | extern crate serde_derive; 10 | #[macro_use] 11 | extern crate serde_json; 12 | extern crate urlencoded; 13 | extern crate chrono; 14 | extern crate crypto; 15 | extern crate rand; 16 | extern crate toml; 17 | extern crate hyper; 18 | extern crate hyper_native_tls; 19 | extern crate url; 20 | extern crate multipart; 21 | #[macro_use] 22 | extern crate lazy_static; 23 | #[macro_use] 24 | extern crate mime; 25 | extern crate mysql; 26 | extern crate pulldown_cmark; 27 | extern crate regex; 28 | extern crate rss; 29 | extern crate lettre; 30 | extern crate uuid; 31 | extern crate schedule; 32 | extern crate log4rs; 33 | 34 | mod common; 35 | mod routes; 36 | mod controllers; 37 | mod services; 38 | mod models; 39 | 40 | use std::path::Path; 41 | 42 | use iron::Chain; 43 | use mount::Mount; 44 | use staticfile::Static; 45 | use hbs::{HandlebarsEngine, DirectorySource}; 46 | use iron_sessionstorage::SessionStorage; 47 | use iron_sessionstorage::backends::RedisBackend; 48 | 49 | use common::lazy_static::CONFIG; 50 | use common::db::get_redis_config; 51 | use common::middlewares::FlowControl; 52 | use common::utils::mount_template_var; 53 | use controllers::upload::{create_upload_folder, run_clean_temp_task}; 54 | 55 | fn main() { 56 | 57 | log4rs::init_file("log4rs.yaml", Default::default()).unwrap(); 58 | 59 | let mut chain = Chain::new(routes::gen_router()); 60 | 61 | chain.link_before(FlowControl); 62 | 63 | let mut hbs_engine = HandlebarsEngine::new(); 64 | hbs_engine.add(Box::new(DirectorySource::new("views/", ".hbs"))); 65 | hbs_engine.handlebars_mut().register_helper("var", Box::new(mount_template_var)); 66 | hbs_engine.reload().unwrap(); 67 | chain.link_after(hbs_engine); 68 | 69 | let redis_config = get_redis_config(&CONFIG); 70 | chain.link_around(SessionStorage::new(RedisBackend::new(&*redis_config.connect_string, redis_config.expire).unwrap())); 71 | 72 | let mut mount = Mount::new(); 73 | mount.mount("/", chain); 74 | mount.mount("static/", Static::new(Path::new("static"))); 75 | mount.mount("upload/", Static::new(Path::new("upload"))); 76 | 77 | create_upload_folder(); // 创建上传文件夹 78 | run_clean_temp_task(); // 定时清理用户上传但并未保存的文件 79 | 80 | let host = CONFIG.get("host").as_str().unwrap(); 81 | let port: &str = &*CONFIG.get("port").as_integer().unwrap().to_string(); 82 | 83 | println!("http server is listening on port {}!", port); 84 | iron::Iron::new(mount) 85 | .http(host.to_string() + ":" + port) 86 | .unwrap(); 87 | } -------------------------------------------------------------------------------- /views/common/aside.hbs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/scripts/app/comment-editor.js: -------------------------------------------------------------------------------- 1 | $(function () { 2 | 3 | var commentEditor = { 4 | init: function () { 5 | var self = this; 6 | 7 | self.initPlugins(); 8 | self.initElements(); 9 | self.initEvents(); 10 | }, 11 | 12 | initPlugins: function () { 13 | var self = this; 14 | 15 | self.editor = new Editor(); 16 | 17 | self.editor.render($('.editor')[0]); 18 | 19 | var $input = $(self.editor.codemirror.display.input); 20 | 21 | $input.keydown(function(e){ 22 | if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) { 23 | e.preventDefault(); 24 | 25 | self.submit(); 26 | } 27 | }); 28 | }, 29 | 30 | initElements: function () { 31 | var self = this; 32 | 33 | self.$btnSubmit = $('#btn-submit'); 34 | self.$commentId = $('#comment-id') 35 | }, 36 | 37 | initEvents: function () { 38 | var self = this; 39 | 40 | self.$btnSubmit.on('click', function () { 41 | 42 | self.submit(); 43 | }); 44 | }, 45 | 46 | submit: function () { 47 | var self = this; 48 | 49 | if (self.checkValid()) { 50 | 51 | if (self.$btnSubmit.is('.disabled')) return; 52 | 53 | self.$btnSubmit.addClass('disabled'); 54 | 55 | var params = self.getValues(); 56 | 57 | $.ajax({ 58 | url: globalConfig.path + '/edit-comment/' + params.id, 59 | type: 'PUT', 60 | data: params, 61 | success: function (res) { 62 | 63 | if (res.success) { 64 | 65 | window.location.href = res.data; 66 | } else { 67 | 68 | alert(res.message); 69 | } 70 | }, 71 | complete: function () { 72 | self.$btnSubmit.removeClass('disabled'); 73 | } 74 | }); 75 | } 76 | }, 77 | 78 | checkValid: function () { 79 | var self = this; 80 | var isValid = true; 81 | 82 | if (!$.trim(self.editor.codemirror.getValue())) { 83 | 84 | alert("回复内容不能为空!"); 85 | 86 | isValid = false; 87 | return isValid; 88 | } 89 | 90 | return isValid; 91 | }, 92 | 93 | getValues: function () { 94 | var self = this; 95 | 96 | return { 97 | id: $.trim(self.$commentId.val()), 98 | content: $.trim(self.editor.codemirror.getValue()) 99 | }; 100 | } 101 | }; 102 | 103 | commentEditor.init(); 104 | }); -------------------------------------------------------------------------------- /views/common/nav.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 | 9 |
10 |
11 | 15 | 26 |
27 |
28 |
29 | 30 |
31 | 32 |
33 | {{#if user}} 34 | 未读消息{{#if has_unread_message}}{{/if}} 35 | 发布话题 36 | {{else}} 37 | 注册 38 | 登录 39 | {{/if}} 40 |
41 |
42 | 43 |
44 |
45 | 46 | 47 | 55 | 56 |
57 |
58 | -------------------------------------------------------------------------------- /views/register.hbs: -------------------------------------------------------------------------------- 1 | {{var "title" "注册"}} 2 | {{var "styles" 3 | [ 4 | "/styles/font-awesome.min.css", 5 | "/styles/frame.css", 6 | "/styles/override.css", 7 | "/styles/mobile.css" 8 | ] 9 | }} 10 | {{var "scripts" 11 | [ 12 | "/scripts/lib/jquery-3.2.1.min.js", 13 | "/scripts/app/frame.js", 14 | "/scripts/app/validator.js", 15 | "/scripts/app/register.js" 16 | ] 17 | }} 18 | 19 | {{~> common/header ~}} 20 | {{~> common/nav ~}} 21 | 22 |
23 |
24 |
25 | {{~> common/crumbs ~}} 26 |
27 |
28 |
29 |
30 | 31 |
32 |
33 |
34 | 35 |
36 |
37 |
38 | 39 |
40 |
41 |
42 | 43 |
44 |
45 |
46 |
47 | 48 |
49 |
50 |
51 | 54 |
55 |
56 |
57 |
58 |
59 | 60 | {{~> common/aside ~}} 61 | {{~> common/footer ~}} -------------------------------------------------------------------------------- /static/styles/scss/override.scss: -------------------------------------------------------------------------------- 1 | @import "./base/mixin"; 2 | @import "./base/extends"; 3 | @import "./base/reset"; 4 | @import "./base/config"; 5 | 6 | // editor 7 | .editor-toolbar { 8 | font-size: 1.4rem; 9 | 10 | &:before, &:after { 11 | 12 | background: $bd-color; 13 | } 14 | 15 | .eicon-bold { 16 | margin-left: .8rem; 17 | } 18 | 19 | .eicon-fullscreen { 20 | margin-right: .8rem; 21 | } 22 | } 23 | 24 | .editor-preview { 25 | z-index: 80 !important; 26 | background:#eee; 27 | } 28 | 29 | .editor-statusbar { 30 | display: none; 31 | } 32 | 33 | .editor-preview { 34 | 35 | ul, ol { 36 | padding: 0; 37 | margin: 0 0 10px 25px; 38 | } 39 | 40 | ul { 41 | list-style: disc; 42 | } 43 | 44 | ol { 45 | list-style: decimal; 46 | } 47 | 48 | hr { 49 | margin: 20px 0; 50 | border: 0; 51 | border-top: 1px solid #eeeeee; 52 | border-bottom: 1px solid #ffffff; 53 | } 54 | 55 | abbr[title], 56 | abbr[data-original-title] { 57 | cursor: help; 58 | border-bottom: 1px dotted #999999; 59 | } 60 | 61 | abbr { 62 | font-size: 90%; 63 | text-transform: uppercase; 64 | } 65 | 66 | blockquote { 67 | padding: 0 0 0 15px; 68 | margin: 0 0 20px; 69 | border-left: 5px solid #eeeeee; 70 | } 71 | 72 | blockquote p { 73 | margin-bottom: 0; 74 | font-size: 17.5px; 75 | font-weight: 300; 76 | line-height: 1.25; 77 | } 78 | 79 | blockquote small { 80 | display: block; 81 | line-height: 20px; 82 | color: #999999; 83 | } 84 | 85 | blockquote small:before { 86 | content: '\2014 \00A0'; 87 | } 88 | 89 | q:before, 90 | q:after, 91 | blockquote:before, 92 | blockquote:after { 93 | content: ''; 94 | } 95 | 96 | address { 97 | display: block; 98 | margin-bottom: 20px; 99 | font-style: normal; 100 | line-height: 20px; 101 | } 102 | 103 | code, 104 | pre { 105 | padding: 0 3px 2px; 106 | font-family: Monaco, Menlo, Consolas, "Courier New", monospace; 107 | font-size: 12px; 108 | color: #333333; 109 | -webkit-border-radius: 3px; 110 | -moz-border-radius: 3px; 111 | border-radius: 3px; 112 | } 113 | 114 | code { 115 | padding: 2px 4px; 116 | color: #d14; 117 | white-space: nowrap; 118 | background-color: #f7f7f9; 119 | border: 1px solid #e1e1e8; 120 | } 121 | 122 | pre { 123 | display: block; 124 | padding: 9.5px; 125 | margin: 0 0 10px; 126 | font-size: 13px; 127 | line-height: 20px; 128 | word-break: break-all; 129 | word-wrap: break-word; 130 | white-space: pre-wrap; 131 | background-color: #f5f5f5; 132 | border: 1px solid rgba(0, 0, 0, 0.15); 133 | -webkit-border-radius: 4px; 134 | -moz-border-radius: 4px; 135 | border-radius: 4px; 136 | } 137 | 138 | pre code { 139 | padding: 0; 140 | color: inherit; 141 | white-space: pre-wrap; 142 | background-color: transparent; 143 | border: 0; 144 | } 145 | } 146 | 147 | // atwho 148 | .atwho-view .cur{ 149 | background: $theme-color !important; 150 | } 151 | 152 | // github markdown 153 | .markdown-body { 154 | 155 | ul{ 156 | list-style: disc; 157 | } 158 | 159 | ol{ 160 | list-style: decimal; 161 | } 162 | } -------------------------------------------------------------------------------- /static/styles/scss/base/_mixin.scss: -------------------------------------------------------------------------------- 1 | @mixin setHeightGroup($height) { 2 | height: $height; 3 | line-height: $height; 4 | } 5 | 6 | @mixin boxShadow($prop...) { 7 | -webkit-box-shadow: $prop; 8 | -moz-box-shadow: $prop; 9 | box-shadow: $prop; 10 | } 11 | 12 | @mixin boxSizing($prop) { 13 | -webkit-box-sizing: $prop; 14 | -moz-box-sizing: $prop; 15 | box-sizing: $prop; 16 | } 17 | 18 | @mixin borderRadius($prop) { 19 | -webkit-border-radius: $prop; 20 | -moz-border-radius: $prop; 21 | border-radius: $prop; 22 | } 23 | 24 | @mixin opacity($prop) { 25 | opacity: $prop; 26 | filter: alpha(opacity = $prop * 100 ); 27 | } 28 | 29 | @mixin transition($prop...) { // ie10+ 30 | -webkit-transition: $prop; 31 | -moz-transition: $prop; 32 | transition: $prop; 33 | } 34 | 35 | @mixin transform($prop...) { 36 | -webkit-transform: $prop; 37 | -moz-transform: $prop; 38 | -ms-transform: $prop; // ie9 39 | transform: $prop; 40 | } 41 | 42 | @mixin placeholderColor($color, $font-size: 12px) { 43 | input::-webkit-input-placeholder { 44 | color: $color !important; 45 | font-size: $font-size; 46 | } 47 | input::-moz-placeholder { 48 | color: $color !important; 49 | font-size: $font-size; 50 | } 51 | input:-ms-input-placeholder { 52 | color: $color !important; 53 | font-size: $font-size; 54 | } 55 | } 56 | 57 | @mixin selection($bg-color, $color) { 58 | ::-moz-selection { 59 | background: $bg-color; 60 | color: $color; 61 | } 62 | ::selection { 63 | background: $bg-color; 64 | color: $color; 65 | } 66 | } 67 | 68 | $prefix-list: '-webkit-' '-moz-' ''; 69 | @mixin createAnimation($name, $duration: 1s, $func: ease-in-out, $delay: 0, $count: infinite, $direction: normal, $state: running, $mode: none) { // ie10+ 70 | @each $prefix in $prefix-list { 71 | #{$prefix}animation-name: $name; 72 | #{$prefix}animation-duration: $duration; 73 | #{$prefix}animation-timing-function: $func; 74 | #{$prefix}animation-delay: $delay; 75 | #{$prefix}animation-iteration-count: $count; 76 | #{$prefix}animation-direction: $direction; 77 | #{$prefix}animation-play-state: $state; 78 | #{$prefix}animation-fill-mode: $mode; 79 | } 80 | } 81 | 82 | @mixin createKeyframes($name) { // ie10+ 83 | @-webkit-keyframes #{$name} { 84 | @content; 85 | } 86 | @-moz-keyframes #{$name} { 87 | @content; 88 | } 89 | @keyframes #{$name} { 90 | @content; 91 | } 92 | } 93 | 94 | @mixin linearGradient($begin-color: #fff, $end-color: #fff, $direction: top-bottom) { 95 | @if $direction == top-bottom { 96 | background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,$begin-color), color-stop(100%,$end-color)); // Chrome,Safari4+ 97 | background: -webkit-linear-gradient(top, $begin-color 0%,$end-color 100%); // Chrome10+,Safari5.1+ 98 | background: linear-gradient(to bottom, $begin-color 0%,$end-color 100%); // W3C 99 | } @else if $direction == left-right { 100 | background: -webkit-gradient(linear, left top, right top, color-stop(0%,$begin-color), color-stop(100%,$end-color)); // Chrome,Safari4+ 101 | background: -webkit-linear-gradient(left, $begin-color 0%,$end-color 100%); // Chrome10+,Safari5.1+ 102 | background: linear-gradient(to right, $begin-color 0%,$end-color 100%); // W3C 103 | } 104 | } 105 | 106 | -------------------------------------------------------------------------------- /static/scripts/app/user.js: -------------------------------------------------------------------------------- 1 | $(function () { 2 | 3 | var user = { 4 | 5 | init: function () { 6 | var self = this; 7 | 8 | self.initElements(); 9 | self.initPlugins(); 10 | self.initEvents(); 11 | }, 12 | 13 | initElements: function () { 14 | var self = this; 15 | 16 | self.$btnSaveUser = $('#btn-save-user'); 17 | self.$btnChangePassword = $('#btn-change-password'); 18 | }, 19 | 20 | initPlugins: function () { 21 | var self = this; 22 | 23 | self.userValidator = Validator ? new Validator({ 24 | form: '#form-user', 25 | submit: self.saveUser.bind(this) 26 | }) : null; 27 | 28 | self.passwordValidator = Validator ? new Validator({ 29 | form: '#form-change-password', 30 | submit: self.changePassword.bind(this) 31 | }) : null; 32 | }, 33 | 34 | initEvents: function () { 35 | var self = this; 36 | 37 | self.$btnSaveUser.on('click', function () { 38 | 39 | self.saveUser(); 40 | }); 41 | 42 | self.$btnChangePassword.on('click', function () { 43 | 44 | self.changePassword(); 45 | }); 46 | }, 47 | 48 | saveUser: function () { 49 | var self = this; 50 | 51 | if (!self.userValidator.isValid()) return; 52 | if (self.$btnSaveUser.is('.disabled')) return; 53 | 54 | self.$btnSaveUser.addClass('disabled'); 55 | 56 | var params = self.userValidator.getValues(); 57 | 58 | $.ajax({ 59 | url: globalConfig.path + '/user/update', 60 | type: 'PUT', 61 | data: params, 62 | success: function (res) { 63 | 64 | if (res.success) { 65 | 66 | window.location.href = res.data; 67 | } else { 68 | 69 | self.userValidator.showError(null, res.message); 70 | } 71 | }, 72 | complete: function () { 73 | self.$btnSaveUser.removeClass('disabled'); 74 | } 75 | }); 76 | }, 77 | 78 | changePassword: function () { 79 | var self = this; 80 | 81 | if (!self.passwordValidator.isValid()) return; 82 | if (self.$btnChangePassword.is('.disabled')) return; 83 | 84 | self.$btnChangePassword.addClass('disabled'); 85 | 86 | var params = self.passwordValidator.getValues(); 87 | 88 | $.ajax({ 89 | url: globalConfig.path + '/user/change-password', 90 | type: 'PUT', 91 | data: params, 92 | success: function (res) { 93 | 94 | if (res.success) { 95 | 96 | window.location.href = res.data; 97 | } else { 98 | 99 | self.passwordValidator.showError(null, res.message); 100 | } 101 | }, 102 | complete: function () { 103 | self.$btnChangePassword.removeClass('disabled'); 104 | } 105 | }); 106 | } 107 | }; 108 | 109 | user.init(); 110 | }); -------------------------------------------------------------------------------- /static/scripts/app/frame.js: -------------------------------------------------------------------------------- 1 | $(function () { 2 | var frame = { 3 | init: function () { 4 | var self = this; 5 | 6 | self.initElements(); 7 | self.initPlugins(); 8 | self.initEvents(); 9 | 10 | self.checkBackToTop(); 11 | }, 12 | 13 | initElements: function () { 14 | var self = this; 15 | 16 | self.$window = $(window); 17 | self.$mBtnMenu = $('#m-btn-menu'); 18 | self.$mNav = $('#m-nav'); 19 | self.$mNavBg = $('#m-nav-bg'); 20 | self.$backToTop = $('.back-to-top'); 21 | self.$inputSearchList = $('.input-search'); 22 | self.$btnSearchList = $('.btn-search'); 23 | }, 24 | 25 | initPlugins: function () { 26 | var self = this; 27 | var $datetimeAgoList = $('.datetime-ago'); 28 | 29 | for (var i = 0; i < $datetimeAgoList.length; i++) { 30 | 31 | var $datetimeAgo = $($datetimeAgoList[i]); 32 | var datetime = $datetimeAgo.data('datetime'); 33 | 34 | $datetimeAgo.html(moment && moment.utc(datetime).fromNow()); 35 | } 36 | }, 37 | 38 | initEvents: function () { 39 | var self = this; 40 | 41 | self.$mBtnMenu.on('click', function () { 42 | 43 | if (self.$mNav.is(':visible')) { 44 | 45 | self.$mNav.hide(); 46 | self.$mNavBg.hide(); 47 | } else { 48 | 49 | self.$mNav.show(); 50 | self.$mNavBg.show(); 51 | } 52 | }); 53 | 54 | self.$mNavBg.on('click', function () { 55 | 56 | self.$mNav.hide(); 57 | self.$mNavBg.hide(); 58 | }); 59 | 60 | self.$backToTop.on('click', function () { 61 | 62 | $('html, body').animate({ 63 | scrollTop: 0 64 | }, 300); 65 | }); 66 | 67 | self.$window.scroll(function() { 68 | 69 | self.checkBackToTop(); 70 | }); 71 | 72 | self.$inputSearchList.on('keydown', function (e) { 73 | 74 | if (e.keyCode === 13) { 75 | e.preventDefault(); 76 | if (self.validSearch(this)) { 77 | $(this).closest('.search').submit(); 78 | } 79 | } 80 | }); 81 | 82 | self.$btnSearchList.on('click', function () { 83 | 84 | if (self.validSearch(this)) { 85 | $(this).closest('.search').submit(); 86 | } 87 | }); 88 | }, 89 | 90 | checkBackToTop: function () { 91 | var self = this; 92 | 93 | if (self.$window.scrollTop() > 200) { 94 | 95 | self.$backToTop.fadeIn(); 96 | 97 | } else { 98 | self.$backToTop.fadeOut(); 99 | } 100 | }, 101 | 102 | validSearch: function (that) { 103 | var self = this; 104 | var $input = $(that).closest('.search').find('.input-search'); 105 | 106 | if ($input.val().length === 0) { 107 | 108 | alert('关键字不可为空!'); 109 | return false; 110 | } else { 111 | 112 | return true; 113 | } 114 | } 115 | }; 116 | 117 | frame.init(); 118 | }); 119 | -------------------------------------------------------------------------------- /static/styles/mobile.css.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "mappings": "AAEA,uMAAwM;EACvM,OAAO,EAAE,CAAC;EACV,MAAM,EAAE,CAAC;EACT,MAAM,EAAE,CAAC;ECOR,kBAAkB,EDNA,UAAU;ECO5B,eAAe,EDPG,UAAU;ECQ5B,UAAU,EDRQ,UAAU;;;AAG9B,UAAW;EACV,UAAU,EAAE,IAAI;;;AAGjB,oDAAqD;EACpD,OAAO,EAAE,KAAK;;;AC4Bb,iFAAiC;EAC/B,KAAK,EAAE,cAAiB;EACxB,SAAS,EAH+B,IAAI;;AAK9C,+DAAwB;EACtB,KAAK,EAAE,cAAiB;EACxB,SAAS,EAP+B,IAAI;;AAS9C,uEAA4B;EAC1B,KAAK,EAAE,cAAiB;EACxB,SAAS,EAX+B,IAAI;;;ADnBhD,kCAAmC;EAClC,OAAO,EAAE,IAAI;;;AAGd,iFAAkF;EACjF,kBAAkB,EAAE,IAAI;ECTvB,qBAAqB,EDUA,CAAC;ECTtB,kBAAkB,EDSG,CAAC;ECRtB,aAAa,EDQQ,CAAC;EACvB,MAAM,EAAC,CAAC;;;AAGT,kDAAkD;EACjD,2BAA2B,EAAE,sBAAmB;;;AAKhD,gBAAa;EACZ,OAAO,EAAE,IAAI;;AAGd,iBAAa;EACZ,OAAO,EAAC,IAAI;;AAGb,mCAA+B;EAC9B,OAAO,EAAE,IAAI;;;AAIf,CAAE;EACD,eAAe,EAAE,IAAI;;;AE/CtB,eAAe;EACd,KAAK,EAAC,IAAI;EACV,MAAM,EAAC,MAAM;EACb,QAAQ,EAAE,KAAK;EACf,UAAU,EAAE,IAAI;EAChB,OAAO,EAAC,IAAI;EACZ,GAAG,EAAC,CAAC;EACL,OAAO,EAAC,GAAG;EACX,aAAa,EAAE,oBAA6B;;AAE5C,oBAAK;EACJ,MAAM,EAAC,MAAM;EACb,UAAU,EAAE,MAAM;EAClB,WAAW,EAAE,MAAM;EACnB,SAAS,EAAC,MAAM;EAChB,KAAK,ECnBM,IAAI;EDoBf,OAAO,EAAC,OAAO;;AAGhB,yBAAU;EACT,KAAK,EAAC,IAAI;EACV,SAAS,EAAC,IAAI;EACd,KAAK,EAAC,MAAM;EACZ,OAAO,EAAC,CAAC;;AAGV,yBAAS;EACR,KAAK,EAAC,KAAK;EACX,MAAM,EAAC,MAAM;EACb,WAAW,EAAE,MAAM;;AAGpB,6BAAc;EACb,QAAQ,EAAE,QAAQ;EAClB,KAAK,EAAE,KAAK;EACZ,MAAM,EAAE,KAAK;EDtBb,qBAAqB,ECuBC,GAAG;EDtBzB,kBAAkB,ECsBI,GAAG;EDrBzB,aAAa,ECqBS,GAAG;EACzB,UAAU,EAAE,GAAG;EACf,GAAG,EAAE,MAAM;;AAGZ,sBAAM;EACL,OAAO,EAAC,OAAO;EACf,UAAU,EAAE,MAAM;ED/ClB,MAAM,ECgDkB,MAAM;ED/C9B,WAAW,EC+Ca,MAAM;;AAE9B,wBAAC;EACA,OAAO,EAAE,YAAY;EACrB,MAAM,EAAC,MAAM;EACb,KAAK,EAAC,MAAM;EACZ,cAAc,EAAE,MAAM;;AAGvB,0BAAI;EACH,MAAM,EAAC,MAAM;EACb,KAAK,EAAC,MAAM;EACZ,cAAc,EAAE,GAAG;;;AAKtB,SAAU;EACT,UAAU,EAAE,kBAAc;EAC1B,QAAQ,EAAE,KAAK;EACf,IAAI,EAAC,CAAC;EACN,GAAG,EAAC,CAAC;EACL,KAAK,EAAC,CAAC;EACP,MAAM,EAAC,CAAC;EACR,OAAO,EAAC,EAAE;EACV,OAAO,EAAE,IAAI;;;AAGd,MAAM;EACL,KAAK,EAAC,IAAI;EACV,QAAQ,EAAE,KAAK;EACf,GAAG,EAAC,MAAM;EACV,UAAU,EAAC,IAAI;EACf,OAAO,EAAE,IAAI;EACb,OAAO,EAAE,GAAG;;AAEZ,QAAE;EACD,OAAO,EAAE,KAAK;EACd,KAAK,EAAC,IAAI;EACV,MAAM,EAAC,MAAM;EACb,WAAW,EAAE,MAAM;EACnB,aAAa,EAAE,oBAA6B;EAC5C,OAAO,EAAC,aAAa;EACrB,SAAS,EAAC,MAAM;EAChB,KAAK,EC5FM,IAAI;ED6Ff,QAAQ,EAAE,QAAQ;;AAElB,cAAK;EACJ,KAAK,EAAC,MAAM;EDhGb,MAAM,ECiGmB,MAAM;EDhG/B,WAAW,ECgGc,MAAM;EAC9B,QAAQ,EAAC,QAAQ;EACjB,IAAI,EAAC,CAAC;EACN,GAAG,EAAC,CAAC;EACL,UAAU,EAAE,MAAM;EAClB,SAAS,EAAE,IAAI;;;AAKlB,SAAS;EACR,OAAO,EAAE,IAAI;;AAEb,iBAAQ;EACP,MAAM,EAAE,IAAI;EACZ,OAAO,EAAE,uBAAuB;EAChC,QAAQ,EAAE,QAAQ;;AAElB,uBAAM;EACL,KAAK,EAAE,IAAI;EACX,OAAO,EAAE,sBAAsB;;AAGhC,6BAAY;EACX,QAAQ,EAAE,QAAQ;EAClB,KAAK,EAAE,IAAI;ED1HZ,MAAM,EC2HmB,IAAI;ED1H7B,WAAW,EC0Hc,IAAI;EAC5B,KAAK,EAAE,MAAM;EACb,GAAG,EAAE,IAAI;EACT,OAAO,EAAE,EAAE;EACX,UAAU,EAAE,MAAM;EAClB,SAAS,EAAE,MAAM;EACjB,KAAK,EChHW,IAAoB;;;ADqHvC,yCAA0C;EAEzC,IAAI;IACH,SAAS,EAAC,KAAK;;;EAGhB,cAAc;IACb,KAAK,EAAE,IAAI;IACX,OAAO,EAAE,KAAK;IACd,SAAS,EAAC,KAAK;IACf,SAAS,EAAC,OAAO;;;EAGlB,aAAc;IACb,OAAO,EAAE,IAAI;;;EAGd,cAAe;IACd,OAAO,EAAE,UAAU;;EAEnB,0BAAY;IACX,KAAK,EAAE,IAAI;IACX,OAAO,EAAE,KAAK;;;EAIhB,aAAa;IACZ,OAAO,EAAC,WAAW;;;EASjB,2BAAU;IAET,UAAU,EAAC,KAAK;IAChB,KAAK,EAAC,IAAI;IACV,KAAK,EAAC,IAAI;;;EAMd,YAAY;IACX,OAAO,EAAE,KAAK;IACd,KAAK,EAAE,IAAI;IACX,YAAY,EAAC,CAAC;;;EAMb,gBAAO;IACN,KAAK,EAAC,IAAI;IACV,KAAK,EAAE,IAAI;IACX,UAAU,EAAC,KAAK;;;EAKnB,eAAe;IACd,OAAO,EAAE,KAAK;;;EAGf,SAAS;IACR,OAAO,EAAE,KAAK;;;EAOb,6BAAK;IACJ,KAAK,EAAC,KAAK;;EAGZ,sCAAc;IACb,YAAY,EAAC,OAAO;;EAEpB,4CAAK;IACJ,KAAK,EAAE,IAAI;;EAOb,oCAAc;IACb,OAAO,EAAE,aAAa;IACtB,KAAK,EAAE,IAAI;;EAMZ,sCAAe;IACd,OAAO,EAAE,aAAa;IACtB,KAAK,EAAE,IAAI;;EAMZ,kCAAa;IACZ,OAAO,EAAE,aAAa;IACtB,KAAK,EAAE,IAAI;;;EAOb,gBAAK;IACJ,SAAS,EAAC,MAAM;IAChB,WAAW,EAAE,MAAM;;EAGpB,YAAC;IACA,SAAS,EAAC,MAAM;;;EAIlB,QAAQ;IDnQP,MAAM,ECoQkB,IAAI;IDnQ5B,WAAW,ECmQa,IAAI", 4 | "sources": ["scss/base/_reset.scss","scss/base/_mixin.scss","scss/mobile.scss","scss/base/_config.scss"], 5 | "names": [], 6 | "file": "mobile.css" 7 | } -------------------------------------------------------------------------------- /src/services/message.rs: -------------------------------------------------------------------------------- 1 | use mysql::from_row; 2 | use mysql::error::Error::MySqlError; 3 | use serde_json::Value; 4 | 5 | use common::utils::*; 6 | use common::lazy_static::{SQL_POOL, RECORDS_COUNT_PER_PAGE}; 7 | 8 | pub fn create_message(message: &Value) -> Option { 9 | 10 | let create_time = gen_datetime().to_string(); 11 | let comment_id = message["comment_id"].as_str().unwrap(); 12 | let message_id = gen_md5(&*(comment_id.to_string() + &*create_time)); 13 | 14 | let mut stmt = SQL_POOL.prepare(r#" 15 | INSERT INTO message 16 | (id, comment_id, topic_id, from_user_id, to_user_id, type, create_time) 17 | VALUES (?, ?, ?, ?, ?, ?, ?); 18 | "#).unwrap(); 19 | 20 | let result = stmt.execute(( 21 | &*message_id, 22 | comment_id, 23 | message["topic_id"].as_str().unwrap(), 24 | message["from_user_id"].as_str().unwrap(), 25 | message["to_user_id"].as_u64().unwrap(), 26 | message["type"].as_u64().unwrap(), 27 | &*create_time 28 | )); 29 | 30 | if let Err(MySqlError(ref err)) = result { 31 | 32 | println!("{:?}", err); 33 | return None; 34 | } 35 | 36 | Some(message_id) 37 | } 38 | 39 | pub fn get_user_message_list(user_id: u16, page: u32) -> Vec { 40 | 41 | let offset; 42 | 43 | if page <= 1 { 44 | offset = 0; 45 | } else { 46 | offset = (page - 1) * RECORDS_COUNT_PER_PAGE; 47 | } 48 | 49 | let sql = r#" 50 | SELECT 51 | m.id as message_id, m.comment_id, m.topic_id, t.title, u.username, m.type 52 | FROM message AS m 53 | LEFT JOIN topic AS t 54 | ON m.topic_id = t.id 55 | LEFT JOIN user AS u 56 | ON m.from_user_id = u.id 57 | WHERE m.to_user_id = ? 58 | ORDER BY m.create_time DESC 59 | LIMIT ? OFFSET ? 60 | "#; 61 | 62 | let result = SQL_POOL.prep_exec(sql, (user_id, RECORDS_COUNT_PER_PAGE, offset)).unwrap(); 63 | 64 | result.map(|row_wrapper| row_wrapper.unwrap()) 65 | .map(|mut row| { 66 | json!({ 67 | "message_id": row.get::(0).unwrap(), 68 | "comment_id": row.get::(1).unwrap(), 69 | "topic_id": row.get::(2).unwrap(), 70 | "title": row.get::(3).unwrap(), 71 | "username": row.get::(4).unwrap(), 72 | "type": row.get::(5).unwrap() 73 | }) 74 | }) 75 | .collect() 76 | } 77 | 78 | pub fn get_user_message_list_count(user_id: u16) -> u32 { 79 | 80 | let mut result = SQL_POOL.prep_exec("SELECT count(id) FROM message WHERE to_user_id = ?", (user_id, )).unwrap(); 81 | let row_wrapper = result.next(); 82 | 83 | if row_wrapper.is_none() { 84 | return 0; 85 | } 86 | 87 | let row = row_wrapper.unwrap().unwrap(); 88 | 89 | let (count, ) = from_row::<(u32, )>(row); 90 | 91 | count 92 | } 93 | 94 | pub fn delete_message(message_id: &str) -> Option { 95 | 96 | let result = SQL_POOL.prep_exec("DELETE FROM message WHERE id = ?", (message_id, )); 97 | 98 | if let Err(MySqlError(ref err)) = result { 99 | println!("{:?}", err.message); 100 | return None; 101 | } 102 | 103 | Some(message_id.to_string()) 104 | } 105 | 106 | pub fn delete_all_message_by_user_id(user_id: u16) -> Option { 107 | 108 | let result = SQL_POOL.prep_exec("DELETE FROM message WHERE to_user_id = ?", (user_id, )); 109 | 110 | if let Err(MySqlError(ref err)) = result { 111 | println!("{:?}", err.message); 112 | return None; 113 | } 114 | 115 | Some(1) 116 | } -------------------------------------------------------------------------------- /static/scripts/app/topic-editor.js: -------------------------------------------------------------------------------- 1 | $(function () { 2 | 3 | var topicEditor = { 4 | init: function () { 5 | var self = this; 6 | 7 | self.initPlugins(); 8 | self.initElements(); 9 | self.initEvents(); 10 | }, 11 | 12 | initPlugins: function () { 13 | var self = this; 14 | 15 | self.editor = new Editor(); 16 | 17 | self.editor.render($('.editor')[0]); 18 | 19 | var $input = $(self.editor.codemirror.display.input); 20 | 21 | $input.keydown(function(e){ 22 | if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) { 23 | e.preventDefault(); 24 | 25 | self.submit(); 26 | } 27 | }); 28 | }, 29 | 30 | initElements: function () { 31 | var self = this; 32 | 33 | self.$btnSubmit = $('#btn-submit'); 34 | self.$category = $('#category'); 35 | self.$title = $('#title'); 36 | self.$topicId = $('#topic-id') 37 | }, 38 | 39 | initEvents: function () { 40 | var self = this; 41 | 42 | self.$btnSubmit.on('click', function () { 43 | 44 | self.submit(); 45 | }); 46 | }, 47 | 48 | submit: function () { 49 | var self = this; 50 | 51 | if (self.checkValid()) { 52 | 53 | if (self.$btnSubmit.is('.disabled')) return; 54 | 55 | self.$btnSubmit.addClass('disabled'); 56 | 57 | var params = self.getValues(); 58 | var options = params.id ? { 59 | url: globalConfig.path + '/edit-topic/' + params.id, 60 | method: 'PUT' 61 | } : { 62 | url: globalConfig.path + '/create-topic', 63 | method: 'POST' 64 | }; 65 | 66 | $.ajax({ 67 | url: options.url, 68 | type: options.method, 69 | data: params, 70 | success: function (res) { 71 | 72 | if (res.success) { 73 | 74 | window.location.href = res.data; 75 | } else { 76 | 77 | alert(res.message); 78 | } 79 | }, 80 | complete: function () { 81 | self.$btnSubmit.removeClass('disabled'); 82 | } 83 | }); 84 | } 85 | }, 86 | 87 | checkValid: function () { 88 | var self = this; 89 | var isValid = true; 90 | 91 | if (!$.trim(self.$category.find('option:selected').val())) { 92 | 93 | alert("版块不能为空!"); 94 | 95 | isValid = false; 96 | return isValid; 97 | } 98 | 99 | if (!$.trim(self.$title.val())) { 100 | 101 | alert("话题标题不能为空!"); 102 | 103 | isValid = false; 104 | return isValid; 105 | } 106 | 107 | if (!$.trim(self.editor.codemirror.getValue())) { 108 | 109 | alert("话题内容不能为空!"); 110 | 111 | isValid = false; 112 | return isValid; 113 | } 114 | 115 | return isValid; 116 | }, 117 | 118 | getValues: function () { 119 | var self = this; 120 | 121 | return { 122 | id: $.trim(self.$topicId.val()), 123 | category: $.trim(self.$category.find('option:selected').val()), 124 | title: $.trim(self.$title.val()), 125 | content: $.trim(self.editor.codemirror.getValue()) 126 | }; 127 | } 128 | }; 129 | 130 | topicEditor.init(); 131 | }); -------------------------------------------------------------------------------- /views/topic-list.hbs: -------------------------------------------------------------------------------- 1 | {{var "styles" 2 | [ 3 | "/styles/font-awesome.min.css", 4 | "/styles/frame.css", 5 | "/styles/override.css", 6 | "/styles/mobile.css" 7 | ] 8 | }} 9 | {{var "scripts" 10 | [ 11 | "/scripts/lib/jquery-3.2.1.min.js", 12 | "/scripts/lib/moment.min.js", 13 | "/scripts/app/frame.js" 14 | ] 15 | }} 16 | 17 | {{~> common/header ~}} 18 | {{~> common/nav ~}} 19 | 20 |
21 |
22 |
23 | {{#if is_show_crumbs}} 24 | {{~> common/crumbs ~}} 25 | {{else}} 26 |
27 | 默认 28 | 精华 29 | 最新 30 | 待回复 31 | 问答 32 | 分享 33 | 招聘 34 |
35 | {{/if}} 36 |
37 |
38 | {{#if has_topic_list}} 39 | {{#each topic_list}} 40 |
41 |
42 | 43 |
44 |
45 |
46 |
47 | {{#if topic.sticky}} 48 | 置顶 49 | {{/if}} 50 | {{#if topic.essence}} 51 | 精华 52 | {{/if}} 53 | {{topic.category_name}} 54 | {{topic.title}} 55 |
56 |
57 | {{topic.author_name}} 发布于 58 | {{topic.comment_count}} 个回复 59 | {{topic.view_count}} 次浏览 60 | {{#if comment.username}} 61 | 最后由 {{comment.username}} 回复于 62 | {{/if}} 63 |
64 |
65 |
66 |
67 | {{/each}} 68 | {{else}} 69 |
70 | 暂无数据 71 |
72 | {{/if}} 73 |
74 | {{#if has_topic_list}} 75 | 78 | {{/if}} 79 |
80 |
81 | 82 | {{~> common/aside ~}} 83 | {{~> common/footer ~}} -------------------------------------------------------------------------------- /src/services/topic_vote.rs: -------------------------------------------------------------------------------- 1 | use mysql::from_row; 2 | use mysql::error::Error::MySqlError; 3 | 4 | use common::utils::*; 5 | use common::lazy_static::SQL_POOL; 6 | 7 | pub fn is_voted(user_id: &str, topic_id: &str) -> bool { 8 | 9 | let mut result = SQL_POOL.prep_exec(r#" 10 | SELECT count(id) FROM topic_vote 11 | WHERE 12 | user_id = ? AND topic_id = ? 13 | "#, (user_id, topic_id)).unwrap(); 14 | 15 | let row_wrapper = result.next(); 16 | 17 | if row_wrapper.is_none() { 18 | return false; 19 | } 20 | 21 | let row = row_wrapper.unwrap().unwrap(); 22 | let (count, ) = from_row::<(u8, )>(row); 23 | 24 | if count == 0 { 25 | false 26 | } else { 27 | true 28 | } 29 | } 30 | 31 | pub fn is_agreed(user_id: &str, topic_id: &str) -> bool { 32 | 33 | let mut result = SQL_POOL.prep_exec(r#" 34 | SELECT count(id) FROM topic_vote 35 | WHERE 36 | user_id = ? AND topic_id = ? AND state = 1 37 | "#, (user_id, topic_id)).unwrap(); 38 | 39 | let row_wrapper = result.next(); 40 | 41 | if row_wrapper.is_none() { 42 | return false; 43 | } 44 | 45 | let row = row_wrapper.unwrap().unwrap(); 46 | let (count, ) = from_row::<(u8, )>(row); 47 | 48 | if count == 0 { 49 | false 50 | } else { 51 | true 52 | } 53 | } 54 | 55 | pub fn is_disagreed(user_id: &str, topic_id: &str) -> bool { 56 | 57 | let mut result = SQL_POOL.prep_exec(r#" 58 | SELECT count(id) FROM topic_vote 59 | WHERE 60 | user_id = ? AND topic_id = ? AND state = -1 61 | "#, (user_id, topic_id)).unwrap(); 62 | 63 | let row_wrapper = result.next(); 64 | 65 | if row_wrapper.is_none() { 66 | return false; 67 | } 68 | 69 | let row = row_wrapper.unwrap().unwrap(); 70 | let (count, ) = from_row::<(u8, )>(row); 71 | 72 | if count == 0 { 73 | false 74 | } else { 75 | true 76 | } 77 | } 78 | 79 | pub fn create_topic_vote(user_id: &str, topic_id: &str, state: &str) -> Option { 80 | 81 | let create_time = gen_datetime().to_string(); 82 | let mut stmt = SQL_POOL.prepare(r#" 83 | INSERT INTO topic_vote 84 | (user_id, topic_id, state, create_time, update_time) 85 | VALUES 86 | (?, ?, ?, ?, ?) 87 | "#).unwrap(); 88 | 89 | let result = stmt.execute((user_id, topic_id, state, &*create_time, &*create_time)); 90 | 91 | if let Err(MySqlError(ref err)) = result { 92 | println!("{:?}", err.message); 93 | return None; 94 | } 95 | 96 | Some(1) 97 | } 98 | 99 | 100 | pub fn update_topic_vote(user_id: &str, topic_id: &str, state: &str) -> Option { 101 | 102 | let update_time = gen_datetime().to_string(); 103 | let mut stmt = SQL_POOL.prepare(r#" 104 | UPDATE topic_vote SET 105 | state = ?, 106 | update_time = ? 107 | WHERE 108 | user_id = ? AND topic_id = ? 109 | "#).unwrap(); 110 | 111 | let result = stmt.execute((state, &*update_time, user_id, topic_id)); 112 | 113 | if let Err(MySqlError(ref err)) = result { 114 | println!("{:?}", err.message); 115 | return None; 116 | } 117 | 118 | Some(1) 119 | } 120 | 121 | pub fn delete_topic_vote(user_id: &str, topic_id: &str) -> Option { 122 | 123 | let mut stmt = SQL_POOL.prepare(r#" 124 | DELETE FROM topic_vote 125 | WHERE 126 | user_id = ? AND topic_id = ? 127 | "#).unwrap(); 128 | 129 | let result = stmt.execute((user_id, topic_id)); 130 | 131 | if let Err(MySqlError(ref err)) = result { 132 | println!("{:?}", err.message); 133 | return None; 134 | } 135 | 136 | Some(1) 137 | } -------------------------------------------------------------------------------- /src/services/comment_vote.rs: -------------------------------------------------------------------------------- 1 | use mysql::from_row; 2 | use mysql::error::Error::MySqlError; 3 | 4 | use common::utils::*; 5 | use common::lazy_static::SQL_POOL; 6 | 7 | pub fn is_voted(user_id: &str, comment_id: &str) -> bool { 8 | 9 | let mut result = SQL_POOL.prep_exec(r#" 10 | SELECT count(id) FROM comment_vote 11 | WHERE 12 | user_id = ? AND comment_id = ? 13 | "#, (user_id, comment_id)).unwrap(); 14 | 15 | let row_wrapper = result.next(); 16 | 17 | if row_wrapper.is_none() { 18 | return false; 19 | } 20 | 21 | let row = row_wrapper.unwrap().unwrap(); 22 | let (count, ) = from_row::<(u8, )>(row); 23 | 24 | if count == 0 { 25 | false 26 | } else { 27 | true 28 | } 29 | } 30 | 31 | pub fn is_agreed(user_id: &str, comment_id: &str) -> bool { 32 | 33 | let mut result = SQL_POOL.prep_exec(r#" 34 | SELECT count(id) FROM comment_vote 35 | WHERE 36 | user_id = ? AND comment_id = ? AND state = 1 37 | "#, (user_id, comment_id)).unwrap(); 38 | 39 | let row_wrapper = result.next(); 40 | 41 | if row_wrapper.is_none() { 42 | return false; 43 | } 44 | 45 | let row = row_wrapper.unwrap().unwrap(); 46 | let (count, ) = from_row::<(u8, )>(row); 47 | 48 | if count == 0 { 49 | false 50 | } else { 51 | true 52 | } 53 | } 54 | 55 | pub fn is_disagreed(user_id: &str, comment_id: &str) -> bool { 56 | 57 | let mut result = SQL_POOL.prep_exec(r#" 58 | SELECT count(id) FROM comment_vote 59 | WHERE 60 | user_id = ? AND comment_id = ? AND state = -1 61 | "#, (user_id, comment_id)).unwrap(); 62 | 63 | let row_wrapper = result.next(); 64 | 65 | if row_wrapper.is_none() { 66 | return false; 67 | } 68 | 69 | let row = row_wrapper.unwrap().unwrap(); 70 | let (count, ) = from_row::<(u8, )>(row); 71 | 72 | if count == 0 { 73 | false 74 | } else { 75 | true 76 | } 77 | } 78 | 79 | pub fn create_comment_vote(user_id: &str, comment_id: &str, state: &str) -> Option { 80 | 81 | let create_time = gen_datetime().to_string(); 82 | let mut stmt = SQL_POOL.prepare(r#" 83 | INSERT INTO comment_vote 84 | (user_id, comment_id, state, create_time, update_time) 85 | VALUES 86 | (?, ?, ?, ?, ?) 87 | "#).unwrap(); 88 | 89 | let result = stmt.execute((user_id, comment_id, state, &*create_time, &*create_time)); 90 | 91 | if let Err(MySqlError(ref err)) = result { 92 | println!("{:?}", err.message); 93 | return None; 94 | } 95 | 96 | Some(1) 97 | } 98 | 99 | 100 | pub fn update_comment_vote(user_id: &str, comment_id: &str, state: &str) -> Option { 101 | 102 | let update_time = gen_datetime().to_string(); 103 | let mut stmt = SQL_POOL.prepare(r#" 104 | UPDATE comment_vote SET 105 | state = ?, 106 | update_time = ? 107 | WHERE 108 | user_id = ? AND comment_id = ? 109 | "#).unwrap(); 110 | 111 | let result = stmt.execute((state, &*update_time, user_id, comment_id)); 112 | 113 | if let Err(MySqlError(ref err)) = result { 114 | println!("{:?}", err.message); 115 | return None; 116 | } 117 | 118 | Some(1) 119 | } 120 | 121 | pub fn delete_comment_vote(user_id: &str, comment_id: &str) -> Option { 122 | 123 | let mut stmt = SQL_POOL.prepare(r#" 124 | DELETE FROM comment_vote 125 | WHERE 126 | user_id = ? AND comment_id = ? 127 | "#).unwrap(); 128 | 129 | let result = stmt.execute((user_id, comment_id)); 130 | 131 | if let Err(MySqlError(ref err)) = result { 132 | println!("{:?}", err.message); 133 | return None; 134 | } 135 | 136 | Some(1) 137 | } -------------------------------------------------------------------------------- /src/controllers/login.rs: -------------------------------------------------------------------------------- 1 | use std::io::Read; 2 | 3 | use iron::prelude::*; 4 | use iron_sessionstorage::traits::SessionRequestExt; 5 | use hyper::header::UserAgent; 6 | use serde_json::{Value}; 7 | use url::{Url, form_urlencoded}; 8 | 9 | use common::http::*; 10 | use common::utils::*; 11 | use common::lazy_static::{HTTPS_CLIENT, CONFIG_TABLE, GITHUB_LOGIN_PATH}; 12 | use services::user::*; 13 | 14 | pub fn render_login(req: &mut Request) -> IronResult { 15 | 16 | let mut data = ViewData::new(req); 17 | 18 | data.insert("github_login_path", json!(GITHUB_LOGIN_PATH.to_string())); 19 | 20 | respond_view("login", &data) 21 | } 22 | 23 | pub fn login(req: &mut Request) -> IronResult { 24 | 25 | let params = get_request_body(req); 26 | let username = ¶ms.get("username").unwrap()[0]; 27 | let password = ¶ms.get("password").unwrap()[0]; 28 | let username_wrapper = check_user_login(username, password); 29 | 30 | let mut data = JsonData::new(); 31 | 32 | if username_wrapper.is_none() { 33 | 34 | data.success = false; 35 | data.message = "登录失败,用户名或密码不正确!".to_owned(); 36 | 37 | return respond_json(&data); 38 | } 39 | 40 | let username = username_wrapper.unwrap(); 41 | let user = get_user(&*username).unwrap(); 42 | 43 | req.session().set(SessionData { 44 | user: json_stringify(&user) 45 | }).unwrap(); 46 | 47 | data.data = json!("/"); 48 | 49 | respond_json(&data) 50 | } 51 | 52 | pub fn github_auth_callback(req: &mut Request) -> IronResult { 53 | 54 | let params = get_request_query(req); 55 | let code = ¶ms.get("code").unwrap()[0]; 56 | let github_config = CONFIG_TABLE.get("github").unwrap().as_table().unwrap(); 57 | let client_id = github_config.get("client_id").unwrap().as_str().unwrap(); 58 | let client_secret = github_config.get("client_secret").unwrap().as_str().unwrap(); 59 | 60 | let access_token = get_github_access_token(&code, &client_id, &client_secret); 61 | 62 | let user_info = get_github_user_info(&access_token); 63 | let id = user_info["id"].as_u64().unwrap(); 64 | 65 | let username_wrapper; 66 | 67 | if is_github_user_binded(id) { // 该用户已绑定 68 | 69 | username_wrapper = update_github_user(&user_info); 70 | } else { 71 | 72 | username_wrapper = bind_github_user(&user_info); 73 | } 74 | 75 | if username_wrapper.is_some() { 76 | 77 | let username = username_wrapper.unwrap(); 78 | 79 | let user = get_user(&*username).unwrap(); 80 | 81 | req.session().set(SessionData { 82 | user: json_stringify(&user) 83 | }).unwrap(); 84 | 85 | redirect_to("/") 86 | } else { // 该github用户名已被本站用户注册 87 | 88 | let username = user_info["login"].as_str().unwrap(); 89 | 90 | let mut data = ViewData::new(req); 91 | data.insert("username", json!(username)); 92 | data.insert("message", json!("该github用户名已被本站用户注册,请填写新的用户名后,点击绑定")); 93 | data.insert("user_info", json!(json_stringify(&user_info))); 94 | 95 | respond_view("bind-user", &data) 96 | } 97 | } 98 | 99 | fn get_github_access_token(code: &str, client_id: &str, client_secret: &str) -> String { 100 | 101 | let mut url = Url::parse("https://github.com/login/oauth/access_token").unwrap(); 102 | url.query_pairs_mut() 103 | .append_pair("code", code) 104 | .append_pair("client_id", client_id) 105 | .append_pair("client_secret", client_secret); 106 | 107 | let mut body = String::new(); 108 | HTTPS_CLIENT.get(url.as_str()).send().unwrap().read_to_string(&mut body).unwrap(); 109 | 110 | let mut access_token = String::new(); 111 | for (key, value) in form_urlencoded::parse(body.as_bytes()).into_owned() { 112 | if key == "access_token" { 113 | access_token = value; 114 | } 115 | } 116 | 117 | access_token 118 | } 119 | 120 | fn get_github_user_info( access_token: &str) -> Value { 121 | 122 | let mut url = Url::parse("https://api.github.com/user").unwrap(); 123 | url.query_pairs_mut() 124 | .append_pair("access_token", access_token); 125 | 126 | let mut body = String::new(); 127 | HTTPS_CLIENT.get(url.as_str()) 128 | .header(UserAgent("runner".to_string())) // UserAgent必须指定,但值可以为任意值 129 | .send() 130 | .unwrap() 131 | .read_to_string(&mut body) 132 | .unwrap(); 133 | 134 | json_parse(&*body) 135 | } -------------------------------------------------------------------------------- /static/styles/scss/mobile.scss: -------------------------------------------------------------------------------- 1 | @import "./base/mixin"; 2 | @import "./base/extends"; 3 | @import "./base/reset"; 4 | @import "./base/config"; 5 | 6 | .m-frame-header{ 7 | width:100%; 8 | height:4.5rem; 9 | position: fixed; 10 | background: #fff; 11 | display:none; 12 | top:0; 13 | z-index:100; 14 | border-bottom: .1rem solid $lighter-bd-color; 15 | 16 | .btn { 17 | height:4.4rem; 18 | text-align: center; 19 | line-height: 4.4rem; 20 | font-size:1.4rem; 21 | color: $font-color; 22 | padding:0 .5rem; 23 | } 24 | 25 | .btn-menu { 26 | float:left; 27 | font-size:2rem; 28 | width:4.4rem; 29 | padding:0; 30 | } 31 | 32 | .box-btns{ 33 | float:right; 34 | height:4.4rem; 35 | line-height: 4.4rem; 36 | } 37 | 38 | .icon-red-dot { 39 | position: relative; 40 | width: .6rem; 41 | height: .6rem; 42 | @include borderRadius(50%); 43 | background: red; 44 | top: -.8rem; 45 | } 46 | 47 | .title{ 48 | padding:0 15rem; 49 | text-align: center; 50 | @include setHeightGroup(4.4rem); 51 | 52 | a{ 53 | display: inline-block; 54 | height:3.2rem; 55 | width:3.2rem; 56 | vertical-align: middle; 57 | } 58 | 59 | img { 60 | height:3.2rem; 61 | width:3.2rem; 62 | vertical-align: top; 63 | } 64 | } 65 | } 66 | 67 | .m-nav-bg { 68 | background: rgba(0,0,0,.5); 69 | position: fixed; 70 | left:0; 71 | top:0; 72 | right:0; 73 | bottom:0; 74 | z-index:90; 75 | display: none; 76 | } 77 | 78 | .m-nav{ 79 | width:100%; 80 | position: fixed; 81 | top:4.5rem; 82 | background:#fff; 83 | display: none; 84 | z-index: 100; 85 | 86 | a { 87 | display: block; 88 | width:100%; 89 | height:3.7rem; 90 | line-height: 3.6rem; 91 | border-bottom: .1rem solid $lighter-bd-color; 92 | padding:0 1rem 0 5rem; 93 | font-size:1.4rem; 94 | color: $font-color; 95 | position: relative; 96 | 97 | .icon{ 98 | width:4.4rem; 99 | @include setHeightGroup(3.6rem); 100 | position:absolute;; 101 | left:0; 102 | top:0; 103 | text-align: center; 104 | font-size: 2rem; 105 | } 106 | } 107 | } 108 | 109 | .m-search{ 110 | display: none; 111 | 112 | .search { 113 | height: 5rem; 114 | padding: 1rem 1.5rem 1rem 1.5rem; 115 | position: relative; 116 | 117 | input { 118 | width: 100%; 119 | padding: .4rem 3rem .4rem .8rem; 120 | } 121 | 122 | .btn-search { 123 | position: absolute; 124 | width: 3rem; 125 | @include setHeightGroup(3rem); 126 | right: 1.5rem; 127 | top: 1rem; 128 | z-index: 10; 129 | text-align: center; 130 | font-size: 1.8rem; 131 | color: $lightest-font-color; 132 | } 133 | } 134 | } 135 | 136 | @media only screen and (max-width: 768px) { 137 | 138 | body{ 139 | min-width:32rem; 140 | } 141 | 142 | .frame-wrapper{ 143 | width: 100%; 144 | display: block; 145 | min-width:32rem; 146 | max-width:76.8rem; 147 | } 148 | 149 | .frame-header { 150 | display: none; 151 | } 152 | 153 | .frame-content { 154 | padding: 4.5rem 0 0; 155 | 156 | .block-main { 157 | width: 100%; 158 | display: block; 159 | } 160 | } 161 | 162 | .frame-footer{ 163 | padding:1.5rem 1rem; 164 | } 165 | 166 | .comment { 167 | 168 | .info { 169 | 170 | dt{ 171 | 172 | .operator { 173 | 174 | margin-top:.5rem; 175 | width:100%; 176 | float:left; 177 | } 178 | } 179 | } 180 | } 181 | 182 | .block-aside{ 183 | display: block; 184 | width: 100%; 185 | padding-left:0; 186 | } 187 | 188 | .topic { 189 | 190 | dd { 191 | .right { 192 | float:left; 193 | width: 100%; 194 | margin-top:.4rem; 195 | } 196 | } 197 | } 198 | 199 | .m-frame-header{ 200 | display: block; 201 | } 202 | 203 | .m-search{ 204 | display: block; 205 | } 206 | 207 | .frame-form{ 208 | 209 | .input-line { 210 | 211 | label{ 212 | width:10rem; 213 | } 214 | 215 | .input-wrapper{ 216 | padding-left:11.5rem; 217 | 218 | input{ 219 | width: 100%; 220 | } 221 | } 222 | } 223 | 224 | .help-line { 225 | 226 | .help-wrapper { 227 | padding: 0 0 0 11.5rem; 228 | width: 100%; 229 | } 230 | } 231 | 232 | .error-line { 233 | 234 | .error-wrapper { 235 | padding: 0 0 0 11.5rem; 236 | width: 100%; 237 | } 238 | } 239 | 240 | .btn-line { 241 | 242 | .btn-wrapper { 243 | padding: 0 0 0 11.5rem; 244 | width: 100%; 245 | } 246 | } 247 | } 248 | 249 | .not-found{ 250 | 251 | .info{ 252 | font-size:2.8rem; 253 | line-height: 3.8rem; 254 | } 255 | 256 | a{ 257 | font-size:1.8rem; 258 | } 259 | } 260 | 261 | .no-data{ 262 | @include setHeightGroup(6rem); 263 | } 264 | } 265 | -------------------------------------------------------------------------------- /src/controllers/user.rs: -------------------------------------------------------------------------------- 1 | use iron::prelude::*; 2 | use iron_sessionstorage::traits::SessionRequestExt; 3 | 4 | use common::http::*; 5 | use common::utils::*; 6 | use services::user::*; 7 | 8 | pub fn render_user(req: &mut Request) -> IronResult { 9 | 10 | let params = get_router_params(req); 11 | let username = params.find("username").unwrap(); 12 | 13 | let user_wrapper = get_user(username); 14 | 15 | if user_wrapper.is_none() { 16 | 17 | redirect_to("/not-found") 18 | } else { 19 | 20 | let is_login = is_login(req); 21 | let mut data = ViewData::new(req); 22 | let user = user_wrapper.unwrap(); 23 | 24 | if is_login { 25 | 26 | let session = get_session_obj(req); 27 | let is_user_self; 28 | 29 | if session["username"].as_str().unwrap() == username { // 访问用户自己 30 | 31 | is_user_self = true; 32 | } else { 33 | 34 | is_user_self = false; 35 | } 36 | 37 | data.insert("is_user_self", json!(is_user_self)); 38 | } else { 39 | 40 | data.insert("is_user_self", json!(false)); 41 | } 42 | 43 | data.insert("is_login", json!(is_login)); 44 | data.insert("cur_user", json!(user)); 45 | 46 | respond_view("user", &data) 47 | } 48 | } 49 | 50 | pub fn update_user_info(req: &mut Request) -> IronResult { 51 | 52 | let params = get_request_body(req); 53 | let session = get_session_obj(req); 54 | let username = session["username"].as_str().unwrap(); 55 | let register_source = session["register_source"].as_u64().unwrap(); 56 | let old_avatar_url = session["avatar_url"].as_str().unwrap(); 57 | let new_username = ¶ms.get("username").unwrap()[0]; 58 | let email = ¶ms.get("email").unwrap()[0]; 59 | 60 | let mut data = JsonData::new(); 61 | 62 | if new_username == "" { 63 | 64 | data.success = false; 65 | data.message = "用户名不可为空!".to_owned(); 66 | 67 | return respond_json(&data); 68 | } 69 | 70 | if email == "" { 71 | 72 | data.success = false; 73 | data.message = "邮箱不可为空!".to_owned(); 74 | 75 | return respond_json(&data); 76 | } 77 | 78 | if new_username != username && is_user_created(new_username) { 79 | 80 | data.success = false; 81 | data.message = "该用户名已被注册!".to_owned(); 82 | 83 | return respond_json(&data); 84 | } 85 | 86 | let avatar_url; 87 | 88 | if register_source == 1 { // 如果是github用户,不更新头像 89 | 90 | avatar_url = old_avatar_url.to_string(); 91 | } else { 92 | 93 | avatar_url = gen_gravatar_url(email); 94 | } 95 | 96 | let result = update_user(username, &json!({ 97 | "username": new_username.to_string(), 98 | "github_account": params.get("githubAccount").unwrap()[0], 99 | "site": params.get("site").unwrap()[0], 100 | "qq": params.get("qq").unwrap()[0], 101 | "email": email.to_string(), 102 | "avatar_url": avatar_url, 103 | "location": params.get("location").unwrap()[0], 104 | "signature": params.get("signature").unwrap()[0] 105 | })); 106 | 107 | if result.is_none() { 108 | 109 | data.success = false; 110 | data.message = "用户信息设置失败!".to_owned(); 111 | 112 | return respond_json(&data); 113 | } 114 | 115 | data.data = json!("/login"); 116 | 117 | req.session().clear().unwrap(); 118 | 119 | respond_json(&data) 120 | } 121 | 122 | pub fn change_password(req: &mut Request) -> IronResult { 123 | 124 | let params = get_request_body(req); 125 | let session = get_session_obj(req); 126 | let username = session["username"].as_str().unwrap(); 127 | let old_password = ¶ms.get("oldPassword").unwrap()[0]; 128 | let new_password = ¶ms.get("newPassword").unwrap()[0]; 129 | let username_wrapper = check_user_login(username, old_password); 130 | 131 | let mut data = JsonData::new(); 132 | 133 | if username_wrapper.is_none() { 134 | 135 | data.success = false; 136 | data.message = "您输入的当前密码不正确!".to_owned(); 137 | 138 | return respond_json(&data); 139 | } 140 | 141 | let result = update_password(username, new_password); 142 | 143 | if result.is_none() { 144 | 145 | data.success = false; 146 | data.message = "密码更改失败,请重新尝试!".to_owned(); 147 | 148 | return respond_json(&data); 149 | } else { 150 | 151 | data.data = json!("/login"); 152 | 153 | req.session().clear().unwrap(); 154 | 155 | respond_json(&data) 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/common/http.rs: -------------------------------------------------------------------------------- 1 | use iron::prelude::*; 2 | use iron::status; 3 | use iron::Url; 4 | use iron::modifiers::Redirect; 5 | use hbs::Template; 6 | use serde_json::value::{Map, Value}; 7 | use iron_sessionstorage::Value as SessionValue; 8 | use iron_sessionstorage::traits::SessionRequestExt; 9 | 10 | use common::utils::*; 11 | use common::lazy_static::{SESSION_KEY, PATH, STATIC_PATH, UPLOAD_PATH}; 12 | use services::user::get_user_count; 13 | use services::topic::get_topic_count; 14 | use services::comment::get_comment_count; 15 | use services::message::get_user_message_list_count; 16 | 17 | #[derive(Debug, Clone)] 18 | pub struct SessionData { 19 | pub user: String 20 | } 21 | 22 | impl SessionValue for SessionData { 23 | 24 | fn get_key() -> &'static str { 25 | 26 | &SESSION_KEY 27 | } 28 | 29 | fn into_raw(self) -> String { 30 | 31 | self.user 32 | } 33 | 34 | fn from_raw(value: String) -> Option { 35 | 36 | if value.is_empty() { 37 | 38 | None 39 | } else { 40 | 41 | Some(SessionData { 42 | user: value 43 | }) 44 | } 45 | } 46 | } 47 | 48 | #[derive(Debug, Clone)] 49 | pub struct ViewData(Map); 50 | 51 | impl ViewData { 52 | 53 | pub fn new(req: &mut Request) -> ViewData { 54 | 55 | let session_wrapper = req.session().get::().unwrap(); 56 | 57 | let mut map = Map::new(); 58 | map.insert("path".to_owned(), json!(PATH.to_owned())); 59 | map.insert("static_path".to_owned(), json!(STATIC_PATH.to_owned())); 60 | map.insert("upload_path".to_owned(), json!(UPLOAD_PATH.to_owned())); 61 | map.insert("user_count".to_owned(), json!(get_user_count())); 62 | map.insert("topic_count".to_owned(), json!(get_topic_count())); 63 | map.insert("comment_count".to_owned(), json!(get_comment_count())); 64 | 65 | if session_wrapper.is_some() { 66 | let session_obj = json_parse(&*session_wrapper.unwrap().into_raw()); 67 | let user_id = session_obj["id"].as_u64().unwrap(); 68 | 69 | map.insert("user".to_owned(), session_obj); 70 | 71 | let unread_message_count = get_user_message_list_count(user_id as u16); 72 | 73 | map.insert("has_unread_message".to_owned(), json!(unread_message_count)); 74 | } 75 | 76 | ViewData(map) 77 | } 78 | 79 | pub fn insert(&mut self, key: &str, value: Value) -> &mut Self { 80 | 81 | self.0.insert(key.to_owned(), value); 82 | self 83 | } 84 | } 85 | 86 | #[derive(Debug, Clone, Serialize, Deserialize)] 87 | pub struct JsonData { 88 | pub success: bool, 89 | pub message: String, 90 | pub data: Value 91 | } 92 | 93 | impl JsonData { 94 | 95 | pub fn new() -> JsonData { 96 | 97 | JsonData { 98 | success: true, 99 | message: "".to_owned(), 100 | data: json!("") 101 | } 102 | } 103 | } 104 | 105 | pub fn respond_view(template_path: &str, data: &ViewData) -> IronResult { 106 | 107 | let mut res = Response::new(); 108 | 109 | res.set_mut(status::Ok) 110 | .set_mut(Template::new(template_path, data.0.clone())); 111 | 112 | Ok(res) 113 | } 114 | 115 | pub fn respond_unauthorized_json(data: &JsonData) -> IronResult { 116 | 117 | let mut res = Response::new(); 118 | 119 | res.set_mut(status::Unauthorized) 120 | .set_mut(mime!(Application/Json)) 121 | .set_mut(json_stringify(data)); 122 | 123 | Ok(res) 124 | } 125 | 126 | pub fn respond_forbidden_json(data: &JsonData) -> IronResult { 127 | 128 | let mut res = Response::new(); 129 | 130 | res.set_mut(status::Forbidden) 131 | .set_mut(mime!(Application/Json)) 132 | .set_mut(json_stringify(data)); 133 | 134 | Ok(res) 135 | } 136 | 137 | pub fn respond_json(data: &JsonData) -> IronResult { 138 | 139 | let mut res = Response::new(); 140 | 141 | res.set_mut(status::Ok) 142 | .set_mut(mime!(Application/Json)) 143 | .set_mut(json_stringify(data)); 144 | 145 | Ok(res) 146 | } 147 | 148 | pub fn respond_text(text: &str) -> IronResult { 149 | 150 | let mut res = Response::new(); 151 | 152 | res.set_mut(status::Ok) 153 | .set_mut(mime!(Text/Plain)) 154 | .set_mut(text.to_string()); 155 | 156 | Ok(res) 157 | } 158 | 159 | pub fn redirect_to(url: &str) -> IronResult { 160 | 161 | let complete_url = PATH.to_owned() + url; 162 | 163 | let url = Url::parse(&*complete_url).unwrap(); 164 | let res = Response::with((status::Found, Redirect(url))); 165 | 166 | return Ok(res); 167 | } -------------------------------------------------------------------------------- /static/styles/override.css: -------------------------------------------------------------------------------- 1 | body, div, ul, dl, ol, dt, dd, li, p, a, label, span, i, img, form, input, textarea, header, footer, article, section, nav, aside, figure, h1, h2, h3, h4, h5, h6, table, thead, tbody, tr, th, td, pre { 2 | padding: 0; 3 | margin: 0; 4 | border: 0; 5 | -webkit-box-sizing: border-box; 6 | -moz-box-sizing: border-box; 7 | box-sizing: border-box; 8 | } 9 | 10 | ul, dl, ol { 11 | list-style: none; 12 | } 13 | 14 | header, footer, article, section, nav, aside, figure { 15 | display: block; 16 | } 17 | 18 | input input::-webkit-input-placeholder, textarea input::-webkit-input-placeholder { 19 | color: red !important; 20 | font-size: 12px; 21 | } 22 | input input::-moz-placeholder, textarea input::-moz-placeholder { 23 | color: red !important; 24 | font-size: 12px; 25 | } 26 | input input:-ms-input-placeholder, textarea input:-ms-input-placeholder { 27 | color: red !important; 28 | font-size: 12px; 29 | } 30 | 31 | input, textarea, a, button, select { 32 | outline: none; 33 | } 34 | 35 | input[type="button"], input[type="submit"], input[type="reset"], textarea, button { 36 | -webkit-appearance: none; 37 | -webkit-border-radius: 0; 38 | -moz-border-radius: 0; 39 | border-radius: 0; 40 | border: 0; 41 | } 42 | 43 | input:focus, textarea:focus, a:focus, button:focus { 44 | -webkit-tap-highlight-color: rgba(255, 255, 255, 0); 45 | } 46 | 47 | input::-ms-clear { 48 | display: none; 49 | } 50 | input::-ms-reveal { 51 | display: none; 52 | } 53 | input::-webkit-search-cancel-button { 54 | display: none; 55 | } 56 | 57 | a { 58 | text-decoration: none; 59 | } 60 | 61 | .editor-toolbar { 62 | font-size: 1.4rem; 63 | } 64 | .editor-toolbar:before, .editor-toolbar:after { 65 | background: #ccc; 66 | } 67 | .editor-toolbar .eicon-bold { 68 | margin-left: .8rem; 69 | } 70 | .editor-toolbar .eicon-fullscreen { 71 | margin-right: .8rem; 72 | } 73 | 74 | .editor-preview { 75 | z-index: 80 !important; 76 | background: #eee; 77 | } 78 | 79 | .editor-statusbar { 80 | display: none; 81 | } 82 | 83 | .editor-preview ul, .editor-preview ol { 84 | padding: 0; 85 | margin: 0 0 10px 25px; 86 | } 87 | .editor-preview ul { 88 | list-style: disc; 89 | } 90 | .editor-preview ol { 91 | list-style: decimal; 92 | } 93 | .editor-preview hr { 94 | margin: 20px 0; 95 | border: 0; 96 | border-top: 1px solid #eeeeee; 97 | border-bottom: 1px solid #ffffff; 98 | } 99 | .editor-preview abbr[title], 100 | .editor-preview abbr[data-original-title] { 101 | cursor: help; 102 | border-bottom: 1px dotted #999999; 103 | } 104 | .editor-preview abbr { 105 | font-size: 90%; 106 | text-transform: uppercase; 107 | } 108 | .editor-preview blockquote { 109 | padding: 0 0 0 15px; 110 | margin: 0 0 20px; 111 | border-left: 5px solid #eeeeee; 112 | } 113 | .editor-preview blockquote p { 114 | margin-bottom: 0; 115 | font-size: 17.5px; 116 | font-weight: 300; 117 | line-height: 1.25; 118 | } 119 | .editor-preview blockquote small { 120 | display: block; 121 | line-height: 20px; 122 | color: #999999; 123 | } 124 | .editor-preview blockquote small:before { 125 | content: '\2014 \00A0'; 126 | } 127 | .editor-preview q:before, 128 | .editor-preview q:after, 129 | .editor-preview blockquote:before, 130 | .editor-preview blockquote:after { 131 | content: ''; 132 | } 133 | .editor-preview address { 134 | display: block; 135 | margin-bottom: 20px; 136 | font-style: normal; 137 | line-height: 20px; 138 | } 139 | .editor-preview code, 140 | .editor-preview pre { 141 | padding: 0 3px 2px; 142 | font-family: Monaco, Menlo, Consolas, "Courier New", monospace; 143 | font-size: 12px; 144 | color: #333333; 145 | -webkit-border-radius: 3px; 146 | -moz-border-radius: 3px; 147 | border-radius: 3px; 148 | } 149 | .editor-preview code { 150 | padding: 2px 4px; 151 | color: #d14; 152 | white-space: nowrap; 153 | background-color: #f7f7f9; 154 | border: 1px solid #e1e1e8; 155 | } 156 | .editor-preview pre { 157 | display: block; 158 | padding: 9.5px; 159 | margin: 0 0 10px; 160 | font-size: 13px; 161 | line-height: 20px; 162 | word-break: break-all; 163 | word-wrap: break-word; 164 | white-space: pre-wrap; 165 | background-color: #f5f5f5; 166 | border: 1px solid rgba(0, 0, 0, 0.15); 167 | -webkit-border-radius: 4px; 168 | -moz-border-radius: 4px; 169 | border-radius: 4px; 170 | } 171 | .editor-preview pre code { 172 | padding: 0; 173 | color: inherit; 174 | white-space: pre-wrap; 175 | background-color: transparent; 176 | border: 0; 177 | } 178 | 179 | .atwho-view .cur { 180 | background: #1e90ff !important; 181 | } 182 | 183 | .markdown-body ul { 184 | list-style: disc; 185 | } 186 | .markdown-body ol { 187 | list-style: decimal; 188 | } 189 | 190 | /*# sourceMappingURL=override.css.map */ 191 | -------------------------------------------------------------------------------- /src/routes.rs: -------------------------------------------------------------------------------- 1 | use router::Router; 2 | 3 | use common::middlewares::authorize; 4 | use controllers::*; 5 | 6 | pub fn gen_router() -> Router { 7 | 8 | let mut router = Router::new(); 9 | 10 | router.get("/", topic_list::render_default_topic_list, "render_default_topic_list"); 11 | router.get("/topics/essence", topic_list::render_essence_topic_list, "render_essence_topic_list"); 12 | router.get("/topics/latest", topic_list::render_latest_topic_list, "render_latest_topic_list"); 13 | router.get("/topics/no-reply", topic_list::render_no_reply_topic_list, "render_no_reply_topic_list"); 14 | router.get("/topics/ask", topic_list::render_ask_topic_list, "render_ask_topic_list"); 15 | router.get("/topics/share", topic_list::render_share_topic_list, "render_share_topic_list"); 16 | router.get("/topics/job", topic_list::render_job_topic_list, "render_job_topic_list"); 17 | router.get("/:username/topics", topic_list::render_user_topics, "render_user_topics"); 18 | router.get("/:username/comments", topic_list::render_user_comments, "render_user_comments"); 19 | router.get("/:username/collections", topic_list::render_user_collections, "render_user_collections"); 20 | 21 | router.get("/search", topic_list::render_search_result, "render_search_result"); 22 | 23 | router.get("/login", login::render_login, "render_login"); 24 | router.post("/login", login::login, "login"); 25 | 26 | router.get("/register", register::render_register, "render_register"); 27 | router.post("/register", register::register, "register"); 28 | 29 | router.get("/github/auth", login::github_auth_callback, "github_auth_callback"); 30 | router.post("/bind-user", register::bind_user, "bind_user"); 31 | 32 | router.get("/logout", logout::logout, "logout"); 33 | 34 | router.get("/topic/:topic_id", topic::render_topic, "render_topic"); 35 | router.get("/create-topic", authorize(topic::render_create_topic, true, false), "render_create_topic"); 36 | router.post("/create-topic", authorize(topic::create_topic, true, false), "create_topic"); 37 | router.get("/edit-topic/:topic_id", authorize(topic::render_edit_topic, true, false), "render_edit_topic"); 38 | router.put("/edit-topic/:topic_id", authorize(topic::edit_topic, true, false), "edit_topic"); 39 | router.delete("/delete-topic/:topic_id", authorize(topic::delete_topic, true, false), "delete_topic"); 40 | router.post("/topic/collect/:topic_id", authorize(topic::collect_topic, true, false), "collect_topic"); 41 | router.post("/topic/vote/:topic_id", authorize(topic::vote_topic, true, false), "vote_topic"); 42 | router.post("/topic/stick/:topic_id", authorize(topic::stick_topic, true, true), "stick_topic"); 43 | router.post("/topic/essence/:topic_id", authorize(topic::essence_topic, true, true), "essence_topic"); 44 | 45 | router.post("/create-comment", authorize(comment::create_comment, true, false), "create_comment"); 46 | router.get("/edit-comment/:comment_id", authorize(comment::render_edit_comment, true, false), "render_edit_comment"); 47 | router.put("/edit-comment/:comment_id", authorize(comment::edit_comment, true, false), "edit_comment"); 48 | router.delete("/delete-comment/:comment_id", authorize(comment::delete_comment, true, false), "delete_comment"); 49 | router.post("/comment/vote/:comment_id", authorize(comment::vote_comment, true, false), "vote_comment"); 50 | 51 | router.get("/:username/message/unread", authorize(message::render_unread_message, true, false), "render_unread_message"); 52 | router.get("/read-message/:message_id", authorize(message::read_message, true, false), "read_message"); 53 | router.get("/read-all-message", authorize(message::read_all_message, true, false), "read_all_message"); 54 | 55 | router.get("/user/:username", user::render_user, "render_user"); 56 | router.put("/user/update", authorize(user::update_user_info, true, false), "update_user_info"); 57 | router.put("/user/change-password", authorize(user::change_password, true, false), "change_password"); 58 | 59 | router.get("/reset-password", reset_password::render_reset_password, "render_find_password"); 60 | router.post("/reset-password", reset_password::send_reset_password_email, "send_reset_password_email"); 61 | router.get("/set-new-password", reset_password::render_set_new_password, "render_set_new_password"); 62 | router.post("/set-new-password", reset_password::set_new_password, "set_new_password"); 63 | 64 | router.get("/resource", simple_render::render_resource, "resource"); 65 | 66 | router.get("/about-site", simple_render::render_about_site, "about_site"); 67 | 68 | router.post("/upload-image", authorize(upload::upload_file, true, false), "upload"); 69 | 70 | router.get("/rss", rss::render_rss, "render_rss"); 71 | 72 | router.get("/forbidden", error::render_forbidden, "render_forbidden"); 73 | 74 | router.get("/*", error::render_not_found, "render_not_found"); 75 | 76 | router 77 | } 78 | 79 | -------------------------------------------------------------------------------- /src/controllers/reset_password.rs: -------------------------------------------------------------------------------- 1 | use iron::prelude::*; 2 | use iron_sessionstorage::traits::SessionRequestExt; 3 | use lettre::email::EmailBuilder; 4 | use lettre::transport::smtp::{SecurityLevel, SmtpTransportBuilder}; 5 | use lettre::transport::smtp::authentication::Mechanism; 6 | use lettre::transport::EmailTransport; 7 | use uuid::Uuid; 8 | 9 | use common::http::*; 10 | use common::utils::*; 11 | use common::lazy_static::{CONFIG_TABLE, PATH}; 12 | use services::user::{get_username_by_email, update_retrieve, get_retrieve_time, update_password}; 13 | 14 | pub fn render_reset_password(req: &mut Request) -> IronResult { 15 | 16 | let data = ViewData::new(req); 17 | 18 | respond_view("reset-password", &data) 19 | } 20 | 21 | pub fn send_reset_password_email(req: &mut Request) -> IronResult { 22 | 23 | let params = get_request_body(req); 24 | let email_str = &*params.get("email").unwrap()[0]; 25 | let smtp_config = CONFIG_TABLE.get("smtp").unwrap().as_table().unwrap(); 26 | let smtp_host = smtp_config.get("host").unwrap().as_str().unwrap(); 27 | let smtp_port = smtp_config.get("port").unwrap().as_integer().unwrap() as u16; 28 | let smtp_username = smtp_config.get("username").unwrap().as_str().unwrap(); 29 | let smtp_password = smtp_config.get("password").unwrap().as_str().unwrap(); 30 | 31 | let mut data = JsonData::new(); 32 | 33 | let username_wrapper = get_username_by_email(email_str); 34 | 35 | if username_wrapper.is_none() { 36 | data.success = false; 37 | data.message = "该邮箱并未在本站注册账号,请检查邮箱地址!".to_string(); 38 | 39 | return respond_json(&data); 40 | } 41 | 42 | let username = username_wrapper.unwrap(); 43 | let retrieve_token = Uuid::new_v4().to_string(); 44 | 45 | let email = EmailBuilder::new() 46 | .to(email_str) 47 | .from(smtp_username) 48 | .subject("重置密码") 49 | .html(&*format!(r#" 50 |

51 | 请点击 52 | {0}/set-new-password?username={1}&token={2} 53 | ,进行密码重置!该链接的有效时间为 24 小时! 54 |

55 | "#, PATH.to_owned(), username, retrieve_token)) 56 | .build() 57 | .unwrap(); 58 | 59 | let mut mailer = SmtpTransportBuilder::new((smtp_host, smtp_port)).unwrap() 60 | .credentials(smtp_username, smtp_password) 61 | .security_level(SecurityLevel::AlwaysEncrypt) 62 | .smtp_utf8(true) 63 | .authentication_mechanism(Mechanism::Plain) 64 | .connection_reuse(true) 65 | .build(); 66 | 67 | let result = mailer.send(email); 68 | 69 | if result.is_ok() { 70 | 71 | update_retrieve(&*username, &*retrieve_token); 72 | 73 | data.message = "验证邮件已发送,该邮件的有效时间为24小时,请注意查收!".to_string(); 74 | data.data = json!("/"); 75 | } else { 76 | data.success = false; 77 | data.message = "验证邮件发送失败,请重新尝试!".to_string(); 78 | } 79 | 80 | respond_json(&data) 81 | } 82 | 83 | pub fn render_set_new_password(req: &mut Request) -> IronResult { 84 | 85 | let params = get_request_query(req); 86 | let username = &*params.get("username").unwrap()[0]; 87 | let token = &*params.get("token").unwrap()[0]; 88 | 89 | let mut data = ViewData::new(req); 90 | 91 | if token == "" { 92 | 93 | data.insert("retrieve_message", json!("该验证地址已失效!")); 94 | return respond_view("new-password", &data); 95 | } 96 | 97 | let retrieve_time_wrapper = get_retrieve_time(username, token); 98 | 99 | 100 | if retrieve_time_wrapper.is_none() { 101 | 102 | data.insert("retrieve_message", json!("该验证地址已失效!")); 103 | } else { 104 | 105 | let now = gen_datetime().timestamp(); 106 | let one_day = 60 * 60 * 24; 107 | let retrieve_time = retrieve_time_wrapper.unwrap().timestamp(); 108 | 109 | if now - retrieve_time > one_day { // 该地址未验证时间超过24小时 110 | data.insert("retrieve_message", json!("该验证地址已失效!")); 111 | } else { 112 | data.insert("username", json!(username)); 113 | } 114 | } 115 | 116 | respond_view("new-password", &data) 117 | } 118 | 119 | pub fn set_new_password(req: &mut Request) -> IronResult { 120 | 121 | let params = get_request_body(req); 122 | let username = ¶ms.get("username").unwrap()[0]; 123 | let new_password = ¶ms.get("newPassword").unwrap()[0]; 124 | 125 | let mut data = JsonData::new(); 126 | 127 | let result = update_password(username, new_password); 128 | 129 | if result.is_none() { 130 | 131 | data.success = false; 132 | data.message = "新密码设置失败,请重新尝试!".to_owned(); 133 | } else { 134 | 135 | update_retrieve(username, ""); // 清空retrieve_token,使得验证地址失效 136 | data.message = "新密码设置成功,即将前往登录页!".to_owned(); 137 | data.data = json!("/login"); 138 | } 139 | 140 | req.session().clear().unwrap(); 141 | 142 | respond_json(&data) 143 | } 144 | -------------------------------------------------------------------------------- /tables.sql: -------------------------------------------------------------------------------- 1 | SET FOREIGN_KEY_CHECKS = 0; 2 | SET GLOBAL SQL_MODE = "STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION"; # fix GROUP BY时,报"ONLY_FULL_GROUP_BY"错误,(该模式下,SELECT中的列,必须都存在于GROUP BY中) 3 | 4 | DROP TABLE IF EXISTS collection; 5 | DROP TABLE IF EXISTS topic_vote; 6 | DROP TABLE IF EXISTS comment_vote; 7 | DROP TABLE IF EXISTS message; 8 | DROP TABLE IF EXISTS comment; 9 | DROP TABLE IF EXISTS topic; 10 | DROP TABLE IF EXISTS category; 11 | DROP TABLE IF EXISTS github_user; 12 | DROP TABLE IF EXISTS user; 13 | 14 | # 用户表 15 | CREATE TABLE user ( 16 | id int(16) PRIMARY KEY AUTO_INCREMENT NOT NULL, -- 用户id 17 | username varchar(32) NOT NULL, -- 用户账号 18 | nickname varchar(32) DEFAULT "", -- 用户昵称 19 | user_role tinyint(2) unsigned DEFAULT 1, -- 0-禁言用户, 1-普通用户, 2-管理员 20 | register_source tinyint(2) unsigned DEFAULT 0, -- 注册来源,0-本站注册, 1-github 21 | gender tinyint(2) unsigned DEFAULT 1, -- 0-female, 1-male 22 | signature varchar(200) DEFAULT "", -- 用户签名 23 | email varchar(32) NOT NULL, -- 邮箱 24 | avatar_url varchar(200) NOT NULL, -- 头像地址 25 | qq varchar(32) DEFAULT "", -- qq号码 26 | location varchar(32) DEFAULT "", -- 地址 27 | site varchar(32) DEFAULT "", -- 用户个人网站 28 | github_account varchar(32) DEFAULT "", -- 用户github账号 29 | password varchar(32) NOT NULL, -- 密码 30 | salt varchar(64) NOT NULL, -- 密码加盐 31 | create_time datetime NOT NULL, -- 用户创建时间 32 | update_time datetime NOT NULL, -- 用户更新时间 33 | retrieve_token varchar(36) DEFAULT "", -- 重置密码链接的token 34 | retrieve_time datetime DEFAULT now(), -- 重置密码链接的生成时间 35 | UNIQUE KEY username (username), 36 | UNIQUE KEY email (email) 37 | ) ENGINE = InnoDB DEFAULT CHARSET = utf8; 38 | 39 | # github用户表 40 | CREATE TABLE github_user ( 41 | id int(32) PRIMARY KEY NOT NULL, -- github id 42 | user_id int(16) NOT NULL, -- 本站id 43 | username varchar(32) NOT NULL, -- github用户名 44 | nickname varchar(32) NOT NULL, -- github昵称 45 | email varchar(32) NOT NULL, -- github邮箱 46 | avatar_url varchar(200) NOT NULL, -- github头像地址 47 | home_url varchar(200) NOT NULL, -- github用户主页地址 48 | site varchar(200) DEFAULT "", -- github用户个人站点地址 49 | location varchar(200) DEFAULT "", -- github用户地址 50 | bio varchar(200) DEFAULT "", -- github用户个人简介 51 | create_time datetime NOT NULL, -- 绑定时间 52 | update_time datetime NOT NULL, -- 更新绑定时间 53 | KEY user_id (user_id), 54 | CONSTRAINT github_user_ibfK_1 FOREIGN KEY (user_id) REFERENCES user (id) 55 | ) ENGINE = InnoDB DEFAULT CHARSET = utf8; 56 | 57 | # 话题分类表 58 | CREATE TABLE category ( 59 | id tinyint(4) unsigned PRIMARY KEY NOT NULL, -- 1-问答, 2-分享, 3-招聘 60 | name varchar(64) NOT NULL 61 | ) ENGINE = InnoDB DEFAULT CHARSET = utf8; 62 | 63 | # 话题表 64 | CREATE TABLE topic ( 65 | id varchar(32) PRIMARY KEY NOT NULL, 66 | user_id int(16) NOT NULL, 67 | category_id tinyint(4) unsigned DEFAULT 1, 68 | title varchar(200) NOT NULL, 69 | content mediumtext NOT NULL, 70 | status tinyint(2) unsigned DEFAULT 1, 71 | sticky tinyint(2) unsigned DEFAULT 0, -- 0-普通话题, 1-置顶 72 | essence tinyint(2) unsigned DEFAULT 0, -- 0-普通话题, 1-精华 73 | view_count int(32) DEFAULT 0, 74 | create_time datetime NOT NULL, 75 | update_time datetime NOT NULL, 76 | KEY user_id (user_id), 77 | KEY category_id (category_id), 78 | CONSTRAINT topic_ibfk_1 FOREIGN KEY (user_id) REFERENCES user (id), 79 | CONSTRAINT topic_ibfk_2 FOREIGN KEY (category_id) REFERENCES category (id) 80 | ) ENGINE = InnoDB DEFAULT CHARSET = utf8; 81 | 82 | # 评论表 83 | CREATE TABLE comment ( 84 | id varchar(32) PRIMARY KEY NOT NULL, 85 | user_id int(16) NOT NULL, 86 | topic_id varchar(32) NOT NULL, 87 | content mediumtext NOT NULL, 88 | status tinyint(2) unsigned DEFAULT 1, 89 | create_time datetime NOT NULL, 90 | update_time datetime NOT NULL 91 | ) ENGINE = InnoDB DEFAULT CHARSET = utf8; 92 | 93 | # 消息表 94 | CREATE TABLE message ( 95 | id varchar(32) PRIMARY KEY NOT NULL, 96 | topic_id varchar(32) NOT NULL, 97 | comment_id varchar(32) NOT NULL, 98 | from_user_id int(16) NOT NULL, 99 | to_user_id int(16) NOT NULL, 100 | status tinyint(2) unsigned DEFAULT 1, 101 | type tinyint(2) unsigned NOT NULL, -- 0-回复话题, 1-@某人 102 | create_time datetime NOT NULL 103 | ) ENGINE = InnoDB DEFAULT CHARSET = utf8; 104 | 105 | # 话题点赞表 106 | CREATE TABLE topic_vote ( 107 | id int(64) PRIMARY KEY AUTO_INCREMENT NOT NULL, 108 | user_id int(16) NOT NULL, 109 | topic_id varchar(32) NOT NULL, 110 | state tinyint(4) signed NOT NULL DEFAULT 0, -- 1-赞, -1-踩 111 | create_time datetime NOT NULL, 112 | update_time datetime NOT NULL 113 | ) ENGINE = InnoDB DEFAULT CHARSET = utf8; 114 | 115 | # 评论点赞表 116 | CREATE TABLE comment_vote ( 117 | id int(64) PRIMARY KEY AUTO_INCREMENT NOT NULL, 118 | user_id int(16) NOT NULL, 119 | comment_id varchar(32) NOT NULL, 120 | state tinyint(4) signed NOT NULL DEFAULT 0, -- 1-赞, -1-踩 121 | create_time datetime NOT NULL, 122 | update_time datetime NOT NULL 123 | ) ENGINE = InnoDB DEFAULT CHARSET = utf8; 124 | 125 | # 话题收藏表 126 | CREATE TABLE collection ( 127 | id int(64) PRIMARY KEY AUTO_INCREMENT NOT NULL, 128 | user_id int(16) NOT NULL, 129 | topic_id varchar(32) NOT NULL, 130 | create_time datetime NOT NULL 131 | ) ENGINE = InnoDB DEFAULT CHARSET = utf8; 132 | 133 | # 初始化数据 134 | # 初始化category 135 | INSERT INTO category (id, name) values (1, "问答"), (2, "分享"), (3, "招聘"); 136 | -------------------------------------------------------------------------------- /static/scripts/app/validator.js: -------------------------------------------------------------------------------- 1 | $(function () { 2 | 3 | function Validator(options) { 4 | 5 | var $form = $(options.form); 6 | var $needCheckInputs = $form.find('.input-line input'); 7 | var $errorLine = $form.find('#error-line'); 8 | var $errorMessage = $form.find('#error-message'); 9 | var autoFocusErrorInput = true; 10 | 11 | $needCheckInputs.on({ 12 | blur: function () { 13 | validator.isValid(); 14 | autoFocusErrorInput = false; 15 | } 16 | }); 17 | 18 | $needCheckInputs.on('keyup', function (e) { 19 | 20 | if (e.keyCode === 13) { 21 | 22 | autoFocusErrorInput = true; 23 | options.submit && options.submit(); 24 | } 25 | }); 26 | 27 | var validator = { 28 | 29 | isValid: function() { 30 | var self = this; 31 | var isValid = true; 32 | 33 | self.hideError(); 34 | 35 | for (var i = 0; i < $needCheckInputs.length; i++) { 36 | 37 | if (!self.checkInput($needCheckInputs[i])) { 38 | 39 | isValid = false; 40 | break; 41 | } 42 | } 43 | 44 | return isValid; 45 | }, 46 | 47 | checkInput: function (input) { 48 | var self = this; 49 | var validTasks = ['required', 'min-length', 'max-length', 'pattern', 'equal']; 50 | var $input = $(input); 51 | var isValid = true; 52 | 53 | for (var i = 0; i < validTasks.length; i++) { 54 | 55 | var optsStr = $input.data(validTasks[i]); 56 | 57 | if (!optsStr) continue; 58 | 59 | var opts = JSON.parse(optsStr.replace(/\'/g, '\"')); // 将字符串中的',替换成",否则,JSON.parse报错 60 | 61 | switch (validTasks[i]) { 62 | case 'required': 63 | 64 | if ($.trim($input.val()).length === 0) { 65 | 66 | self.showError($input, opts.message); 67 | 68 | isValid = false; 69 | return isValid; 70 | } 71 | 72 | break; 73 | case 'min-length': 74 | 75 | if ($.trim($input.val()).length < parseInt(opts.value || 0)) { 76 | 77 | self.showError($input, opts.message); 78 | 79 | isValid = false; 80 | return isValid; 81 | } 82 | 83 | break; 84 | case 'max-length': 85 | 86 | if ($.trim($input.val()).length > parseInt(opts.value || 0)) { 87 | 88 | self.showError($input, opts.message); 89 | 90 | isValid = false; 91 | return isValid; 92 | } 93 | 94 | break; 95 | case 'pattern': 96 | 97 | var reg = new RegExp(opts.value); 98 | 99 | if (!reg.test($.trim($input.val()))) { 100 | 101 | self.showError($input, opts.message); 102 | 103 | isValid = false; 104 | return isValid; 105 | } 106 | 107 | break; 108 | case 'equal': 109 | 110 | var $comparedInput = $form.find(opts.value); 111 | 112 | if ($comparedInput.val() !== $input.val()) { 113 | 114 | self.showError($input, opts.message); 115 | 116 | isValid = false; 117 | return isValid; 118 | } 119 | 120 | break; 121 | default: 122 | break; 123 | } 124 | } 125 | 126 | return isValid; 127 | }, 128 | 129 | getValues: function () { 130 | var self = this; 131 | var values = {}; 132 | 133 | for (var i = 0; i < $needCheckInputs.length; i++) { 134 | 135 | var input = $needCheckInputs[i]; 136 | 137 | values[input.name] = $.trim(input.value); 138 | } 139 | 140 | return values; 141 | }, 142 | 143 | showError: function ($input, message) { 144 | var self = this; 145 | 146 | $input && $input.addClass('error'); 147 | $errorLine.show(); 148 | $errorMessage.html(message); 149 | 150 | if (autoFocusErrorInput) { 151 | autoFocusErrorInput = false; 152 | $input && $input.focus(); 153 | } 154 | }, 155 | 156 | hideError: function () { 157 | var self = this; 158 | 159 | $needCheckInputs.removeClass('error'); 160 | $errorLine.hide(); 161 | $errorMessage.html(''); 162 | } 163 | }; 164 | 165 | return validator; 166 | } 167 | 168 | window.Validator = Validator; 169 | }); -------------------------------------------------------------------------------- /static/scripts/app/uploader.js: -------------------------------------------------------------------------------- 1 | $(function () { 2 | 3 | function Uploader(options) { 4 | 5 | var mainTemplate = 6 | '
\ 7 |
\ 8 |
上传图片
\ 9 |
\ 10 |
\ 11 | \ 12 |
\ 13 |
\ 14 | 选择文件\ 15 | 上传\ 16 |
\ 17 |
\ 18 |
\ 19 |
\ 20 |
'; 21 | 22 | var fileTemplate = '
${filename}
'; 23 | 24 | return { 25 | show: function () { 26 | var self = this; 27 | 28 | self.init(); 29 | 30 | $('body').append(mainTemplate); 31 | 32 | self.initElements(); 33 | self.initEvents(); 34 | }, 35 | 36 | hide: function () { 37 | var self = this; 38 | 39 | self.$dialogBg.remove(); 40 | self.$dialogWrapper.remove(); 41 | }, 42 | 43 | init: function () { 44 | var self = this; 45 | 46 | self.files = []; 47 | 48 | }, 49 | 50 | initElements: function () { 51 | var self = this; 52 | 53 | self.$dialogBg = $('#dialog-bg'); 54 | self.$dialogWrapper = $('#dialog-wrapper'); 55 | self.$btnCloseDialog = $('#btn-close-dialog'); 56 | self.$btnSelectFiles = $('#btn-select-files'); 57 | self.$btnUpload = $('#btn-upload'); 58 | self.$formUpload = $('#form-upload'); 59 | self.$inputFiles = $('#input-files'); 60 | self.$fileList = $('#file-list'); 61 | }, 62 | 63 | initEvents: function () { 64 | var self = this; 65 | 66 | self.$btnCloseDialog.on('click', function () { 67 | 68 | self.hide(); 69 | }); 70 | 71 | self.$btnSelectFiles.on('click', function () { 72 | 73 | self.$inputFiles.click(); 74 | }); 75 | 76 | self.$inputFiles.on('change', function (e) { 77 | 78 | self.files = e.target.files ? Array.prototype.slice.call(e.target.files) : []; // 不支持ie9图片上传 79 | 80 | self.renderFileList(); 81 | }); 82 | 83 | self.$btnUpload.on('click', function () { 84 | 85 | if (!self.files.length) return alert('当前未选中任何图片文件'); 86 | 87 | if (!self.checkFileType()) return alert('只能上传图片文件'); 88 | 89 | var formData = new FormData(); 90 | 91 | for (var i = 0; i < self.files.length; i++) { 92 | 93 | formData.append('file' + i, self.files[i]); 94 | } 95 | 96 | $.ajax({ 97 | url: '/upload-image', 98 | type: 'POST', 99 | data: formData, 100 | cache: false, 101 | contentType: false, 102 | processData: false, // 是否需要序列化data 103 | success: function () { 104 | 105 | self.hide(); 106 | options && options.success && options.success(arguments); 107 | }, 108 | error: function () { 109 | 110 | options && options.error && options.error(arguments); 111 | } 112 | }); 113 | }); 114 | 115 | self.$dialogWrapper.on('click', function (e) { 116 | var $target = $(e.target); 117 | 118 | if ($target.closest('.btn-delete-file').length) { 119 | 120 | var $file = $target.closest('.file'); 121 | var index = $file.data('index'); 122 | 123 | $file.remove(); 124 | 125 | self.files && self.files.splice(index, 1); 126 | 127 | self.renderFileList(); 128 | } 129 | }); 130 | }, 131 | 132 | renderFileList: function () { 133 | var self = this; 134 | var str = ''; 135 | 136 | for (var i = 0; i < self.files.length; i++) { 137 | 138 | str += fileTemplate 139 | .replace(/\$\{filename\}/g, self.files[i].name) 140 | .replace(/\$\{index\}/g, i); 141 | } 142 | 143 | self.$fileList.html(str); 144 | }, 145 | 146 | checkFileType: function () { 147 | var self = this; 148 | var isValid = true; 149 | 150 | for (var i = 0; i < self.files.length; i++) { 151 | 152 | var type = self.files[i].type.split('/')[0]; 153 | 154 | if (type !== 'image') { 155 | 156 | isValid = false; 157 | break; 158 | } 159 | } 160 | 161 | return isValid; 162 | } 163 | }; 164 | } 165 | 166 | window.Uploader = Uploader; 167 | }); 168 | 169 | 170 | 171 | -------------------------------------------------------------------------------- /static/scripts/lib/jquery.caret.min.js: -------------------------------------------------------------------------------- 1 | /*! jquery.caret 2016-02-27 */ 2 | !function(a,b){"function"==typeof define&&define.amd?define(["jquery"],function(c){return a.returnExportsGlobal=b(c)}):"object"==typeof exports?module.exports=b(require("jquery")):b(jQuery)}(this,function(a){"use strict";var b,c,d,e,f,g,h,i,j,k,l;k="caret",b=function(){function b(a){this.$inputor=a,this.domInputor=this.$inputor[0]}return b.prototype.setPos=function(a){var b,c,d,e;return(e=j.getSelection())&&(d=0,c=!1,(b=function(a,f){var g,i,j,k,l,m;for(l=f.childNodes,m=[],j=0,k=l.length;k>j&&(g=l[j],!c);j++)if(3===g.nodeType){if(d+g.length>=a){c=!0,i=h.createRange(),i.setStart(g,a-d),e.removeAllRanges(),e.addRange(i);break}m.push(d+=g.length)}else m.push(b(a,g));return m})(a,this.domInputor)),this.domInputor},b.prototype.getIEPosition=function(){return this.getPosition()},b.prototype.getPosition=function(){var a,b;return b=this.getOffset(),a=this.$inputor.offset(),b.left-=a.left,b.top-=a.top,b},b.prototype.getOldIEPos=function(){var a,b;return b=h.selection.createRange(),a=h.body.createTextRange(),a.moveToElementText(this.domInputor),a.setEndPoint("EndToEnd",b),a.text.length},b.prototype.getPos=function(){var a,b,c;return(c=this.range())?(a=c.cloneRange(),a.selectNodeContents(this.domInputor),a.setEnd(c.endContainer,c.endOffset),b=a.toString().length,a.detach(),b):h.selection?this.getOldIEPos():void 0},b.prototype.getOldIEOffset=function(){var a,b;return a=h.selection.createRange().duplicate(),a.moveStart("character",-1),b=a.getBoundingClientRect(),{height:b.bottom-b.top,left:b.left,top:b.top}},b.prototype.getOffset=function(){var b,c,d,e,f;return j.getSelection&&(d=this.range())?(d.endOffset-1>0&&d.endContainer!==this.domInputor&&(b=d.cloneRange(),b.setStart(d.endContainer,d.endOffset-1),b.setEnd(d.endContainer,d.endOffset),e=b.getBoundingClientRect(),c={height:e.height,left:e.left+e.width,top:e.top},b.detach()),c&&0!==(null!=c?c.height:void 0)||(b=d.cloneRange(),f=a(h.createTextNode("|")),b.insertNode(f[0]),b.selectNode(f[0]),e=b.getBoundingClientRect(),c={height:e.height,left:e.left,top:e.top},f.remove(),b.detach())):h.selection&&(c=this.getOldIEOffset()),c&&(c.top+=a(j).scrollTop(),c.left+=a(j).scrollLeft()),c},b.prototype.range=function(){var a;if(j.getSelection)return a=j.getSelection(),a.rangeCount>0?a.getRangeAt(0):null},b}(),c=function(){function b(a){this.$inputor=a,this.domInputor=this.$inputor[0]}return b.prototype.getIEPos=function(){var a,b,c,d,e,f,g;return b=this.domInputor,f=h.selection.createRange(),e=0,f&&f.parentElement()===b&&(d=b.value.replace(/\r\n/g,"\n"),c=d.length,g=b.createTextRange(),g.moveToBookmark(f.getBookmark()),a=b.createTextRange(),a.collapse(!1),e=g.compareEndPoints("StartToEnd",a)>-1?c:-g.moveStart("character",-c)),e},b.prototype.getPos=function(){return h.selection?this.getIEPos():this.domInputor.selectionStart},b.prototype.setPos=function(a){var b,c;return b=this.domInputor,h.selection?(c=b.createTextRange(),c.move("character",a),c.select()):b.setSelectionRange&&b.setSelectionRange(a,a),b},b.prototype.getIEOffset=function(a){var b,c,d,e;return c=this.domInputor.createTextRange(),a||(a=this.getPos()),c.move("character",a),d=c.boundingLeft,e=c.boundingTop,b=c.boundingHeight,{left:d,top:e,height:b}},b.prototype.getOffset=function(b){var c,d,e;return c=this.$inputor,h.selection?(d=this.getIEOffset(b),d.top+=a(j).scrollTop()+c.scrollTop(),d.left+=a(j).scrollLeft()+c.scrollLeft(),d):(d=c.offset(),e=this.getPosition(b),d={left:d.left+e.left-c.scrollLeft(),top:d.top+e.top-c.scrollTop(),height:e.height})},b.prototype.getPosition=function(a){var b,c,e,f,g,h,i;return b=this.$inputor,f=function(a){return a=a.replace(/<|>|`|"|&/g,"?").replace(/\r\n|\r|\n/g,"
"),/firefox/i.test(navigator.userAgent)&&(a=a.replace(/\s/g," ")),a},void 0===a&&(a=this.getPos()),i=b.val().slice(0,a),e=b.val().slice(a),g=""+f(i)+"",g+="|",g+=""+f(e)+"",h=new d(b),c=h.create(g).rect()},b.prototype.getIEPosition=function(a){var b,c,d,e,f;return d=this.getIEOffset(a),c=this.$inputor.offset(),e=d.left-c.left,f=d.top-c.top,b=d.height,{left:e,top:f,height:b}},b}(),d=function(){function b(a){this.$inputor=a}return b.prototype.css_attr=["borderBottomWidth","borderLeftWidth","borderRightWidth","borderTopStyle","borderRightStyle","borderBottomStyle","borderLeftStyle","borderTopWidth","boxSizing","fontFamily","fontSize","fontWeight","height","letterSpacing","lineHeight","marginBottom","marginLeft","marginRight","marginTop","outlineWidth","overflow","overflowX","overflowY","paddingBottom","paddingLeft","paddingRight","paddingTop","textAlign","textOverflow","textTransform","whiteSpace","wordBreak","wordWrap"],b.prototype.mirrorCss=function(){var b,c=this;return b={position:"absolute",left:-9999,top:0,zIndex:-2e4},"TEXTAREA"===this.$inputor.prop("tagName")&&this.css_attr.push("width"),a.each(this.css_attr,function(a,d){return b[d]=c.$inputor.css(d)}),b},b.prototype.create=function(b){return this.$mirror=a("
"),this.$mirror.css(this.mirrorCss()),this.$mirror.html(b),this.$inputor.after(this.$mirror),this},b.prototype.rect=function(){var a,b,c;return a=this.$mirror.find("#caret"),b=a.position(),c={left:b.left,top:b.top,height:a.height()},this.$mirror.remove(),c},b}(),e={contentEditable:function(a){return!(!a[0].contentEditable||"true"!==a[0].contentEditable)}},g={pos:function(a){return a||0===a?this.setPos(a):this.getPos()},position:function(a){return h.selection?this.getIEPosition(a):this.getPosition(a)},offset:function(a){var b;return b=this.getOffset(a)}},h=null,j=null,i=null,l=function(a){var b;return(b=null!=a?a.iframe:void 0)?(i=b,j=b.contentWindow,h=b.contentDocument||j.document):(i=void 0,j=window,h=document)},f=function(a){var b;h=a[0].ownerDocument,j=h.defaultView||h.parentWindow;try{return i=j.frameElement}catch(c){b=c}},a.fn.caret=function(d,f,h){var i;return g[d]?(a.isPlainObject(f)?(l(f),f=void 0):l(h),i=e.contentEditable(this)?new b(this):new c(this),g[d].apply(i,[f])):a.error("Method "+d+" does not exist on jQuery.caret")},a.fn.caret.EditableCaret=b,a.fn.caret.InputCaret=c,a.fn.caret.Utils=e,a.fn.caret.apis=g}); -------------------------------------------------------------------------------- /static/styles/mobile.css: -------------------------------------------------------------------------------- 1 | body, div, ul, dl, ol, dt, dd, li, p, a, label, span, i, img, form, input, textarea, header, footer, article, section, nav, aside, figure, h1, h2, h3, h4, h5, h6, table, thead, tbody, tr, th, td, pre { 2 | padding: 0; 3 | margin: 0; 4 | border: 0; 5 | -webkit-box-sizing: border-box; 6 | -moz-box-sizing: border-box; 7 | box-sizing: border-box; 8 | } 9 | 10 | ul, dl, ol { 11 | list-style: none; 12 | } 13 | 14 | header, footer, article, section, nav, aside, figure { 15 | display: block; 16 | } 17 | 18 | input input::-webkit-input-placeholder, textarea input::-webkit-input-placeholder { 19 | color: red !important; 20 | font-size: 12px; 21 | } 22 | input input::-moz-placeholder, textarea input::-moz-placeholder { 23 | color: red !important; 24 | font-size: 12px; 25 | } 26 | input input:-ms-input-placeholder, textarea input:-ms-input-placeholder { 27 | color: red !important; 28 | font-size: 12px; 29 | } 30 | 31 | input, textarea, a, button, select { 32 | outline: none; 33 | } 34 | 35 | input[type="button"], input[type="submit"], input[type="reset"], textarea, button { 36 | -webkit-appearance: none; 37 | -webkit-border-radius: 0; 38 | -moz-border-radius: 0; 39 | border-radius: 0; 40 | border: 0; 41 | } 42 | 43 | input:focus, textarea:focus, a:focus, button:focus { 44 | -webkit-tap-highlight-color: rgba(255, 255, 255, 0); 45 | } 46 | 47 | input::-ms-clear { 48 | display: none; 49 | } 50 | input::-ms-reveal { 51 | display: none; 52 | } 53 | input::-webkit-search-cancel-button { 54 | display: none; 55 | } 56 | 57 | a { 58 | text-decoration: none; 59 | } 60 | 61 | .m-frame-header { 62 | width: 100%; 63 | height: 4.5rem; 64 | position: fixed; 65 | background: #fff; 66 | display: none; 67 | top: 0; 68 | z-index: 100; 69 | border-bottom: 0.1rem solid #f0f0f0; 70 | } 71 | .m-frame-header .btn { 72 | height: 4.4rem; 73 | text-align: center; 74 | line-height: 4.4rem; 75 | font-size: 1.4rem; 76 | color: #333; 77 | padding: 0 .5rem; 78 | } 79 | .m-frame-header .btn-menu { 80 | float: left; 81 | font-size: 2rem; 82 | width: 4.4rem; 83 | padding: 0; 84 | } 85 | .m-frame-header .box-btns { 86 | float: right; 87 | height: 4.4rem; 88 | line-height: 4.4rem; 89 | } 90 | .m-frame-header .icon-red-dot { 91 | position: relative; 92 | width: .6rem; 93 | height: .6rem; 94 | -webkit-border-radius: 50%; 95 | -moz-border-radius: 50%; 96 | border-radius: 50%; 97 | background: red; 98 | top: -.8rem; 99 | } 100 | .m-frame-header .title { 101 | padding: 0 15rem; 102 | text-align: center; 103 | height: 4.4rem; 104 | line-height: 4.4rem; 105 | } 106 | .m-frame-header .title a { 107 | display: inline-block; 108 | height: 3.2rem; 109 | width: 3.2rem; 110 | vertical-align: middle; 111 | } 112 | .m-frame-header .title img { 113 | height: 3.2rem; 114 | width: 3.2rem; 115 | vertical-align: top; 116 | } 117 | 118 | .m-nav-bg { 119 | background: rgba(0, 0, 0, 0.5); 120 | position: fixed; 121 | left: 0; 122 | top: 0; 123 | right: 0; 124 | bottom: 0; 125 | z-index: 90; 126 | display: none; 127 | } 128 | 129 | .m-nav { 130 | width: 100%; 131 | position: fixed; 132 | top: 4.5rem; 133 | background: #fff; 134 | display: none; 135 | z-index: 100; 136 | } 137 | .m-nav a { 138 | display: block; 139 | width: 100%; 140 | height: 3.7rem; 141 | line-height: 3.6rem; 142 | border-bottom: 0.1rem solid #f0f0f0; 143 | padding: 0 1rem 0 5rem; 144 | font-size: 1.4rem; 145 | color: #333; 146 | position: relative; 147 | } 148 | .m-nav a .icon { 149 | width: 4.4rem; 150 | height: 3.6rem; 151 | line-height: 3.6rem; 152 | position: absolute; 153 | left: 0; 154 | top: 0; 155 | text-align: center; 156 | font-size: 2rem; 157 | } 158 | 159 | .m-search { 160 | display: none; 161 | } 162 | .m-search .search { 163 | height: 5rem; 164 | padding: 1rem 1.5rem 1rem 1.5rem; 165 | position: relative; 166 | } 167 | .m-search .search input { 168 | width: 100%; 169 | padding: .4rem 3rem .4rem .8rem; 170 | } 171 | .m-search .search .btn-search { 172 | position: absolute; 173 | width: 3rem; 174 | height: 3rem; 175 | line-height: 3rem; 176 | right: 1.5rem; 177 | top: 1rem; 178 | z-index: 10; 179 | text-align: center; 180 | font-size: 1.8rem; 181 | color: #999; 182 | } 183 | 184 | @media only screen and (max-width: 768px) { 185 | body { 186 | min-width: 32rem; 187 | } 188 | 189 | .frame-wrapper { 190 | width: 100%; 191 | display: block; 192 | min-width: 32rem; 193 | max-width: 76.8rem; 194 | } 195 | 196 | .frame-header { 197 | display: none; 198 | } 199 | 200 | .frame-content { 201 | padding: 4.5rem 0 0; 202 | } 203 | .frame-content .block-main { 204 | width: 100%; 205 | display: block; 206 | } 207 | 208 | .frame-footer { 209 | padding: 1.5rem 1rem; 210 | } 211 | 212 | .comment .info dt .operator { 213 | margin-top: .5rem; 214 | width: 100%; 215 | float: left; 216 | } 217 | 218 | .block-aside { 219 | display: block; 220 | width: 100%; 221 | padding-left: 0; 222 | } 223 | 224 | .topic dd .right { 225 | float: left; 226 | width: 100%; 227 | margin-top: .4rem; 228 | } 229 | 230 | .m-frame-header { 231 | display: block; 232 | } 233 | 234 | .m-search { 235 | display: block; 236 | } 237 | 238 | .frame-form .input-line label { 239 | width: 10rem; 240 | } 241 | .frame-form .input-line .input-wrapper { 242 | padding-left: 11.5rem; 243 | } 244 | .frame-form .input-line .input-wrapper input { 245 | width: 100%; 246 | } 247 | .frame-form .help-line .help-wrapper { 248 | padding: 0 0 0 11.5rem; 249 | width: 100%; 250 | } 251 | .frame-form .error-line .error-wrapper { 252 | padding: 0 0 0 11.5rem; 253 | width: 100%; 254 | } 255 | .frame-form .btn-line .btn-wrapper { 256 | padding: 0 0 0 11.5rem; 257 | width: 100%; 258 | } 259 | 260 | .not-found .info { 261 | font-size: 2.8rem; 262 | line-height: 3.8rem; 263 | } 264 | .not-found a { 265 | font-size: 1.8rem; 266 | } 267 | 268 | .no-data { 269 | height: 6rem; 270 | line-height: 6rem; 271 | } 272 | } 273 | 274 | /*# sourceMappingURL=mobile.css.map */ 275 | -------------------------------------------------------------------------------- /src/controllers/comment.rs: -------------------------------------------------------------------------------- 1 | use iron::prelude::*; 2 | use regex::{Regex, Captures}; 3 | 4 | use common::http::*; 5 | use common::utils::*; 6 | use services::comment::*; 7 | use services::comment::create_comment as service_create_comment; 8 | use services::comment::delete_comment as service_delete_comment; 9 | use services::comment_vote::*; 10 | use services::user::get_user_id; 11 | use services::topic::get_topic; 12 | use services::message::create_message; 13 | use controllers::upload::sync_upload_file; 14 | 15 | pub fn create_comment(req: &mut Request) -> IronResult { 16 | 17 | let params = get_request_body(req); 18 | let user_id = ¶ms.get("userId").unwrap()[0]; 19 | let topic_id = ¶ms.get("topicId").unwrap()[0]; 20 | let content = ¶ms.get("content").unwrap()[0]; 21 | 22 | let reg = Regex::new(r"\B@([\da-zA-Z_]+)").unwrap(); 23 | let mut mentions: Vec = Vec::new(); 24 | let new_content = reg.replace_all(&content, |caps: &Captures| { 25 | 26 | let username = caps.get(1).unwrap().as_str(); 27 | let user_id = get_user_id(username); 28 | 29 | if user_id == 0 { 30 | 31 | format!("@{}", username) 32 | } else { 33 | mentions.push(user_id); 34 | 35 | format!("[@{}]({}{})", username, "/user/", username) 36 | } 37 | }); 38 | 39 | let obj = json!({ 40 | "user_id": user_id.to_owned(), 41 | "topic_id": topic_id.to_owned(), 42 | "content": sync_upload_file(&*new_content.to_string()) 43 | }); 44 | 45 | let result = service_create_comment(&obj); 46 | 47 | let mut data = JsonData::new(); 48 | 49 | if result.is_none() { 50 | 51 | data.success = false; 52 | data.message = "回复失败".to_string(); 53 | 54 | return respond_json(&data); 55 | } 56 | 57 | let comment_id = result.unwrap(); 58 | let topic = get_topic(topic_id).unwrap(); 59 | 60 | if topic.user_id != user_id.parse::().unwrap() { // 忽略作者自己的回复 61 | create_message(&json!({ 62 | "comment_id": comment_id, 63 | "topic_id": topic_id.to_owned(), 64 | "from_user_id": user_id.to_owned(), 65 | "to_user_id": topic.user_id, 66 | "type": 0 67 | })); 68 | } 69 | 70 | mentions.dedup(); 71 | 72 | for mention in mentions.iter().filter(|&id| *id != topic.user_id && *id != user_id.parse::().unwrap()) { // 忽略@作者或自己 73 | 74 | create_message(&json!({ 75 | "comment_id": comment_id, 76 | "topic_id": topic_id.to_owned(), 77 | "from_user_id": user_id.to_owned(), 78 | "to_user_id": mention, 79 | "type": 1 80 | })); 81 | } 82 | 83 | data.message = "发表评论成功".to_owned(); 84 | data.data = json!("/topic/".to_string() + topic_id); 85 | 86 | respond_json(&data) 87 | } 88 | 89 | pub fn render_edit_comment(req: &mut Request) -> IronResult { 90 | 91 | let params = get_router_params(req); 92 | let comment_id = params.find("comment_id").unwrap(); 93 | 94 | if !is_comment_created(comment_id) { 95 | 96 | return redirect_to("/not-found"); 97 | } 98 | 99 | let content_wrapper = get_comment_content(comment_id); 100 | 101 | if content_wrapper.is_none() { 102 | 103 | return redirect_to("/not-found"); 104 | } 105 | 106 | let content = content_wrapper.unwrap(); 107 | 108 | let mut data = ViewData::new(req); 109 | 110 | data.insert("comment_id", json!(comment_id.to_string())); 111 | data.insert("content", json!(content)); 112 | 113 | respond_view("comment-editor", &data) 114 | } 115 | 116 | pub fn edit_comment(req: &mut Request) -> IronResult { 117 | 118 | let params = get_router_params(req); 119 | let body = get_request_body(req); 120 | let comment_id = params.find("comment_id").unwrap(); 121 | let content = &body.get("content").unwrap()[0]; 122 | 123 | let mut data = JsonData::new(); 124 | 125 | if !is_comment_created(comment_id) { 126 | 127 | data.success = false; 128 | data.message = "未找到该回复".to_owned(); 129 | 130 | return respond_json(&data); 131 | } 132 | 133 | let result = update_comment(comment_id, &json!({ 134 | "comment_id": comment_id.to_owned(), 135 | "content": sync_upload_file(content) 136 | })); 137 | 138 | if result.is_none() { 139 | 140 | data.success = false; 141 | data.message = "修改回复失败".to_owned(); 142 | 143 | return respond_json(&data); 144 | } 145 | 146 | let topic_id = &*get_comment(comment_id).unwrap().topic_id; 147 | 148 | data.message = "修改回复成功".to_owned(); 149 | data.data = json!("/topic/".to_string() + topic_id); 150 | 151 | respond_json(&data) 152 | } 153 | 154 | pub fn delete_comment(req: &mut Request) -> IronResult { 155 | 156 | let params = get_router_params(req); 157 | let comment_id = params.find("comment_id").unwrap(); 158 | let body = get_request_body(req); 159 | let topic_id = &body.get("topicId").unwrap()[0]; 160 | 161 | let mut data = JsonData::new(); 162 | 163 | if !is_comment_created(comment_id) { 164 | 165 | data.success = false; 166 | data.message = "未找到该回复".to_owned(); 167 | 168 | return respond_json(&data); 169 | } 170 | 171 | let result = service_delete_comment(comment_id); 172 | 173 | if result.is_none() { 174 | 175 | data.success = false; 176 | data.message = "删除回复失败".to_owned(); 177 | 178 | return respond_json(&data); 179 | } 180 | 181 | data.message = "删除回复成功".to_owned(); 182 | data.data = json!("/topic/".to_owned() + topic_id); 183 | 184 | respond_json(&data) 185 | } 186 | 187 | pub fn vote_comment(req: &mut Request) -> IronResult { 188 | 189 | let params = get_router_params(req); 190 | let comment_id = params.find("comment_id").unwrap(); 191 | let body = get_request_body(req); 192 | let user_id = &body.get("userId").unwrap()[0]; 193 | let state = &body.get("state").unwrap()[0]; 194 | let result; 195 | 196 | if state == "0" { 197 | 198 | result = delete_comment_vote(user_id, comment_id); 199 | } else { 200 | 201 | if is_voted(user_id, comment_id) { 202 | result = update_comment_vote(user_id, comment_id, state); 203 | } else { 204 | result = create_comment_vote(user_id, comment_id, state); 205 | } 206 | } 207 | 208 | let mut data = JsonData::new(); 209 | 210 | if result.is_none() { 211 | 212 | data.success = false; 213 | data.message = "更新失败".to_owned(); 214 | } 215 | 216 | respond_json(&data) 217 | } 218 | -------------------------------------------------------------------------------- /src/controllers/topic_list.rs: -------------------------------------------------------------------------------- 1 | use iron::prelude::*; 2 | use serde_json::Value; 3 | 4 | use common::http::*; 5 | use common::utils::*; 6 | use services::topic::*; 7 | use services::user::get_user_id; 8 | use services::comment::get_last_comment_by_topic_id; 9 | 10 | pub fn render_default_topic_list(req: &mut Request) -> IronResult { 11 | 12 | render_topic_list("default", req) 13 | } 14 | 15 | pub fn render_essence_topic_list(req: &mut Request) -> IronResult { 16 | 17 | render_topic_list("essence", req) 18 | } 19 | 20 | pub fn render_latest_topic_list(req: &mut Request) -> IronResult { 21 | 22 | render_topic_list("latest", req) 23 | } 24 | 25 | pub fn render_no_reply_topic_list(req: &mut Request) -> IronResult { 26 | 27 | render_topic_list("no-reply", req) 28 | } 29 | 30 | pub fn render_ask_topic_list(req: &mut Request) -> IronResult { 31 | 32 | render_topic_list("ask", req) 33 | } 34 | 35 | pub fn render_share_topic_list(req: &mut Request) -> IronResult { 36 | 37 | render_topic_list("share", req) 38 | } 39 | 40 | pub fn render_job_topic_list(req: &mut Request) -> IronResult { 41 | 42 | render_topic_list("job", req) 43 | } 44 | 45 | fn render_topic_list(tab_code: &str, req: &mut Request) -> IronResult { 46 | 47 | let page: u32 = get_query_page(req); 48 | let mut data = ViewData::new(req); 49 | let base_url; 50 | 51 | match tab_code { 52 | "essence" => { 53 | data.insert("title", json!("首页-精华")); 54 | data.insert("is_essence_active", json!(true)); 55 | base_url = "/topics/essence?page="; 56 | } 57 | "latest" => { 58 | data.insert("title", json!("首页-最新")); 59 | data.insert("is_latest_active", json!(true)); 60 | base_url = "/topics/latest?page="; 61 | } 62 | "no-reply" => { 63 | data.insert("title", json!("首页-待回复")); 64 | data.insert("is_no_reply_active", json!(true)); 65 | base_url = "/topics/no-reply?page="; 66 | } 67 | "ask" => { 68 | data.insert("title", json!("首页-问答")); 69 | data.insert("is_ask_active", json!(true)); 70 | base_url = "/topics/ask?page="; 71 | } 72 | "share" => { 73 | data.insert("title", json!("首页-分享")); 74 | data.insert("is_share_active", json!(true)); 75 | base_url = "/topics/share?page="; 76 | } 77 | "job" => { 78 | data.insert("title", json!("首页-招聘")); 79 | data.insert("is_job_active", json!(true)); 80 | base_url = "/topics/job?page="; 81 | } 82 | _ => { 83 | data.insert("title", json!("首页")); 84 | data.insert("is_default_active", json!(true)); 85 | base_url = "/?page="; 86 | } 87 | } 88 | 89 | let list = get_topic_list(tab_code, page); 90 | let list_count = get_topic_list_count(tab_code); 91 | 92 | let topic_list = rebuild_topic_list(&list); 93 | let pagination = build_pagination(page, list_count, base_url); 94 | 95 | data.insert("has_topic_list", json!(topic_list.len())); 96 | data.insert("topic_list", json!(topic_list)); 97 | data.insert("pagination", json!(pagination)); 98 | 99 | respond_view("topic-list", &data) 100 | } 101 | 102 | pub fn render_user_topics(req: &mut Request) -> IronResult { 103 | 104 | render_user_topic_list("topics", req) 105 | } 106 | 107 | pub fn render_user_comments(req: &mut Request) -> IronResult { 108 | 109 | render_user_topic_list("comments", req) 110 | } 111 | 112 | pub fn render_user_collections(req: &mut Request) -> IronResult { 113 | 114 | render_user_topic_list("collections", req) 115 | } 116 | 117 | fn render_user_topic_list(tab_code: &str, req: &mut Request) -> IronResult { 118 | 119 | let params = get_router_params(req); 120 | let username = params.find("username").unwrap(); 121 | let username_string = username.to_string(); 122 | let user_id = get_user_id(username); 123 | let page: u32 = get_query_page(req); 124 | let mut data = ViewData::new(req); 125 | let base_url; 126 | 127 | match tab_code { 128 | "comments" => { 129 | data.insert("title", json!(username_string + "的回复")); 130 | base_url = "/".to_string() + username + "/comments?page="; 131 | } 132 | "collections" => { 133 | data.insert("title", json!(username_string + "的收藏")); 134 | base_url = "/".to_string() + username + "/collections?page="; 135 | } 136 | _ => { 137 | data.insert("title", json!(username_string + "的话题")); 138 | base_url = "/".to_string() + username + "/topics?page="; 139 | } 140 | } 141 | 142 | data.insert("is_show_crumbs", json!(true)); 143 | 144 | let list = get_user_topic_list(tab_code, user_id, page); 145 | let list_count = get_user_topic_list_count(tab_code, user_id); 146 | 147 | let topic_list = rebuild_topic_list(&list); 148 | let pagination = build_pagination(page, list_count, &*base_url); 149 | 150 | data.insert("has_topic_list", json!(topic_list.len())); 151 | data.insert("topic_list", json!(topic_list)); 152 | data.insert("pagination", json!(pagination)); 153 | 154 | respond_view("topic-list", &data) 155 | } 156 | 157 | pub fn render_search_result(req: &mut Request) -> IronResult { 158 | 159 | let page: u32 = get_query_page(req); 160 | let params = get_request_query(req); 161 | let keyword = &*params.get("keyword").unwrap()[0]; 162 | let base_url = "/search?keyword=".to_string() + keyword + "&page="; 163 | 164 | let mut data = ViewData::new(req); 165 | 166 | data.insert("title", json!("搜索结果")); 167 | data.insert("is_show_crumbs", json!(true)); 168 | 169 | let list = get_search_topic_list(keyword, page); 170 | let list_count = get_search_topic_list_count(keyword); 171 | 172 | let topic_list = rebuild_topic_list(&list); 173 | let pagination = build_pagination(page, list_count, &*base_url); 174 | 175 | data.insert("search_keyword", json!(keyword)); 176 | data.insert("has_topic_list", json!(topic_list.len())); 177 | data.insert("topic_list", json!(topic_list)); 178 | data.insert("pagination", json!(pagination)); 179 | 180 | respond_view("topic-list", &data) 181 | } 182 | 183 | fn rebuild_topic_list(topics: &Vec) -> Vec { 184 | 185 | let mut vec = Vec::new(); 186 | 187 | for topic in topics.into_iter() { 188 | 189 | let topic_id = topic["topic_id"].as_str().unwrap(); 190 | 191 | vec.push(json!({ 192 | "topic": topic, 193 | "comment": get_last_comment_by_topic_id(topic_id) 194 | })); 195 | } 196 | 197 | vec 198 | } 199 | -------------------------------------------------------------------------------- /src/controllers/upload.rs: -------------------------------------------------------------------------------- 1 | use std::fs::{File, DirBuilder, read_dir, metadata, remove_file}; 2 | use std::path::Path; 3 | use std::error::Error; 4 | use std::io::prelude::*; 5 | use std::time::{Duration, SystemTime}; 6 | use std::thread::{self, sleep}; 7 | 8 | use iron::prelude::*; 9 | use uuid::Uuid; 10 | use serde_json::Value; 11 | use multipart::server::{Multipart, Entries, SaveResult, SavedFile}; 12 | use schedule::{Agenda, Job}; 13 | use regex::{Regex, Captures}; 14 | 15 | use common::http::*; 16 | use common::utils::get_file_ext; 17 | use common::lazy_static::{CONFIG_TABLE, UPLOAD_PATH, UPLOAD_TEMP_PATH, UPLOAD_ASSETS_PATH}; 18 | 19 | pub fn create_upload_folder() { 20 | 21 | DirBuilder::new() 22 | .recursive(true) 23 | .create(&*UPLOAD_TEMP_PATH).unwrap(); 24 | 25 | DirBuilder::new() 26 | .recursive(true) 27 | .create(&*UPLOAD_ASSETS_PATH).unwrap(); 28 | } 29 | 30 | pub fn upload_file(req: &mut Request) -> IronResult { 31 | 32 | match Multipart::from_request(req) { 33 | 34 | Ok(mut multipart) => { 35 | 36 | match multipart.save().temp() { 37 | 38 | SaveResult::Full(entries) => process_entries(entries), 39 | 40 | SaveResult::Partial(_entries, _reason) => { 41 | 42 | respond_text("部分保存成功") 43 | } 44 | 45 | SaveResult::Error(_err) => { 46 | 47 | respond_text("保存失败") 48 | } 49 | } 50 | } 51 | 52 | _ => { 53 | 54 | respond_text("上传出错") 55 | } 56 | } 57 | } 58 | 59 | fn process_entries(entries: Entries) -> IronResult { 60 | 61 | let mut temp_file_list = vec![]; 62 | 63 | for (_name, files) in entries.files { 64 | 65 | for file in files { 66 | 67 | create_temp_file(&file, &mut temp_file_list); 68 | } 69 | } 70 | 71 | let mut data = JsonData::new(); 72 | 73 | data.data = json!(&temp_file_list); 74 | 75 | respond_json(&data) 76 | } 77 | 78 | fn create_temp_file(saved_file: &SavedFile, temp_file_list: &mut Vec ) { 79 | 80 | let original_filename = &*saved_file.filename.clone().unwrap(); 81 | let ext = get_file_ext(original_filename).unwrap_or(""); 82 | let uuid_filename = Uuid::new_v4().to_string() + "." + ext; 83 | let dest_path = UPLOAD_TEMP_PATH.to_owned() + "/" + &*uuid_filename; 84 | let path = Path::new(&dest_path); 85 | let dest_name = path.display(); 86 | let mut data = Vec::new(); 87 | 88 | let mut temp_file = match File::open(&saved_file.path) { 89 | Ok(file) => file, 90 | Err(err) => panic!("can't open file: {}", err.description()) 91 | }; 92 | 93 | temp_file_list.push(json!({ 94 | "filename": saved_file.filename.clone().unwrap(), 95 | "path": &path.to_owned() 96 | })); 97 | 98 | temp_file.read_to_end(&mut data).expect("unable to read data"); 99 | 100 | let mut new_file = match File::create(&path) { 101 | Ok(file) => file, 102 | Err(err) => panic!("can't create file {}: {}", dest_name, err.description()) 103 | }; 104 | 105 | match new_file.write_all(&data) { 106 | Ok(_) => (), 107 | Err(err) => panic!("can't wrote to file {}: {}", dest_path, err.description()) 108 | } 109 | } 110 | 111 | pub fn run_clean_temp_task() { 112 | 113 | let upload_config = CONFIG_TABLE.get("upload").unwrap().as_table().unwrap(); 114 | let ttl = upload_config.get("clean_temp_dir_ttl").unwrap().as_integer().unwrap() as u64; 115 | let upload_temp_path = upload_config.get("temp_path").unwrap().as_str().unwrap(); 116 | 117 | thread::Builder::new() 118 | .name("run_clean_temp_task".to_string()) 119 | .stack_size(4 * 1024 * 1024) 120 | .spawn(move || { 121 | 122 | let mut agenda = Agenda::new(); 123 | let temp_dir_path = Path::new(&*upload_temp_path); 124 | 125 | agenda.add(Job::new(move || { 126 | 127 | let now = SystemTime::now(); 128 | let one_day = Duration::from_millis(1000 * 60 * 60 * 24); 129 | 130 | for file_wrapper in read_dir(&temp_dir_path).unwrap() { 131 | let file = file_wrapper.unwrap(); 132 | let file_path = file.path(); 133 | let create_time = metadata(&file_path).unwrap().created().unwrap(); 134 | 135 | if now.duration_since(create_time).unwrap() > one_day { // 已创建但未保存时间超过一天 136 | 137 | remove_file(&file_path).unwrap(); 138 | } 139 | } 140 | 141 | }, "* * * * * *".parse().unwrap())); 142 | 143 | loop { 144 | agenda.run_pending(); 145 | 146 | sleep(Duration::from_millis(ttl)); 147 | } 148 | }).unwrap(); 149 | } 150 | 151 | /// 将临时文件夹中的相关文件,剪切到UPLOAD_ASSETS_PATH文件夹中 152 | pub fn sync_upload_file(content: &str) -> String { 153 | 154 | let upload_temp_path = UPLOAD_PATH.to_owned() + "/" + &*UPLOAD_TEMP_PATH.to_owned() + "/"; 155 | let upload_assets_path = UPLOAD_PATH.to_owned() + "/" + &*UPLOAD_ASSETS_PATH.to_owned() + "/"; 156 | let reg_str = format!("\\({0}([-._0-9a-zA-Z]+).?\\)", upload_temp_path); 157 | 158 | let reg = Regex::new(&*reg_str).unwrap(); 159 | let mut files: Vec = Vec::new(); 160 | let new_content = reg.replace_all(&content, |caps: &Captures| { 161 | 162 | let filename = caps.get(1).unwrap().as_str(); 163 | 164 | files.push(filename.to_owned()); 165 | 166 | format!("({0}{1})", upload_assets_path, filename) 167 | }); 168 | 169 | 170 | for filename in files { 171 | 172 | let source_str = UPLOAD_TEMP_PATH.to_owned() + "/" + &*filename; 173 | let dest_str = UPLOAD_ASSETS_PATH.to_owned() + "/" + &*filename; 174 | 175 | { 176 | let source_path = Path::new(&*source_str); 177 | let dest_path = Path::new(&*dest_str); 178 | 179 | copy_and_delete_file(&*source_path, &*dest_path); 180 | } 181 | } 182 | 183 | new_content.to_string() 184 | } 185 | 186 | fn copy_and_delete_file(source_path: &Path, dest_path: &Path) { 187 | 188 | let mut data = Vec::new(); 189 | 190 | let mut temp_file = match File::open(source_path) { 191 | Ok(file) => file, 192 | Err(err) => panic!("can't open file: {}", err.description()) 193 | }; 194 | 195 | temp_file.read_to_end(&mut data).expect("unable to read data"); 196 | 197 | let mut new_file = match File::create(dest_path) { 198 | Ok(file) => file, 199 | Err(err) => panic!("can't create file {}", err.description()) 200 | }; 201 | 202 | match new_file.write_all(&data) { 203 | Ok(_) => { 204 | remove_file(source_path).unwrap(); 205 | () 206 | }, 207 | Err(err) => panic!("can't wrote to file {:?}: {}", dest_path, err.description()) 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /src/services/comment.rs: -------------------------------------------------------------------------------- 1 | use mysql::from_row; 2 | use mysql::error::Error::MySqlError; 3 | use serde_json::Value; 4 | use chrono::NaiveDateTime; 5 | 6 | use common::utils::*; 7 | use common::lazy_static::SQL_POOL; 8 | use models::comment::Comment; 9 | 10 | pub fn create_comment(comment: &Value) -> Option { 11 | 12 | let create_time = gen_datetime().to_string(); 13 | let topic_id = comment["topic_id"].as_str().unwrap(); 14 | let comment_id = gen_md5(&*(topic_id.to_string() + &*create_time)); 15 | 16 | let mut stmt = SQL_POOL.prepare(r#" 17 | INSERT INTO comment 18 | (id, user_id, topic_id, content, create_time, update_time) 19 | VALUES (?, ?, ?, ?, ?, ?); 20 | "#).unwrap(); 21 | 22 | let result = stmt.execute(( 23 | &*comment_id, 24 | comment["user_id"].as_str().unwrap(), 25 | topic_id, 26 | comment["content"].as_str().unwrap(), 27 | &*create_time, 28 | &*create_time 29 | )); 30 | 31 | if let Err(MySqlError(ref err)) = result { 32 | 33 | println!("{:?}", err.message); 34 | return None; 35 | } 36 | 37 | Some(comment_id) 38 | } 39 | 40 | pub fn update_comment(comment_id: &str, comment: &Value) -> Option { 41 | 42 | let update_time = gen_datetime().to_string(); 43 | 44 | let mut stmt = SQL_POOL.prepare(r#" 45 | UPDATE comment SET 46 | content = ?, 47 | update_time = ? 48 | WHERE id = ? 49 | "#).unwrap(); 50 | let result = stmt.execute(( 51 | comment["content"].as_str().unwrap(), 52 | &*update_time, 53 | comment_id 54 | )); 55 | 56 | if let Err(MySqlError(ref err)) = result { 57 | 58 | println!("{:?}", err.message); 59 | return None; 60 | } 61 | 62 | Some(comment_id.to_string()) 63 | } 64 | 65 | pub fn delete_comment(comment_id: &str) -> Option { 66 | 67 | let result = SQL_POOL.prep_exec("DELETE FROM comment WHERE id = ?", (comment_id, )); 68 | 69 | if let Err(MySqlError(ref err)) = result { 70 | 71 | println!("{:?}", err.message); 72 | return None; 73 | } 74 | 75 | Some(comment_id.to_string()) 76 | } 77 | 78 | pub fn is_comment_created(comment_id: &str) -> bool { 79 | 80 | let mut result = SQL_POOL.prep_exec("SELECT count(id) FROM comment WHERE id = ?", (comment_id, )).unwrap(); 81 | let row_wrapper = result.next(); 82 | 83 | if row_wrapper.is_none() { 84 | return false; 85 | } 86 | 87 | let row = row_wrapper.unwrap().unwrap(); 88 | let (count, ) = from_row::<(u8, )>(row); 89 | 90 | if count == 0 { 91 | false 92 | } else { 93 | true 94 | } 95 | } 96 | 97 | pub fn get_comment(comment_id: &str) -> Option { 98 | 99 | let mut result = SQL_POOL.prep_exec(r#" 100 | SELECT 101 | c.id, user_id, username, avatar_url, topic_id, content, 102 | (SELECT count(id) FROM comment_vote WHERE state = 1 AND comment_id = c.id) AS agree_count, 103 | (SELECT count(id) FROM comment_vote WHERE state = -1 AND comment_id = c.id) AS disagree_count, 104 | c.status, c.create_time, c.update_time 105 | FROM comment AS c 106 | LEFT JOIN 107 | user AS u 108 | ON c.user_id = u.id 109 | WHERE c.id = ? 110 | "#, (comment_id, )).unwrap(); 111 | let row_wrapper = result.next(); 112 | 113 | if row_wrapper.is_none() { 114 | return None; 115 | } 116 | 117 | let mut row = row_wrapper.unwrap().unwrap(); 118 | 119 | Some(Comment { 120 | id: row.get::(0).unwrap(), 121 | user_id: row.get::(1).unwrap(), 122 | username: row.get::(2).unwrap(), 123 | avatar_url: row.get::(3).unwrap(), 124 | topic_id: row.get::(4).unwrap(), 125 | content: parse_to_html(&*row.get::(5).unwrap()), 126 | agree_count: row.get::(6).unwrap(), 127 | disagree_count: row.get::(7).unwrap(), 128 | status: row.get::(8).unwrap(), 129 | create_time: row.get::(9).unwrap(), 130 | update_time: row.get::(10).unwrap() 131 | }) 132 | } 133 | 134 | pub fn get_comment_content(comment_id: &str) -> Option { 135 | 136 | let mut result = SQL_POOL.prep_exec("SELECT content FROM comment WHERE id = ?", (comment_id, )).unwrap(); 137 | let row_wrapper = result.next(); 138 | 139 | if row_wrapper.is_none() { 140 | return None; 141 | } 142 | 143 | let mut row = row_wrapper.unwrap().unwrap(); 144 | 145 | Some(row.get::(0).unwrap()) 146 | } 147 | 148 | pub fn get_comments_by_topic_id(topic_id: &str) -> Vec { 149 | 150 | let result = SQL_POOL.prep_exec(r#" 151 | SELECT 152 | c.id, user_id, username, avatar_url, topic_id, content, 153 | (SELECT count(id) FROM comment_vote WHERE state = 1 AND comment_id = c.id) AS agree_count, 154 | (SELECT count(id) FROM comment_vote WHERE state = -1 AND comment_id = c.id) AS disagree_count, 155 | c.status, c.create_time, c.update_time 156 | FROM comment AS c 157 | LEFT JOIN 158 | user AS u 159 | ON c.user_id = u.id 160 | WHERE topic_id = ? 161 | ORDER BY create_time ASC 162 | "#, (topic_id, )).unwrap(); 163 | 164 | result.map(|row_wrapper| row_wrapper.unwrap()) 165 | .map(|mut row| { 166 | Comment { 167 | id: row.get::(0).unwrap(), 168 | user_id: row.get::(1).unwrap(), 169 | username: row.get::(2).unwrap(), 170 | avatar_url: row.get::(3).unwrap(), 171 | topic_id: row.get::(4).unwrap(), 172 | content: parse_to_html(&*row.get::(5).unwrap()), 173 | agree_count: row.get::(6).unwrap(), 174 | disagree_count: row.get::(7).unwrap(), 175 | status: row.get::(8).unwrap(), 176 | create_time: row.get::(9).unwrap(), 177 | update_time: row.get::(10).unwrap() 178 | } 179 | }) 180 | .collect() 181 | } 182 | 183 | pub fn get_comment_count() -> u64 { 184 | 185 | let mut result = SQL_POOL.prep_exec("SELECT count(id) FROM comment", ()).unwrap(); 186 | let row_wrapper = result.next(); 187 | 188 | if row_wrapper.is_none() { 189 | return 0; 190 | } 191 | 192 | let row = row_wrapper.unwrap().unwrap(); 193 | 194 | let (count, ) = from_row::<(u64, )>(row); 195 | 196 | count 197 | } 198 | 199 | 200 | pub fn get_last_comment_by_topic_id(topic_id: &str) -> Option { 201 | 202 | let mut result = SQL_POOL.prep_exec(r#" 203 | SELECT 204 | c.id, user_id, username, avatar_url, c.create_time 205 | FROM comment AS c 206 | LEFT JOIN 207 | user AS u 208 | ON c.user_id = u.id 209 | WHERE topic_id = ? 210 | ORDER BY create_time DESC LIMIT 1 211 | "#, (topic_id, )).unwrap(); 212 | 213 | let row_wrapper = result.next(); 214 | 215 | if row_wrapper.is_none() { 216 | return None; 217 | } 218 | 219 | let mut row = row_wrapper.unwrap().unwrap(); 220 | 221 | Some(json!({ 222 | "id": row.get::(0).unwrap(), 223 | "user_id": row.get::(1).unwrap(), 224 | "username": row.get::(2).unwrap(), 225 | "avatar_url": row.get::(3).unwrap(), 226 | "create_time": row.get::(4).unwrap(), 227 | })) 228 | } 229 | --------------------------------------------------------------------------------