Commit e16e0bc3 authored by 郭灿峰's avatar 郭灿峰

Initial commit

parents
Pipeline #4 failed with stages
FROM maven:3.5.4-jdk-8
ENV LANG C.UTF-8
\ No newline at end of file
This diff is collapsed.
> Instructions: Please mark in a conspicuous place `powered by pybbs`
## Document
[Document](https://tomoya92.github.io/pybbs/)
The documentation is written using the open source tool [docsify](https://docsify.js.org/#/quickstart)
## Technology
- Spring-Boot
- Shiro
- MyBatis-Plus
- Bootstrap
- MySQL
- Freemarker
- Redis
- ElasticSearch
- WebSocket
- I18N
## Feature
- This project integrates many third-party services, Such as redis, elasticsearch, websocket, etc.
- you can use it to build your own website, or you can use it as a project to learn related technologies.
- you can configure different third-party services on the backend.
- You can start a website service by using maven, docker, or downloading the zip package in the release.
- Integrated flyway makes it easy to iterate on database operations
- I18n support, so that language is not a barrier to communication
- The document is very detailed
## Getting Started
[Getting Started Document](https://tomoya92.github.io/pybbs/#%E5%BF%AB%E9%80%9F%E5%BC%80%E5%A7%8B)
**Special thanks to github user [@zzzzbw](https://github.com/zzzzbw) for helping to develop dockerfile**
## Manual package
```bash
mvn clean package
```
After the package is complete, a `pybbs.jar` file will be generated in the target directory under the project root directory, run `java -jar pybbs.jar` to start the forum service.
In addition, the tar.gz file generated after manual packaging is the latest release package in the release on github. After downloading, the extracted content is the same.
## Test
Project test cases have not been written yet!
## Feedback
- [开发俱乐部](https://17dev.club/)
- [issues](https://github.com/tomoya92/pybbs/issues)
*Please clearly describe the problem recurring steps when asking questions*
## Contribution
Welcome everyone to submit issues and pr
## Donation
![image](https://cloud.githubusercontent.com/assets/6915570/18000010/9283d530-6bae-11e6-8c34-cd27060b9074.png)
![image](https://cloud.githubusercontent.com/assets/6915570/17999995/7c2a4db4-6bae-11e6-891c-4b6bc4f00f4b.png)
**If you feel that this project is helpful to you, welcome to donate!**
## License
GNU AGPLv3
This diff is collapsed.
version: '3'
services:
mysql:
container_name: bbs-mysql
image: mysql/mysql-server:5.7
environment:
MYSQL_DATABASE: pybbs
MYSQL_ROOT_PASSWORD: root
MYSQL_ROOT_HOST: '%'
TZ: Asia/Shanghai
expose:
- "3306"
volumes:
- ./src/main/resources/db/migration:/docker-entrypoint-initdb.d
- ./mysql/mysql_data:/var/lib/mysql
restart: always
server:
container_name: bbs-server
build: .
working_dir: /app
environment:
TZ: Asia/Shanghai
volumes:
- ./:/app
- ~/.m2:/root/.m2
- ./logs:/app/logs
- ./static:/app/static
ports:
- "8080:8080"
command: mvn clean spring-boot:run -Dspring-boot.run.profiles=docker -Dmaven.test.skip=true
depends_on:
- mysql
restart: always
target
.idea
*.iml
*~
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ /*
~ * JBoss, Home of Professional Open Source.
~ * Copyright 2014, Red Hat, Inc., and individual contributors
~ * as indicated by the @author tags. See the copyright.txt file in the
~ * distribution for a full listing of individual contributors.
~ *
~ * This is free software; you can redistribute it and/or modify it
~ * under the terms of the GNU Lesser General Public License as
~ * published by the Free Software Foundation; either version 2.1 of
~ * the License, or (at your option) any later version.
~ *
~ * This software is distributed in the hope that it will be useful,
~ * but WITHOUT ANY WARRANTY; without even the implied warranty of
~ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
~ * Lesser General Public License for more details.
~ *
~ * You should have received a copy of the GNU Lesser General Public
~ * License along with this software; if not, write to the Free
~ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
~ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
~ */
-->
<assembly xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.2 http://maven.apache.org/xsd/assembly-1.1.2.xsd">
<id>pybbs-docs</id>
<formats>
<format>zip</format>
</formats>
<includeBaseDirectory>true</includeBaseDirectory>
<fileSets>
<fileSet>
<directory>target/generated-docs</directory>
<outputDirectory/>
</fileSet>
<fileSet>
<outputDirectory>images</outputDirectory>
</fileSet>
</fileSets>
</assembly>
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>co.yiiu</groupId>
<artifactId>pybbs-doc</artifactId>
<version>1.0</version>
<packaging>pom</packaging>
<properties>
<version.asciidoctor>1.5.2</version.asciidoctor>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.asciidoctor</groupId>
<artifactId>asciidoctor-maven-plugin</artifactId>
<version>${version.asciidoctor}</version>
<executions>
<execution>
<id>output-html</id>
<phase>generate-resources</phase>
<goals>
<goal>process-asciidoc</goal>
</goals>
<configuration>
<backend>html5</backend>
<sourceHighlighter>coderay</sourceHighlighter>
<attributes>
<imagesdir>./</imagesdir>
<linkcss>false</linkcss>
<icons>font</icons>
<sectnums>true</sectnums>
<toc>left</toc>
<!-- set the idprefix to blank
<idprefix/>
<sectanchors>true</sectanchors>
<idseparator>-</idseparator>
<docinfo1>true</docinfo1>-->
</attributes>
</configuration>
</execution>
</executions>
</plugin>
<!--<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<executions>
<execution>
<id>assemble</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
<configuration>
<descriptors>
<descriptor>assembly.xml</descriptor>
</descriptors>
&lt;!&ndash;<recompressZippedFiles>true</recompressZippedFiles>&ndash;&gt;
<finalName>undertow-docs-${project.version}</finalName>
<appendAssemblyId>false</appendAssemblyId>
<outputDirectory>target/</outputDirectory>
<workDirectory>target/assembly/work</workDirectory>
<tarLongFileMode>gnu</tarLongFileMode>
</configuration>
</execution>
</executions>
</plugin>-->
</plugins>
</build>
</project>
This diff is collapsed.
=== 获取token
*系统默认开启了cors访问,任何源都可以访问 `/api/**` 下的资源,如果想关闭的话,可以通过修改源码的方式关闭,源码位置 `co.yiiu.pybbs.config.WebMvcConfig`*
有的接口请求要求带上用户的`token`参数,这个token是在用户注册的时候自动生成的,可以在个人设置页面重新生成
token的获取方式:
用户登录上论坛,打开页面最上方的 设置,在页面右边可以查看到自己的token,如下图
image:./images/QQ20190109-111822.png[]
=== 请求接口示例
pybbs上的接口风格已经全都换成`RESTFUL`风格的了,调用方式也有了相应的调整
1. 所有需要传token的接口,token参数要放在请求头里(headers)
2. 所有需要传参数的接口,参数都以 json 的形式传递
3. 请求不单单是get, post了,还加入了put, delete,请仔细查看接口文档
下面给一个发帖的jQuery调用示例:
[source,js,indent=0]
----
$.ajax({
url: '/api/topic',
type: 'post',
cache: false,
async: false,
headers: {
'token': '8f2e6b0d-5a7a-44eb-9c96-4f87d55c212e'
},
contentType: 'application/json',
data: JSON.stringify({
title: title,
content: content,
tags: tags,
}),
success: function(data) {
if (data.code === 200) {
window.location.href = "/topic/" + data.detail.id
} else {
alert(data.description);
}
}
})
----
=== 接口返回对象
接口返回对象就只有一个 `Result`
这个类是在程序里自定义的,共三个属性
[source,java,indent=0]
----
public class Result {
private Integer code;
private String description;
private Object detail;
// getter, setter
}
----
- code : 返回时的状态值,成功:200, 失败:201
- description: 失败时的一些描述信息放在这个属性里
- detail: 一般放成功后的返回值,它是一个Object类型的属性,可以放任何对象
=== 接口返回分页对象
如果接口涉及到分页的话,就会返回 `Result(IPage)` 就是将查询后封装好的分页对象放在Result对象的detail属性里,再转成json返给前端
IPage对象是MyBatis-Plus内置的一个分页对象,其中调用接口可能会用到的属性有如下几个
- records: 查询出的列表对象
- pages: 分页后的总页数
- total: 总条数
- current: 当前页数
- size: 每页条数
遗憾的是它没有像jpa那样封装两个属性 `last` `next` 这样就可以直接拿它们的值来判断是不是第一页或最后一页了
不过也可以通过 `current` 和 `pages` 来判断是第一页还是最后一页
=== 基本配置
这版的配置相对其它版本的配置要简单的多,唯一要配置的就是数据库相关的配置了(如果你的数据库用户名是root 密码是空的,数据库又是跟程序在一个机器上,那就不需要配置)
配置数据库连接找到配置文件修改如下配置
- `src/main/resources/application-dev.yml` 开发启动时的配置文件
- `src/main/resources/application-prod.yml` 部署时的配置文件
- `src/main/resources/application-docker.yml` 通过docker启动时的配置文件
[source,yml,indent=0]
----
datasource_driver: com.mysql.cj.jdbc.Driver
datasource_url: jdbc:mysql://localhost:3306/pybbs?useSSL=false&characterEncoding=utf8
datasource_username: root
datasource_password:
----
关于其它的配置,启动程序 -> 访问后台 -> 系统设置 如图
image:./images/2019-01-03T07-26-04.441Z.png[]
有几个地方是必须要修改的,如下图中红框中的配置
image:./images/QQ20190103-155656.png[]
*注意:*
1. 网站的访问域名如果为 `http://example.com` 那么 `网站部署后访问的域名,注意这个后面没有 "/"` 这个说明下的内容就应该替换成 `http://example.com`
2. 第一步配置好域名后,cookie 的域名设置也要做相应的修改,否则用户登录的记录没法保存下来,在 `存cookie时用到的域名,要与网站部署后访问的域名一致` 这个说明下将 `localhost` 替换成 `example.com` 即可
3. 除了上面两条必须要修改外,网站的上传路径也要提前做好配置,具体参见 上传配置
其它的配置根据自己环境做相应的修改即可
欢迎大家提交使用pybbs部署的网站地址, 可以给我发邮件告知 py2qiuse@gmail.com
|===
| 版本 | 域名
|===
== 部署步骤
1. 购买域名,域名提供商非常多,选一个自己喜欢的购买一个就可以了
2. 去服务器运营商购买服务器,建议阿里云,购买的时候看清区域,国内做论坛 *必须要备案的* ,不过阿里云也有国外的节点,购买的时候请注意
3. 安装java8,mysql5.7
4. 按照 快速开始 中的部署方法部署
== nginx配置
如果你服务器上就只一个论坛项目,那直接将程序里的端口改成80即可,如果你还想折腾点其它的东西,那就要用到nginx做代理转发请求了,具体配置如下
假如 example.com 是你的域名,程序启动端口是 8080 ,配置如下
[source,nginx,indent=0]
----
server {
server_name example.com;
location / {
proxy_pass http://127.0.0.1:8080/;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
client_max_body_size 2m;
client_body_buffer_size 128k;
proxy_connect_timeout 300;
proxy_send_timeout 300;
proxy_read_timeout 300;
proxy_buffers 32 32k;
proxy_buffer_size 64k;
proxy_busy_buffers_size 128k;
}
}
----
== frp映射配置
关于这个配置可以参见我的一篇博客 https://tomoya92.github.io/2018/10/18/frp-tutorial/[利用frp内网穿透实现用自家电脑发布网站(不用买服务器了)]
== 配置https
https强烈推荐使用 letsencrypt 配置简单,主要是免费,唯一的缺点就是要3个月续一下时间,配置参见文档: https://tomoya92.github.io/2016/08/28/letsencrypt-nginx-https/[letsencrypt结合nginx配置https备忘]
配置外网环境运气好,很快就可以搭建好,运气不好,折腾两天是常事,淡定慢慢配
[TIP]
*自己实在部署不好,朋也也可以代劳,不过是有偿的哦*
=== 邮箱配置
用户注册没有做邮箱验证,但用户可以在自己的设置页面添加邮箱
添加邮箱的时候,要发邮箱验证码,这时候就要在后台配置发邮件的邮箱配置了
我使用qq邮箱测试是没有问题的,具体配置方法,在后台已经内置了一些信息,稍做修改即可使用
[WARNING]
我只测试了QQ邮箱, 配置邮箱请首先以QQ邮箱配置, 如果没有问题, 可以尝试换成其它平台的邮箱
---
发现还有好多人配不好qq的邮件,这里介绍一下qq邮件的密码获取方式
登录qq邮件 mail.qq.com ,然后打开设置 -> 帐户 往下翻,找到 `POP3/IMAP/SMTP/Exchange/CardDAV/CalDAV服务`
image:./images/TIM20190417185246.png[]
image:./images/TIM20190417185315.png[]
点击 `生成授权码` 然后用手机扫描授权(直接使用手机qq的扫描功能就可以了),成功后,会有一个授权码,这个就是邮件的密码了
image:./images/TIM20190417185600.png[]
---
image:./images/QQ20190103-154507.png[]
=== ElasticSearch配置
程序内置了elasticsearch功能,不过要增加相应的配置才能用
此功能默认是关闭的,具体的配置方法如下
1. 下载elasticsearch,版本建议 6.5.3 我用的就是这个版本做的开发的
2. 安装ik分词插件,如果你懂命令行操作,可以执行这条命令 `cd elasticsearch-6.5.3/bin && ./elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v6.5.3/elasticsearch-analysis-ik-6.5.3.zip`
3. 启动程序,进入后台,打开系统设置
4. 具体操作如下图所示
打开搜索功能的开关
image:./images/QQ20190103-135005.png[]
配置ES的连接(连接信息根据自己环境做相应的修改,不一定是图上所示的配置)
image:./images/QQ20190103-135046.png[]
=== 开发人员搭建
- git clone https://github.com/tomoya92/pybbs
- 使用idea打开,项目用的是idea开发的,如果你对eclipse熟悉,也是可以的
- idea打开它会自动构建项目,构建工具是maven
- 修改配置文件 `src/main/resources/application-dev.yml` 里的数据库相关配置
- 找到`co.yiiu.pybbs.PybbsApplication`类,直接运行main方法即可启动
- 浏览器运行 `http://localhost:8080` , 后台地址 `http://localhost:8080/adminlogin` 后台用户名 admin 密码 123123
[WARNING]
如果要自己打包,可使用命令:`mvn clean compile package` 进行打包,不要尝试使用其它方式打包
=== 非开发人员搭建
- 首先保证你服务器上配置好了 java 环境,版本 jdk1.8 和 MySQL服务器,版本 5.7.x 其它可选环境配置参见 [网站准备工作](zh-cn/ready)
- 然后下载最新的一键启动压缩包,下载地址:https://github.com/tomoya92/pybbs/releases
- 解压, 修改解压出来的文件夹里的 `application-prod.yml` 文件,只需要修改一个地方,就是数据库的连接信息,[配置方法](zh-cn/base)
- 运行压缩包里的脚本 `sh start.sh`
- 关闭服务运行 `sh shutdown.sh`
- 查看启动日志 `tail -200f log.file`
- 查看服务是否启动 `ps -ef|grep pybbs` 如果有pybbs的进程,就说明服务启动了
- 浏览器运行 `http://localhost:8080` , 后台地址 `http://localhost:8080/adminlogin` 后台用户名 admin 密码 123123
- 网站的其它配置,参见文档
=== docker运行
- 保证服务器有docker和docker-compose环境
- `git clone https://github.com/tomoya92/pybbs` 或 下载最新版
- cd pybbs进入项目
- 运行 `docker-compose up -d` 命令启动容器,-d是后台运行的意思
- 浏览器运行 `http://localhost:8080` , 后台地址 `http://localhost:8080/adminlogin` 后台用户名 admin 密码 123123
- 关闭容器 `docker-compose down`
- 查看日志 `docker-compose logs -f server`
*第一次运行会比较慢,视服务器性能和网速决定*
*项目根目录下会生成 `mysql` 文件夹为数据库文件,注意谨慎操作,另外论坛启动后,用户上传的图片和系统生成的默认头像会自动同步到根目录下的 `static` 文件夹下*
*这个Dockerfile是 https://github.com/zzzzbw[@zzzzbw] 大佬帮忙开发的 万分感谢!!*
[TIP]
*自己实在部署不好,朋也也可以代劳,不过是有偿的哦*
= 朋也社区文档
:Author: 朋也
:Email: <py2qiuse@gmail.com>
:Date: 2019
:Revision: 1.0
:revdate: {docdate}
:toclevels: 3
:sectnumlevels: 5
== 简介
include::introduction.asciidoc[leveloffset=+1]
== 快速开始
include::ready.asciidoc[]
include::getting-started.asciidoc[]
== 朋也社区案例
include::case.asciidoc[]
== 系统配置
include::base-config.asciidoc[]
include::email-config.asciidoc[]
include::es-config.asciidoc[]
include::redis-config.asciidoc[]
include::upload-config.asciidoc[]
include::oauth-config.asciidoc[]
include::sms-config.asciidoc[]
include::ws-config.asciidoc[]
include::theme.asciidoc[]
== 公网部署
include::deploy.asciidoc[leveloffset=+1]
== 主题开发
include::theme-dev.asciidoc[leveloffset=+1]
== 接口文档
include::api-gettoken.asciidoc[]
include::api-request-demo.asciidoc[]
include::api-return-object.asciidoc[]
include::api-doc.asciidoc[]
== Q&A
include::qa.asciidoc[]
为高度定制化而生的论坛, 内置了异常强大的功能, 使用主流Java web开发框架(SpringBoot)开发, 更方便你进行二次开发, 还在犹豫什么, 用就是了
== 为什么使用它?
1. 异常详细的文档
2. 丰富多彩的主题
3. 随心所欲的开启/关闭各种集成的服务
4. 傻瓜式部署,让不懂开发的你也能搭建自己的论坛
5. 强大的API接口,为学习编程的你提供服务端环境
6. 更多功能,期待你去发现
== 使用朋也社区收费吗?
开源协议是 `GNU AGPLv3`
*完全免费* 源代码毫不保留的全部开源,怕留后门的,可以自行查看,不过要遵守开源协议哦
觉得好用(对你有帮助的话) 也可以请朋也喝杯茶
[TIP]
====
*如果你跟朋也一样, 囊中羞涩, 也可以考虑帮忙点点朋也博客上的广告, 先行谢过!* 博客地址: https://tomoya92.github.io
*如果你想二次开发,咱们关上门好商量* Email: py2qiuse@gmail.com
====
image:https://cloud.githubusercontent.com/assets/6915570/18000010/9283d530-6bae-11e6-8c34-cd27060b9074.png[]
image:https://cloud.githubusercontent.com/assets/6915570/17999995/7c2a4db4-6bae-11e6-891c-4b6bc4f00f4b.png[]
== 使用过程中碰到问题怎么办?
1. 一定要仔细看文档,基本上能碰到的问题我都在文档中说明了
2. 往下翻, 仔细查看Q&A
3. 去Github Issue上提问,我每天都会打开Github,看到有消息立即就会回复
4. QQ群 1048094312 https://jq.qq.com/?_wv=1027&k=nGLY4QmH[点击链接加入群聊【朋也社区 - 问与答】]
5. 去 https://17dev.club[开发俱乐部] 上提问,这个网站是朋也自行搭建的,目的是提供一个无嘲讽的学习编程的环境
=== Github登录配置方法
申请clientId, clientSecret地址:https://github.com/settings/developers 前提要先登录github
打开页面后,点击 `New OAuth APP` 按钮
image:./images/QQ20190107-135811.png[]
填上必要的信息
image:./images/QQ20190107-140155.png[]
填写好之后,保存,跳转的页面上就有 clientId, clientSecret信息了,如下图
image:./images/QQ20190107-135903.png[]
拷贝上图中红框内容,粘贴到网站后台系统设置页面里的 Github 配置信息里
刷新登录页面即可出现相应按钮
image:./images/QQ20190418-153321.png[]
*注意*
- 网站域名必须外网能访问,如是你要在内网测试,可以使用ngrok,frp等工具来做内网穿透,具体使用方法百度吧,网上很多
- 回调地址格式是 网站域名+/oauth/github/callback 假如你的域名是 `http://example.com` 那么这里的回调地址就是 `http://example.com/oauth/github/callback` 不要填错了
配置好之后,保存,再次回到首页,就可以看到页面 header 上就有了`Github登录`的入口了
=== 微信登录配置方法
注册开放平台 http://open.weixin.qq.com 然后创建web应用,跟着步骤一步一步来就可以了,最后可以拿到 `appid` `appsecret` 加上在创建应用的时候填上的 `callback`
总共三个参数,都配置在`朋也社区`后台设置页面里`微信登录`区域,然后刷新登录页面,就有微信登录按钮了!!
image:./images/QQ20190418-153321.png[]
*感谢 https://github.com/gdhua[@gdhua] 提供的微信联合登录要用到的 `appid` `appsecret`*
=== 数据库在哪?
启动项目只需要配置好数据源连接地址,用户名,密码,其它都不用管,如果数据源连接信息都配置的对,启动还是报错,请查看你配置的MySQL用户是否有创建数据库以及表和对表的CRUD的权限
=== 发帖技巧
- default主题的编辑器用的是 codemirror 实现的,可以对代码进行高亮,也可以通过 shift+tab/tab 进行缩进
- 上传图片,上传视频都有相应的链接,点一下就可以了
- 对视频网站的链接解析,目前支持了 `youtube` `bilibili` `qq` `youku` 四个网站的视频链接,直接把视频网页的链接拷贝过来贴在编辑器里即可,当渲染详情页的时候服务器会自动解析
=== 上传的图片为啥不显示
*说明*
- 后台配置的静态资源`static`文件夹路径要跟程序启动的jar文件在一个目录内且同级请不要配置在其它地方,如果是用nginx映射的静态资源文件夹则没有这限制
- windows下的路径要么是 `\\` 要么是 `/` 请不要用windows自带的一个 `\` 做路径,程序不认,会导致上传失败
首先这是个只会在用IDEA开发启动时出现的问题,原因是IDEA在启动SpringBoot项目的时候会把项目中的 `resources` 加载到内存里,pybbs中的`static`文件夹在resources里
所以项目启动之后,再上传到`static`文件夹里的文件就不会被idea自动加载了,这时候只需要在idea里build一下项目即可,不用重启
=== 上传的视频格式有哪些
因为html标签中的 video 标签默认支持 mp4 格式,所以我在default主题里的上传组件里限制了上传视频文件的类型为 `mp4` ,同时对图片的格式也做了限制 `png,jpg,gif`
=== 上传视频报413错误
这是nginx报的错,需要对nginx配置一下 `client_max_body_size`
```
location / {
root html;
index index.html index.htm;
client_max_body_size 20m; # 限20MB大小
}
```
不过在程序的后台也有配置大小,那个配置会在controller里做校验,超出的话,会返回一个文件过大的信息
=== 后台用户名密码是多少?
快速开始文档里有
=== 启动项目时报错
[IMPORTANT]
错误信息: `java.lang.IllegalStateException: Failed to execute CommandLineRunner`
这个错误一般是主题文件夹没有找到的错,可以参考一下这篇文章,如果是一样的错就对了 https://17dev.club/article/5c98adb7bbe14024b9e067b3[https://17dev.club/article/5c98adb7bbe14024b9e067b3]
如果你是按照文档上的`快速开始`来启动的,就不会出现这个问题,开发环境加载的是`resources/templates`下的主题文件夹,正式环境加载的是 `./templates/theme`
所以解决这个问题的方法就是区分开你是正式环境还是部署环境启动的
---
[IMPORTANT]
错误信息: `No timezone mapping entry for 'GMT 8'`
这是MySQL时区的问题,只在windows上有问题,我本机测试是把数据源里url链接后面的 `&serverTimezone=GMT%2B8` 删了就可以了,不过也有用户反馈这种方法不行
那就换成另一种写法 `&serverTimezone=Asia/Shanghai` 也是可以的
=== redis配置失败
redis请不要开启auth,程序内集成的代码没有支持auth的配置
如果你非要支持auth选项,可以自行修改源码,源码类名是 `RedisService.java`
=== 发帖或者评论有emoji时提交失败
[IMPORTANT]
错误信息: `### Error updating database. Cause: java.sql.SQLException: Incorrect string value: '\xF0\x9F\x98\x82' for column 'content' at row 1`
mysql默认不支持emoji,要手动配置一下,让它支持就可以了,方法如下
修改mysql的配置文件,ubuntu的配置文件在 `/etc/mysql/mysql.conf.d/mysqld.cnf`,其它系统的配置文件自己找一下
打开找到 [mysqld] 在下面加上
[source,conf,indent=0]
----
[mysqld]
character-set-client-handshake = FALSE
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci
init_connect='SET NAMES utf8mb4'
----
加好后保存,然后重启mysql服务
接着把pybbs的表内字段有可能会出现emoji的字段的编码都改成 `utf8mb4_unicode_ci` 这个类型,再发帖就没问题了
=== 准备环境[必须]
1. Java环境,项目是用java开发的,java运行环境是必须的,版本:jdk8
2. MySQL数据库服务,项目采用MySQL存储数据,所以MySQL是必须的,版本5.7.x
=== 准备环境[非必须]
1. redis
2. elasticsearch
这两个服务在下面配置中有介绍
就这两样,就可以搭建一个自己的论坛了,当然这只限于局域网内使用,如果想搭建上线,请查看下面的配置方法
=== Redis配置
程序也内置了redis做缓存的功能
配置地址:启动程序 -> 登录后台 -> 系统设置
根据自己的环境配置上redis的信息,保存就可以用了,不用重启哦!!
image:./images/QQ20190103-154553.png[]
=== 短信验证码登录/注册
在群友提供的阿里云短信的key, secret及模板的帮助下,朋也社区也有了短信验证码登录的功能了,配置如下
登录 http://aliyun.com ,打开 https://dysms.console.aliyun.com/dysms.htm?spm=5176.8195934.1283918..623230c9cuQEpk#/domestic/text/sign 链接
配置 `签名` `模板` `区域`
然后在 https://usercenter.console.aliyun.com/?spm=5176.12207334.0.0.8dae1cbeSq0lnd#/manage/ak 这个页面拿到 `key` `secret`
最后配置在项目后台的系统设置里,刷新登录页面,就会有手机号登录的按钮出现
image:./images/QQ20190418-153321.png[]
[WARNING]
====
短信服务目前只支持阿里云短信服务
申请模板的时候,动态内容的变量名是 `${code}` 请不要写成其它的
====
*感谢 https://github.com/sunkaifei[@sunkaifei] 提供的阿里云短信验证码登录要用到的 `key` `secret`*
This diff is collapsed.
=== 内置主题
- default: 默认主题(使用Bootstrap3开发的)
- simple: 黑白简洁主题(仿hacker news开发的, 有时候看着也挺像kindle风格的)
=== 更换主题
启动后,登录后台,可进行选择使用哪套,选择后保存,立即生效
image:./images/QQ20190131-173707.png[]
想体验的,可以在体验环境上修改查看
关于主题怎么开发,下一篇介绍
=== 上传配置
程序启动后,要配置上传文件保存路径,否则用户注册会看不到自己的头像
配置地址:启动程序 -> 登录后台 -> 系统设置
image:./images/QQ20190725-111655.png[]
[WARNING]
*路径一定要是绝对路径*
=== 非nginx静态映射配置
如果你没有使用nginx做静态文件映射,就请配置在程序启动目录下,举个例子:
你下载的jar包存放在 `/opt/pybbs/pybbs.jar` 那么这里的地址就应该是 `/opt/pybbs/static/upload/`
如果你用的是docker部署的服务,那这个路径配置就是固定的 `/app/static` 了,上传的图片会自动同步到docker启动目录下的static文件夹里
=== nginx静态文件映射配置方法
nginx静态文件映射配置
[source,indent=0]
----
server {
#...
location /static/ {
root /opt/cdn/;
autoindex on;
}
}
----
那么你这个地上的配置就应该是 `/opt/cdn/static/upload/`
=== 静态文件访问地址
默认给的是 `http://localhost:8080/static/upload/` 如果你的访问域名是 `http://example.com` 那这里就要换成 `http://example.com/static/upload/`
=== 开启WebSocket
重新更新了一下websocket的实现,换成了javax.websocket包下的类实现,没有了之间的关闭服务还要等会的问题了
而且页面上也不用再多引入一个socket.io的js了,纯原生的js实现
[TIP]
====
注意,因为网站是服务端渲染的,所以每次请求页面都会刷新,websocket也会重连,这就有点蛋疼了
不过也不是没办法, 可以给网站加上pjax支持,也可以将这个功能用在纯js渲染的网站上,很显然这两个功能pybbs都还没有
====
目前围绕ws开发的功能有如下
- 自己的话题被收藏了,会收到通知
- 自己的话题被评论了,会收到通知
- 自己的评论被回复了,会收到通知
- 进入页面后,ws会自动获取未读消息数,然后展示在页面上的Header和`document.title`上
效果如下
image:./images/7C56195B1FE6F942649D30D65416EE80.jpg[]
当然开启websocket服务也不是没有好处的,比如:
- 上图中别人回复了自己的评论就会立即收到消息
- 发一个帖子等着别人回复,不用一直刷新页面看有没有新消息了
---
如果你看了上面的说明后,还是想开启,配置如下
image:./images/QQ20190123-103144.png[]
只有两个配置
1. 开启功能,不多说
2. ws连接地址,协议是 `ws` 或者 `wss`(如果你网站访问用的是https,那这个就应该是wss),其它跟上面配置的网站访问域名一样
然后直接启动系统即可,注意,ws服务在前端只有登录后的用户才有效
== pybbs插件
朋也开发的插件都在这个plugins目录里,这里放的都是源码,如果想用的话,可以自己通过mvn打包然后使用,用法如下
当前目录中的插件
- comment-layer-plugin: 对话题评论进行盖楼排序
- redis-cache-plugin: 对部分查询提供redis缓存
- theme-simple-plugin: simple主题
== 原理
[TIP]
====
首先你要知道spring的切面编程思想,其次要会用aop
在插件里也可以使用pybbs里已经开发好的service以及mapper
====
插件的实现原理是使用spring的aop切面编程思想实现的,pybbs里提供了service包下几乎所有service方法的切入点
开发插件就是围绕着这些切入点来实现的功能,这样做可以做到对系统实现0侵入
具体用法可能参见当前目录下的插件写法
== 打包
项目是纯maven项目,所以打包也是用的mvn命令打的
[source,bash]
----
mvn clean package
----
[TIP]
====
注意不要将pybbs给打到jar包里了,在引入信赖的时候 `scope` 请配置成 `provided`
====
== 使用
目前还在测试阶段,文档没有更新,这里简单介绍一下用法
1. 将pybbs主项目打包,命令 `mvn clean package`
2. 将打包好的pybbs.jar拷贝到你想放的目录下,比如 `/opt/pybbs/`
3. 在`/opt/pybbs/`目录下创建一个文件夹 `plugins`
4. 将插件打包好后的jar包拷贝到 `plugins` 目录中
5. 启动项目使用命令 `java -Dloader.path=./plugins -jar pybbs.jar`
运行浏览器,打开 http://localhost:8080/ 即可
== 开发插件
规范
1. 插件名要以 `-plugin` 结尾,如 `xxx-plugin`,例如 `redis-cache-plugin`
2. 如果是主题插件,要以 `theme` 开头,以 `-plugin` 结尾,例如 `theme-simple-plugin`
3. 插件包名必须为 `co.yiiu.pybbs.plugin` (主题插件除外)
4. 插件打包必须要使用jdk8编译
5. 附上插件说明,越详细越好
首先下载pybbs项目,将`pom.xml`里的下面代码注释掉
[source,xml]
----
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<layout>ZIP</layout>
</configuration>
</plugin>
----
然后运行命令 `mvn clean install` 将pybbs安装到本地maven库,这样在插件里就可以通过下面信赖引入了
[source,xml]
----
<dependency>
<groupId>co.yiiu</groupId>
<artifactId>pybbs</artifactId>
<version>5.2.1</version>
<scope>provided</scope>
</dependency>
----
然后就可以开发了
== 开发主题
主题也可以当成插件被加载,当前目录下的 `theme-simple-plugin` 就是一个主题插件,通过 `mvn clean package` 将其打成jar包,按照上面使用方法中启动项目也是可以加载的
开发很简单
1. 创建maven项目
2. 在 `src/main/resources/templates/theme/xxx` 下开发主题文件,其中 xxx 就是主题的名字(文件夹不存在,自行创建)
3. 静态资源文件放在 `src/main/resources/static/theme/xxx` 其中 xxx 就是主题的名字(文件夹不存在,自行创建)
开发好之后,打包即可
== 测试
测试插件功能
首先将插件项目通过 `mvn clean install` 安装到本地
然后在pybbs项目里的pom.xml里引入插件
最后启动pybbs,即可测试插件功能了
== 注意
[TIP]
====
插件功能目前还处在测试阶段,请不要更新master分支的代码去部署线上服务
====
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>co.yiiu.pybbs</groupId>
<artifactId>comment-layer-plugin</artifactId>
<version>1.0</version>
<dependencies>
<dependency>
<groupId>co.yiiu</groupId>
<artifactId>pybbs</artifactId>
<version>5.2.1</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<!-- 编译指定jdk版本号 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
<showWarnings>true</showWarnings>
</configuration>
</plugin>
<!-- 打包发布时,跳过单元测试 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<skipTests>true</skipTests>
</configuration>
</plugin>
</plugins>
</build>
</project>
package co.yiiu.pybbs.plugin;
import co.yiiu.pybbs.model.vo.CommentsByTopic;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
/**
* Created by tomoya.
* Copyright (c) 2018, All Rights Reserved.
* https://yiiu.co
*/
@Component
@Aspect
public class CommentLayerPlugin {
@Around("co.yiiu.pybbs.hook.CommentServiceHook.selectByTopicId()")
public Object selectByTopicId(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
List<CommentsByTopic> newComments =
(List<CommentsByTopic>) proceedingJoinPoint.proceed(proceedingJoinPoint.getArgs());
// 盖楼显示评论
return this.sortByLayer(newComments);
}
// 从列表里查找指定值的下标
private int findLastIndex(List<CommentsByTopic> newComments, String key, Integer value) {
int index = -1;
for (int i = 0; i < newComments.size(); i++) {
if (key.equals("commentId")) {
if (value.equals(newComments.get(i).getCommentId())) {
index = i;
}
} else if (key.equals("id")) {
if (value.equals(newComments.get(i).getId())) {
index = i;
}
}
}
return index;
}
// 盖楼排序
private List<CommentsByTopic> sortByLayer(List<CommentsByTopic> comments) {
List<CommentsByTopic> newComments = new ArrayList<>();
comments.forEach(comment -> {
if (comment.getCommentId() == null) {
newComments.add(comment);
} else {
int index = this.findLastIndex(newComments, "commentId", comment.getCommentId());
if (index == -1) {
int upIndex = this.findLastIndex(newComments, "id", comment.getCommentId());
if (upIndex == -1) {
newComments.add(comment);
} else {
int layer = newComments.get(upIndex).getLayer() + 1;
comment.setLayer(layer);
newComments.add(upIndex + 1, comment);
}
} else {
int layer = newComments.get(index).getLayer();
comment.setLayer(layer);
newComments.add(index + 1, comment);
}
}
});
return newComments;
}
}
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>co.yiiu.pybbs</groupId>
<artifactId>redis-cache-plugin</artifactId>
<version>1.0</version>
<dependencies>
<dependency>
<groupId>co.yiiu</groupId>
<artifactId>pybbs</artifactId>
<version>5.2.1</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<!-- 编译指定jdk版本号 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
<showWarnings>true</showWarnings>
</configuration>
</plugin>
<!-- 打包发布时,跳过单元测试 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<skipTests>true</skipTests>
</configuration>
</plugin>
</plugins>
</build>
</project>
package co.yiiu.pybbs.plugin;
import co.yiiu.pybbs.config.service.BaseService;
import co.yiiu.pybbs.model.SystemConfig;
import co.yiiu.pybbs.service.ISystemConfigService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.DependsOn;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
import redis.clients.jedis.params.SetParams;
/**
* Created by tomoya.
* Copyright (c) 2018, All Rights Reserved.
* https://yiiu.co
*/
@Component
@DependsOn("mybatisPlusConfig")
public class RedisService implements BaseService<JedisPool> {
@Autowired
private ISystemConfigService systemConfigService;
private JedisPool jedisPool;
private Logger log = LoggerFactory.getLogger(RedisService.class);
public void setJedis(JedisPool jedisPool) {
this.jedisPool = jedisPool;
}
public JedisPool instance() {
try {
if (this.jedisPool != null) return this.jedisPool;
// 获取redis的连接
// host
SystemConfig systemConfigHost = systemConfigService.selectByKey("redis_host");
String host = systemConfigHost.getValue();
// port
SystemConfig systemConfigPort = systemConfigService.selectByKey("redis_port");
String port = systemConfigPort.getValue();
// password
SystemConfig systemConfigPassword = systemConfigService.selectByKey("redis_password");
String password = systemConfigPassword.getValue();
password = StringUtils.isEmpty(password) ? null : password;
// database
SystemConfig systemConfigDatabase = systemConfigService.selectByKey("redis_database");
String database = systemConfigDatabase.getValue();
// timeout
SystemConfig systemConfigTimeout = systemConfigService.selectByKey("redis_timeout");
String timeout = systemConfigTimeout.getValue();
if (!this.isRedisConfig()) {
log.info("redis配置信息不全或没有配置...");
return null;
}
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
// 配置jedis连接池最多空闲多少个实例,源码默认 8
jedisPoolConfig.setMaxIdle(7);
// 配置jedis连接池最多创建多少个实例,源码默认 8
jedisPoolConfig.setMaxTotal(20);
//在borrow(引入)一个jedis实例时,是否提前进行validate操作;如果为true,则得到的jedis实例均是可用的;
jedisPoolConfig.setTestOnBorrow(true);
//return 一个jedis实例给pool时,是否检查连接可用性(ping())
jedisPoolConfig.setTestOnReturn(true);
jedisPool = new JedisPool(jedisPoolConfig, host, Integer.parseInt(port), Integer.parseInt(timeout), password,
Integer.parseInt(database));
log.info("redis连接对象获取成功...");
return this.jedisPool;
} catch (Exception e) {
log.error("配置redis连接池报错,错误信息: {}", e.getMessage());
return null;
}
}
// 判断redis是否配置了
public boolean isRedisConfig() {
SystemConfig systemConfigHost = systemConfigService.selectByKey("redis_host");
String host = systemConfigHost.getValue();
// port
SystemConfig systemConfigPort = systemConfigService.selectByKey("redis_port");
String port = systemConfigPort.getValue();
// database
SystemConfig systemConfigDatabase = systemConfigService.selectByKey("redis_database");
String database = systemConfigDatabase.getValue();
// timeout
SystemConfig systemConfigTimeout = systemConfigService.selectByKey("redis_timeout");
String timeout = systemConfigTimeout.getValue();
return !StringUtils.isEmpty(host) && !StringUtils.isEmpty(port) && !StringUtils.isEmpty(database) && !StringUtils
.isEmpty(timeout);
}
// 获取String值
public String getString(String key) {
JedisPool instance = this.instance();
if (StringUtils.isEmpty(key) || instance == null) return null;
Jedis jedis = instance.getResource();
String value = jedis.get(key);
jedis.close();
return value;
}
public void setString(String key, String value) {
this.setString(key, value, 300); // 如果不指定过时时间,默认为5分钟
}
/**
* 带有过期时间的保存数据到redis,到期自动删除
*
* @param key
* @param value
* @param expireTime 单位 秒
*/
public void setString(String key, String value, int expireTime) {
JedisPool instance = this.instance();
if (StringUtils.isEmpty(key) || StringUtils.isEmpty(value) || instance == null) return;
Jedis jedis = instance.getResource();
SetParams params = new SetParams();
params.px(expireTime * 1000);
jedis.set(key, value, params);
jedis.close();
}
public void delString(String key) {
JedisPool instance = this.instance();
if (StringUtils.isEmpty(key) || instance == null) return;
Jedis jedis = instance.getResource();
jedis.del(key); // 返回值成功是 1
jedis.close();
}
// TODO 后面有需要会补充获取 list, map 等方法
}
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>co.yiiu.pybbs</groupId>
<artifactId>theme-simple-plugin</artifactId>
<version>1.0</version>
</project>
body {
margin: 0 auto;
width: 100%;
min-width: 1000px;
overflow-x: hidden; }
* {
color: #000;
word-break: break-word;
word-wrap: break-word; }
a {
color: #000; }
a:visited, a:hover, a:focus {
color: #000; }
input[type='text'], input[type='password'], input[type='email'], input[type='number'], button, textarea {
border: 1px solid #000; }
input[type='text'], input[type='password'], input[type='email'], input[type='number'], textarea {
width: calc(100% - 14px);
padding: 7px;
resize: none; }
input[type='text']:focus, input[type='password']:focus, input[type='email']:focus, input[type='number']:focus, textarea:focus {
outline: 0; }
button {
padding: 5px 10px; }
button:focus {
outline: 0; }
ol, ul {
margin-left: 0;
padding-left: 25px; }
ol li, ul li {
line-height: 1.7em; }
.pull-left {
float: left; }
.pull-right {
float: right; }
.text-center {
text-align: center; }
.text-right {
text-align: right; }
.clear {
clear: both; }
.container header {
width: 100%;
height: 44px;
background-color: #000; }
.container header .title {
font-size: 18px;
font-weight: 500; }
.container header ul {
list-style-type: none;
padding: 0;
margin: 0; }
.container header ul li {
float: left;
padding: 0 7px;
line-height: 44px; }
.container header ul li a {
color: #fff;
text-decoration: none; }
.container header ul li.active {
background-color: #fff; }
.container header ul li.active a {
color: #000; }
.container header ul li:first-child {
padding-left: 15px; }
.container header ul:last-child {
float: right; }
.container header ul:last-child li:first-child {
padding-left: 7px; }
.container header ul:last-child li:last-child {
padding-right: 15px; }
.container header #notReadCount {
color: #000;
background-color: #fff;
padding: 0 4px;
display: none; }
.container section {
margin: 0 auto;
width: 1000px;
padding: 15px 0; }
.container section .box {
margin: 0 auto;
border: 1px solid #000; }
.container section .box .box-heading {
padding: 10px 15px;
border-bottom: 1px solid #000;
overflow: hidden; }
.container section .box .box-footer {
padding: 10px 15px;
border-top: 1px solid #000;
overflow: hidden; }
.container section .box .box-body {
padding: 15px;
overflow: hidden; }
.container section .topics .title {
font-size: 18px;
text-decoration: none; }
.container section .topics .divide {
padding-bottom: 10px;
border-bottom: 1px solid #000; }
.container section .title {
font-size: 22px;
font-weight: 500; }
.container section .content img {
max-width: 100%;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3), 0 0 40px rgba(0, 0, 0, 0.1) inset; }
.container section .content pre {
padding: 10px;
background-color: #f5f5f5; }
.container section .content pre code {
white-space: pre-wrap; }
.container section .content table {
width: 100%;
border-collapse: collapse;
border: 1px solid #000; }
.container section .content table td, .container section .content table th {
border: 1px solid #000;
padding: 5px; }
.container section .content .embed-responsive {
position: relative;
display: block;
height: 0;
padding: 0;
overflow: hidden; }
.container section .content .embed-responsive .embed-responsive-item,
.container section .content .embed-responsive iframe,
.container section .content .embed-responsive embed,
.container section .content .embed-responsive object,
.container section .content .embed-responsive video {
position: absolute;
top: 0;
left: 0;
bottom: 0;
height: 100%;
width: 100%;
border: 0; }
.container section .content .embed-responsive-16by9 {
padding-bottom: 56.25%; }
.container section .tags {
overflow: hidden; }
.container section .tags .item {
float: left;
width: 25%;
height: 88px;
overflow: hidden;
margin-bottom: 10px; }
.container section .tags .item .description {
font-size: 14px;
margin-top: 10px;
overflow: hidden; }
.container section .tag {
text-decoration: none;
background-color: #000;
color: #fff;
padding: 2px 8px; }
.container section .comments img {
max-width: 100%; }
.container section .comments .fa {
cursor: pointer; }
.container section .user {
overflow: hidden; }
.container section .user .left {
float: left;
width: 200px; }
.container section .user .right {
width: 785px;
float: right; }
.container section .user .menu {
position: fixed; }
.container section .user .settings p {
margin-bottom: 5px; }
.container section .user .settings button {
margin-top: 10px; }
.container footer {
padding: 15px;
border-top: 1px solid #000;
text-align: center; }
/*# sourceMappingURL=app.css.map */
{
"version": 3,
"mappings": "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",
"sources": ["app.scss"],
"names": [],
"file": "app.css"
}
$text-color: #000;
$text-color-reverse: #fff;
$header-height: 44px;
$header-bg-color: $text-color;
$section-width: 1000px;
body {
margin: 0 auto;
width: 100%;
min-width: $section-width;
overflow-x: hidden;
}
* {
color: $text-color;
word-break: break-word;
word-wrap: break-word;
}
a {
color: $text-color;
&:visited, &:hover, &:focus {
color: $text-color;
}
}
input[type='text'], input[type='password'], input[type='email'], input[type='number'], button, textarea {
border: 1px solid $text-color;
}
input[type='text'], input[type='password'], input[type='email'], input[type='number'], textarea {
width: calc(100% - 14px);
padding: 7px;
resize: none;
&:focus {
outline: 0;
}
}
//textarea {
// width: $section-width - 14px;
// resize: none;
//}
button {
padding: 5px 10px;
&:focus {
outline: 0;
}
}
ol, ul {
margin-left: 0;
padding-left: 25px;
li {
line-height: 1.7em;
}
}
.pull-left {
float: left;
}
.pull-right {
float: right;
}
.text-center {
text-align: center;
}
.text-right {
text-align: right;
}
.clear {
clear: both;
}
.container {
header {
width: 100%;
height: $header-height;
background-color: $header-bg-color;
.title {
font-size: 18px;
font-weight: 500;
}
ul {
list-style-type: none;
padding: 0;
margin: 0;
li {
float: left;
padding: 0 7px;
line-height: $header-height;
a {
color: $text-color-reverse;
text-decoration: none;
}
&.active {
background-color: $text-color-reverse;
a {
color: $text-color;
}
}
}
li:first-child {
padding-left: 15px;
}
}
ul:last-child {
float: right;
li:first-child {
padding-left: 7px;
}
li:last-child {
padding-right: 15px;
}
}
#notReadCount {
color: $text-color;
background-color: $text-color-reverse;
padding: 0 4px;
display: none;
}
}
section {
margin: 0 auto;
width: $section-width;
padding: 15px 0;
.box {
margin: 0 auto;
border: 1px solid $text-color;
.box-heading {
padding: 10px 15px;
border-bottom: 1px solid $text-color;
overflow: hidden;
}
.box-footer {
padding: 10px 15px;
border-top: 1px solid $text-color;
overflow: hidden;
}
.box-body {
padding: 15px;
overflow: hidden;
}
}
.topics {
.title {
font-size: 18px;
text-decoration: none;
}
.divide {
padding-bottom: 10px;
border-bottom: 1px solid $text-color;
}
}
.title {
font-size: 22px;
font-weight: 500;
}
.content {
img {
max-width: 100%;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3), 0 0 40px rgba(0, 0, 0, 0.1) inset;
}
pre {
padding: 10px;
background-color: #f5f5f5;
code {
white-space: pre-wrap;
}
}
table {
width: 100%;
border-collapse: collapse;
border: 1px solid $text-color;
td, th {
border: 1px solid $text-color;
padding: 5px;
}
}
.embed-responsive {
position: relative;
display: block;
height: 0;
padding: 0;
overflow: hidden;
.embed-responsive-item,
iframe,
embed,
object,
video {
position: absolute;
top: 0;
left: 0;
bottom: 0;
height: 100%;
width: 100%;
border: 0;
}
}
// Modifier class for 16:9 aspect ratio
.embed-responsive-16by9 {
padding-bottom: 56.25%;
}
}
.tags {
overflow: hidden;
.item {
float: left;
width: 25%;
height: 88px;
overflow: hidden;
margin-bottom: 10px;
.description {
font-size: 14px;
margin-top: 10px;
overflow: hidden;
}
}
}
.tag {
text-decoration: none;
background-color: $text-color;
color: $text-color-reverse;
padding: 2px 8px;
}
.comments {
img {
max-width: 100%;
}
.fa {
cursor: pointer;
}
}
.user {
overflow: hidden;
.left {
float: left;
width: 200px;
}
.right {
width: $section-width - 215px;
float: right;
}
.menu {
position: fixed;
}
.settings {
p {
margin-bottom: 5px;
}
button {
margin-top: 10px;
}
}
}
}
footer {
padding: 15px;
border-top: 1px solid $text-color;
text-align: center;
}
}
<#include "../layout/layout.ftl"/>
<@html page_title="编辑话题" page_tab="">
<div>
<div>
<a href="/">主页</a> / <a href="/topic/${topic.id}">${topic.title}</a> / 编辑评论
<a href="javascript:uploadImageBtn();" class="pull-right">上传图片</a>
</div>
<div style="margin-top: 10px;">
<textarea name="content" id="content" rows="15">${comment.content?html}</textarea>
</div>
<div>
<button id="btn">更新</button>
</div>
</div>
<script>
$(function () {
$("#btn").click(function () {
var content = $("#content").val();
if (!content) {
alert("请输入内容");
return ;
}
$.ajax({
url: '/api/comment/${comment.id}',
type: 'put',
cache: false,
async: false,
contentType: 'application/json',
dataType: 'json',
data: JSON.stringify({
content: content,
}),
headers: {
'token': '${_user.token}'
},
success: function (data) {
if (data.code === 200) {
window.location.href = "/topic/${comment.topicId}";
} else {
alert(data.description);
}
}
})
})
});
</script>
<#include "../components/upload.ftl"/>
</@html>
<style>
.loading {
display: none;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
z-index: 999;
}
.spinner {
width: 60px;
height: 60px;
position: relative;
margin: 200px auto;
}
.double-bounce1, .double-bounce2 {
width: 100%;
height: 100%;
border-radius: 50%;
background-color: #000;
opacity: 0.6;
position: absolute;
top: 0;
left: 0;
-webkit-animation: bounce 2.0s infinite ease-in-out;
animation: bounce 2.0s infinite ease-in-out;
}
.double-bounce2 {
-webkit-animation-delay: -1.0s;
animation-delay: -1.0s;
}
@-webkit-keyframes bounce {
0%, 100% { -webkit-transform: scale(0.0) }
50% { -webkit-transform: scale(1.0) }
}
@keyframes bounce {
0%, 100% {
transform: scale(0.0);
-webkit-transform: scale(0.0);
} 50% {
transform: scale(1.0);
-webkit-transform: scale(1.0);
}
}
</style>
<div class="loading">
<div class="spinner">
<div class="double-bounce1"></div>
<div class="double-bounce2"></div>
</div>
</div>
<#macro notification userId read limit>
<@tag_notifications userId=userId read=read limit=limit>
<#list notifications as notification>
<div class="notification_${read}">
<img width="24" src="${notification.avatar!}">
<a href="/user/${notification.username}">${notification.username}</a>
<span>${model.formatDate(notification.inTime)}</span>
<#if notification.action == "COMMENT">
评论了你的话题 <a href="/topic/${notification.topicId}">${notification.title}</a>
<#elseif notification.action == "REPLY">
在话题 <a href="/topic/${notification.topicId}">${notification.title}</a> 下回复了你
<#elseif notification.action == "COLLECT">
收藏了你的话题 <a href="/topic/${notification.topicId}">${notification.title}</a>
</#if>
</div>
<div class="content">${model.formatContent(notification.content)}</div>
</#list>
<#if notifications?size == 0>
</#if>
</@tag_notifications>
</#macro>
<#macro topics page>
<table style="border: 0; width: 100%;" class="topics">
<#list page.records as topic>
<tr><td style="padding-top: 10px;"></td></tr>
<tr>
<td rowspan="2" width="50"><img src="${topic.avatar!}" width="48" style="vertical-align: middle;" alt=""></td>
<td><a class="title" href="/topic/${topic.id}">${topic.title}</a></td>
</tr>
<tr>
<td style="font-size: 14px;">
<#if topic.top>
<span class="tag">置顶</span>
<#elseif topic.good == true>
<span class="tag">精华</span>
</#if>
<span>${topic.view} 次点击</span>&nbsp;
<a href="/user/${topic.username}">${topic.username}</a>&nbsp;
<span>发布于 ${model.formatDate(topic.inTime)}</span>&nbsp;|
<a href="/topic/${topic.id}">${topic.commentCount} 评论</a>&nbsp;
</td>
</tr>
<tr>
<td colspan="2" class="divide" <#if !topic_has_next>style="border: 0;"</#if>></td>
</tr>
</#list>
</table>
</#macro>
<form onsubmit="return;" id="uploadImageForm">
<input type="hidden" id="type" value="image"/>
<input type="file" style="display: none;" onchange="uploadImageFileChange()" id="uploadImageFileEle" multiple
accept="image/jpeg,image/jpg,image/png,image/gif,video/mp4"/>
</form>
<script>
function uploadFile(type) {
$("#type").val(type);
$("#uploadImageFileEle").click();
}
function uploadImageFileChange() {
var fd = new FormData();
var type = $("#type").val();
fd.append("file", document.getElementById("uploadImageFileEle").files[0]);
fd.append("type", type);
fd.append("token", "${_user.token}");
$.post({
url: "/api/upload",
data: fd,
dataType: 'json',
headers: {
'token': '${_user.token}'
},
processData: false,
contentType: false,
success: function (data) {
if (data.code === 200) {
var content = $("#content");
var text = content.val();
if (text.length > 0) text += '\n\n';
var insertContent = "";
for (var j = 0; j < data.detail.urls.length; j++) {
var url = data.detail.urls[j];
if (type === "topic") {
insertContent += "![image](" + url + ")\n\n"
} else if (type === "video") {
insertContent += "<video class='embed-responsive embed-responsive-16by9' controls><source src='" + url + "' type='video/mp4'></video>\n\n";
}
}
content.val(text + insertContent);
content.focus();
$("#uploadImageForm")[0].reset();
} else {
alert(data.description);
}
}
})
}
</script>
<#macro user_collects username pageNo pageSize paginate=false>
<@tag_user_collects username=username pageNo=pageNo pageSize=pageSize>
<ul>
<#list collects.records as collect>
<li><a href="/topic/${collect.id}">${collect.title}</a></li>
</#list>
</ul>
<#if paginate && collects.current &lt; collects.pages>
<a href="/user/${username}/collects?pageNo=${collects.current + 1}">查看更多</a>
</#if>
</@tag_user_collects>
</#macro>
<#macro user_comments username pageNo pageSize paginate=false>
<@tag_user_comments username=username pageNo=pageNo pageSize=pageSize>
<ul>
<#list comments.records as comment>
<li>
<div>
<a href="/user/${comment.commentUsername}">${comment.commentUsername}</a>
${model.formatDate(comment.inTime)!}
评论了
<a href="/user/${comment.topicUsername}">${comment.topicUsername}</a>
创建的话题 › <a href="/topic/${comment.topicId}">${comment.title!?html}</a>
</div>
<div class="content" style=" margin: 10px 0;">
${model.formatContent(comment.content)}
</div>
</li>
</#list>
</ul>
<#if paginate && comments.current &lt; comments.pages>
<a href="/user/${username}/comments?pageNo=${comments.current + 1}">查看更多</a>
</#if>
</@tag_user_comments>
</#macro>
<#macro user_topics username pageNo pageSize paginate=false>
<@tag_user_topics username=username pageNo=pageNo pageSize=pageSize>
<ul>
<#list topics.records as topic>
<li><a href="/topic/${topic.id}">${topic.title}</a></li>
</#list>
</ul>
<#if paginate && topics.current &lt; topics.pages>
<a href="/user/${username}/topics?pageNo=${topics.current + 1}">查看更多</a>
</#if>
</@tag_user_topics>
</#macro>
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<#--<meta name="viewport"-->
<#--content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">-->
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Ooooops, 出错了~~</title>
<link rel="icon" href="https://yiiu.co/favicon.ico">
<#--css-->
<link rel="stylesheet" href="/static/theme/simple/css/app.css"/>
</head>
<body>
<div class="container">
<section>
<h1>: (</h1>
<h3>${errorCode}</h3>
<p>${exception.message!}</p>
</section>
</div>
</body>
</html>
<#include "layout/layout.ftl"/>
<@html page_title="首页" page_tab=tab>
<#if active?? && active>
<div style="color: #3c763d;">激活成功</div>
</#if>
<@tag_topics pageNo=pageNo tab=tab>
<#include "components/topics.ftl"/>
<@topics page=page/>
<#if page.current < page.pages>
<a href="/?tab=${tab}&pageNo=${page.current + 1}">查看更多</a>
</#if>
</@tag_topics>
</@html>
<footer>
Site powered by <a href="https://github.com/tomoya92/pybbs">pybbs</a>, Theme designed by <a
href="https://github.com/tomoya92">tomoya92</a>
<div style="margin: 10px auto 0; width: 20%;">
<form action="/search" method="get">
<input type="search" name="keyword" placeholder="搜点啥?">
</form>
</div>
</footer>
<#macro header page_tab>
<header>
<ul>
<li><a href="/" class="title">${site.name}</a></li>
<li <#if page_tab == "all">class="active"</#if>><a href="/?tab=all">全部</a></li>
<li <#if page_tab == "good">class="active"</#if>><a href="/?tab=good">精华</a></li>
<li <#if page_tab == "hot">class="active"</#if>><a href="/?tab=hot">最热</a></li>
<li <#if page_tab == "newest">class="active"</#if>><a href="/?tab=newest">最新</a></li>
<li <#if page_tab == "noanswer">class="active"</#if>><a href="/?tab=noanswer">无人问津</a></li>
<li <#if page_tab == "tags">class="active"</#if>><a href="/tags">标签</a></li>
</ul>
<ul>
<#if _user??>
<li <#if page_tab == "create">class="active"</#if>><a href="/topic/create">创建话题</a></li>
<li <#if page_tab == "notifications">class="active"</#if>><a href="/notifications">通知 <span id="notReadCount">0</span></a></li>
<li <#if page_tab == "user">class="active"</#if>><a href="/user/${_user.username}">${_user.username}</a></li>
<li <#if page_tab == "settings">class="active"</#if>><a href="/settings">设置</a></li>
<li><a href="javascript:if(confirm('确定要登出吗?'))location.href='/logout'">登出</a></li>
<#else>
<li <#if page_tab == "login">class="active"</#if>><a href="/login">登录</a></li>
<li <#if page_tab == "register">class="active"</#if>><a href="/register">注册</a></li>
<li><a href="/oauth/github">Github 登录</a></li>
</#if>
</ul>
</header>
<#if _user??>
<script>
$.ajax({
url: '/api/notification/notRead',
cache: false,
async: false,
type: 'get',
dataType: 'json',
contentType: 'application/json',
headers: {
'token': '${_user.token}'
},
success: function (data) {
if (data.code === 200) {
if (data.detail > 0) {
document.title = "("+data.detail+") " + document.title;
$("#notReadCount").text(data.detail);
$("#notReadCount").show();
}
}
}
})
</script>
</#if>
</#macro>
<#macro html page_title page_tab>
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<#--<meta name="viewport"-->
<#--content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">-->
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>${page_title!} - ${site.name}</title>
<link rel="icon" href="https://yiiu.co/favicon.ico">
<#--css-->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css"/>
<link rel="stylesheet" href="/static/theme/${site.theme}/css/app.css"/>
<#--javascript-->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
</head>
<body>
<#include "../components/loading.ftl"/>
<div class="container">
<#include "header.ftl"/>
<@header page_tab=page_tab/>
<section><#nested /></section>
<#include "footer.ftl"/>
</div>
</body>
</html>
</#macro>
<#include "layout/layout.ftl"/>
<@html page_title="登录" page_tab="login">
<div class="box" style="width: 500px;">
<div class="box-heading">登录</div>
<div class="box-body">
<table style="width: 100%;border-spacing: 5px;">
<tr>
<td width="60">用户名</td>
<td colspan="2"><input type="text" name="username" id="username" placeholder="用户名"/></td>
</tr>
<tr>
<td>密码</td>
<td colspan="2"><input type="password" name="password" id="password" placeholder="密码"/></td>
</tr>
<tr>
<td>验证码</td>
<td><input type="text" name="captcha" id="captcha" placeholder="验证码"/></td>
<td width="122" align="right"><img src="/common/captcha" id="captchaImage" onclick="changeCaptcha()"/></td>
</tr>
</table>
</div>
<div class="box-footer">
<a href="/register">注册</a>
<button class="pull-right" onclick="login()">登录</button>
</div>
</div>
<script>
function changeCaptcha() {
var date = new Date();
$("#captchaImage").attr("src", "/common/captcha?ver=" + date.getTime());
}
function login() {
var username = $("#username").val();
var password = $("#password").val();
var captcha = $("#captcha").val();
if (!username) {
alert("请输入用户名");
return;
}
if (!password) {
alert("请输入密码");
return;
}
if (!captcha) {
alert("请输入验证码");
return;
}
$(".loading").show();
$.ajax({
url: '/api/login',
type: 'post',
cache: false,
async: false,
contentType: 'application/json',
data: JSON.stringify({
username: username,
password: password,
captcha: captcha,
}),
success: function(data) {
if (data.code === 200) {
window.location.href = "/";
} else {
alert(data.description);
}
$(".loading").hide();
}
})
}
</script>
</@html>
<#include "layout/layout.ftl"/>
<@html page_title="通知" page_tab="notification">
<div>
<b>
新消息
<a id="markRead" href="javascript:markRead()" class="pull-right" style="display: none">标记已读</a>
</b>
<hr>
<#include "components/notification.ftl"/>
<@notification userId=_user.id read=0 limit=-1/>
</div>
<div style="margin-top: 10px;">
<b>已读消息</b>
<hr>
<#include "components/notification.ftl"/>
<@notification userId=_user.id read=1 limit=20/>
</div>
<script>
$(function () {
if ($(".notification_0").length > 0) {
$("#markRead").show();
}
});
function markRead() {
$.ajax({
url: '/api/notification/markRead',
cache: false,
async: false,
type: 'get',
dataType: 'json',
contentType: 'application/json',
headers: {
'token': '${_user.token}'
},
success: function (data) {
if (data.code === 200) {
window.location.reload();
}
}
})
}
</script>
</@html>
<#include "layout/layout.ftl"/>
<@html page_title="注册" page_tab="register">
<div class="box" style="width: 500px;">
<div class="box-heading">注册</div>
<div class="box-body">
<table style="width: 100%;border-spacing: 5px;">
<tr>
<td width="60">用户名</td>
<td colspan="2"><input type="text" name="username" id="username" placeholder="用户名"/></td>
</tr>
<tr>
<td>密码</td>
<td colspan="2"><input type="password" name="password" id="password" placeholder="密码"/></td>
</tr>
<tr>
<td>验证码</td>
<td><input type="text" name="captcha" id="captcha" placeholder="验证码"/></td>
<td width="122" align="right"><img src="/common/captcha" id="captchaImage" onclick="changeCaptcha()"/></td>
</tr>
</table>
</div>
<div class="box-footer">
<a href="/login">登录</a>
<button class="pull-right" onclick="register()">注册</button>
</div>
</div>
<script>
function changeCaptcha() {
var date = new Date();
$("#captchaImage").attr("src", "/common/captcha?ver=" + date.getTime());
}
function register() {
var username = $("#username").val();
var password = $("#password").val();
var captcha = $("#captcha").val();
if (!username) {
alert("请输入用户名");
return;
}
if (!password) {
alert("请输入密码");
return;
}
if (!captcha) {
alert("请输入验证码");
return;
}
$(".loading").show();
$.ajax({
url: '/api/register',
type: 'post',
cache: false,
async: false,
contentType: 'application/json',
data: JSON.stringify({
username: username,
password: password,
captcha: captcha,
}),
success: function(data) {
if (data.code === 200) {
window.location.href = "/";
} else {
alert(data.description);
}
$(".loading").hide();
}
})
}
</script>
</@html>
<#include "layout/layout.ftl"/>
<@html page_title="搜索" page_tab="">
搜索 <b>${keyword!} 结果</b>
<@tag_search pageNo=pageNo keyword=keyword>
<table border="0" style="width: 100%; margin-top: 10px;">
<#list page.records as map>
<tr>
<td><a href="/topic/${map.id!}" target="_blank" style="font-size: 16px;">${map.title!}</a></td>
</tr>
</#list>
</table>
<#if page.current < page.pages>
<a href="/search?keyword=${keyword!}&pageNo=${page.current + 1}">查看更多</a>
</#if>
</@tag_search>
</@html>
<#include "../layout/layout.ftl"/>
<@html page_title="标签" page_tab="">
<table border="0" style="width: 100%">
<tr>
<td>
<#if tag.icon??>
<img width="24" style="vertical-align: middle;" src="${tag.icon}" alt="">&nbsp;
</#if>
<b>${tag.name}</b>&nbsp;
<span>共有 ${tag.topicCount!0} 篇话题</span>
<#if _user??>
<a class="pull-right" href="/topic/create?tag=${tag.name}">发布话题</a>
</#if>
</td>
</tr>
<#if tag.description??>
<tr>
<td style="font-size: 14px; padding-top: 10px;">${tag.description}</td>
</tr>
</#if>
</table>
<hr>
<#include "../components/topics.ftl"/>
<@topics page=page/>
</@html>
<#include "../layout/layout.ftl"/>
<@html page_title="标签" page_tab="tags">
<div class="tags">
<@tag_tags pageNo=pageNo pageSize=40>
<#list page.records as tag>
<div class="item">
<#if tag.icon??>
<img src="${tag.icon}" width="24" style="vertical-align: middle;" alt="">
</#if>
<b><a href="/topic/tag/${tag.name}">${tag.name}</a></b> X ${tag.topicCount!0}
<#if tag.description??>
<div class="description">${tag.description}</div>
</#if>
</div>
<#if (tag_index + 1) % 4 == 0>
<hr>
</#if>
</#list>
<div class="clear"></div>
<#if page.current < page.pages>
<a href="/tags?pageNo=${page.current + 1}">查看更多</a>
</#if>
</@tag_tags>
</div>
</@html>
<#include "layout/layout.ftl"/>
<@html page_title="Top100" page_tab="">
<b>Top100 积分排行</b>
<@tag_score limit=100>
<table border="0" style="width: 100%; margin-top: 10px;">
<#list users as user>
<tr>
<td width="30">
<img src="${user.avatar!}" style="vertical-align: middle;" width="24" alt="">
</td>
<td><a href="/user/${user.username}">${user.username}</a></td>
<td align="right">${user.score!0}</td>
</tr>
</#list>
</table>
</@tag_score>
</@html>
<#include "../layout/layout.ftl"/>
<@html page_title="${username}收藏的话题" page_tab="">
用户 <a href="/user/${username}">${username}</a> / 收藏的话题
<#include "../components/user_collects.ftl"/>
<@user_collects username=username pageNo=pageNo pageSize=site.page_size paginate=true/>
</@html>
<#include "../layout/layout.ftl"/>
<@html page_title="${username}评论的话题" page_tab="">
用户 <a href="/user/${username}">${username}</a> / 评论的话题
<#include "../components/user_comments.ftl"/>
<@user_comments username=username pageNo=pageNo pageSize=site.page_size paginate=true/>
</@html>
<#include "../layout/layout.ftl"/>
<@html page_title=username + " 的个人主页" page_tab="user">
<div class="user">
<div class="left">
<img src="${user.avatar!}" width="100%" alt="">
<div style="margin-top: 10px;">
<div style="font-size: 18px;">${user.username}</div>
<ul style="font-size: 14px;">
<li>积分: <a href="/top100">${user.score!0}</a></li>
<li>收藏话题: <a href="/user/${user.username}/collects">${collectCount!0}</a></li>
<li>入驻时间: ${model.formatDate(user.inTime)}</li>
<#if user.email?? && user.email != "">
<li><a href="mailto:${user.email}">${user.email}</a></li>
</#if>
<#if user.website?? && user.website != "">
<li><a href="${user.website}" target="_blank">${user.website}</a></li>
</#if>
<#if user.githubName?? && user.githubName != "">
<li>Github: <a href="https://github.com/${githubLogin}" target="_blank">${githubLogin}</a></li>
</#if>
<#if user.bio?? && user.bio != "">
<li><i>${user.bio}</i></li>
</#if>
</ul>
</div>
</div>
<div class="right">
<div>
<b>${username} 近期的话题</b>
<a href="/user/${username}/topics" class="pull-right">查看更多</a>
<hr>
<#include "../components/user_topics.ftl"/>
<@user_topics username=username pageNo=1 pageSize=10/>
</div>
<div>
<b>${username} 参与的评论</b>
<a href="/user/${username}/comments" class="pull-right">查看更多</a>
<hr>
<#include "../components/user_comments.ftl"/>
<@user_comments username=username pageNo=1 pageSize=10/>
</div>
</div>
</div>
</@html>
This diff is collapsed.
#!/bin/bash -e
ps -ef | grep pybbs.jar | grep -v grep | cut -c 9-15 | xargs kill
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment