Skip to content

Latest commit

 

History

History
406 lines (319 loc) · 10.6 KB

README.org

File metadata and controls

406 lines (319 loc) · 10.6 KB

My Own VBlog

介绍

这个项目参考了 江南一点雨 大佬的 VBlog 项目,原先后端都抄了一遍,抄明白后,自己用 Kotlin 写了自己的后端 我并没有使用 mybatis 来作为 ORM ,而是使用 JPA 作为与数据库的交互,主要是因为 mybatis 抄的时候发现自己用的是 Kotlin ,里面的构造方法要求 完整,而原来的项目中数据库表的定义与数据库模型的定义有很大的不同,所以我决定自己改写一遍,不用 Java 前端也是抄的,不过使用了 Vue3 和 Typescript 语法,编辑器插件使用了其他的插件 总结下来,这个项目我觉得非常适合供人学习,我的代码尽量写的简洁,没有冗余代码,欢迎大家 star

注意,这个项目还只是个演示项目,后续需要增加用户注册功能

技术栈

前端

  • Vue3 + setup
  • Element Plus
  • scratch components (自己写的)

后端

  • SpringBoot 3.0
  • Spring Security
  • Spring Data JPA
  • Spring Validation
  • MySQL
  • Kotlin

Start Up

配置项

backend/src/main/resources 目录下有 application.yml 文件, 修改他的内容

server:
  port: 8082
  servlet:
    context-path: /api

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/vblog
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: steiner
    password: your password
  jpa:
    hibernate:
      ddl-auto: none
      naming:
        physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
    properties:
      hibernate:
        format_sql: true

jwt:
  secret: iot.technology

