├── .gitignore ├── LICENSE ├── README.md ├── wu-server ├── .babelrc ├── .editorconfig ├── .eslintrc.json ├── .rsync-exclude ├── .vscode │ └── launch.json ├── Dockerfile ├── README.md ├── bin │ └── server.js ├── config │ ├── env │ │ ├── common.js │ │ ├── development.js │ │ ├── production.js │ │ └── test.js │ ├── index.js │ └── passport.js ├── index.js ├── nginx.conf ├── package.json ├── release.sh └── src │ ├── middleware │ ├── index.js │ └── validators.js │ ├── models │ ├── bookshelfs.js │ ├── chapters.js │ ├── novels.js │ └── users.js │ ├── modules │ ├── auth │ │ ├── controller.js │ │ └── router.js │ ├── bookshelves.js │ │ ├── controller.js │ │ └── router.js │ ├── chapters.js │ │ ├── controller.js │ │ └── router.js │ ├── index.js │ ├── novels.js │ │ ├── controller.js │ │ └── router.js │ ├── test │ │ ├── controller.js │ │ └── router.js │ └── users │ │ ├── controller.js │ │ └── router.js │ └── utils │ ├── auth.js │ ├── crawler.js │ ├── handle │ └── index.js │ ├── redis.js │ ├── schedule │ └── updateChapter.js │ └── updateNovel.js └── wyfReader ├── .babelrc ├── .buckconfig ├── .editorconfig ├── .eslintrc ├── .flowconfig ├── .gitattributes ├── .gitignore ├── .watchmanconfig ├── App.js ├── __tests__ └── App.js ├── android ├── app │ ├── BUCK │ ├── build.gradle │ ├── proguard-rules.pro │ └── src │ │ └── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ └── com │ │ │ └── wyfreader │ │ │ ├── MainActivity.java │ │ │ └── MainApplication.java │ │ └── res │ │ ├── mipmap-hdpi │ │ └── ic_launcher.png │ │ ├── mipmap-mdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxhdpi │ │ └── ic_launcher.png │ │ └── values │ │ ├── strings.xml │ │ └── styles.xml ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── keystores │ ├── BUCK │ └── debug.keystore.properties └── settings.gradle ├── app.json ├── app ├── containers │ ├── Account.js │ ├── Detail.js │ ├── Directory.js │ ├── Home.js │ ├── Login.js │ ├── Reader.js │ ├── Register.js │ └── Search.js ├── images │ ├── 2.png │ ├── back.png │ ├── clock.png │ ├── directory.png │ ├── down.png │ ├── house.png │ ├── person.png │ ├── search.png │ ├── search_1.png │ ├── tabbar1.png │ ├── test.jpg │ └── up.png ├── index.js ├── libs │ └── request.js ├── models │ ├── app.js │ ├── reader.js │ └── router.js ├── router.js ├── services │ ├── app.js │ └── auth.js └── utils │ ├── dva.js │ ├── index.js │ ├── request.js │ ├── setAuthorizationToken.js │ └── utils.js ├── index.js ├── ios ├── wyfReader-tvOS │ └── Info.plist ├── wyfReader-tvOSTests │ └── Info.plist ├── wyfReader.xcodeproj │ ├── project.pbxproj │ └── xcshareddata │ │ └── xcschemes │ │ ├── wyfReader-tvOS.xcscheme │ │ └── wyfReader.xcscheme ├── wyfReader │ ├── AppDelegate.h │ ├── AppDelegate.m │ ├── Base.lproj │ │ └── LaunchScreen.xib │ ├── Images.xcassets │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── Contents.json │ ├── Info.plist │ └── main.m └── wyfReaderTests │ ├── Info.plist │ └── wyfReaderTests.m ├── jsconfig.json ├── package.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | ### Node ### 3 | # Logs 4 | wu-server/logs 5 | *.log 6 | wu-server/npm-debug.log* 7 | 8 | # Runtime data 9 | wu-server/pids 10 | wu-server/*.pid 11 | wu-server/*.seed 12 | 13 | # Directory for instrumented libs generated by jscoverage/JSCover 14 | wu-server/lib-cov 15 | 16 | # Coverage directory used by tools like istanbul 17 | wu-server/coverage 18 | 19 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 20 | wu-server/.grunt 21 | 22 | # node-waf configuration 23 | wu-server/.lock-wscript 24 | 25 | # Compiled binary addons (http://nodejs.org/api/addons.html) 26 | wu-server/build/Release 27 | 28 | # Dependency directory 29 | wu-server/node_modules 30 | 31 | # Optional npm cache directory 32 | wu-server/.npm 33 | 34 | # Optional REPL history 35 | wu-server/.node_repl_history 36 | 37 | 38 | ### SublimeText ### 39 | # cache files for sublime text 40 | wu-server/*.tmlanguage.cache 41 | wu-server/*.tmPreferences.cache 42 | wu-server/*.stTheme.cache 43 | 44 | # workspace files are user-specific 45 | wu-server/*.sublime-workspace 46 | 47 | # project files should be checked into the repository, unless a significant 48 | # proportion of contributors will probably not be using SublimeText 49 | wu-server/*.sublime-project 50 | 51 | # sftp configuration file 52 | wu-server/sftp-config.json 53 | 54 | #Documentation 55 | wu-server/docs 56 | 57 | 58 | # OSX wyfReader 59 | # 60 | .DS_Store 61 | 62 | # Xcode 63 | # 64 | wyfReader/build/ 65 | wyfReader/*.pbxuser 66 | !wyfReader/default.pbxuser 67 | wyfReader/*.mode1v3 68 | !wyfReader/default.mode1v3 69 | wyfReader/*.mode2v3 70 | !wyfReader/default.mode2v3 71 | wyfReader/*.perspectivev3 72 | !wyfReader/default.perspectivev3 73 | wyfReader/xcuserdata 74 | wyfReader/*.xccheckout 75 | wyfReader/*.moved-aside 76 | wyfReader/DerivedData 77 | wyfReader/*.hmap 78 | wyfReader/*.ipa 79 | wyfReader/*.xcuserstate 80 | wyfReader/project.xcworkspace 81 | 82 | # Android/IntelliJ 83 | # 84 | wyfReader/build/ 85 | wyfReader/.idea 86 | wyfReader/.gradle 87 | wyfReader/local.properties 88 | wyfReader/*.iml 89 | 90 | # node.js 91 | # 92 | wyfReader/node_modules/ 93 | wyfReader/npm-debug.log 94 | wyfReader/yarn-error.log 95 | 96 | # BUCK 97 | wyfReader/buck-out/ 98 | wyfReader/\.buckd/ 99 | wyfReader/*.keystore 100 | 101 | # fastlane 102 | # 103 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 104 | # screenshots whenever they are needed. 105 | # For more information about the recommended setup visit: 106 | # https://docs.fastlane.tools/best-practices/source-control/ 107 | 108 | wyfReader/*/fastlane/report.xml 109 | wyfReader/*/fastlane/Preview.html 110 | wyfReader/*/fastlane/screenshots 111 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 wuyafeiJS 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## RN+dva+node+mongo+nginx+docker 从开发到部署,全栈入坑指引! 2 | 3 | ### 前言 4 | 5 | 作为一个优秀前端er,除了要精通前端基础外,其他的如后台,运维,linux等都要有所了解。这样你才能对自己所负责的项目有一个整体的把握,不同端开发思维的碰撞,有助于你形成良好的代码习惯,写出高效优质的代码。话不多说,我们开始吧! 6 | 7 | ### 背景 8 | 9 | 这是个学习型的项目,还有些需要优化的地方,项目是参考 [https://github.com/dlyt/YCool](https://github.com/dlyt/YCool) ,利用dva代替redux(个人认为dva比redux好学啊有木有,觉得redux概念不好理解的完全可以从dva入手啊,学完dva,redux秒懂), 然后新增了一些功能。利用工作之余的时间写出来,希望能帮助到大家~ 10 | 11 | ### 目录结构 12 | ``` 13 | . 14 | ├── wu-server // 后台服务 15 | │   ├── Dockerfile // dockfile 16 | │   ├── README.md 17 | │   ├── bin // 入口文件 18 | │   │   └── server.js 19 | │   ├── config // 配置目录 20 | │   │   ├── env 21 | │   │   ├── index.js 22 | │   │   └── passport.js // 登录认证服务 23 | │   ├── index.js 24 | │   ├── nginx.conf // nginx 配置 25 | │   ├── package-lock.json 26 | │   ├── package.json 27 | │   ├── release.sh // 一键部署shell脚本 28 | │   └── src 29 | │   ├── middleware // 中间件 30 | │   ├── models // mongo model 31 | │   ├── modules // 接口目录 32 | │   └── utils // 公用工具 33 | └── wyfReader // app端 34 | ├── App.js 35 | ├── __tests__ 36 | │   └── App.js 37 | ├── app 38 | │   ├── containers // UI组件 39 | │   ├── images 40 | │   ├── index.js 41 | │   ├── libs // 公用库 42 | │   ├── models // dva models 43 | │   ├── router.js // 路由 44 | │   ├── services // 接口服务 45 | │   └── utils 46 | ├── app.json 47 | ├── index.js 48 | ├── jsconfig.json 49 | ├── package-lock.json 50 | ├── package.json 51 | └── yarn.lock 52 | ``` 53 | ### 前端 54 | 55 | 即RN项目,仅做了ios端的兼容(偷个懒^-^)。完全版的dva包含了react-router,我们这边不需要,所以只用了[dva-core](https://github.com/dvajs/dva/tree/master/packages/dva-core) 56 | 57 | 基本功能: 58 | >* 小说搜索,动态结果列表显示,支持模糊搜索。 59 | >* 加入书架,阅读,小说删除功能 60 | >* 登录注册功能,node实现验证码 61 | 62 | #### 效果图 63 | ![](http://og1m0yoqf.bkt.clouddn.com/1.gif) 64 | ![](http://og1m0yoqf.bkt.clouddn.com/2.gif) 65 | ![](http://og1m0yoqf.bkt.clouddn.com/3.gif) 66 | 67 | ### 后端 68 | 69 | 框架采用的koa2,passport作为登录认证,cheerio实现爬虫。 70 | 71 | 基本功能: 72 | >* 提供小说操作相关的所有api 73 | >* 提供登录注册相关api 74 | >* node实现svg验证码 75 | >* 定期自动更新小说爬虫 76 | 77 | #### 部署 78 | 79 | 运行`sh release.sh`即可实现一键部署。 80 | 81 | 流行的有两种方案:pm2和docker,现在docker这么流行,咱果断选择它,写了一个自动构建脚本:release.sh 82 | 83 | 大概流程就是: 把代码上传到自己主机上面 > 构建镜像 > 然后以守护进程方式运行。 84 | 85 | 如果还想更近一步的实现自动部署的话,可以试试[travis CI](https://www.travis-ci.org/) 开源免费。它可以通过git的钩子,直接在项目提交到git的时候自动运行构建脚本 86 | 87 | #### nginx 了解一下 88 | 89 | 90 | 它是一个高性能的HTTP和反向代理服务区。学习成本很低,这里咱只是简单的用nginx做了一下代理。 91 | ``` 92 | server { 93 | listen 80; 94 | location / { 95 | root /opt/html; 96 | index index.html index.html; 97 | } 98 | location /server/ { 99 | expires -1; 100 | add_header Cache-Control no-cache,no-store; 101 | proxy_pass http://120.79.161.255:8080/; 102 | } 103 | } 104 | ``` 105 | 就是原先咱需写端口访问如: http://120.79.161.255:8080/ 现在直接访问http://120.79.161.255/server/ 即可,是不是变漂亮了很多? 106 | 107 | 当然nginx远不止这点功能,比如你可以用它做负载均衡、解决跨域问题、处理缓存 防盗链等HTTP相关问题,处理起来也很容易,只需在配置文件加上相关配置即可,有兴趣朋友可以深入一下。不过话说现在service mesh好像比较流行,Envoy好像想取代它的样子~~哈哈,扯远了~ 108 | 109 | 110 | ### 总结 111 | 112 | 总的来说这个项目算是一个全栈练手项目,也没有花多长时间。有些地方还是有点粗糙的, 113 | 但是不妨碍大家学习。还有些概念和工具只是大概提了一下,目的是给初学的朋友留下一个印象(万一今后有用到呢~)。感兴趣的朋友可以基于这个多多优化,加上自己idea。最后,欢迎`star & fork!!!` 114 | 115 | 116 | 117 | -------------------------------------------------------------------------------- /wu-server/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015-node5", 4 | "stage-0" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /wu-server/.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | # A special property that should be specified at the top of the file outside of 4 | # any sections. Set to true to stop .editor config file search on current file 5 | root = true 6 | 7 | [*] 8 | # Indentation style 9 | # Possible values - tab, space 10 | indent_style = space 11 | 12 | # Indentation size in single-spaced characters 13 | # Possible values - an integer, tab 14 | indent_size = 2 15 | 16 | # Line ending file format 17 | # Possible values - lf, crlf, cr 18 | end_of_line = lf 19 | 20 | # File character encoding 21 | # Possible values - latin1, utf-8, utf-16be, utf-16le 22 | charset = utf-8 23 | 24 | # Denotes whether to trim whitespace at the end of lines 25 | # Possible values - true, false 26 | trim_trailing_whitespace = true 27 | 28 | # Denotes whether file should end with a newline 29 | # Possible values - true, false 30 | insert_final_newline = true 31 | -------------------------------------------------------------------------------- /wu-server/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": "standard", 4 | "env": { 5 | "node": true, 6 | "mocha": true 7 | }, 8 | "rules": { 9 | "eqeqeq": 0 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /wu-server/.rsync-exclude: -------------------------------------------------------------------------------- 1 | .git/* 2 | logs/* 3 | node_modules/* 4 | npm-* 5 | coverage -------------------------------------------------------------------------------- /wu-server/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // 使用 IntelliSense 了解相关属性。 3 | // 悬停以查看现有属性的描述。 4 | // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Launch Program", 11 | "program": "${workspaceFolder}/index.js" 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /wu-server/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM hub.c.163.com/library/node:8.4-slim 2 | MAINTAINER wuyafei 3 | 4 | RUN mkdir -p /opt/reader-server 5 | ADD . /opt/reader-server 6 | 7 | COPY ./ /opt/reader-server 8 | WORKDIR /opt/reader-server 9 | # RUN npm config set registry http://10.2.144.44:8081/repository/npm && \ 10 | # NODEJS_ORG_MIRROR=http://10.2.144.44:8081/repository/node npm i --production --verbose 11 | RUN npm i 12 | 13 | EXPOSE 5000 14 | 15 | CMD NODE_ENV=production node index.js 16 | -------------------------------------------------------------------------------- /wu-server/README.md: -------------------------------------------------------------------------------- 1 | ## 简介 2 | 基于koa2的api服务器,数据库mongoodb。 3 | 4 | ## 安装 5 | 请先确定安装了 mongoodb。 6 | ```bash 7 | git clone https://github.com/dlyt/YCool_Server.git 8 | npm install 9 | ``` 10 | 11 | ## 开发 12 | ```bash 13 | npm run dev 14 | ``` 15 | 16 | ## API文档 17 | 18 | ```bash 19 | npm run docs 20 | ``` 21 | 22 | 运行项目后查看: http://localhost:5000/docs/ 23 | 24 | ## License 25 | 26 | 仅供学习使用 27 | -------------------------------------------------------------------------------- /wu-server/bin/server.js: -------------------------------------------------------------------------------- 1 | import Koa from 'koa' 2 | import bodyParser from 'koa-bodyparser' 3 | import convert from 'koa-convert' 4 | import logger from 'koa-logger' 5 | import mongoose from 'mongoose' 6 | import session from 'koa-generic-session' 7 | import passport from '../config/passport' 8 | import mount from 'koa-mount' 9 | import serve from 'koa-static' 10 | 11 | import config from '../config' 12 | import handle from '../src/utils/handle' 13 | import { errorMiddleware } from '../src/middleware' 14 | 15 | // 性能监控 16 | // const easyMonitor = require('easy-monitor') 17 | // easyMonitor({ 18 | // cluster: true, 19 | // bootstrap: 'embrace', 20 | // project_name: 'wu-server', 21 | // /** 22 | // @param {string} tcp_host 填写你部署的 dashboard 进程所在的服务器 ip 23 | // @param {number} tcp_port 填写你部署的 dashboard 进程所在的服务器 端口 24 | // **/ 25 | // embrace: { 26 | // tcp_host: '127.0.0.1', 27 | // tcp_port: 26666 28 | // } 29 | // }) 30 | 31 | global.Handle = handle 32 | 33 | const app = new Koa() 34 | app.keys = [config.session] 35 | 36 | mongoose.Promise = global.Promise 37 | mongoose.connect(config.database) 38 | 39 | app.use(convert(logger())) 40 | app.use(bodyParser()) 41 | app.use(convert(session())) 42 | app.use(errorMiddleware()) 43 | 44 | app.use(convert(mount('/docs', serve(`${process.cwd()}/docs`)))) 45 | 46 | // require('../config/passport') 47 | app.use(passport.initialize()) 48 | app.use(passport.session()) 49 | 50 | const modules = require('../src/modules') 51 | modules(app) 52 | 53 | app.listen(config.port, () => { 54 | console.log(`Server started on ${config.port}`) 55 | }) 56 | 57 | // 每天凌晨准时更新小说爬虫 58 | // const UpdateNovel = require('../src/utils/updateNovel') 59 | // if (app.env === 'production') { 60 | // UpdateNovel.update() 61 | // } 62 | require('../src/utils/schedule/updateChapter') 63 | 64 | export default app 65 | -------------------------------------------------------------------------------- /wu-server/config/env/common.js: -------------------------------------------------------------------------------- 1 | export default { 2 | port: process.env.PORT || 5000, 3 | crawlerUrl: "https://www.qu.la" 4 | } 5 | -------------------------------------------------------------------------------- /wu-server/config/env/development.js: -------------------------------------------------------------------------------- 1 | export default { 2 | session: 'secret-boilerplate-token', 3 | token: 'secret-jwt-token', 4 | database: 'mongodb://120.79.161.225:27017/wu', 5 | redis: { 6 | host: 'localhost', 7 | port: 6379, 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /wu-server/config/env/production.js: -------------------------------------------------------------------------------- 1 | export default { 2 | session: 'secret-boilerplate-token', 3 | token: 'secret-jwt-token', 4 | database: 'mongodb://120.79.161.225:27017/wu', 5 | redis: { 6 | host: 'localhost', 7 | port: 6379, 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /wu-server/config/env/test.js: -------------------------------------------------------------------------------- 1 | export default { 2 | session: 'secret-boilerplate-token', 3 | token: 'secret-jwt-token', 4 | database: 'mongodb://localhost:27017/wu' 5 | } 6 | -------------------------------------------------------------------------------- /wu-server/config/index.js: -------------------------------------------------------------------------------- 1 | import common from './env/common' 2 | 3 | const env = process.env.NODE_ENV || 'development' 4 | const config = require(`./env/${env}`).default 5 | 6 | export default Object.assign({}, common, config) 7 | -------------------------------------------------------------------------------- /wu-server/config/passport.js: -------------------------------------------------------------------------------- 1 | import passport from 'koa-passport' 2 | import User from '../src/models/users' 3 | import { Strategy } from 'passport-local' 4 | 5 | /** 6 | * @param username 用户输入的用户名 7 | * @param password 用户输入的密码 8 | * @param done 验证完成后的回调函数 9 | */ 10 | passport.use('local', new Strategy({ 11 | usernameField: 'username', 12 | passwordField: 'password' 13 | }, async (username, password, done) => { 14 | try { 15 | const user = await User.findOne({ username }) 16 | if (!user) { return done(null, false) } 17 | 18 | try { 19 | const isMatch = await user.validatePassword(password) 20 | 21 | if (!isMatch) { return done(null, false) } 22 | 23 | done(null, user) 24 | } catch (err) { 25 | done(err) 26 | } 27 | } catch (err) { 28 | return done(err) 29 | } 30 | })) 31 | // serializeUser 在用户登录验证成功后将会把用户的数据储存到 session 中 32 | passport.serializeUser((user, done) => { 33 | console.log('kkkkkkk') 34 | done(null, user.id) 35 | }) 36 | 37 | // deserializeUser 在每次请求时候将从 session 中读用户对象 38 | passport.deserializeUser(async (id, done) => { 39 | console.log('deserializeUser, id: ' + id) 40 | try { 41 | const user = await User.findById(id, '-password') 42 | done(null, user) 43 | } catch (err) { 44 | done(err) 45 | } 46 | }) 47 | 48 | export default passport 49 | -------------------------------------------------------------------------------- /wu-server/index.js: -------------------------------------------------------------------------------- 1 | require('babel-core/register')() 2 | require('babel-polyfill') 3 | require('./bin/server.js') 4 | -------------------------------------------------------------------------------- /wu-server/nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | location / { 4 | root /opt/html; 5 | index index.html index.html; 6 | } 7 | location /server/ { 8 | expires -1; 9 | add_header Cache-Control no-cache,no-store; 10 | proxy_pass http://120.79.161.255:8080/; 11 | } 12 | } -------------------------------------------------------------------------------- /wu-server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wu_server", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "nodemon index.js", 8 | "serve:prod": "node NODE_ENV=production index.js", 9 | "dev": "./node_modules/.bin/nodemon index.js", 10 | "test": "NODE_ENV=test ./node_modules/.bin/mocha --compilers js:babel-register --require babel-polyfill", 11 | "lint": "eslint src/**/*.js", 12 | "docs": "./node_modules/.bin/apidoc -i src/ -o docs" 13 | }, 14 | "keywords": [ 15 | "api", 16 | "koa", 17 | "koa2", 18 | "boilerplate", 19 | "es6", 20 | "mongoose", 21 | "passportjs", 22 | "apidoc" 23 | ], 24 | "author": "XiaoWu", 25 | "license": "MIT", 26 | "apidoc": { 27 | "title": "YCool--API文档", 28 | "url": "localhost:5000" 29 | }, 30 | "repository": { 31 | "type": "git", 32 | "url": "https://github.com/dlyt/YCool_Server.git" 33 | }, 34 | "dependencies": { 35 | "apidoc": "^0.17.5", 36 | "babel-core": "^6.5.1", 37 | "babel-polyfill": "^6.5.0", 38 | "babel-preset-es2015-node5": "^1.2.0", 39 | "babel-preset-stage-0": "^6.5.0", 40 | "bcrypt": "^1.0.2", 41 | "cheerio": "^0.22.0", 42 | "easy-monitor": "^2.2.1", 43 | "glob": "^7.0.0", 44 | "iconv-lite": "^0.4.15", 45 | "joi": "^13.1.1", 46 | "jsonwebtoken": "^7.1.9", 47 | "koa": "^2.0.0-alpha.6", 48 | "koa-bodyparser": "^3.0.0", 49 | "koa-convert": "^1.2.0", 50 | "koa-generic-session": "^1.10.1", 51 | "koa-logger": "^2.0.0", 52 | "koa-mount": "^1.3.0", 53 | "koa-passport": "^2.0.1", 54 | "koa-router": "^7.0.1", 55 | "koa-session2": "^2.2.5", 56 | "koa-static": "^2.0.0", 57 | "mongoose": "^4.4.3", 58 | "node-schedule": "^1.2.1", 59 | "nodemailer": "^3.1.7", 60 | "nodemailer-smtp-transport": "^2.7.2", 61 | "nodemailer-wellknown": "^0.2.2", 62 | "passport-local": "^1.0.0", 63 | "pinyin": "^2.8.0", 64 | "redis": "^2.7.1", 65 | "request": "^2.79.0", 66 | "simple-pinyin": "^3.0.2", 67 | "svg-captcha": "^1.3.11" 68 | }, 69 | "devDependencies": { 70 | "babel-eslint": "^6.0.2", 71 | "babel-register": "^6.5.1", 72 | "chai": "^3.5.0", 73 | "eslint": "^3.4.0", 74 | "eslint-config-standard": "^6.0.0", 75 | "eslint-plugin-promise": "^2.0.1", 76 | "eslint-plugin-standard": "^2.0.0", 77 | "mocha": "^3.0.2", 78 | "nodemon": "^1.8.1", 79 | "supertest": "^2.0.0" 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /wu-server/release.sh: -------------------------------------------------------------------------------- 1 | BUILD_TIME=`date "+%Y%m%d%H%M"` 2 | SERVER_HOST="root@120.79.161.225" 3 | SERVER_PATH="/opt/wuReader" 4 | IMAGE_NAME="hub.c.163.com/wuyafeijs/reader-server:$BUILD_TIME" 5 | 6 | rsync -cavzP --delete-after ./ --exclude-from='.rsync-exclude' root@120.79.161.225:$SERVER_PATH 7 | ssh $SERVER_HOST "\ 8 | cd $SERVER_PATH; \ 9 | docker kill reader-server; \ 10 | docker rm reader-server; \ 11 | echo "清理过时的镜像"; \ 12 | docker images | awk '/^hub.c.163.com\/wuyafeijs\/reader-server[ ]+/ { print \$3 }' | xargs docker rmi ; \ 13 | docker build -t $IMAGE_NAME .;\ 14 | docker run --name reader-server -p 8080:5000 -d --restart=always $IMAGE_NAME; \ 15 | exit; \ 16 | " 17 | echo "\033[40;32m\n" 18 | echo "Image: $IMAGE_NAME"; \ 19 | # echo "Image run success." 20 | echo "\033[0m" 21 | -------------------------------------------------------------------------------- /wu-server/src/middleware/index.js: -------------------------------------------------------------------------------- 1 | export function errorMiddleware () { 2 | return async (ctx, next) => { 3 | try { 4 | await next() 5 | } catch (err) { 6 | ctx.status = err.status || 500 7 | ctx.body = err.message 8 | ctx.app.emit('error', err, ctx) 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /wu-server/src/middleware/validators.js: -------------------------------------------------------------------------------- 1 | import User from '../models/users' 2 | import config from '../../config' 3 | import { getToken } from '../utils/auth' 4 | import { verify } from 'jsonwebtoken' 5 | 6 | export async function ensureUser (ctx, next) { 7 | const token = getToken(ctx) 8 | 9 | if (!token) { 10 | ctx.throw(401) 11 | } 12 | 13 | let decoded = null 14 | try { 15 | decoded = verify(token, config.token) 16 | } catch (err) { 17 | ctx.throw(401) 18 | } 19 | // 缓存userid 20 | ctx.state.user = await User.findById(decoded.id, '-password') 21 | if (!ctx.state.user) { 22 | ctx.throw(401) 23 | } 24 | 25 | return next() 26 | } 27 | -------------------------------------------------------------------------------- /wu-server/src/models/bookshelfs.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose' 2 | 3 | const Schema = mongoose.Schema 4 | const ObjectId = Schema.Types.ObjectId 5 | 6 | const Bookshelf = new mongoose.Schema({ 7 | user: { type: ObjectId, ref: 'user' }, 8 | novel: { type: ObjectId, ref: 'novel'}, 9 | chapter: { type: ObjectId, ref: 'chapter'}, 10 | number: { type: Number, default: 0}, 11 | progress: { type: Number, default: 0} 12 | }) 13 | 14 | Bookshelf.statics = { 15 | getList: function (id){ 16 | return this 17 | .find({user: id}) 18 | .populate('novel') 19 | .populate('chapter') 20 | .exec() 21 | }, 22 | findByUserAndNovelId: function (options){ 23 | return this 24 | .findOne({user: options.userId, novel: options.novelId}) 25 | .exec() 26 | } 27 | } 28 | 29 | export default mongoose.model('bookshelf', Bookshelf) 30 | -------------------------------------------------------------------------------- /wu-server/src/models/chapters.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose' 2 | 3 | const Schema = mongoose.Schema 4 | const ObjectId = Schema.Types.ObjectId 5 | 6 | const Chapter = new mongoose.Schema({ 7 | chapters: { type: Array }, 8 | novel: { type: ObjectId, ref: 'novel'}, 9 | }) 10 | 11 | Chapter.statics = { 12 | getTitles: function (id){ 13 | return this 14 | .find({novel: id}) 15 | .exec() 16 | }, 17 | getContent: function (id){ 18 | return this 19 | .findById(id) 20 | .populate('novel',['name', 'url']) 21 | .exec() 22 | }, 23 | findByNumber: function (id, num) { 24 | return this 25 | .findOne({number: num, novel: id}) 26 | .exec() 27 | }, 28 | getDirectory: function (options) { 29 | return this 30 | .find(options.where, options.attributes) 31 | .sort({number: options.order}) 32 | .limit() 33 | .exec() 34 | } 35 | } 36 | 37 | export default mongoose.model('chapter', Chapter) 38 | -------------------------------------------------------------------------------- /wu-server/src/models/novels.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose' 2 | 3 | const Schema = mongoose.Schema 4 | const ObjectId = Schema.Types.ObjectId 5 | 6 | const Novel = new mongoose.Schema({ 7 | type: { type: String, default: 'Normal' }, 8 | name: { type: String , unique: true}, 9 | url: { type: String }, 10 | status: { type: Number }, 11 | author: { type: String }, 12 | introduction: { type: String }, 13 | img: { type: String, default: '' }, 14 | updateTime: { type: String }, 15 | lastChapterTitle: { type: String }, 16 | countChapter: { type: Number }, 17 | }) 18 | 19 | 20 | export default mongoose.model('novel', Novel) 21 | -------------------------------------------------------------------------------- /wu-server/src/models/users.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose' 2 | import bcrypt from 'bcrypt' 3 | import config from '../../config' 4 | import jwt from 'jsonwebtoken' 5 | 6 | const User = new mongoose.Schema({ 7 | type: { type: String, default: 'User' }, 8 | name: { type: String }, 9 | email: { type: String }, 10 | username: { type: String }, 11 | password: { type: String }, 12 | uuid: { type: String }, 13 | meta: { 14 | createAt: { 15 | type: Date, 16 | default: Date.now() 17 | }, 18 | update: { 19 | type: Date, 20 | default: Date.now() 21 | } 22 | } 23 | }) 24 | 25 | User.pre('save', function preSave (next) { 26 | const user = this 27 | 28 | if (this.isNew) { 29 | this.meta.createAt = this.meta.update = Date.now() 30 | } else { 31 | this.meta.update = Date.now 32 | } 33 | if (this.uuid && !this.username) { 34 | return next() 35 | } 36 | new Promise((resolve, reject) => { 37 | bcrypt.genSalt(10, (err, salt) => { 38 | if (err) { 39 | return reject(err) 40 | } 41 | resolve(salt) 42 | }) 43 | }) 44 | .then(salt => { 45 | bcrypt.hash(user.password, salt, (err, hash) => { 46 | if (err) { 47 | throw new Error(err) 48 | } 49 | user.password = hash 50 | return next() 51 | }) 52 | }) 53 | .catch(err => { 54 | next(err) 55 | }) 56 | }) 57 | 58 | User.methods.validatePassword = function validatePassword(password) { 59 | const user = this 60 | return new Promise((resolve, reject) => { 61 | bcrypt.compare(password, user.password, (err, isMatch) => { 62 | if (err) { 63 | return reject(err) 64 | } 65 | 66 | resolve(isMatch) 67 | }) 68 | }) 69 | } 70 | 71 | User.methods.generateToken = function generateToken() { 72 | const user = this 73 | 74 | return jwt.sign({ id: user.id }, config.token) 75 | }; 76 | 77 | export default mongoose.model('user', User) 78 | -------------------------------------------------------------------------------- /wu-server/src/modules/auth/controller.js: -------------------------------------------------------------------------------- 1 | import passport from '../../../config/passport' 2 | 3 | /** 4 | * @apiDefine TokenError 5 | * @apiError Unauthorized Invalid JWT token 6 | * 7 | * @apiErrorExample {json} Unauthorized-Error: 8 | * HTTP/1.1 401 Unauthorized 9 | * { 10 | * "status": 401, 11 | * "error": "Unauthorized" 12 | * } 13 | */ 14 | // 认证登录 15 | export async function authUser (ctx, next) { 16 | const code = ctx.request.body.code 17 | const captcha = ctx.session.captcha.toLowerCase() 18 | if (captcha !== code.toLowerCase()) { 19 | ctx.body = { 20 | code: -1, 21 | msg: '验证码错误' 22 | } 23 | return 24 | } 25 | return passport.authenticate('local', (user) => { 26 | if (!user) { 27 | // ctx.throw(401) 28 | ctx.body = { 29 | code: 11, 30 | msg: '账号或密码错误' 31 | } 32 | return 33 | } 34 | 35 | const token = user.generateToken() 36 | 37 | const response = user.toJSON() 38 | delete response.password 39 | ctx.body = { 40 | code: 0, 41 | token, 42 | username: response.username 43 | } 44 | ctx.login(user) 45 | })(ctx, next) 46 | } 47 | // 测试是否认证 48 | export async function testUser (ctx, next) { 49 | console.log(ctx.isAuthenticated(), '=======') 50 | if (ctx.isAuthenticated()) { 51 | ctx.body = '认证通过' 52 | } else { 53 | ctx.throw(401) 54 | ctx.body = '非法访问' 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /wu-server/src/modules/auth/router.js: -------------------------------------------------------------------------------- 1 | import * as auth from './controller' 2 | 3 | export const baseUrl = '/auth' 4 | 5 | export default [ 6 | { 7 | method: 'POST', 8 | route: '/', 9 | handlers: [ 10 | auth.authUser 11 | ] 12 | }, 13 | { 14 | method: 'POST', 15 | route: '/test', 16 | handlers: [ 17 | auth.testUser 18 | ] 19 | } 20 | ] 21 | -------------------------------------------------------------------------------- /wu-server/src/modules/bookshelves.js/controller.js: -------------------------------------------------------------------------------- 1 | import Bookshelf from '../../models/bookshelfs' 2 | import Chapter from '../../models/chapters' 3 | import Novel from '../../models/novels' 4 | 5 | /** 6 | @api {GET} /bookshelfs 获取书架列表 7 | @apiPermission User 8 | @apiVersion 1.0.0 9 | @apiName 获取书架列表 10 | @apiGroup Bookshelfs 11 | 12 | @apiExample Example usage: 13 | curl -H "Content-Type: application/json" "authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjU4YjhmZDRkODUyYTE1YzliNmYyNjI3MSIsImlhdCI6MTQ4ODU1MTc2N30.IEgYwmgyqOBft9s38ool7cmuC2yIlWYVLf4WQzcbqAI" -X GET localhost:5000/bookshelfs 14 | 15 | @apiSuccessExample {json} Success-Response: 16 | HTTP/1.1 200 OK 17 | { 18 | "list": [ 19 | { 20 | "_id": "58c949c5d18e2024b66360fc", 21 | "user": "58c94997d18e2024b66360fb", 22 | "novel": { 23 | "_id": "58c4cb509e4dad30f80d2f84", 24 | "url": "http://www.37zw.com/3/3960/", 25 | "name": "1852铁血中华", 26 | "author": "绯红之月", 27 | "updateTime": "2017-03-12", 28 | "introduction": " 1852,是革命,或者是一场该改朝换代的改良。燃烧的铁与血,最终能创造一个什么样的未来?\n", 29 | "__v": 0, 30 | "countChapter": "1377", 31 | "lastChapterTitle": "第644章 剪影 3", 32 | "img": "http://www.37zw.com/d/image/3/3960/3960s.jpg" 33 | }, 34 | "chapter": { 35 | "_id": "58c4cb509e4dad30f80d2f85", 36 | "number": 0 37 | }, 38 | "__v": 0 39 | } 40 | ] 41 | } 42 | 43 | @apiErrorExample {json} Error-Response: 44 | HTTP/1.1 422 Unprocessable Entity 45 | { 46 | "status": 422, 47 | "error": "" 48 | } 49 | */ 50 | export async function getBookshelf (ctx) { 51 | const user = ctx.state.user 52 | try { 53 | var list = await Bookshelf.getList(user.id) 54 | } catch (e) { 55 | Handle.sendEmail(e.message) 56 | ctx.throw(422, e.message) 57 | } 58 | 59 | ctx.body = { 60 | list: list 61 | } 62 | } 63 | 64 | /** 65 | @api {POST} /bookshelfs/order 订阅小说 66 | @apiPermission User 67 | @apiVersion 1.0.0 68 | @apiName 订阅小说 69 | @apiGroup Bookshelfs 70 | 71 | @apiExample Example usage: 72 | curl -H "Content-Type: application/json" "authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjU4YjhmZDRkODUyYTE1YzliNmYyNjI3MSIsImlhdCI6MTQ4ODU1MTc2N30.IEgYwmgyqOBft9s38ool7cmuC2yIlWYVLf4WQzcbqAI" -X GET localhost:5000/bookshelfs/order 73 | 74 | @apiParam {String} id 小说ID. 75 | 76 | @apiSuccessExample {json} Success-Response: 77 | HTTP/1.1 200 OK 78 | { 79 | "success": true 80 | } 81 | 82 | @apiErrorExample {json} Error-Response: 83 | HTTP/1.1 422 Unprocessable Entity 84 | { 85 | "status": 422, 86 | "error": "" 87 | } 88 | */ 89 | export async function orderNovel (ctx) { 90 | const user = ctx.state.user 91 | const novelId = ctx.request.body.id 92 | 93 | try { 94 | var titles = await Chapter.getTitles(novelId) 95 | 96 | } catch (err) { 97 | ctx.throw(422, err.message) 98 | } 99 | const chapterId = titles[0]._id 100 | const bookshelf = new Bookshelf({ 101 | user: user.id, 102 | novel: novelId, 103 | chapter: chapterId, 104 | number: 0 105 | }) 106 | 107 | try { 108 | await bookshelf.save() 109 | } catch (e) { 110 | Handle.sendEmail(e.message) 111 | ctx.throw(422, e.message) 112 | } 113 | 114 | ctx.body = { 115 | success: true 116 | } 117 | } 118 | 119 | /** 120 | @api {POST} /bookshelfs/delect 取消订阅 121 | @apiVersion 1.0.0 122 | @apiName 取消订阅 123 | @apiGroup Bookshelfs 124 | 125 | @apiExample Example usage: 126 | curl -H "Content-Type: application/json" -X GET localhost:5000/bookshelfs/delect 127 | 128 | @apiParam {String} id 小说ID. 129 | 130 | @apiSuccessExample {json} Success-Response: 131 | HTTP/1.1 200 OK 132 | { 133 | "success": true 134 | } 135 | 136 | @apiErrorExample {json} Error-Response: 137 | HTTP/1.1 422 Unprocessable Entity 138 | { 139 | "status": 422, 140 | "error": "" 141 | } 142 | */ 143 | export async function delectNovel (ctx) { 144 | const id = ctx.request.body.id 145 | try { 146 | await Bookshelf.remove({_id: id}) 147 | } catch (e) { 148 | Handle.sendEmail(e.message) 149 | ctx.throw(422, err.message) 150 | } 151 | 152 | ctx.body = { 153 | success: true 154 | } 155 | } 156 | 157 | /** 158 | @api {POST} /bookshelfs 记录最后阅读章节 159 | @apiPermission User 160 | @apiVersion 1.0.0 161 | @apiName 记录最后阅读章节 162 | @apiGroup Bookshelfs 163 | 164 | @apiExample Example usage: 165 | curl -H "Content-Type: application/json" "authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjU4YjhmZDRkODUyYTE1YzliNmYyNjI3MSIsImlhdCI6MTQ4ODU1MTc2N30.IEgYwmgyqOBft9s38ool7cmuC2yIlWYVLf4WQzcbqAI" -X GET localhost:5000/bookshelfs/change 166 | 167 | @apiParam num 小说章节 168 | @apiParam x 页数 169 | 170 | @apiSuccessExample {json} Success-Response: 171 | HTTP/1.1 200 OK 172 | 173 | @apiErrorExample {json} Error-Response: 174 | HTTP/1.1 422 Unprocessable Entity 175 | { 176 | "status": 422, 177 | "error": "" 178 | } 179 | */ 180 | export async function changeBookshelf (ctx) { 181 | let chapter,bookshelf 182 | const user = ctx.state.user 183 | const novel = ctx.request.body.novel 184 | const progress = novel.x / 375 185 | const options = { 186 | userId: user.id, 187 | novelId: novel.id 188 | } 189 | try { 190 | bookshelf = await Bookshelf.findByUserAndNovelId(options) 191 | chapter = await Chapter.findByNumber(novel.id, novel.num) 192 | } catch (e) { 193 | Handle.sendEmail(e.message) 194 | ctx.throw(422, e.message) 195 | } 196 | 197 | bookshelf.progress = progress 198 | bookshelf.chapter = chapter.id 199 | try { 200 | await bookshelf.save() 201 | } catch (e) { 202 | Handle.sendEmail(e.message) 203 | ctx.throw(422, e.message) 204 | } 205 | 206 | ctx.body = { 207 | success: true 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /wu-server/src/modules/bookshelves.js/router.js: -------------------------------------------------------------------------------- 1 | import { ensureUser } from '../../middleware/validators' 2 | import * as bookshelf from './controller' 3 | 4 | export const baseUrl = '/bookshelfs' 5 | 6 | export default [ 7 | { 8 | method: 'GET', 9 | route: '/', 10 | handlers: [ 11 | ensureUser, 12 | bookshelf.getBookshelf 13 | ] 14 | }, 15 | { 16 | method: 'POST', 17 | route: '/order', 18 | handlers: [ 19 | ensureUser, 20 | bookshelf.orderNovel 21 | ] 22 | }, 23 | { 24 | method: 'POST', 25 | route: '/delete', 26 | handlers: [ 27 | bookshelf.delectNovel 28 | ] 29 | }, 30 | { 31 | method: 'POST', 32 | route: '/change', 33 | handlers: [ 34 | ensureUser, 35 | bookshelf.changeBookshelf 36 | ] 37 | } 38 | ] 39 | -------------------------------------------------------------------------------- /wu-server/src/modules/chapters.js/controller.js: -------------------------------------------------------------------------------- 1 | import Bookshelf from '../../models/bookshelfs' 2 | import Chapter from '../../models/chapters' 3 | import Novel from '../../models/novels' 4 | import * as Crawler from '../../utils/crawler' 5 | 6 | 7 | /** 8 | @api {POST} /chapters 获取章节信息 9 | @apiVersion 1.0.0 10 | @apiName 获取章节信息 11 | @apiGroup Chapters 12 | 13 | @apiExample Example usage: 14 | curl -H "Content-Type: application/json" -X GET http://localhost:5000/chapters/58c4cbce9e4dad30f80d34e7 15 | 16 | @apiParam {String} novelId 小说ID. 17 | @apiParam {String} num 小说章节. 18 | 19 | @apiSuccessExample {json} Success-Response: 20 | HTTP/1.1 200 OK 21 | { 22 | "detail": { 23 | "_id": "58c4cbce9e4dad30f80d373e", 24 | "title": "第五百九十九章,崩盘(2)", 25 | "postfix": "3455564.html", 26 | "number": 598, 27 | "novel": { 28 | "_id": "58c4cbce9e4dad30f80d34e7", 29 | "url": "http://www.37zw.com/3/3731/", 30 | "name": "1855美国大亨" 31 | }, 32 | "__v": 0, 33 | "content": "    安克雷奇港突然的就安静了下来,自打几个月前危机爆发后,尤其是标准石油宣布将下调燃油和原油价格之后,抵达这里的船只就越来越少了,最近一个星期,平均每天都只有几条船抵达港口。 而这些船当中,真正的最有意义的超级油轮却没多少,这个星期里仅仅只来了一条而已。其他的船都是些渔船呀,捕鲸船呀什么的。据说德国人现在还在购入石油,但是因为从标准直接购入石油的成本已经比从阿拉斯加开采更低了,所以那些油轮也不再往这边跑了。    十五的地步。剩下的那些没有失业的工人,他们的工资也大幅下降了两成到五成。虽然信奉工联主义的社民党努力的安抚,但是自发的罢工,乃至暴.动也开始此起彼伏的发生。    [三七中文手机版 m.37zw.com]" 34 | } 35 | } 36 | 37 | @apiErrorExample {json} Error-Response: 38 | HTTP/1.1 422 Unprocessable Entity 39 | { 40 | "status": 422, 41 | "error": "" 42 | } 43 | */ 44 | export async function getChapterInfo (ctx) { 45 | let {id, num} = ctx.request.body 46 | let detail, chapter 47 | const user = ctx.state.user 48 | 49 | try { 50 | chapter = await Chapter.findOne({novel: id}) 51 | detail = chapter.chapters[num] 52 | } catch (e) { 53 | Handle.sendEmail(e.message) 54 | ctx.throw(422, e.message) 55 | } 56 | 57 | // 如果没有内容,会去网站爬取 58 | if (detail.content) { 59 | ctx.body = { 60 | success: true 61 | } 62 | } else { 63 | const url = `${detail.postfix}` 64 | try { 65 | const content = await Crawler.getChapterContent(url) 66 | chapter.chapters[num].content = content 67 | await Chapter.update({novel: id}, { chapters: chapter.chapters }) 68 | } catch (e) { 69 | Handle.sendEmail(e.message) 70 | ctx.throw(422, e.message) 71 | } 72 | } 73 | const response = detail 74 | await Bookshelf.update({ user: user._id, novel: id }, { number: num }) 75 | ctx.body = { 76 | response 77 | } 78 | } 79 | 80 | /** 81 | @api {GET} /chapters/firstRender/:id 获取首次渲染章节 82 | @apiPermission User 83 | @apiVersion 1.0.0 84 | @apiName 获取首次渲染章节 85 | @apiGroup Chapters 86 | 87 | @apiExample Example usage: 88 | curl -H "Content-Type: application/json" "authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjU4YjhmZDRkODUyYTE1YzliNmYyNjI3MSIsImlhdCI6MTQ4ODU1MTc2N30.IEgYwmgyqOBft9s38ool7cmuC2yIlWYVLf4WQzcbqAI" -X GET http://localhost:5000/chapters/firstRender/58c4cbce9e4dad30f80d34e7 89 | 90 | @apiSuccess {Object} progress 章节进度 91 | 92 | @apiSuccessExample {json} Success-Response: 93 | HTTP/1.1 200 OK 94 | { 95 | "response": { 96 | "chapters": [ 97 | { 98 | "_id": "58ccc05923ff5c6b9d5053cc", 99 | "title": "上架感言", 100 | "postfix": "2352954.html", 101 | "number": 0, 102 | "novel": "58ccc05923ff5c6b9d5053cb", 103 | "__v": 0, 104 | "content": "    《1852铁血中华》已经上架。    这次写太平天国时代有很多原因,不过最重要的原因之一,是因为太平天国运动是清末民初最后一次体制外的造反,也是中国到现在为止的最后一次大规模农民起义。    一提起太平天国来,现在的人很容易想起的就是拜上帝教、洪教主,而实际情况又是如何?    在那个时代,满清朝廷虽然给洋人跪了,但是整个中国各阶层却远没有20世纪初面临亡国灭种危局的绝望之情。    这就是太平天国时代的中国。    在全世界主要大国正向着空前激烈的时代突飞猛进的时候,中国的变化远没有世界来的激烈。本书的主角要承担的责任,就是在中国掀起比世界更加激烈的变化。    对大家以往的支持,绯红万分感谢!绯红一定会努力继续写书,请大家继续支持,继续订阅!谢谢!!    [三七中文 www.37zw.com]百度搜索“37zw.com”" 105 | }, 106 | { 107 | "_id": "58ccc05923ff5c6b9d5053cd", 108 | "title": "第1章 韦泽(一)", 109 | "postfix": "2352958.html", 110 | "number": 1, 111 | "novel": "58ccc05923ff5c6b9d5053cb", 112 | "__v": 0, 113 | "content": "    1852年2月6日上午,广西大瑶山一处山岭上的树林旁边。    二十几名年轻的战士正在战场上,他们都穿着广西普通百姓的服色,身上是黑色的粗布短衣短裤,腰间束了白色的粗布腰带,腿上打着白布绑腿,脚上则是草鞋。因为天冷,战士们脚上缠了原本可能是白色,现在已经脏兮兮看不出颜色的裹脚布。众人脑袋上并不是广西那种包头布,因为大家都把长发在头上扎了一个发髻,所以在脑袋上箍了一条白色粗布发带,猛看上仿佛是一支奔丧的队伍。    一位浓眉大眼的青年停在战场中央,棱角分明脸庞有着少年轻人特有的圆润感觉,怎么看都不超过20岁的模样。这名笑道:“辫子又不重,带回去正好请功!”    广西号称百万大山,大瑶山山峦叠翠,在战场附近就有山谷。清军的尸体被抛入山谷,转眼就没了踪影。清军还能留在战场上的是他们的武器,十几名清军的武器中一半是长枪,另一半则是火绳枪。这就是韦泽回到这个时代之后另一件不能立刻接受的事情。这个时业化的日本进行着艰苦卓绝的战斗,并且顽强的不断扩大敌后根据地,把中国的国土从侵略者手中一寸寸的夺回来。    满清的火绳枪固然与这时代流行的燧发枪有不小差距,却远没有到达一场战斗只有五发子弹的八路军与敌人之间的差距。在武器装备差距有限的局面下还能被打得签署了无数丧权辱国的条约,这样的满清是必须消灭掉的。不消灭掉满清,就注定没有中国的未来。韦泽对此坚信不移。    十几名清军携带的钱财不多,韦泽登记造册后让负责后勤的伍长林阿生把财物收起来。    “出发!”韦泽命道。26人的小队扛起自己的装备,在韦泽的带领下向着东北方向继续前进了。    [三七中文 www.37zw.com]百度搜索“37zw.com”" 114 | } 115 | ], 116 | "progress": 0 117 | } 118 | } 119 | 120 | @apiErrorExample {json} Error-Response: 121 | HTTP/1.1 422 Unprocessable Entity 122 | { 123 | "status": 422, 124 | "error": "" 125 | } 126 | */ 127 | export async function getFirstRenderChapter (ctx) { 128 | let bookshelf, chapter, nextChapter, chapters, currentChapter 129 | const id = ctx.query.id 130 | const num = ctx.query.num * 1 131 | const user = ctx.state.user 132 | await Bookshelf.update({user: user._id}, { number: num }) 133 | try { 134 | bookshelf = await Bookshelf.findOne({user: user._id, novel: id}).populate('novel') 135 | chapter = await Chapter.findById(bookshelf.chapter) 136 | nextChapter = chapter.chapters[num + 1] 137 | if (bookshelf.novel.countChapter != num + 1 && !nextChapter.content) { 138 | nextChapter = chapter.chapters[num + 1] 139 | if (!nextChapter.content) { 140 | const url = `${nextChapter.postfix}` 141 | 142 | const content = await Crawler.getChapterContent(url) 143 | nextChapter.content = content 144 | chapter.chapters[num + 1].content = content 145 | await Chapter.update({_id: bookshelf.chapter}, { chapters: chapter.chapters }) 146 | // await chapter.save() 147 | } 148 | } 149 | } catch (e) { 150 | console.log(e) 151 | Handle.sendEmail(e.message) 152 | ctx.throw(423, e.message) 153 | } 154 | currentChapter = chapter.chapters[num] 155 | if (!currentChapter.content) { 156 | const url = `${currentChapter.postfix}` 157 | try { 158 | const content = await Crawler.getChapterContent(url) 159 | currentChapter.content = content 160 | chapter.chapters[num].content = content 161 | await Chapter.update({_id: bookshelf.chapter}, { chapters: chapter.chapters }) 162 | } catch (e) { 163 | Handle.sendEmail(e.message) 164 | ctx.throw(424, e.message) 165 | } 166 | } 167 | 168 | if (nextChapter) { 169 | chapters = [currentChapter, nextChapter] 170 | } else { 171 | chapters = [currentChapter] 172 | } 173 | const response = { 174 | chapters: chapters, 175 | progress: bookshelf.progress, 176 | countChapter: bookshelf.novel.countChapter 177 | } 178 | 179 | ctx.body = { 180 | response 181 | } 182 | } 183 | 184 | /** 185 | @api {GET} /chapter/next/:id 获取下一章信息 186 | @apiVersion 1.0.0 187 | @apiName 获取下一章信息 188 | @apiGroup Chapters 189 | 190 | @apiExample Example usage: 191 | curl -H "Content-Type: application/json" -X GET http://localhost:5000/chapters/next/58bc1ec43f9cdc31b9bea8dc 192 | 193 | 194 | @apiSuccessExample {json} Success-Response: 195 | HTTP/1.1 200 OK 196 | { 197 | "detail": { 198 | "_id": "58c4cbce9e4dad30f80d373e", 199 | "title": "第五百九十九章,崩盘(2)", 200 | "postfix": "3455564.html", 201 | "number": 598, 202 | "novel": { 203 | "_id": "58c4cbce9e4dad30f80d34e7", 204 | "url": "http://www.37zw.com/3/3731/", 205 | "name": "1855美国大亨" 206 | }, 207 | "__v": 0, 208 | "content": "    安克雷奇港突然的就安静了下来,自打几个月前危机爆发后,尤其是标准石油宣布将下调燃油和原油价格之后,抵达这里的船只就越来越少了,最近一个星期,平均每天都只有几条船抵达港口。 而这些船当中,真正的最有意义的超级油轮却没多少,这个星期里仅仅只来了一条而已。其他的船都是些渔船呀,捕鲸船呀什么的。据说德国人现在还在购入石油,但是因为从标准直接购入石油的成本已经比从阿拉斯加开采更低了,所以那些油轮也不再往这边跑了。    十五的地步。剩下的那些没有失业的工人,他们的工资也大幅下降了两成到五成。虽然信奉工联主义的社民党努力的安抚,但是自发的罢工,乃至暴.动也开始此起彼伏的发生。    [三七中文手机版 m.37zw.com]" 209 | } 210 | } 211 | 212 | @apiErrorExample {json} Error-Response: 213 | HTTP/1.1 422 Unprocessable Entity 214 | { 215 | "status": 422, 216 | "error": "" 217 | } 218 | */ 219 | export async function getNextChapterInfo (ctx) { 220 | const id = ctx.params.id 221 | try { 222 | var currentDetail = await Chapter.getContent(id) 223 | } catch (e) { 224 | Handle.sendEmail(e.message) 225 | ctx.throw(422, e.message) 226 | } 227 | 228 | const chapterNum = currentDetail.number + 1 229 | const novelId = currentDetail.novel.id 230 | try { 231 | var nextDetail = await Chapter.findByNumber(novelId, chapterNum) 232 | } catch (e) { 233 | Handle.sendEmail(e.message) 234 | ctx.throw(422, e.message) 235 | } 236 | 237 | if (nextDetail.content) { 238 | ctx.body = { 239 | success: true 240 | } 241 | } else { 242 | const url = `${currentDetail.novel.url}${nextDetail.postfix}` 243 | try { 244 | const content = await Crawler.getChapterContent(url) 245 | nextDetail.content = content 246 | await nextDetail.save() 247 | } catch (err) { 248 | Handle.sendEmail(e.message) 249 | ctx.throw(422, e.message) 250 | } 251 | } 252 | 253 | const response = nextDetail.toJSON() 254 | 255 | ctx.body = { 256 | detail: response 257 | } 258 | } 259 | 260 | /** 261 | @api {GET} /chapter/last/:id 获取上一章信息 262 | @apiVersion 1.0.0 263 | @apiName 获取上一章信息 264 | @apiGroup Chapters 265 | 266 | @apiExample Example usage: 267 | curl -H "Content-Type: application/json" -X GET http://localhost:5000/chapters/last/58bc1ec43f9cdc31b9bea8dc 268 | 269 | @apiSuccessExample {json} Success-Response: 270 | HTTP/1.1 200 OK 271 | { 272 | "detail": { 273 | "_id": "58c4cbce9e4dad30f80d373e", 274 | "title": "第五百九十九章,崩盘(2)", 275 | "postfix": "3455564.html", 276 | "number": 598, 277 | "novel": { 278 | "_id": "58c4cbce9e4dad30f80d34e7", 279 | "url": "http://www.37zw.com/3/3731/", 280 | "name": "1855美国大亨" 281 | }, 282 | "__v": 0, 283 | "content": "    安克雷奇港突然的就安静了下来,自打几个月前危机爆发后,尤其是标准石油宣布将下调燃油和原油价格之后,抵达这里的船只就越来越少了,最近一个星期,平均每天都只有几条船抵达港口。 而这些船当中,真正的最有意义的超级油轮却没多少,这个星期里仅仅只来了一条而已。其他的船都是些渔船呀,捕鲸船呀什么的。据说德国人现在还在购入石油,但是因为从标准直接购入石油的成本已经比从阿拉斯加开采更低了,所以那些油轮也不再往这边跑了。    十五的地步。剩下的那些没有失业的工人,他们的工资也大幅下降了两成到五成。虽然信奉工联主义的社民党努力的安抚,但是自发的罢工,乃至暴.动也开始此起彼伏的发生。    [三七中文手机版 m.37zw.com]" 284 | } 285 | } 286 | 287 | @apiErrorExample {json} Error-Response: 288 | HTTP/1.1 422 Unprocessable Entity 289 | { 290 | "status": 422, 291 | "error": "" 292 | } 293 | */ 294 | export async function getLastChapterInfo (ctx) { 295 | const id = ctx.params.id 296 | try { 297 | var currentDetail = await Chapter.getContent(id) 298 | } catch (e) { 299 | Handle.sendEmail(e.message) 300 | ctx.throw(422, err.message) 301 | } 302 | 303 | const chapterNum = currentDetail.number - 1 304 | const novelId = currentDetail.novel.id 305 | try { 306 | var lastDetail = await Chapter.findByNumber(novelId, chapterNum) 307 | } catch (e) { 308 | Handle.sendEmail(e.message) 309 | ctx.throw(422, e.message) 310 | } 311 | 312 | if (lastDetail.content) { 313 | ctx.body = { 314 | success: true 315 | } 316 | } else { 317 | const url = `${currentDetail.novel.url}${lastDetail.postfix}` 318 | try { 319 | const content = await Crawler.getChapterContent(url) 320 | lastDetail.content = content 321 | await lastDetail.save() 322 | } catch (e) { 323 | Handle.sendEmail(e.message) 324 | ctx.throw(422, e.message) 325 | } 326 | } 327 | 328 | const response = lastDetail.toJSON() 329 | 330 | ctx.body = { 331 | detail: response 332 | } 333 | } 334 | -------------------------------------------------------------------------------- /wu-server/src/modules/chapters.js/router.js: -------------------------------------------------------------------------------- 1 | import { ensureUser } from '../../middleware/validators' 2 | import * as chapter from './controller' 3 | 4 | export const baseUrl = '/chapters' 5 | 6 | export default [ 7 | { 8 | method: 'POST', 9 | route: '/', 10 | handlers: [ 11 | ensureUser, 12 | chapter.getChapterInfo 13 | ] 14 | }, 15 | { 16 | method: 'GET', 17 | route: '/firstRender', 18 | handlers: [ 19 | ensureUser, 20 | chapter.getFirstRenderChapter 21 | ] 22 | }, 23 | { 24 | method: 'GET', 25 | route: '/next/:id', 26 | handlers: [ 27 | chapter.getNextChapterInfo 28 | ] 29 | }, 30 | { 31 | method: 'GET', 32 | route: '/last/:id', 33 | handlers: [ 34 | chapter.getLastChapterInfo 35 | ] 36 | }, 37 | ] 38 | -------------------------------------------------------------------------------- /wu-server/src/modules/index.js: -------------------------------------------------------------------------------- 1 | import glob from 'glob' 2 | import Router from 'koa-router' 3 | 4 | exports = module.exports = function initModules (app) { 5 | glob(`${__dirname}/*`, { ignore: '**/index.js' }, (err, matches) => { 6 | if (err) { throw err } 7 | 8 | matches.forEach((mod) => { 9 | const router = require(`${mod}/router`) 10 | 11 | const routes = router.default 12 | const baseUrl = router.baseUrl 13 | const instance = new Router({ prefix: baseUrl }) 14 | 15 | routes.forEach((config) => { 16 | const { 17 | method = '', 18 | route = '', 19 | handlers = [] 20 | } = config 21 | 22 | const lastHandler = handlers.pop()// 会修改原数组 23 | // 集成路由,支持多重回调 24 | instance[method.toLowerCase()](route, ...handlers, async function(ctx) { 25 | return await lastHandler(ctx) 26 | }) 27 | 28 | app 29 | .use(instance.routes()) 30 | .use(instance.allowedMethods()) 31 | }) 32 | }) 33 | }) 34 | } 35 | -------------------------------------------------------------------------------- /wu-server/src/modules/novels.js/controller.js: -------------------------------------------------------------------------------- 1 | import Novel from '../../models/novels' 2 | import Bookshelf from '../../models/bookshelfs' 3 | import Chapter from '../../models/chapters' 4 | 5 | import * as Crawler from '../../utils/crawler' 6 | import simplePinyin from 'simple-pinyin' 7 | import cheerio from 'cheerio' 8 | import config from '../../../config' 9 | 10 | /** 11 | @api {GET} /novels/directory/:id 获取小说目录 12 | @apiDescription order是目录顺序1(升序)-1(降序) 13 | @apiVersion 1.0.0 14 | @apiName 获取小说目录 15 | @apiGroup Novels 16 | 17 | @apiExample Example usage: 18 | curl -H "Content-Type: application/json" -X GET http://localhost:5000/novels/directory/58c4cbce9e4dad30f80d34e7?order=1 19 | 20 | @apiSuccessExample {json} Success-Response: 21 | HTTP/1.1 200 OK 22 | { 23 | "directory": [ 24 | { 25 | "_id": "58c4cbce9e4dad30f80d34e8", 26 | "title": "第一章,缺钱的超级大腿", 27 | "number": 0 28 | }, 29 | { 30 | "_id": "58c4cbce9e4dad30f80d34e9", 31 | "title": "第二章,辍学的念头", 32 | "number": 1 33 | } 34 | ] 35 | } 36 | @apiError UnprocessableEntity 37 | 38 | @apiErrorExample {json} Error-Response: 39 | HTTP/1.1 422 Unprocessable Entity 40 | { 41 | "status": 422, 42 | "error": "" 43 | } 44 | */ 45 | export async function downloadChapters (ctx) { 46 | let results 47 | const id = ctx.params.id 48 | const options = { 49 | attributes: ['title', 'content', 'number'], 50 | order: 1 51 | } 52 | try { 53 | results = await Chapter.getDirectory(id, options) 54 | } catch (err) { 55 | Handle.sendEmail(e.message) 56 | ctx.throw(422, err.message) 57 | } 58 | 59 | ctx.body = { 60 | results 61 | } 62 | } 63 | 64 | /** 65 | @api {GET} /novels/search/zh 搜索小说名称 66 | @apiDescription 67 | @apiVersion 1.0.0 68 | @apiName 搜索小说名称 69 | @apiGroup Novels 70 | @apiExample Example usage: 71 | curl -H "Content-Type: application/json" -X GET http://localhost:5000/novels/search/zh?keyword=永夜 72 | 73 | @apiParam {String} keyword 搜索关键词. 74 | 75 | @apiSuccessExample {json} Success-Response: 76 | 纵横 77 | HTTP/1.1 200 OK 78 | { 79 | "response": { 80 | "q": "yongye", 81 | "r": [ 82 | { 83 | "word": "永夜君王" 84 | }, 85 | { 86 | "word": "永夜之帝国双璧" 87 | }, 88 | { 89 | "word": "永夜帝王" 90 | }, 91 | { 92 | "word": "永夜王座" 93 | } 94 | ] 95 | } 96 | } 97 | 起点 98 | { 99 | "response": { 100 | "code": "0", 101 | "query": "Unit", 102 | "suggestions": [ 103 | { 104 | "value": "永夜君王", 105 | data:{ category } 106 | }, 107 | ] 108 | } 109 | } 110 | @apiErrorExample {json} Error-Response: 111 | HTTP/1.1 422 Unprocessable Entity 112 | { 113 | "status": 422, 114 | "error": "" 115 | } 116 | */ 117 | export async function searchFromZH (ctx) { 118 | const keyword = ctx.query.keyword 119 | //将汉字转拼音 120 | const words = simplePinyin(keyword, { pinyinOnly: false }) 121 | let word = '' 122 | words.forEach(function (item) { 123 | word += item 124 | }) 125 | 126 | // 调用纵横api接口 127 | const url = `http://search.zongheng.com/search/mvc/suggest.do?keyword=${word}` 128 | // 起点api 129 | const qdUrl = `https://www.qidian.com/ajax/Search/AutoComplete?_csrfToken=&siteid=1&query=${word}` 130 | try { 131 | var body = await Crawler.getBody(qdUrl) 132 | } catch (e) { 133 | Handle.sendEmail(e.message) 134 | ctx.throw(422, err.message) 135 | } 136 | const response = JSON.parse(body) 137 | let r = [] 138 | response.suggestions.map(v => { 139 | r.push({ word: v.value }) 140 | }) 141 | ctx.body = { 142 | response: r, 143 | q: word 144 | } 145 | } 146 | 147 | /** 148 | @api {GET} novels/search/bqk 获取小说列表 149 | @apiDescription 150 | @apiVersion 1.0.0 151 | @apiName 获取小说列表 152 | @apiGroup Novels 153 | @apiExample Example usage: 154 | curl -H "Content-Type: application/json" -X GET http://localhost:5000/novels/search/bqk?name=永夜 155 | 156 | @apiParam {String} keyword 搜索关键词. 157 | 158 | @apiSuccessExample {json} Success-Response: 159 | HTTP/1.1 200 OK 160 | { 161 | "response": [ 162 | { 163 | "title": "永夜君王", 164 | "img": "http://www.37zw.com/d/image/2/2790/2790s.jpg", 165 | "url": "http://www.37zw.com/2/2790/", 166 | "introduction": "千夜自困苦中崛起,在背叛中坠落。自此一个人,一把枪,行在永夜与黎明之间,却走出一段传奇。若永夜注定是他的命运,那他也要成为主宰的王。", 167 | "author": "烟雨江南", 168 | "type": "玄幻小说" 169 | }, 170 | { 171 | "title": "永镇天渊", 172 | "img": "http://www.37zw.com/d/image/5/5926/5926s.jpg", 173 | "url": "http://www.37zw.com/5/5926/", 174 | "introduction": "从天坠落,当永耀烈阳归于寂灭……当诸天银河黯淡无光,当万事万物化为墟烬……这是被天渊笼罩的世界这是注定沦于破灭的宇宙然而高川降临了所以未来的一切便都被", 175 | "author": "阴天神隐", 176 | "type": "修真小说" 177 | } 178 | ] 179 | } 180 | @apiErrorExample {json} Error-Response: 181 | HTTP/1.1 422 Unprocessable Entity 182 | { 183 | "status": 422, 184 | "error": "" 185 | } 186 | */ 187 | export async function searchFromBQK (ctx) { 188 | const name = ctx.query.name 189 | 190 | // 书趣阁搜索网站 191 | const url = `http://zhannei.baidu.com/cse/search?s=920895234054625192&q=${name}` 192 | try { 193 | var body = await Crawler.request(encodeURI(url)) 194 | } catch (e) { 195 | Handle.sendEmail(e.message) 196 | ctx.throw(422, e.message) 197 | } 198 | const $ = cheerio.load(body, {decodeEntities: false}) 199 | 200 | const length = $('.result-game-item-detail').length 201 | 202 | //爬取小说信息,用于展示 203 | let arr = [] 204 | for (let i = 0; i < length; i++) { 205 | const title = $('.result-game-item-title a')[i].attribs.title 206 | const img = $('.result-game-item-pic img')[i].attribs.src 207 | const url = $('.result-game-item-title a')[i].attribs.href 208 | const introduction = $('.result-game-item-desc').eq(i).text().replace(/\s/g, "") 209 | const author = $('.result-game-item-info').eq(i).find('p:first-child span:last-child').text().replace(/\s/g, "") 210 | const type = $('.result-game-item-info').eq(i).find('p:nth-child(2) span:last-child').text().replace(/\s/g, "") 211 | let json = { 212 | title: title, 213 | img: img, 214 | url: url, 215 | introduction: introduction, 216 | author: author, 217 | type: type 218 | } 219 | if (url.indexOf('.php') == -1) { 220 | 221 | arr.push(json) 222 | } 223 | } 224 | 225 | ctx.body = { 226 | response: arr 227 | } 228 | } 229 | 230 | /** 231 | @api {POST} /novels/acquire 爬取小说 232 | @apiPermission User 233 | @apiVersion 1.0.0 234 | @apiName 爬取小说 235 | @apiGroup Novels 236 | 237 | @apiExample Example usage: 238 | curl -H "Content-Type: application/json" "authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjU4YjhmZDRkODUyYTE1YzliNmYyNjI3MSIsImlhdCI6MTQ4ODU1MTc2N30.IEgYwmgyqOBft9s38ool7cmuC2yIlWYVLf4WQzcbqAI" -X GET http://localhost:5000/novels/acquire 239 | 240 | @apiParam {Object} novel 小说对象 (必需) 241 | @apiParam {String} novel.name 小说名称. 242 | @apiParam {String} novel.url 爬取网站的url. 243 | 244 | @apiSuccessExample {json} Success-Response: 245 | HTTP/1.1 200 OK 246 | { 247 | "success": true 248 | } 249 | @apiError UnprocessableEntity 250 | 251 | @apiErrorExample {json} Error-Response: 252 | HTTP/1.1 422 Unprocessable Entity 253 | { 254 | "status": 422, 255 | "error": "" 256 | } 257 | */ 258 | export async function getNovel (ctx) { 259 | const user = ctx.state.user 260 | const { name, url } = ctx.request.body.novel 261 | try { 262 | var novel = await Novel.findOne({name: name}) 263 | } catch (e) { 264 | ctx.throw(422, e.message) 265 | } 266 | 267 | // 判断数据库中是否有该小说,没有在去网站爬取 268 | if (novel) { 269 | try { 270 | var bookshelf = await Bookshelf.findOne({novel: novel.id, user: user.id}) 271 | } catch (e) { 272 | Handle.sendEmail(e.message) 273 | ctx.throw(422, e.message) 274 | } 275 | const response = novel.toJSON() 276 | if (bookshelf) { 277 | response.join = true 278 | } else { 279 | response.join = false 280 | } 281 | 282 | ctx.body = { 283 | novelInfo: response 284 | } 285 | } else { 286 | try { 287 | var $ = await Crawler.getHtml(url) 288 | } catch (e) { 289 | Handle.sendEmail(e.message) 290 | ctx.throw(422, e.message) 291 | } 292 | let novelInfo = {} 293 | const author = $('#info p').eq(0).text() 294 | if (!author) { 295 | ctx.body = { 296 | code: 404, 297 | msg: '无小说信息' 298 | } 299 | return 300 | } 301 | const updateTime = $('#info p').eq(2).text() 302 | const img = $('#fmimg img')[0].attribs.src 303 | novelInfo.url = url 304 | novelInfo.name = $('#info h1').text() 305 | novelInfo.author = author.split(':')[1].trim() 306 | novelInfo.img = config.crawlerUrl + img 307 | novelInfo.updateTime = updateTime.substring(5, updateTime.length) 308 | novelInfo.introduction = $('#intro').text() 309 | novel = new Novel(novelInfo) 310 | try { 311 | await novel.save() 312 | } catch (e) { 313 | Handle.sendEmail(e.message) 314 | ctx.throw(423, e.message) 315 | } 316 | const novelId = novel.id 317 | await Crawler.getNovel($, novelId, url) 318 | try { 319 | var titles = await Chapter.getTitles(novelId) 320 | var chapters = titles[0].chapters 321 | var count = chapters.length 322 | } catch (e) { 323 | Handle.sendEmail(e.message) 324 | ctx.throw(424, e.message) 325 | } 326 | novel.lastChapterTitle = chapters[chapters.length - 1].title 327 | novel.countChapter = count 328 | try { 329 | await novel.save() 330 | } catch (e) { 331 | Handle.sendEmail(e.message) 332 | ctx.throw(425, e.message) 333 | } 334 | const response = novel.toJSON() 335 | response.join = false 336 | ctx.body = { 337 | code: 0, 338 | novelInfo: response 339 | } 340 | } 341 | } 342 | 343 | /** 344 | @api {GET} /novels/directory/:id 获取小说目录 345 | @apiDescription order是目录顺序1(升序)-1(降序) 346 | @apiVersion 1.0.0 347 | @apiName 获取小说目录 348 | @apiGroup Novels 349 | 350 | @apiExample Example usage: 351 | curl -H "Content-Type: application/json" -X GET http://localhost:5000/novels/directory/58c4cbce9e4dad30f80d34e7?order=1 352 | 353 | @apiSuccessExample {json} Success-Response: 354 | HTTP/1.1 200 OK 355 | { 356 | "directory": [ 357 | { 358 | "_id": "58c4cbce9e4dad30f80d34e8", 359 | "title": "第一章,缺钱的超级大腿", 360 | "number": 0 361 | }, 362 | { 363 | "_id": "58c4cbce9e4dad30f80d34e9", 364 | "title": "第二章,辍学的念头", 365 | "number": 1 366 | } 367 | ] 368 | } 369 | @apiError UnprocessableEntity 370 | 371 | @apiErrorExample {json} Error-Response: 372 | HTTP/1.1 422 Unprocessable Entity 373 | { 374 | "status": 422, 375 | "error": "" 376 | } 377 | */ 378 | export async function getDirectory (ctx) { 379 | let results 380 | const id = ctx.params.id 381 | const order = ctx.query.order 382 | let chapters; 383 | try { 384 | results = await Chapter.findOne({novel: id}) 385 | chapters = results.chapters 386 | } catch (e) { 387 | Handle.sendEmail(e.message) 388 | ctx.throw(422, e.message) 389 | } 390 | if (order === '-1') { 391 | chapters = chapters.reverse() 392 | } 393 | ctx.body = { 394 | chapters 395 | } 396 | } 397 | 398 | // export async function createNovel (ctx) { 399 | // const novel = new Novel(ctx.request.body.novel) 400 | // try { 401 | // await novel.save() 402 | // } catch (err) { 403 | // ctx.throw(422, err.message) 404 | // } 405 | // 406 | // ctx.body = { 407 | // success: true 408 | // } 409 | // } 410 | // 411 | // export async function searchNovel (ctx) { 412 | // const keyword = ctx.query.keyword 413 | // 414 | // const url = `http://zhannei.baidu.com/cse/search?q=${keyword}&s=2041213923836881982` 415 | // 416 | // if (keyword) { 417 | // var list = await Crawler.getSearchLists(encodeURI(url)) 418 | // } 419 | // else { 420 | // var list = '' 421 | // } 422 | // 423 | // ctx.body = { 424 | // list: list 425 | // } 426 | // } 427 | -------------------------------------------------------------------------------- /wu-server/src/modules/novels.js/router.js: -------------------------------------------------------------------------------- 1 | import { ensureUser } from '../../middleware/validators' 2 | import * as novel from './controller' 3 | 4 | export const baseUrl = '/novels' 5 | 6 | export default [ 7 | { 8 | method: 'GET', 9 | route: '/:id', 10 | handlers: [ 11 | novel.downloadChapters 12 | ] 13 | }, 14 | { 15 | method: 'GET', 16 | route: '/search/zh', 17 | handlers: [ 18 | novel.searchFromZH 19 | ] 20 | }, 21 | { 22 | method: 'GET', 23 | route: '/search/bqk', 24 | handlers: [ 25 | novel.searchFromBQK 26 | ] 27 | }, 28 | { 29 | method: 'POST', 30 | route: '/search', 31 | handlers: [ 32 | novel.searchNovel 33 | ] 34 | }, 35 | { 36 | method: 'POST', 37 | route: '/acquire', 38 | handlers: [ 39 | ensureUser, 40 | novel.getNovel 41 | ] 42 | }, 43 | { 44 | method: 'GET', 45 | route: '/directory/:id', 46 | handlers: [ 47 | novel.getDirectory 48 | ] 49 | } 50 | ] 51 | -------------------------------------------------------------------------------- /wu-server/src/modules/test/controller.js: -------------------------------------------------------------------------------- 1 | import Chapter from '../../models/chapters' 2 | import Novel from '../../models/novels' 3 | import * as Crawler from '../../utils/crawler' 4 | import * as UpdateNovel from '../../utils/updateNovel' 5 | 6 | 7 | export async function getChapters (ctx) { 8 | await UpdateNovel.update() 9 | ctx.body = { 10 | success: true 11 | } 12 | } 13 | 14 | export async function getImgs (ctx) { 15 | const type = ctx.query.type 16 | // const data = await Crawler.request('http://112.74.34.241:3000/meizi/random?type=%E5%8F%B0%E6%B9%BE') 17 | // const url = JSON.parse(data).url 18 | 19 | ctx.body = { 20 | // url: url 21 | url: "http://og1m0yoqf.bkt.clouddn.com/banner_index.png" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /wu-server/src/modules/test/router.js: -------------------------------------------------------------------------------- 1 | import * as test from './controller' 2 | 3 | export const baseUrl = '/test' 4 | 5 | export default [ 6 | { 7 | method: 'GET', 8 | route: '/', 9 | handlers: [ 10 | test.getChapters 11 | ] 12 | }, 13 | { 14 | method: 'GET', 15 | route: '/img', 16 | handlers: [ 17 | test.getImgs 18 | ] 19 | } 20 | ] 21 | -------------------------------------------------------------------------------- /wu-server/src/modules/users/controller.js: -------------------------------------------------------------------------------- 1 | import Joi from 'joi' 2 | import User from '../../models/users' 3 | 4 | /** 5 | @api {POST} /users/tourists 新增游客 6 | @apiDescription 7 | @apiVersion 1.0.0 8 | @apiName 新增游客 9 | @apiGroup Users 10 | @apiExample Example usage: 11 | curl -H "Content-Type: application/json" -X GET http://localhost:5000/users/tourists 12 | 13 | @apiParam {Object} user 用户对象 (必需) 14 | @apiParam {String} user.uuid ios唯一标识. 15 | 16 | @apiSuccessExample {json} Success-Response: 17 | HTTP/1.1 200 OK 18 | { 19 | "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjU4Yzk0OTk3ZDE4ZTIwMjRiNjYzNjBmYiIsImlhdCI6MTQ4OTYzNjI5M30.OHd22AfwQaUZAuNC2A4w28THizwtf4UgRvWhc3lBuSI" 20 | } 21 | @apiErrorExample {json} Error-Response: 22 | HTTP/1.1 422 Unprocessable Entity 23 | { 24 | "status": 422, 25 | "error": "" 26 | } 27 | */ 28 | export async function createTourist (ctx) { 29 | const uuid = ctx.request.body.user.uuid 30 | // 先查 31 | try { 32 | var user = await User.findOne({ uuid: uuid }) 33 | } catch (err) { 34 | Handle.sendEmail(err.message) 35 | ctx.throw(422, err.message) 36 | } 37 | 38 | if (user) { 39 | var token = user.generateToken() 40 | } else { 41 | user = new User(ctx.request.body.user) 42 | try { 43 | await user.save() 44 | } catch (err) { 45 | Handle.sendEmail(err.message) 46 | ctx.throw(422, err.message) 47 | } 48 | 49 | var token = user.generateToken() 50 | } 51 | 52 | ctx.body = { 53 | token 54 | } 55 | } 56 | 57 | export async function register (ctx) { 58 | const req = ctx.request.body 59 | const bodySchema = Joi.object().keys({ 60 | username: Joi.string().optional(), 61 | password: Joi.string().optional(), 62 | uuid: Joi.string().optional(), 63 | code: Joi.string().optional() 64 | }).unknown() 65 | const result = Joi.validate(req, bodySchema, { allowUnknown: true }) 66 | const { username, password, code } = result.value 67 | const captcha = ctx.session.captcha.toLowerCase() 68 | if (captcha !== code.toLowerCase()) { 69 | ctx.body = { 70 | code: -1, 71 | msg: '验证码错误' 72 | } 73 | return 74 | } 75 | delete req.code 76 | // 先查 77 | try { 78 | var user = await User.findOne({ username }) 79 | } catch (err) { 80 | Handle.sendEmail(err.message) 81 | ctx.throw(422, err.message) 82 | } 83 | if (user) { 84 | console.log(user, '88888s') 85 | ctx.body = { 86 | code: -1, 87 | msg: '账户已注册' 88 | } 89 | return 90 | } else { 91 | req.uuid = Date.now().toString() 92 | user = new User(req) 93 | try { 94 | await user.save() 95 | } catch (err) { 96 | Handle.sendEmail(err.message) 97 | ctx.throw(425, err.message) 98 | } 99 | 100 | var token = user.generateToken() 101 | } 102 | 103 | ctx.body = { 104 | code: 0, 105 | token, 106 | username 107 | } 108 | } 109 | 110 | export async function getCaptcha (ctx) { 111 | const svgCaptcha = require('svg-captcha') 112 | const captcha = svgCaptcha.create({ 113 | width: 80, 114 | height: 40, 115 | ignoreChars: '0o1i' 116 | }) 117 | if (captcha.data) { 118 | ctx.session.captcha = captcha.text 119 | ctx.type = 'svg' 120 | ctx.body = { 121 | code: 0, 122 | response: captcha.data 123 | } 124 | } else { 125 | ctx.throw(422, '生成验证码失败') 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /wu-server/src/modules/users/router.js: -------------------------------------------------------------------------------- 1 | import { ensureUser } from '../../middleware/validators' 2 | import * as user from './controller' 3 | 4 | export const baseUrl = '/users' 5 | 6 | export default [ 7 | { 8 | method: 'POST', 9 | route: '/tourists', 10 | handlers: [ 11 | user.createTourist 12 | ] 13 | }, 14 | { 15 | method: 'POST', 16 | route: '/register', 17 | handlers: [ 18 | user.register 19 | ] 20 | }, 21 | { 22 | method: 'GET', 23 | route: '/captcha', 24 | handlers: [ 25 | user.getCaptcha 26 | ] 27 | } 28 | ] 29 | 30 | -------------------------------------------------------------------------------- /wu-server/src/utils/auth.js: -------------------------------------------------------------------------------- 1 | export function getToken (ctx) { 2 | const header = ctx.request.header.authorization 3 | if (!header) { 4 | return null 5 | } 6 | 7 | const parts = header.split(' ') 8 | if (parts.length !== 2) { 9 | return null 10 | } 11 | const scheme = parts[0] 12 | const token = parts[1] 13 | if (/^Bearer$/i.test(scheme)) { 14 | return token 15 | } 16 | return null 17 | } 18 | -------------------------------------------------------------------------------- /wu-server/src/utils/crawler.js: -------------------------------------------------------------------------------- 1 | import http from 'http' 2 | import Request from 'request' 3 | import cheerio from 'cheerio' 4 | import iconv from 'iconv-lite' 5 | import Chapter from '../models/chapters' 6 | import config from '../../config' 7 | 8 | export function getSearchLists (url) { 9 | return new Promise(function (resolve, reject) { 10 | Request(url, function (err, res, body) { 11 | if (!err) { 12 | let results = [] 13 | 14 | const $ = cheerio.load(body) 15 | 16 | const list = $('.result-item-title a') 17 | 18 | for (let i = 0, len = list.length; i < len; i++) { 19 | let name = list[i].attribs.title 20 | let url = list[i].attribs.href 21 | 22 | results.push({ 23 | name: name, 24 | url: url 25 | }) 26 | } 27 | resolve(results) 28 | } else { 29 | reject(err) 30 | } 31 | }) 32 | }) 33 | } 34 | 35 | export async function getNovel ($, id, url) { 36 | const list = $('#list dd a') 37 | let chapters = [] 38 | for (let i = 0, len = list.length; i < len; i++) { 39 | let title = list[i].children[0].data 40 | let href = config.crawlerUrl + '/' + list[i].attribs.href 41 | chapters.push({ 42 | title: title, 43 | postfix: href, 44 | number: i, 45 | content: '' 46 | }) 47 | } 48 | let chapter = new Chapter({ 49 | novel: id, 50 | chapters 51 | }) 52 | await chapter.save() 53 | } 54 | 55 | export function getChapterContent (url) { 56 | return new Promise(function (resolve, reject) { 57 | Request({url: url, encoding: null}, function (err, res, body) { 58 | if (!err) { 59 | const html = iconv.decode(body, 'utf-8') 60 | const $ = cheerio.load(html, {decodeEntities: false}) 61 | const content = $('#content').text() 62 | resolve(content) 63 | } else { 64 | reject(err) 65 | } 66 | }) 67 | }) 68 | } 69 | 70 | export function getHtml (url) { 71 | return new Promise(function (resolve, reject) { 72 | Request({url: url, encoding: null}, function (err, res, body) { 73 | if (!err) { 74 | const html = iconv.decode(body, 'utf-8') 75 | const $ = cheerio.load(html, {decodeEntities: false}) 76 | resolve($) 77 | } else { 78 | console.log(err) 79 | reject(err) 80 | } 81 | }) 82 | }) 83 | } 84 | 85 | export function getBody (url) { 86 | return new Promise(function (resolve, reject) { 87 | Request({url: url, encoding: null}, function (err, res, body) { 88 | if (!err) { 89 | const _body = iconv.decode(body, 'utf-8') 90 | resolve(_body) 91 | } else { 92 | reject(err) 93 | } 94 | }) 95 | }) 96 | } 97 | 98 | export function request (url) { 99 | return new Promise(function (resolve, reject) { 100 | http.get(url, function (res) { 101 | let body = '' 102 | 103 | res.on('data', function (data) { 104 | body += data 105 | }) 106 | res.on('end', function () { 107 | resolve(body) 108 | }) 109 | }).on('error', function (e) { 110 | reject(e) 111 | }) 112 | }) 113 | } 114 | 115 | async function saveChapters (url, list, first, last) { 116 | for (let i = first; i < last; i++) { 117 | let title = list[i].children[0].data 118 | let href = list[i].attribs.href 119 | let num = href.substring(0, href.length - 5) 120 | let contentUrl = `${url}${href}` 121 | let content = await getContent(contentUrl) 122 | 123 | let chapter = new Chapter({ 124 | title: title, 125 | number: num, 126 | content: content 127 | }) 128 | 129 | await chapter.save() 130 | } 131 | 132 | return true 133 | } 134 | -------------------------------------------------------------------------------- /wu-server/src/utils/handle/index.js: -------------------------------------------------------------------------------- 1 | //全局函数 2 | 3 | import nodemailer from 'nodemailer' 4 | import smtpTransport from 'nodemailer-smtp-transport' 5 | import wellknown from 'nodemailer-wellknown' 6 | import app from '../../../bin/server' 7 | // import * as Redis from '../redis' 8 | 9 | const handle = {} 10 | 11 | //统计api访问量 12 | handle.count = async (key) => { 13 | try { 14 | return true 15 | // let num = await Redis.get(key) 16 | // let newNum = num === 'NaN' ? 0 : parseInt(num) + 1 17 | // await Redis.set(key, newNum) 18 | } catch (e) { 19 | Handle.sendEmail(e.message) 20 | } 21 | } 22 | 23 | handle.success = (data = {}) => { 24 | return { 25 | success: true, 26 | code: 0, 27 | data: data 28 | } 29 | } 30 | 31 | //报错时,发送邮件 32 | handle.sendEmail = (mes) => { 33 | 34 | const config = wellknown("QQ") 35 | config.auth = { 36 | user: '', 37 | pass: '', 38 | } 39 | 40 | const transporter = nodemailer.createTransport(smtpTransport(config)) 41 | 42 | const mailOptions = { 43 | from: '', 44 | to: '', 45 | subject: '妈的又出Bug了,赶紧去调。', 46 | text: '', 47 | html: mes, 48 | } 49 | 50 | if (app.env === 'production') { 51 | transporter.sendMail(mailOptions, function(error, info){ 52 | if(error){ 53 | console.log(error) 54 | } 55 | else { 56 | console.log('Message sent: ' + info.response) 57 | } 58 | 59 | }) 60 | } 61 | 62 | } 63 | 64 | 65 | 66 | 67 | module.exports = handle 68 | -------------------------------------------------------------------------------- /wu-server/src/utils/redis.js: -------------------------------------------------------------------------------- 1 | // import Config from '../../config' 2 | 3 | 4 | // const redis = require('redis').createClient(Config.redis) 5 | // redis.on("error", function (err) { 6 | // console.log("Error " + err) 7 | // }) 8 | 9 | // //存入redis 10 | // export function set(key, value, expire) { 11 | // return new Promise(function(resolve, reject) { 12 | // const multi = redis.multi() 13 | // multi.set(key, value) 14 | // if (expire) { 15 | // multi.expire(key, expire) 16 | // } 17 | // try { 18 | // multi.exec() 19 | // } catch (e) { 20 | // reject(e) 21 | // } 22 | // resolve() 23 | // }) 24 | // } 25 | 26 | // //从redis中取出数据 27 | // export function get(key) { 28 | // return new Promise(function(resolve, reject) { 29 | // redis.get(key, (e, result) => { 30 | // if (e) { 31 | // reject(e) 32 | // } 33 | // else { 34 | // resolve(result) 35 | // } 36 | // }) 37 | // }) 38 | // } 39 | -------------------------------------------------------------------------------- /wu-server/src/utils/schedule/updateChapter.js: -------------------------------------------------------------------------------- 1 | let schedule = require('node-schedule') 2 | let cucurrentRule = new schedule.RecurrenceRule() 3 | import * as UpdateNovel from '../updateNovel' 4 | // let scheduleTime = [10, 51, 81] // 检查频率 5 | // cucurrentRule.dayOfWeek = 0 6 | cucurrentRule.hour = 24 7 | cucurrentRule.minute = 0 8 | // cucurrentRule.second = scheduleTime 9 | console.log('===scheduler start ========.') 10 | schedule.scheduleJob(cucurrentRule, async function () { 11 | try { 12 | console.info('===update started at: ' + new Date()) 13 | await UpdateNovel.update() 14 | console.log('====down at: ' + new Date()) 15 | } catch (error) { 16 | console.error('process error: ' + error) 17 | } finally { 18 | console.info('===ended at: ' + new Date()) 19 | } 20 | }) 21 | -------------------------------------------------------------------------------- /wu-server/src/utils/updateNovel.js: -------------------------------------------------------------------------------- 1 | import Bookshelf from '../models/bookshelfs' 2 | import Novel from '../models/novels' 3 | import * as Crawler from './crawler' 4 | 5 | import nodemailer from 'nodemailer' 6 | import wellknown from 'nodemailer-wellknown' 7 | import smtpTransport from 'nodemailer-smtp-transport' 8 | 9 | export async function update () { 10 | let crawlerList, // 获取所有需要更新提醒的小说 11 | $, // DOM 12 | length, // 章节数量 13 | chapterArr, // 所有a标签里包含的数组 14 | count 15 | try { 16 | // 将需要爬取的小说类型设置成VIP 17 | crawlerList = await Novel.find({ type: 'VIP' }) 18 | } catch (e) { 19 | Handle.sendEmail(e.message) 20 | } 21 | 22 | if (crawlerList) { 23 | for (let item of crawlerList.values()) { 24 | try { 25 | $ = await Crawler.getHtml(item.url) 26 | } catch (e) { 27 | Handle.sendEmail(e.message) 28 | } 29 | 30 | chapterArr = $('#list dd a') 31 | length = $('#list dd').length 32 | count = parseInt(item.countChapter) 33 | console.log(length, count, '===========') 34 | // 如果爬取的章节列表数量不等于数据库中的数量,爬取余下章节 35 | if (count !== length) { 36 | // for (let i = count; i < length; i++) { 37 | // chapter = new Chapter({ 38 | // postfix: chapterArr[i].attribs.href, 39 | // title: chapterArr[i].children[0].data, 40 | // number: i + 1, 41 | // novel: item.id 42 | // }) 43 | // try { 44 | // await chapter.save() 45 | // } catch (e) { 46 | // Handle.sendEmail(e.message) 47 | // } 48 | // } 49 | try { 50 | await Crawler.getNovel($, item._id) 51 | } catch (e) { 52 | Handle.sendEmail(e.message) 53 | } 54 | 55 | item.updateTime = $('#info p')[2].children[0].data.substring( 56 | 5, 57 | $('#info p')[2].children[0].data.length 58 | ) 59 | item.countChapter = length 60 | item.lastChapterTitle = chapterArr[length - 1].children[0].data 61 | try { 62 | await item.save() 63 | } catch (e) { 64 | Handle.sendEmail(e.message) 65 | } 66 | // 发送邮件并更新小说信息 67 | sendRemindEmail(item) 68 | } 69 | } 70 | } 71 | } 72 | 73 | async function sendRemindEmail (novel) { 74 | let userEmails 75 | try { 76 | userEmails = await Bookshelf.find({ novel: novel.id }) 77 | .populate('user', ['email']) 78 | .populate('novel', ['name']) 79 | .exec() 80 | } catch (e) { 81 | Handle.sendEmail(e.message) 82 | } 83 | 84 | userEmails.forEach(function (item) { 85 | const email = item.user.email 86 | const name = item.novel.name 87 | const config = wellknown('QQ') 88 | config.auth = { 89 | user: '', 90 | pass: '' 91 | } 92 | 93 | const transporter = nodemailer.createTransport(smtpTransport(config)) 94 | 95 | const mailOptions = { 96 | from: '', 97 | to: email, 98 | subject: `${name}更新了,Happy!`, 99 | text: '', 100 | html: '' 101 | } 102 | 103 | transporter.sendMail(mailOptions, function (error, info) { 104 | if (error) { 105 | console.log(error) 106 | Handle.sendEmail(error.message) 107 | } else { 108 | console.log('Message sent: ' + info.response) 109 | } 110 | }) 111 | }) 112 | } 113 | -------------------------------------------------------------------------------- /wyfReader/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react-native"], 3 | "plugins": [ 4 | "transform-decorators-legacy", 5 | ["import", { "libraryName": "antd-mobile" }] 6 | ] 7 | } -------------------------------------------------------------------------------- /wyfReader/.buckconfig: -------------------------------------------------------------------------------- 1 | 2 | [android] 3 | target = Google Inc.:Google APIs:23 4 | 5 | [maven_repositories] 6 | central = https://repo1.maven.org/maven2 7 | -------------------------------------------------------------------------------- /wyfReader/.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_size = 2 8 | indent_style = space 9 | insert_final_newline = true 10 | max_line_length = 80 11 | trim_trailing_whitespace = true 12 | 13 | [*.md] 14 | max_line_length = 0 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /wyfReader/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "env": { 4 | "browser": true 5 | }, 6 | "extends": [ 7 | "airbnb", 8 | "prettier", 9 | "prettier/react" 10 | ], 11 | "plugins": [ 12 | "react", 13 | "jsx-a11y", 14 | "import", 15 | "prettier" 16 | ], 17 | "globals": { 18 | "__DEV__": true 19 | }, 20 | "rules": { 21 | "arrow-parens": 0, 22 | "global-require": 0, 23 | "import/prefer-default-export": 0, 24 | "no-console": 0, 25 | "no-mixed-operators": 0, 26 | "no-use-before-define": 0, 27 | "radix": 0, 28 | "react/jsx-filename-extension": 0, 29 | "react/react-in-jsx-scope": 0, 30 | "react/prefer-stateless-function":0, 31 | "no-underscore-dangle":0, 32 | "react/prop-types": 0, 33 | "semi": [0, "never"], 34 | "spaced-comment": 0, 35 | "func-names": 0, 36 | "object-shorthand": 0, 37 | "object-shorthan": 0, 38 | "no-lonely-if": 0, 39 | "one-var": 0, 40 | "prefer-const": 0, 41 | "no-plusplus": 0, 42 | "camelcase": 0, 43 | "eqeqeq": 0, 44 | "react/no-string-refs": 0, 45 | "class-methods-use-this": 0, 46 | "react/self-closing-comp": 0, 47 | "react/jsx-no-bind": 0, 48 | "no-unused-vars": 1, 49 | "no-return-assign": 0, 50 | "react/sort-comp": 0 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /wyfReader/.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | ; We fork some components by platform 3 | .*/*[.]android.js 4 | 5 | ; Ignore "BUCK" generated dirs 6 | /\.buckd/ 7 | 8 | ; Ignore unexpected extra "@providesModule" 9 | .*/node_modules/.*/node_modules/fbjs/.* 10 | 11 | ; Ignore duplicate module providers 12 | ; For RN Apps installed via npm, "Libraries" folder is inside 13 | ; "node_modules/react-native" but in the source repo it is in the root 14 | .*/Libraries/react-native/React.js 15 | 16 | ; Ignore polyfills 17 | .*/Libraries/polyfills/.* 18 | 19 | [include] 20 | 21 | [libs] 22 | node_modules/react-native/Libraries/react-native/react-native-interface.js 23 | node_modules/react-native/flow/ 24 | 25 | [options] 26 | emoji=true 27 | 28 | module.system=haste 29 | 30 | munge_underscores=true 31 | 32 | module.name_mapper='^[./a-zA-Z0-9$_-]+\.\(bmp\|gif\|jpg\|jpeg\|png\|psd\|svg\|webp\|m4v\|mov\|mp4\|mpeg\|mpg\|webm\|aac\|aiff\|caf\|m4a\|mp3\|wav\|html\|pdf\)$' -> 'RelativeImageStub' 33 | 34 | suppress_type=$FlowIssue 35 | suppress_type=$FlowFixMe 36 | suppress_type=$FlowFixMeProps 37 | suppress_type=$FlowFixMeState 38 | suppress_type=$FixMe 39 | 40 | suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(>=0\\.\\(5[0-6]\\|[1-4][0-9]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\) 41 | suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(>=0\\.\\(5[0-6]\\|[1-4][0-9]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)?:? #[0-9]+ 42 | suppress_comment=\\(.\\|\n\\)*\\$FlowFixedInNextDeploy 43 | suppress_comment=\\(.\\|\n\\)*\\$FlowExpectedError 44 | 45 | unsafe.enable_getters_and_setters=true 46 | 47 | [version] 48 | ^0.56.0 49 | -------------------------------------------------------------------------------- /wyfReader/.gitattributes: -------------------------------------------------------------------------------- 1 | *.pbxproj -text 2 | -------------------------------------------------------------------------------- /wyfReader/.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # Xcode 6 | # 7 | build/ 8 | *.pbxuser 9 | !default.pbxuser 10 | *.mode1v3 11 | !default.mode1v3 12 | *.mode2v3 13 | !default.mode2v3 14 | *.perspectivev3 15 | !default.perspectivev3 16 | xcuserdata 17 | *.xccheckout 18 | *.moved-aside 19 | DerivedData 20 | *.hmap 21 | *.ipa 22 | *.xcuserstate 23 | project.xcworkspace 24 | 25 | # Android/IntelliJ 26 | # 27 | build/ 28 | .idea 29 | .gradle 30 | local.properties 31 | *.iml 32 | 33 | # node.js 34 | # 35 | node_modules/ 36 | npm-debug.log 37 | yarn-error.log 38 | 39 | # BUCK 40 | buck-out/ 41 | \.buckd/ 42 | *.keystore 43 | 44 | # fastlane 45 | # 46 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 47 | # screenshots whenever they are needed. 48 | # For more information about the recommended setup visit: 49 | # https://docs.fastlane.tools/best-practices/source-control/ 50 | 51 | */fastlane/report.xml 52 | */fastlane/Preview.html 53 | */fastlane/screenshots 54 | -------------------------------------------------------------------------------- /wyfReader/.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /wyfReader/App.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Sample React Native App 3 | * https://github.com/facebook/react-native 4 | * @flow 5 | */ 6 | 7 | import React, { Component } from 'react'; 8 | import { 9 | Platform, 10 | StyleSheet, 11 | Text, 12 | View 13 | } from 'react-native'; 14 | 15 | const instructions = Platform.select({ 16 | ios: 'Press Cmd+R to reload,\n' + 17 | 'Cmd+D or shake for dev menu', 18 | android: 'Double tap R on your keyboard to reload,\n' + 19 | 'Shake or press menu button for dev menu', 20 | }); 21 | 22 | export default class App extends Component { 23 | render() { 24 | return ( 25 | 26 | 27 | Welcome to React Native!wyf 28 | 29 | 30 | To get started, edit App.js 31 | 32 | 33 | {instructions} 34 | 35 | 36 | ); 37 | } 38 | } 39 | 40 | const styles = StyleSheet.create({ 41 | container: { 42 | flex: 1, 43 | justifyContent: 'center', 44 | alignItems: 'center', 45 | backgroundColor: '#F5FCFF', 46 | }, 47 | welcome: { 48 | fontSize: 20, 49 | textAlign: 'center', 50 | margin: 10, 51 | }, 52 | instructions: { 53 | textAlign: 'center', 54 | color: '#333333', 55 | marginBottom: 5, 56 | }, 57 | }); 58 | -------------------------------------------------------------------------------- /wyfReader/__tests__/App.js: -------------------------------------------------------------------------------- 1 | import 'react-native'; 2 | import React from 'react'; 3 | import App from '../App'; 4 | 5 | // Note: test renderer must be required after react-native. 6 | import renderer from 'react-test-renderer'; 7 | 8 | it('renders correctly', () => { 9 | const tree = renderer.create( 10 | 11 | ); 12 | }); 13 | -------------------------------------------------------------------------------- /wyfReader/android/app/BUCK: -------------------------------------------------------------------------------- 1 | # To learn about Buck see [Docs](https://buckbuild.com/). 2 | # To run your application with Buck: 3 | # - install Buck 4 | # - `npm start` - to start the packager 5 | # - `cd android` 6 | # - `keytool -genkey -v -keystore keystores/debug.keystore -storepass android -alias androiddebugkey -keypass android -dname "CN=Android Debug,O=Android,C=US"` 7 | # - `./gradlew :app:copyDownloadableDepsToLibs` - make all Gradle compile dependencies available to Buck 8 | # - `buck install -r android/app` - compile, install and run application 9 | # 10 | 11 | lib_deps = [] 12 | 13 | for jarfile in glob(['libs/*.jar']): 14 | name = 'jars__' + jarfile[jarfile.rindex('/') + 1: jarfile.rindex('.jar')] 15 | lib_deps.append(':' + name) 16 | prebuilt_jar( 17 | name = name, 18 | binary_jar = jarfile, 19 | ) 20 | 21 | for aarfile in glob(['libs/*.aar']): 22 | name = 'aars__' + aarfile[aarfile.rindex('/') + 1: aarfile.rindex('.aar')] 23 | lib_deps.append(':' + name) 24 | android_prebuilt_aar( 25 | name = name, 26 | aar = aarfile, 27 | ) 28 | 29 | android_library( 30 | name = "all-libs", 31 | exported_deps = lib_deps, 32 | ) 33 | 34 | android_library( 35 | name = "app-code", 36 | srcs = glob([ 37 | "src/main/java/**/*.java", 38 | ]), 39 | deps = [ 40 | ":all-libs", 41 | ":build_config", 42 | ":res", 43 | ], 44 | ) 45 | 46 | android_build_config( 47 | name = "build_config", 48 | package = "com.wyfreader", 49 | ) 50 | 51 | android_resource( 52 | name = "res", 53 | package = "com.wyfreader", 54 | res = "src/main/res", 55 | ) 56 | 57 | android_binary( 58 | name = "app", 59 | keystore = "//android/keystores:debug", 60 | manifest = "src/main/AndroidManifest.xml", 61 | package_type = "debug", 62 | deps = [ 63 | ":app-code", 64 | ], 65 | ) 66 | -------------------------------------------------------------------------------- /wyfReader/android/app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: "com.android.application" 2 | 3 | import com.android.build.OutputFile 4 | 5 | /** 6 | * The react.gradle file registers a task for each build variant (e.g. bundleDebugJsAndAssets 7 | * and bundleReleaseJsAndAssets). 8 | * These basically call `react-native bundle` with the correct arguments during the Android build 9 | * cycle. By default, bundleDebugJsAndAssets is skipped, as in debug/dev mode we prefer to load the 10 | * bundle directly from the development server. Below you can see all the possible configurations 11 | * and their defaults. If you decide to add a configuration block, make sure to add it before the 12 | * `apply from: "../../node_modules/react-native/react.gradle"` line. 13 | * 14 | * project.ext.react = [ 15 | * // the name of the generated asset file containing your JS bundle 16 | * bundleAssetName: "index.android.bundle", 17 | * 18 | * // the entry file for bundle generation 19 | * entryFile: "index.android.js", 20 | * 21 | * // whether to bundle JS and assets in debug mode 22 | * bundleInDebug: false, 23 | * 24 | * // whether to bundle JS and assets in release mode 25 | * bundleInRelease: true, 26 | * 27 | * // whether to bundle JS and assets in another build variant (if configured). 28 | * // See http://tools.android.com/tech-docs/new-build-system/user-guide#TOC-Build-Variants 29 | * // The configuration property can be in the following formats 30 | * // 'bundleIn${productFlavor}${buildType}' 31 | * // 'bundleIn${buildType}' 32 | * // bundleInFreeDebug: true, 33 | * // bundleInPaidRelease: true, 34 | * // bundleInBeta: true, 35 | * 36 | * // whether to disable dev mode in custom build variants (by default only disabled in release) 37 | * // for example: to disable dev mode in the staging build type (if configured) 38 | * devDisabledInStaging: true, 39 | * // The configuration property can be in the following formats 40 | * // 'devDisabledIn${productFlavor}${buildType}' 41 | * // 'devDisabledIn${buildType}' 42 | * 43 | * // the root of your project, i.e. where "package.json" lives 44 | * root: "../../", 45 | * 46 | * // where to put the JS bundle asset in debug mode 47 | * jsBundleDirDebug: "$buildDir/intermediates/assets/debug", 48 | * 49 | * // where to put the JS bundle asset in release mode 50 | * jsBundleDirRelease: "$buildDir/intermediates/assets/release", 51 | * 52 | * // where to put drawable resources / React Native assets, e.g. the ones you use via 53 | * // require('./image.png')), in debug mode 54 | * resourcesDirDebug: "$buildDir/intermediates/res/merged/debug", 55 | * 56 | * // where to put drawable resources / React Native assets, e.g. the ones you use via 57 | * // require('./image.png')), in release mode 58 | * resourcesDirRelease: "$buildDir/intermediates/res/merged/release", 59 | * 60 | * // by default the gradle tasks are skipped if none of the JS files or assets change; this means 61 | * // that we don't look at files in android/ or ios/ to determine whether the tasks are up to 62 | * // date; if you have any other folders that you want to ignore for performance reasons (gradle 63 | * // indexes the entire tree), add them here. Alternatively, if you have JS files in android/ 64 | * // for example, you might want to remove it from here. 65 | * inputExcludes: ["android/**", "ios/**"], 66 | * 67 | * // override which node gets called and with what additional arguments 68 | * nodeExecutableAndArgs: ["node"], 69 | * 70 | * // supply additional arguments to the packager 71 | * extraPackagerArgs: [] 72 | * ] 73 | */ 74 | 75 | project.ext.react = [ 76 | entryFile: "index.js" 77 | ] 78 | 79 | apply from: "../../node_modules/react-native/react.gradle" 80 | 81 | /** 82 | * Set this to true to create two separate APKs instead of one: 83 | * - An APK that only works on ARM devices 84 | * - An APK that only works on x86 devices 85 | * The advantage is the size of the APK is reduced by about 4MB. 86 | * Upload all the APKs to the Play Store and people will download 87 | * the correct one based on the CPU architecture of their device. 88 | */ 89 | def enableSeparateBuildPerCPUArchitecture = false 90 | 91 | /** 92 | * Run Proguard to shrink the Java bytecode in release builds. 93 | */ 94 | def enableProguardInReleaseBuilds = false 95 | 96 | android { 97 | compileSdkVersion 26 98 | buildToolsVersion '26.0.2' 99 | 100 | defaultConfig { 101 | applicationId "com.wyfreader" 102 | minSdkVersion 16 103 | targetSdkVersion 22 104 | versionCode 1 105 | versionName "1.0" 106 | ndk { 107 | abiFilters "armeabi-v7a", "x86" 108 | } 109 | } 110 | splits { 111 | abi { 112 | reset() 113 | enable enableSeparateBuildPerCPUArchitecture 114 | universalApk false // If true, also generate a universal APK 115 | include "armeabi-v7a", "x86" 116 | } 117 | } 118 | buildTypes { 119 | release { 120 | minifyEnabled enableProguardInReleaseBuilds 121 | proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" 122 | } 123 | } 124 | // applicationVariants are e.g. debug, release 125 | applicationVariants.all { variant -> 126 | variant.outputs.each { output -> 127 | // For each separate APK per architecture, set a unique version code as described here: 128 | // http://tools.android.com/tech-docs/new-build-system/user-guide/apk-splits 129 | def versionCodes = ["armeabi-v7a":1, "x86":2] 130 | def abi = output.getFilter(OutputFile.ABI) 131 | if (abi != null) { // null for the universal-debug, universal-release variants 132 | output.versionCodeOverride = 133 | versionCodes.get(abi) * 1048576 + defaultConfig.versionCode 134 | } 135 | } 136 | } 137 | } 138 | 139 | dependencies { 140 | compile project(':react-native-svg') 141 | compile fileTree(dir: "libs", include: ["*.jar"]) 142 | compile "com.android.support:appcompat-v7:26.0.2" 143 | compile "com.facebook.react:react-native:+" // From node_modules 144 | } 145 | 146 | // Run this once to be able to run the application with BUCK 147 | // puts all compile dependencies into folder libs for BUCK to use 148 | task copyDownloadableDepsToLibs(type: Copy) { 149 | from configurations.compile 150 | into 'libs' 151 | } 152 | -------------------------------------------------------------------------------- /wyfReader/android/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | 19 | # Disabling obfuscation is useful if you collect stack traces from production crashes 20 | # (unless you are using a system that supports de-obfuscate the stack traces). 21 | -dontobfuscate 22 | 23 | # React Native 24 | 25 | # Keep our interfaces so they can be used by other ProGuard rules. 26 | # See http://sourceforge.net/p/proguard/bugs/466/ 27 | -keep,allowobfuscation @interface com.facebook.proguard.annotations.DoNotStrip 28 | -keep,allowobfuscation @interface com.facebook.proguard.annotations.KeepGettersAndSetters 29 | -keep,allowobfuscation @interface com.facebook.common.internal.DoNotStrip 30 | 31 | # Do not strip any method/class that is annotated with @DoNotStrip 32 | -keep @com.facebook.proguard.annotations.DoNotStrip class * 33 | -keep @com.facebook.common.internal.DoNotStrip class * 34 | -keepclassmembers class * { 35 | @com.facebook.proguard.annotations.DoNotStrip *; 36 | @com.facebook.common.internal.DoNotStrip *; 37 | } 38 | 39 | -keepclassmembers @com.facebook.proguard.annotations.KeepGettersAndSetters class * { 40 | void set*(***); 41 | *** get*(); 42 | } 43 | 44 | -keep class * extends com.facebook.react.bridge.JavaScriptModule { *; } 45 | -keep class * extends com.facebook.react.bridge.NativeModule { *; } 46 | -keepclassmembers,includedescriptorclasses class * { native ; } 47 | -keepclassmembers class * { @com.facebook.react.uimanager.UIProp ; } 48 | -keepclassmembers class * { @com.facebook.react.uimanager.annotations.ReactProp ; } 49 | -keepclassmembers class * { @com.facebook.react.uimanager.annotations.ReactPropGroup ; } 50 | 51 | -dontwarn com.facebook.react.** 52 | 53 | # TextLayoutBuilder uses a non-public Android constructor within StaticLayout. 54 | # See libs/proxy/src/main/java/com/facebook/fbui/textlayoutbuilder/proxy for details. 55 | -dontwarn android.text.StaticLayout 56 | 57 | # okhttp 58 | 59 | -keepattributes Signature 60 | -keepattributes *Annotation* 61 | -keep class okhttp3.** { *; } 62 | -keep interface okhttp3.** { *; } 63 | -dontwarn okhttp3.** 64 | 65 | # okio 66 | 67 | -keep class sun.misc.Unsafe { *; } 68 | -dontwarn java.nio.file.* 69 | -dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement 70 | -dontwarn okio.** 71 | -------------------------------------------------------------------------------- /wyfReader/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 9 | 12 | 13 | 19 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /wyfReader/android/app/src/main/java/com/wyfreader/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.wyfreader; 2 | 3 | import com.facebook.react.ReactActivity; 4 | 5 | public class MainActivity extends ReactActivity { 6 | 7 | /** 8 | * Returns the name of the main component registered from JavaScript. 9 | * This is used to schedule rendering of the component. 10 | */ 11 | @Override 12 | protected String getMainComponentName() { 13 | return "wyfReader"; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /wyfReader/android/app/src/main/java/com/wyfreader/MainApplication.java: -------------------------------------------------------------------------------- 1 | package com.wyfreader; 2 | 3 | import android.app.Application; 4 | 5 | import com.facebook.react.ReactApplication; 6 | import com.horcrux.svg.SvgPackage; 7 | import com.facebook.react.ReactNativeHost; 8 | import com.facebook.react.ReactPackage; 9 | import com.facebook.react.shell.MainReactPackage; 10 | import com.facebook.soloader.SoLoader; 11 | 12 | import java.util.Arrays; 13 | import java.util.List; 14 | 15 | public class MainApplication extends Application implements ReactApplication { 16 | 17 | private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) { 18 | @Override 19 | public boolean getUseDeveloperSupport() { 20 | return BuildConfig.DEBUG; 21 | } 22 | 23 | @Override 24 | protected List getPackages() { 25 | return Arrays.asList( 26 | new MainReactPackage(), 27 | new SvgPackage() 28 | ); 29 | } 30 | 31 | @Override 32 | protected String getJSMainModuleName() { 33 | return "index"; 34 | } 35 | }; 36 | 37 | @Override 38 | public ReactNativeHost getReactNativeHost() { 39 | return mReactNativeHost; 40 | } 41 | 42 | @Override 43 | public void onCreate() { 44 | super.onCreate(); 45 | SoLoader.init(this, /* native exopackage */ false); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /wyfReader/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuyafeiJS/Reader/14f5c8dc2aa4148ac84097267aa7601aa2f865ba/wyfReader/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /wyfReader/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuyafeiJS/Reader/14f5c8dc2aa4148ac84097267aa7601aa2f865ba/wyfReader/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /wyfReader/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuyafeiJS/Reader/14f5c8dc2aa4148ac84097267aa7601aa2f865ba/wyfReader/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /wyfReader/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuyafeiJS/Reader/14f5c8dc2aa4148ac84097267aa7601aa2f865ba/wyfReader/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /wyfReader/android/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | wyfReader 3 | 4 | -------------------------------------------------------------------------------- /wyfReader/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /wyfReader/android/build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | repositories { 5 | jcenter() 6 | maven { 7 | url 'https://maven.google.com' 8 | } 9 | google() 10 | } 11 | dependencies { 12 | classpath 'com.android.tools.build:gradle:3.0.1' 13 | 14 | // NOTE: Do not place your application dependencies here; they belong 15 | // in the individual module build.gradle files 16 | } 17 | } 18 | 19 | allprojects { 20 | repositories { 21 | mavenLocal() 22 | jcenter() 23 | maven { 24 | // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm 25 | url "$rootDir/../node_modules/react-native/android" 26 | } 27 | google() 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /wyfReader/android/gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m 13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true 19 | 20 | android.useDeprecatedNdk=true 21 | -------------------------------------------------------------------------------- /wyfReader/android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wuyafeiJS/Reader/14f5c8dc2aa4148ac84097267aa7601aa2f865ba/wyfReader/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /wyfReader/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Thu Apr 26 14:34:44 CST 2018 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-all.zip 7 | -------------------------------------------------------------------------------- /wyfReader/android/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # For Cygwin, ensure paths are in UNIX format before anything is touched. 46 | if $cygwin ; then 47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 48 | fi 49 | 50 | # Attempt to set APP_HOME 51 | # Resolve links: $0 may be a link 52 | PRG="$0" 53 | # Need this for relative symlinks. 54 | while [ -h "$PRG" ] ; do 55 | ls=`ls -ld "$PRG"` 56 | link=`expr "$ls" : '.*-> \(.*\)$'` 57 | if expr "$link" : '/.*' > /dev/null; then 58 | PRG="$link" 59 | else 60 | PRG=`dirname "$PRG"`"/$link" 61 | fi 62 | done 63 | SAVED="`pwd`" 64 | cd "`dirname \"$PRG\"`/" >&- 65 | APP_HOME="`pwd -P`" 66 | cd "$SAVED" >&- 67 | 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 69 | 70 | # Determine the Java command to use to start the JVM. 71 | if [ -n "$JAVA_HOME" ] ; then 72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 73 | # IBM's JDK on AIX uses strange locations for the executables 74 | JAVACMD="$JAVA_HOME/jre/sh/java" 75 | else 76 | JAVACMD="$JAVA_HOME/bin/java" 77 | fi 78 | if [ ! -x "$JAVACMD" ] ; then 79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 80 | 81 | Please set the JAVA_HOME variable in your environment to match the 82 | location of your Java installation." 83 | fi 84 | else 85 | JAVACMD="java" 86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 87 | 88 | Please set the JAVA_HOME variable in your environment to match the 89 | location of your Java installation." 90 | fi 91 | 92 | # Increase the maximum file descriptors if we can. 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 94 | MAX_FD_LIMIT=`ulimit -H -n` 95 | if [ $? -eq 0 ] ; then 96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 97 | MAX_FD="$MAX_FD_LIMIT" 98 | fi 99 | ulimit -n $MAX_FD 100 | if [ $? -ne 0 ] ; then 101 | warn "Could not set maximum file descriptor limit: $MAX_FD" 102 | fi 103 | else 104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 105 | fi 106 | fi 107 | 108 | # For Darwin, add options to specify how the application appears in the dock 109 | if $darwin; then 110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 111 | fi 112 | 113 | # For Cygwin, switch paths to Windows format before running java 114 | if $cygwin ; then 115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /wyfReader/android/gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /wyfReader/android/keystores/BUCK: -------------------------------------------------------------------------------- 1 | keystore( 2 | name = "debug", 3 | properties = "debug.keystore.properties", 4 | store = "debug.keystore", 5 | visibility = [ 6 | "PUBLIC", 7 | ], 8 | ) 9 | -------------------------------------------------------------------------------- /wyfReader/android/keystores/debug.keystore.properties: -------------------------------------------------------------------------------- 1 | key.store=debug.keystore 2 | key.alias=androiddebugkey 3 | key.store.password=android 4 | key.alias.password=android 5 | -------------------------------------------------------------------------------- /wyfReader/android/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'wyfReader' 2 | include ':react-native-svg' 3 | project(':react-native-svg').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-svg/android') 4 | 5 | include ':app' 6 | -------------------------------------------------------------------------------- /wyfReader/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wyfReader", 3 | "displayName": "wyfReader" 4 | } -------------------------------------------------------------------------------- /wyfReader/app/containers/Account.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { StyleSheet, View, Image, Text, Button, AsyncStorage } from 'react-native' 3 | import { connect } from 'react-redux' 4 | import { NavigationActions } from '../utils' 5 | 6 | @connect() 7 | class Account extends Component { 8 | static navigationOptions = { 9 | title: 'Account', 10 | tabBarLabel: 'Account', 11 | tabBarIcon: ({ focused, tintColor }) => 12 | , 16 | } 17 | constructor(props) { 18 | super(props) 19 | this.state = { 20 | username: null 21 | } 22 | } 23 | componentWillMount() { 24 | let that = this 25 | AsyncStorage.getItem('username').then((username) => { 26 | that.setState({ 27 | username 28 | }) 29 | }) 30 | } 31 | gotoLogin = () => { 32 | this.props.dispatch(NavigationActions.navigate({ routeName: 'Login' })) 33 | } 34 | loginout = () => { 35 | AsyncStorage.removeItem('username'); 36 | AsyncStorage.removeItem('userToken'); 37 | this.props.dispatch({type: 'app/receiveToken', payload:{token: ''}}) 38 | this.props.dispatch({type: 'app/getBookList', payload:{}}) 39 | this.setState({ 40 | username: null 41 | }) 42 | } 43 | render() { 44 | console.log(this.state.username,888) 45 | return ( 46 | 47 | {this.state.username ? 欢迎你,{this.state.username}