open:
  urls:
    - /authenticate
    - /image/**
  roles:
    - user
    - admin

file:
  storage:
    url: /home/steiner/workspace/VBlog/storage

你需要修改他的

  • spring.datasource.url 数据库地址
  • spring.datasource.username 数据库用户名
  • spring.datasource.password 数据库密码
  • spring.jpa-hibernate.ddl-auto 应用启动时对数据库的行为, 首先把他设置成 =create= ,下一次运行的时候记得改回来
  • file.storage.url 图片存储路径

开启 Nginx

user steiner;
worker_processes  1;

#error_log  logs/error.log;
#error_log  logs/error.log  notice;
#error_log  logs/error.log  info;

#pid        logs/nginx.pid;


events {
worker_connections  1024;
}


http {
include       mime.types;
# default_type  application/octet-stream;
default_type application/json;

#log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
#                  '$status $body_bytes_sent "$http_referer" '
#                  '"$http_user_agent" "$http_x_forwarded_for"';

#access_log  logs/access.log  main;

sendfile        on;
#tcp_nopush     on;

#keepalive_timeout  0;
keepalive_timeout  65;

#gzip  on;

server {
listen       80;
server_name  localhost;

#charset koi8-r;

#access_log  logs/host.access.log  main;

# location / {
#     root   /usr/share/nginx/html;
#     index  index.html index.htm;
# }

location / {
         root /home/steiner/workspace/VBlog/frontend/dist;
         index index.html;
         try_files $uri $uri/ /index.html;
}

location /api {
         proxy_pass http://localhost:8082/api;
         add_header Access-Control-Allow-Origin * always;
         add_header Access-Control-Allow-Methods * always;
         add_header Access-Control-Allow-Headers * always;

         if ($request_method = 'OPTIONS') {
            add_header Access-Control-Allow-Origin * always;
            add_header Access-Control-Allow-Methods * always;
            add_header Access-Control-Allow-Headers * always;
            return 204;     
         }
}

#error_page  404              /404.html;

# redirect server error pages to the static page /50x.html
#
error_page   500 502 503 504  /50x.html;
location = /50x.html {
         root   /usr/share/nginx/html;
}

# proxy the PHP scripts to Apache listening on 127.0.0.1:80
#
#location ~ \.php$ {
#    proxy_pass   http://127.0.0.1;
#}

# pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
#
#location ~ \.php$ {
#    root           html;
#    fastcgi_pass   127.0.0.1:9000;
#    fastcgi_index  index.php;
#    fastcgi_param  SCRIPT_FILENAME  /scripts$fastcgi_script_name;
#    include        fastcgi_params;
#}

# deny access to .htaccess files, if Apache's document root
# concurs with nginx's one
#
#location ~ /\.ht {
#    deny  all;
#}
}


# another virtual host using mix of IP-, name-, and port-based configuration
#
#server {
#    listen       8000;
#    listen       somename:8080;
#    server_name  somename  alias  another.alias;

#    location / {
#        root   html;
#        index  index.html index.htm;
#    }
#}


# HTTPS server
#
#server {
#    listen       443 ssl;
#    server_name  localhost;

#    ssl_certificate      cert.pem;
#    ssl_certificate_key  cert.key;

#    ssl_session_cache    shared:SSL:1m;
#    ssl_session_timeout  5m;

#    ssl_ciphers  HIGH:!aNULL:!MD5;
#    ssl_prefer_server_ciphers  on;

#    location / {
#        root   html;
#        index  index.html index.htm;
#    }
#}

}

这里你需要更改 location / 这里的 root 选项,把他改为 前端的 dist 目录

后端

backend 目录下,输入

./gradlew test --tests "com.backend.backend.BackendApplicationTests.clearAndInjectData"
./gradlew build -x test

然后,运行

java -jar build/libs/backend-0.0.1-SNAPSHOT.jar

注意,使用的java版本要大于等于17

前端

进入前端目录,执行 yarn build

实例

登录页

images/实例/2023-04-25_22-18-37_screenshot.png

文章列表

images/实例/2023-04-25_22-25-40_screenshot.png

添加文章

images/实例/2023-04-25_22-21-19_screenshot.png

修改文章

images/实例/2023-04-25_22-24-23_screenshot.png

栏目管理

images/实例/2023-04-25_22-25-04_screenshot.png

用户设置(管理员页面)

images/实例/2023-04-25_22-26-09_screenshot.png

添加用户(管理员页面)

images/实例/2023-06-29_21-41-37_screenshot.png

更改头像(管理员页面)

images/实例/2023-06-29_21-43-09_screenshot.png 点击头像即可更改

Progress

Frontend

  • [X] 主页
  • [X] 删除失败
  • [X] 删除和显示不同步
  • [X] 草稿箱功能
  • [X] 回收站功能 v-if ??
  • [X] 删除操作
  • [X] 栏目管理
  • [X] 请求时 state = ?
  • [X] 添加 Category
  • [X] 用户管理
  • [X] 用户管理 + 用户 enabled
  • [X] Update Roles: Unable to locate constructor for embeddable : com.example.backend.model.UserRole$UPK
  • [X] Update Roles: Duplicate entry ‘2-1’ for key ‘PRIMARY’

Backend

  • [X] Article state 添加一个字段 state = DELETED/3, state = DUSTBIN/2

PROBLEM

  • [X] enabledChange(user.enabled) ?

Feature

  • [X] add user
  • [X] user enabled
  • [X] article shorcut

未解决的问题

我的 ArticleRepository 是这样的

@Repository
interface ArticleRepository: JpaRepository<Article, Long> {
    @Query("select new com.example.backend.model.ArticleShortcut(a.id, a.title, a.summary, a.category, a.author, a.publishDate, a.editTime, a.state, a.tags) from Article a where a.state = ?1 and a.author.id = ?2 order by a.editTime desc")
    fun findAllByState(state: Int, uid: Long, pageable: Pageable): Page<ArticleShortcut>

    @Query("select a.tags from Article a where a.state = :state and a.author.id = :uid")
    fun findTest(@Param("state") state: Int, @Param("uid") uid: Long): List<List<Tag>>
}

这是 ArticleShortcut

class ArticleShortcut(
    val id: Long,
    val title: String,
    val summary: String,
    val category: Category,
    val author: User,
    val publishDate: Timestamp,
    val editTime: Timestamp,
    val state: Int,
    val tags: List<Tag>
)

and I found that the findAllByState failed

images/未解决的问题/2023-07-02_22-09-54_screenshot.png

when I remove the `tags` filed in the `ArticleShortcut`, everything is ok but when I try to fetch the tags stand alone, the code is ok

images/未解决的问题/2023-07-02_22-10-50_screenshot.png

and this is my Article with @ManyToMany field

@Entity(name = "Article")
class Article(
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO, generator = "myid")
    @GenericGenerator(name = "myid", strategy = "com.example.backend.generator.ManualInsertGenerator")
    val id: Long?,

    @Column(length = 255, nullable = false)
    var title: String,

    @Lob
    @Column(columnDefinition = "text", nullable = false)
    var markdownContent: String,

    @Lob
    @Column(columnDefinition = "text", nullable = false)
    var htmlContent: String,

    @Lob
    @Column(columnDefinition = "text", nullable = false)
    var summary: String,

    @OneToOne
    @JoinColumn(name = "cid", referencedColumnName = "id")
    var category: Category,

    @ManyToOne
    @JoinColumn(name = "aid", referencedColumnName = "id")
    val author: User,

    @Temporal(TemporalType.TIMESTAMP)
    @Column(nullable = false)
    val publishDate: Timestamp,

    @Temporal(TemporalType.TIMESTAMP)
    @Column(nullable = false)
    var editTime: Timestamp,

    @Column(nullable = false)
    var state: Int,

    @Column(nullable = false)
    val pageView: Int,

    @ManyToMany
    @JoinTable(
        name = "ArticleTag",
        joinColumns = [JoinColumn(name = "articleid", referencedColumnName = "id")],
        inverseJoinColumns = [JoinColumn(name = "tagid", referencedColumnName = "id")]
    )
    var tags: List<Tag>,
) {
    companion object {
        const val SCRATCH = 0
        const val PUBLISHED = 1
        const val DUSTBIN = 2
        const val DELETED = 3
    }
}

你可以在 在这里 查看完整聊天