├── .gitignore ├── README.md ├── algorithm └── src │ ├── BinaryInsertSort.java │ ├── BubbleSort.java │ ├── HeapSort.java │ ├── InsertSort.java │ ├── Main.java │ ├── MergeSort.java │ ├── QuickSort.java │ ├── SelectionSort.java │ ├── ShellSort.java │ ├── aop │ └── DynamicProxy.java │ ├── generics │ ├── Boss.java │ ├── Employee.java │ ├── Generics.java │ ├── Main.java │ ├── Manager.java │ └── SubGenerics.java │ └── ioc │ ├── Autowired.java │ ├── Component.java │ ├── Container.java │ ├── Dao.java │ ├── OrderDao.java │ ├── OrderService.java │ └── test │ └── Test.java ├── aop-demo ├── .gitignore ├── pom.xml └── src │ ├── main │ ├── java │ │ └── com │ │ │ └── example │ │ │ └── demo │ │ │ ├── AopDemoApplication.java │ │ │ ├── aop │ │ │ └── TestAspect.java │ │ │ └── service │ │ │ ├── Test.java │ │ │ └── TestService.java │ └── resources │ │ └── application.properties │ └── test │ └── java │ └── com │ └── example │ └── demo │ └── AopDemoApplicationTests.java ├── client ├── .babelrc ├── .gitignore ├── README.md ├── config-overrides.js ├── package.json ├── public │ ├── index.html │ └── manifest.json ├── src │ ├── App.css │ ├── App.js │ ├── App.test.js │ ├── api │ │ ├── articleApi.js │ │ ├── base.js │ │ ├── blogApi.js │ │ ├── recordApi.js │ │ └── userApi.js │ ├── component │ │ ├── Entry.js │ │ ├── Footer.js │ │ ├── Header.js │ │ ├── Home.js │ │ ├── Login.js │ │ ├── New.js │ │ ├── Register.js │ │ ├── UserInfo.js │ │ ├── article │ │ │ ├── Article.js │ │ │ ├── BraftArticle.js │ │ │ ├── CommentBase.js │ │ │ ├── Contribute.js │ │ │ ├── Editor.js │ │ │ ├── IssueArticle.js │ │ │ ├── IssueNew.js │ │ │ ├── MarkDownArticle.js │ │ │ └── V2exArticle.js │ │ └── record │ │ │ ├── ItemForm.js │ │ │ ├── Record.css │ │ │ ├── RecordForm.js │ │ │ └── RecordList.js │ ├── constant.js │ ├── index.css │ ├── index.js │ └── registerServiceWorker.js └── yarn.lock ├── git-author-rewrite.sh └── server ├── .gitignore ├── Dockerfile ├── docker-compose.yml ├── pom.xml ├── src ├── main │ ├── java │ │ └── com │ │ │ └── yuicon │ │ │ └── blogserver │ │ │ ├── BlogServerApplication.java │ │ │ ├── config │ │ │ ├── RedisConfig.java │ │ │ └── WebConfig.java │ │ │ ├── controller │ │ │ └── ArticleController.java │ │ │ ├── mapper │ │ │ └── ArticleMapper.java │ │ │ ├── model │ │ │ └── Article.java │ │ │ └── service │ │ │ └── ArticleService.java │ └── resources │ │ ├── application-dev.properties │ │ └── application.properties └── test │ └── java │ └── com │ └── yuicon │ └── blogserver │ └── BlogServerApplicationTests.java ├── steup.sql └── zsh.exe.stackdump /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | output/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | 5 | ### STS ### 6 | .apt_generated 7 | .classpath 8 | .factorypath 9 | .project 10 | .settings 11 | .springBeans 12 | 13 | ### IntelliJ IDEA ### 14 | .idea 15 | .vscode 16 | *.iws 17 | *.iml 18 | *.ipr 19 | 20 | ### NetBeans ### 21 | nbproject/private/ 22 | build/ 23 | nbbuild/ 24 | dist/ 25 | nbdist/ 26 | .nb-gradle/ 27 | .mvn/ 28 | mvnw 29 | mvnw.cmd 30 | /src/main/docker/digag-server-0.0.1-SNAPSHOT.jar 31 | /spring.log 32 | /src/main/resources/application-dev.properties 33 | /client/node_modules/ 34 | /server/.idea/ 35 | /server/server.iml 36 | /server/.mvn/ 37 | 38 | application-product.properties 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 博客 2 | [我的个人博客](http://www.penglei.wang/) 3 | 4 | ![](https://user-gold-cdn.xitu.io/2018/8/14/1653778c1eebfea2?w=258&h=258&f=jpeg&s=28476) 5 | 6 | # 目录 7 | 8 | #### - [Java基础-内部类详解](https://juejin.im/post/5b7a54bff265da435a4850c1) 9 | #### - [Java基础-接口、lambda表达式](https://juejin.im/post/5b7a5411e51d4538e63311e3) 10 | #### - [Java多线程基础-CyclicBarrier](https://juejin.im/post/5b6fce4be51d45664c239f14) 11 | #### - [Java基础-泛型详解](https://juejin.im/post/5b61545151882569fd2886bd) 12 | #### - [一致性Hash](https://juejin.im/post/5b44728951882519f974d017) 13 | #### - [Java基础-类加载器以及加载机制](https://juejin.im/post/5b4472c55188251b157b9913) 14 | #### - [Spring理论基础-面向切面编程](https://juejin.im/post/5b38c1a66fb9a00e7a3d6cc9) 15 | #### - [Spring理论基础-控制反转和依赖注入](https://juejin.im/post/5b399eb1e51d4553156c0525) 16 | #### - [面向对象设计原则-依赖倒置](https://juejin.im/post/5b3997a851882574874da653) 17 | #### - [Spring Boot中使用Swagger2构建API文档](https://github.com/Yuicon/blog/issues/1) 18 | #### - [Spring Boot中使用 Spring Security 构建权限系统](https://github.com/Yuicon/blog/issues/2) 19 | #### - [Java多线程基础-ThreadLocal](https://www.penglei.wang/articles/12) 20 | #### - [Java基础-模块系统笔记(1)](https://www.penglei.wang/articles/13) 21 | 22 | #### - [React 实践项目 (一)](https://github.com/DigAg/digag-pc-react/issues/2) 23 | #### - [React 实践项目 (二)](https://github.com/DigAg/digag-pc-react/issues/7) 24 | #### - [React 实践项目 (三)](https://github.com/DigAg/digag-pc-react/issues/8) 25 | #### - [React 实践项目 (四)](https://github.com/DigAg/digag-pc-react/issues/9) 26 | #### - [React 实践项目 (五)](https://github.com/DigAg/digag-pc-react/issues/10) 27 | -------------------------------------------------------------------------------- /algorithm/src/BinaryInsertSort.java: -------------------------------------------------------------------------------- 1 | import java.util.Arrays; 2 | import java.util.List; 3 | 4 | /** 5 | * @author Yuicon 6 | * @Date 2018/6/15 7 | */ 8 | public class BinaryInsertSort { 9 | 10 | /** 11 | * 待排序的列表 12 | */ 13 | private static List list = Arrays.asList(9, 4, 1, 5, 7, 2, 8, 6, 3, 9, 2); 14 | 15 | public static void main(String[] args) { 16 | int tmp; 17 | for (int i = 1; i < list.size(); i++) { 18 | //保存当前位置 i 的元素,其中[0, i - 1]已经有序 19 | tmp = list.get(i); 20 | int mid, left = 0, right = i - 1; 21 | //用二分法找合适的位置 22 | while (left <= right) { 23 | mid = (left + right) / 2; 24 | if (tmp < list.get(mid)) { 25 | right = mid - 1; 26 | } else { 27 | left = mid + 1; 28 | } 29 | } 30 | //在已排序的元素中将大于 i 的元素都后移一位 31 | for (int j = i - 1; j >= left; j--) { 32 | list.set(j + 1, list.get(j)); 33 | } 34 | //插入到合适的位置 35 | list.set(left, tmp); 36 | } 37 | System.out.println(list); 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /algorithm/src/BubbleSort.java: -------------------------------------------------------------------------------- 1 | import java.util.Arrays; 2 | import java.util.List; 3 | 4 | /** 5 | * @author Yuicon 6 | * @Date 2018/6/4 7 | */ 8 | public class BubbleSort { 9 | 10 | /** 11 | * 待排序的列表 12 | */ 13 | private static List list = Arrays.asList(9, 4, 1, 5, 7, 2, 8, 6, 3, 9, 2); 14 | 15 | public static void main(String[] args) { 16 | int tmp; 17 | for (int i = 0; i < list.size() - 1; i++) { 18 | // 将未排序的元素相邻两两比较,并交换位置 19 | for (int j = 0; j < list.size() - i - 1; j++) { 20 | if (list.get(j) > list.get(j + 1)) { 21 | tmp = list.get(j); 22 | list.set(j, list.get(j + 1)); 23 | list.set(j + 1, tmp); 24 | } 25 | } 26 | } 27 | System.out.println(list); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /algorithm/src/HeapSort.java: -------------------------------------------------------------------------------- 1 | import java.util.Arrays; 2 | import java.util.List; 3 | 4 | /** 5 | * @author Yuicon 6 | * @Date 2018/6/21 7 | */ 8 | public class HeapSort { 9 | 10 | /** 11 | * 待排序的列表 12 | */ 13 | private static List list = Arrays.asList(9, 4, 1, 5, 7, 20, 8, 6, 3, 9, 2, 10, 193, 31); 14 | 15 | public static void main(String[] args) { 16 | adjustHeap(list.size() - 1); 17 | for (int j = list.size() - 1; j > 0; j--) { 18 | //将顶点交换至有序区 19 | swap(0, j); 20 | //重新构建大顶堆 21 | adjustHeap(j - 1); 22 | } 23 | System.out.println(list); 24 | } 25 | 26 | /** 27 | * 构建大顶堆 28 | * 29 | * @param e 结束索引 30 | */ 31 | private static void adjustHeap(int e) { 32 | //从最后一个非叶子结点从下至上,从右至左调整结构 33 | for (int i = (e + 1) / 2 - 1; i >= 0; i--) { 34 | fix(i, e); 35 | } 36 | } 37 | 38 | /** 39 | * 调整大顶堆 40 | * 41 | * @param i 非叶子结点索引 42 | * @param e 结束索引 43 | */ 44 | private static void fix(int i, int e) { 45 | int left = 2 * i + 1, right = 2 * i + 2; 46 | //如果右节点存在,将大的子节点交换至左节点并向下调整大顶堆 47 | if (right <= e && list.get(right) > list.get(left)) { 48 | swap(left, right); 49 | fix(right, e); 50 | } 51 | if (left <= e) { 52 | //如果左节点存在且大于父节点进行交换 53 | if (list.get(i) < list.get(left)) { 54 | swap(i, left); 55 | } 56 | //向下调整大顶堆 57 | fix(left, e); 58 | } 59 | } 60 | 61 | /** 62 | * 交换元素 63 | * 64 | * @param i 元素索引 65 | * @param j 另一个元素索引 66 | */ 67 | private static void swap(int i, int j) { 68 | list.set(i, list.get(i) + list.get(j)); 69 | list.set(j, list.get(i) - list.get(j)); 70 | list.set(i, list.get(i) - list.get(j)); 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /algorithm/src/InsertSort.java: -------------------------------------------------------------------------------- 1 | import java.util.Arrays; 2 | import java.util.List; 3 | 4 | /** 5 | * @author Yuicon 6 | * @Date 2018/6/5 7 | */ 8 | public class InsertSort { 9 | 10 | /** 11 | * 待排序的列表 12 | */ 13 | private static List list = Arrays.asList(9, 4, 1, 5, 7, 2, 8, 6, 3, 9, 2); 14 | 15 | public static void main(String[] args) { 16 | int tmp; 17 | for (int i = 1; i < list.size(); i++) { 18 | //保存当前位置 i 的元素,其中[0, i - 1]已经有序 19 | tmp = list.get(i); 20 | int j; 21 | //在已排序的元素中将大于 i 的元素都后移一位 22 | for (j = i; j > 0 && tmp < list.get(j - 1); j--) { 23 | int t = list.get(j); 24 | list.set(j, list.get(j - 1)); 25 | list.set(j - 1, t); 26 | } 27 | //插入到合适的位置 28 | list.set(j, tmp); 29 | } 30 | System.out.println(list); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /algorithm/src/Main.java: -------------------------------------------------------------------------------- 1 | import ioc.Container; 2 | import ioc.OrderService; 3 | 4 | /** 5 | * @author Yuicon 6 | * @Date 2018/7/4 7 | */ 8 | public class Main { 9 | 10 | public static void main(String[] args) { 11 | Container container = new Container(Main.class); 12 | OrderService service = container.getBean(OrderService.class); 13 | service.test(); 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /algorithm/src/MergeSort.java: -------------------------------------------------------------------------------- 1 | import java.util.Arrays; 2 | import java.util.List; 3 | 4 | /** 5 | * @author Yuicon 6 | * @Date 2018/6/11 7 | */ 8 | public class MergeSort { 9 | 10 | /** 11 | * 待排序的列表 12 | */ 13 | private static List list = Arrays.asList(9, 4, 1, 5, 7, 2, 8, 6, 3, 9, 2); 14 | 15 | public static void main(String[] args) { 16 | int[] temp = new int[list.size()]; 17 | sort(0, list.size() - 1, temp); 18 | Arrays.stream(temp).forEach(System.out::println); 19 | } 20 | 21 | 22 | private static void sort(int left, int right, int[] temp) { 23 | if (left < right) { 24 | int mid = (left + right) / 2; 25 | //左边归并排序,使得左子序列有序 26 | sort(left, mid, temp); 27 | //右边归并排序,使得右子序列有序 28 | sort(mid + 1, right, temp); 29 | //将两个有序子数组合并操作 30 | merge(left, mid, right, temp); 31 | } 32 | } 33 | 34 | private static void merge(int left, int mid, int right, int[] temp) { 35 | int i = left; 36 | int k = left, j = mid + 1; 37 | //将两个子序列较小的元素填充进temp的左边 38 | while (k <= mid && j <= right) { 39 | if (list.get(k) <= list.get(j)) { 40 | temp[i++] = list.get(k++); 41 | } else { 42 | temp[i++] = list.get(j++); 43 | } 44 | } 45 | //将左边剩余元素填充进temp中 46 | while (i <= right && k <= mid) { 47 | temp[i++] = list.get(k++); 48 | } 49 | //将右序列剩余元素填充进temp中 50 | while (i <= right && j <= right) { 51 | temp[i++] = list.get(j++); 52 | } 53 | //将temp中的元素全部拷贝到原数组中 54 | for (int p = left; p <= right; p++) { 55 | list.set(p, temp[p]); 56 | } 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /algorithm/src/QuickSort.java: -------------------------------------------------------------------------------- 1 | import java.util.Arrays; 2 | import java.util.List; 3 | 4 | /** 5 | * @author Yuicon 6 | * @Date 2018/6/5 7 | */ 8 | public class QuickSort { 9 | 10 | /** 11 | * 待排序的列表 12 | */ 13 | private static List list = Arrays.asList(9, 4, 1, 5, 7, 2, 8, 6, 3, 9, 2); 14 | 15 | public static void main(String[] args) { 16 | segmentation(0, list.size() - 1); 17 | System.out.println(list); 18 | } 19 | 20 | /** 21 | * 切分 22 | * 23 | * @param s 开始元素索引 24 | * @param e 结束元素索引 25 | */ 26 | private static void segmentation(int s, int e) { 27 | if (s > e) { 28 | return; 29 | } 30 | // 取起点元素为基准数 31 | int temp = list.get(s); 32 | int i = s, j = e; 33 | // 将右边小于基准数的元素和左边大于基准数的元素交换,最终将基准数和相交点交换,得到一个基准数右边的元素都大于它,左边的元素都小于它的数组. 34 | while (i != j) { 35 | while (i < j && list.get(j) >= temp) { 36 | j--; 37 | } 38 | while (i < j && list.get(i) <= temp) { 39 | i++; 40 | } 41 | if (i < j) { 42 | int t = list.get(i); 43 | list.set(i, list.get(j)); 44 | list.set(j, t); 45 | } 46 | } 47 | list.set(s, list.get(i)); 48 | list.set(i, temp); 49 | // 继续切分基准数左边的元素 50 | segmentation(s, i - 1); 51 | // 继续切分基准数右边的元素 52 | segmentation(i + 1, e); 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /algorithm/src/SelectionSort.java: -------------------------------------------------------------------------------- 1 | import java.util.Arrays; 2 | import java.util.List; 3 | 4 | /** 5 | * @author Yuicon 6 | * @Date 2018/6/4 7 | */ 8 | public class SelectionSort { 9 | 10 | /** 11 | * 待排序的列表 12 | */ 13 | private static List arr = Arrays.asList(9, 1, 4, 5, 7, 2, 8, 6, 3, 9, 2); 14 | 15 | public static void main(String[] args) { 16 | int minIndex; 17 | int tmp; 18 | for (int i = 0; i < arr.size() - 1; i++) { 19 | minIndex = i; 20 | // 找出未排序的元素中最小的元素 21 | for (int j = i + 1; j < arr.size(); j++) { 22 | if (arr.get(j) < arr.get(minIndex)) { 23 | // 将 minIndex 指向最小元素的索引 24 | minIndex = j; 25 | } 26 | } 27 | // 交换元素 28 | tmp = arr.get(i); 29 | arr.set(i, arr.get(minIndex)); 30 | arr.set(minIndex, tmp); 31 | } 32 | System.out.println(arr); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /algorithm/src/ShellSort.java: -------------------------------------------------------------------------------- 1 | import java.util.Arrays; 2 | import java.util.List; 3 | 4 | /** 5 | * @author Yuicon 6 | * @Date 2018/6/20 7 | */ 8 | public class ShellSort { 9 | 10 | /** 11 | * 待排序的列表 12 | */ 13 | private static List list = Arrays.asList(9, 4, 1, 5, 7, 2, 8, 6, 3, 9, 2); 14 | 15 | public static void main(String[] args) { 16 | //增量gap,并逐步缩小增量 17 | for (int gap = list.size() / 2; gap > 0; gap /= 2) { 18 | //从第gap个元素,逐个对其所在组进行直接插入排序操作 19 | for (int i = gap; i < list.size(); i++) { 20 | int j = i; 21 | //插入排序操作,从每个分组的第二个元素开始 22 | while (j - gap >= 0 && list.get(j) < list.get(j - gap)) { 23 | swap(j, j - gap); 24 | j -= gap; 25 | } 26 | } 27 | } 28 | } 29 | 30 | private static void swap(int i, int j) { 31 | list.set(i, list.get(i) + list.get(j)); 32 | list.set(j, list.get(i) - list.get(j)); 33 | list.set(i, list.get(i) - list.get(j)); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /algorithm/src/aop/DynamicProxy.java: -------------------------------------------------------------------------------- 1 | package aop; 2 | 3 | import ioc.Dao; 4 | import ioc.OrderDao; 5 | 6 | import java.lang.reflect.InvocationHandler; 7 | import java.lang.reflect.Method; 8 | import java.lang.reflect.Proxy; 9 | 10 | /** 11 | * @author Yuicon 12 | * @Date 2018/7/10 13 | */ 14 | public class DynamicProxy implements InvocationHandler { 15 | 16 | /** 17 | * 被代理类 18 | */ 19 | private Object target; 20 | 21 | public DynamicProxy(Object target) { 22 | this.target = target; 23 | } 24 | 25 | public static Object bind(Object target) { 26 | InvocationHandler invocationHandler = new DynamicProxy(target); 27 | return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), invocationHandler); 28 | } 29 | 30 | @Override 31 | public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { 32 | System.out.println(method.getName() + " 方法执行前"); 33 | //执行被代理类方法 34 | Object ret = method.invoke(target, args); 35 | System.out.println(method.getName() + " 方法执行后"); 36 | return ret; 37 | } 38 | 39 | public static void main(String[] args) { 40 | Dao dao = new OrderDao(); 41 | dao.doSomeThing(); 42 | Dao daoProxy = (Dao) DynamicProxy.bind(dao); 43 | daoProxy.doSomeThing(); 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /algorithm/src/generics/Boss.java: -------------------------------------------------------------------------------- 1 | package generics; 2 | 3 | /** 4 | * @author Yuicon 5 | */ 6 | public class Boss extends Manager { 7 | 8 | @Override 9 | public void say() { 10 | System.out.println("我是boss"); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /algorithm/src/generics/Employee.java: -------------------------------------------------------------------------------- 1 | package generics; 2 | 3 | /** 4 | * @author Yuicon 5 | */ 6 | public interface Employee { 7 | 8 | void say(); 9 | 10 | } 11 | -------------------------------------------------------------------------------- /algorithm/src/generics/Generics.java: -------------------------------------------------------------------------------- 1 | package generics; 2 | 3 | /** 4 | * @author Yuicon 5 | */ 6 | public class Generics { 7 | 8 | private T value; 9 | 10 | public T getValue() { 11 | return value; 12 | } 13 | 14 | public void setValue(T value) { 15 | this.value = value; 16 | } 17 | 18 | public static void main(String[] args) { 19 | Generics generics = new Generics<>(); 20 | generics.setValue("ss"); 21 | System.out.println(generics.getValue()); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /algorithm/src/generics/Main.java: -------------------------------------------------------------------------------- 1 | package generics; 2 | 3 | import java.util.Arrays; 4 | 5 | /** 6 | * @author Yuicon 7 | */ 8 | public class Main { 9 | 10 | Manager manager = new Manager(); 11 | Employee employee = new Manager(); 12 | Object object = new Object(); 13 | Boss boss = new Boss(); 14 | 15 | // public static void main(String[] args) { 16 | // 17 | // Generics generics = new Generics<>(); 18 | // 19 | // } 20 | // 21 | // private void test1(Generics generics) { 22 | // Employee employee = (Employee) generics.getValue(); 23 | // employee.say(); 24 | // } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /algorithm/src/generics/Manager.java: -------------------------------------------------------------------------------- 1 | package generics; 2 | 3 | /** 4 | * @author Yuicon 5 | */ 6 | public class Manager implements Employee { 7 | 8 | @Override 9 | public void say() { 10 | System.out.println("我是一个资本家"); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /algorithm/src/generics/SubGenerics.java: -------------------------------------------------------------------------------- 1 | package generics; 2 | 3 | /** 4 | * @author Yuicon 5 | */ 6 | public class SubGenerics extends Generics { 7 | 8 | @Override 9 | public void setValue(String value) { 10 | System.out.println(value); 11 | } 12 | 13 | public static void main(String[] args) { 14 | Generics generics = new SubGenerics(); 15 | generics.setValue("ss"); 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /algorithm/src/ioc/Autowired.java: -------------------------------------------------------------------------------- 1 | package ioc; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | @Retention(RetentionPolicy.RUNTIME) 9 | @Target({ElementType.FIELD,ElementType.METHOD}) 10 | public @interface Autowired { 11 | } 12 | -------------------------------------------------------------------------------- /algorithm/src/ioc/Component.java: -------------------------------------------------------------------------------- 1 | package ioc; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | @Retention(RetentionPolicy.RUNTIME) 9 | @Target({ElementType.TYPE}) 10 | public @interface Component { 11 | } 12 | -------------------------------------------------------------------------------- /algorithm/src/ioc/Container.java: -------------------------------------------------------------------------------- 1 | package ioc; 2 | 3 | import java.io.File; 4 | import java.lang.reflect.InvocationTargetException; 5 | import java.lang.reflect.Method; 6 | import java.util.*; 7 | 8 | /** 9 | * @author Yuicon 10 | * @Date 2018/7/4 11 | */ 12 | public class Container { 13 | 14 | private List classPaths = new ArrayList<>(); 15 | 16 | private String separator; 17 | 18 | private Map components = new HashMap<>(); 19 | 20 | public Container(Class cls) { 21 | File file = new File(cls.getResource("").getFile()); 22 | separator = file.getName(); 23 | renderClassPaths(new File(this.getClass().getResource("").getFile())); 24 | make(); 25 | di(); 26 | } 27 | 28 | private void make() { 29 | classPaths.forEach(classPath -> { 30 | try { 31 | Class c = Class.forName(classPath); 32 | // 找到有 @ioc.Component 注解的类并实例化 33 | if (c.isAnnotationPresent(Component.class)) { 34 | components.put(c, c.newInstance()); 35 | } 36 | } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) { 37 | e.printStackTrace(); 38 | } 39 | }); 40 | } 41 | 42 | /** 43 | * 注入依赖 44 | */ 45 | private void di() { 46 | components.forEach((aClass, o) -> Arrays.stream(aClass.getDeclaredFields()).forEach(field -> { 47 | if (field.isAnnotationPresent(Autowired.class)) { 48 | try { 49 | String methodName = "set" + field.getType().getName().substring(field.getType().getName().lastIndexOf(".") + 1); 50 | Method method = aClass.getMethod(methodName, field.getType()); 51 | if (field.getType().isInterface()) { 52 | components.keySet().forEach(aClass1 -> { 53 | if (Arrays.stream(aClass1.getInterfaces()).anyMatch(aClass2 -> aClass2.equals(field.getType()))) { 54 | try { 55 | method.invoke(o, components.get(aClass1)); 56 | } catch (IllegalAccessException | InvocationTargetException e) { 57 | e.printStackTrace(); 58 | } 59 | } 60 | }); 61 | } else { 62 | method.invoke(o, components.get(field.getType())); 63 | } 64 | } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { 65 | e.printStackTrace(); 66 | } 67 | } 68 | })); 69 | } 70 | 71 | /** 72 | * 该方法会得到所有的类,将类的绝对路径写入到classPaths中 73 | * 74 | * @param file 包 75 | */ 76 | private void renderClassPaths(File file) { 77 | if (file.isDirectory()) { 78 | File[] files = file.listFiles(); 79 | Arrays.stream(Objects.requireNonNull(files)).forEach(this::renderClassPaths); 80 | } else { 81 | if (file.getName().endsWith(".class")) { 82 | String classPath = file.getPath() 83 | .substring(file.getPath().lastIndexOf(separator) + separator.length() + 1) 84 | .replace('\\', '.') 85 | .replace(".class", ""); 86 | classPaths.add(classPath); 87 | } 88 | } 89 | } 90 | 91 | public T getBean(Class c) { 92 | return (T) components.get(c); 93 | } 94 | 95 | } 96 | -------------------------------------------------------------------------------- /algorithm/src/ioc/Dao.java: -------------------------------------------------------------------------------- 1 | package ioc; 2 | 3 | public interface Dao { 4 | 5 | void doSomeThing(); 6 | 7 | } 8 | -------------------------------------------------------------------------------- /algorithm/src/ioc/OrderDao.java: -------------------------------------------------------------------------------- 1 | package ioc; 2 | 3 | /** 4 | * @author Yuicon 5 | * @Date 2018/7/4 6 | */ 7 | @Component 8 | public class OrderDao implements Dao { 9 | 10 | @Override 11 | public void doSomeThing() { 12 | System.out.println("test"); 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /algorithm/src/ioc/OrderService.java: -------------------------------------------------------------------------------- 1 | package ioc; 2 | 3 | /** 4 | * @author Yuicon 5 | * @Date 2018/7/4 6 | */ 7 | @Component 8 | public class OrderService { 9 | 10 | @Autowired 11 | private Dao dao; 12 | 13 | public void test() { 14 | dao.doSomeThing(); 15 | } 16 | 17 | public Dao getDao() { 18 | return dao; 19 | } 20 | 21 | public void setDao(Dao dao) { 22 | this.dao = dao; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /algorithm/src/ioc/test/Test.java: -------------------------------------------------------------------------------- 1 | package ioc.test; 2 | 3 | /** 4 | * @author Yuicon 5 | * @Date 2018/7/4 6 | */ 7 | public class Test { 8 | } 9 | -------------------------------------------------------------------------------- /aop-demo/.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | !.mvn/wrapper/maven-wrapper.jar 3 | 4 | ### STS ### 5 | .apt_generated 6 | .classpath 7 | .factorypath 8 | .project 9 | .settings 10 | .springBeans 11 | .sts4-cache 12 | 13 | ### IntelliJ IDEA ### 14 | .idea 15 | *.iws 16 | *.iml 17 | *.ipr 18 | 19 | ### NetBeans ### 20 | /nbproject/private/ 21 | /nbbuild/ 22 | /dist/ 23 | /nbdist/ 24 | /.nb-gradle/ 25 | /build/ 26 | -------------------------------------------------------------------------------- /aop-demo/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 2.1.4.BUILD-SNAPSHOT 9 | 10 | 11 | com.example 12 | demo 13 | 0.0.1-SNAPSHOT 14 | aop-demo 15 | Demo project for Spring Boot 16 | 17 | 18 | 1.8 19 | 20 | 21 | 22 | 23 | org.springframework.boot 24 | spring-boot-starter 25 | 26 | 27 | 28 | org.springframework.boot 29 | spring-boot-starter-aop 30 | 31 | 32 | 33 | org.springframework.boot 34 | spring-boot-starter-test 35 | test 36 | 37 | 38 | 39 | 40 | 41 | 42 | org.springframework.boot 43 | spring-boot-maven-plugin 44 | 45 | 46 | 47 | 48 | 49 | 50 | spring-snapshots 51 | Spring Snapshots 52 | https://repo.spring.io/snapshot 53 | 54 | true 55 | 56 | 57 | 58 | spring-milestones 59 | Spring Milestones 60 | https://repo.spring.io/milestone 61 | 62 | 63 | 64 | 65 | spring-snapshots 66 | Spring Snapshots 67 | https://repo.spring.io/snapshot 68 | 69 | true 70 | 71 | 72 | 73 | spring-milestones 74 | Spring Milestones 75 | https://repo.spring.io/milestone 76 | 77 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /aop-demo/src/main/java/com/example/demo/AopDemoApplication.java: -------------------------------------------------------------------------------- 1 | package com.example.demo; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.boot.SpringApplication; 5 | import org.springframework.boot.autoconfigure.SpringBootApplication; 6 | import org.springframework.context.annotation.EnableAspectJAutoProxy; 7 | import com.example.demo.service.TestService; 8 | 9 | /** 10 | * @author Yuicon 11 | */ 12 | @SpringBootApplication 13 | @EnableAspectJAutoProxy 14 | public class AopDemoApplication { 15 | 16 | private final TestService testService; 17 | 18 | @Autowired 19 | public AopDemoApplication(TestService testService) { 20 | this.testService = testService; 21 | } 22 | 23 | public static void main(String[] args) { 24 | SpringApplication.run(AopDemoApplication.class, args); 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /aop-demo/src/main/java/com/example/demo/aop/TestAspect.java: -------------------------------------------------------------------------------- 1 | package com.example.demo.aop; 2 | 3 | import org.aspectj.lang.ProceedingJoinPoint; 4 | import org.aspectj.lang.annotation.After; 5 | import org.aspectj.lang.annotation.Around; 6 | import org.aspectj.lang.annotation.Aspect; 7 | import org.aspectj.lang.annotation.Before; 8 | import org.springframework.stereotype.Component; 9 | 10 | /** 11 | * @author Yuicon 12 | */ 13 | @Aspect 14 | @Component 15 | public class TestAspect { 16 | 17 | @Before("execution(* test(..))") 18 | public void beforeTest() { 19 | System.out.println("AOP test before"); 20 | } 21 | 22 | @After("execution(* test2(..))") 23 | public void afterTest() { 24 | System.out.println("AOP test after"); 25 | } 26 | 27 | @Around("execution(* test*(..))") 28 | public Object aroundTest(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { 29 | System.out.println("AOP test around 1"); 30 | Object o = proceedingJoinPoint.proceed(); 31 | System.out.println("AOP test around 2"); 32 | return o; 33 | 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /aop-demo/src/main/java/com/example/demo/service/Test.java: -------------------------------------------------------------------------------- 1 | package com.example.demo.service; 2 | 3 | public interface Test { 4 | 5 | void test(); 6 | 7 | void test2(); 8 | 9 | } 10 | -------------------------------------------------------------------------------- /aop-demo/src/main/java/com/example/demo/service/TestService.java: -------------------------------------------------------------------------------- 1 | package com.example.demo.service; 2 | 3 | import org.springframework.stereotype.Service; 4 | 5 | /** 6 | * @author Yuicon 7 | */ 8 | @Service 9 | public class TestService implements Test { 10 | @Override 11 | public void test() { 12 | System.out.println(1); 13 | } 14 | 15 | @Override 16 | public void test2() { 17 | System.out.println(2); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /aop-demo/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /aop-demo/src/test/java/com/example/demo/AopDemoApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.example.demo; 2 | 3 | import com.example.demo.service.TestService; 4 | import org.junit.Test; 5 | import org.junit.runner.RunWith; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.boot.test.context.SpringBootTest; 8 | import org.springframework.test.context.junit4.SpringRunner; 9 | 10 | @RunWith(SpringRunner.class) 11 | @SpringBootTest 12 | public class AopDemoApplicationTests { 13 | 14 | @Autowired 15 | private TestService testService; 16 | 17 | @Test 18 | public void contextLoads() { 19 | testService.test(); 20 | testService.test2(); 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /client/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | ["import", {"libraryName": "antd", "libraryDirectory": "es", "style": "css"}] 4 | ] 5 | } -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | -------------------------------------------------------------------------------- /client/config-overrides.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Yuicon 3 | */ 4 | module.exports = function override(config, env) { 5 | // do stuff with the webpack config... 6 | config.module.rules[1].oneOf[1].options.babelrc = true; 7 | return config; 8 | }; -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "antd": "^3.15.2", 7 | "babel-plugin-import": "^1.11.0", 8 | "braft-editor": "^2.3.7", 9 | "gitalk": "^1.4.0", 10 | "highlight.js": "^9.12.0", 11 | "react": "^16.4.2", 12 | "react-app-rewired": "1.6.2", 13 | "react-dom": "^16.4.2", 14 | "react-router-dom": "4.3.1", 15 | "react-scripts": "1.1.4" 16 | }, 17 | "scripts": { 18 | "start": "react-app-rewired start", 19 | "build": "react-app-rewired build", 20 | "test": "react-app-rewired test --env=jsdom", 21 | "eject": "react-scripts eject" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 17 | 21 | 22 | 23 | 24 | 33 | Yuicon Blog 34 | 35 | 36 | 39 |
40 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /client/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /client/src/App.css: -------------------------------------------------------------------------------- 1 | body { 2 | height:100% 3 | } 4 | 5 | .App { 6 | text-align: center; 7 | font-size: 17px; 8 | font-family: "Microsoft YaHei", serif; 9 | line-height: 1.9em; 10 | } 11 | 12 | .App .content { 13 | min-height: 74vh; 14 | background-color: #f5f5f5; 15 | } 16 | 17 | .App-logo { 18 | animation: App-logo-spin infinite 20s linear; 19 | height: 80px; 20 | } 21 | 22 | .App-header { 23 | background-color: white; 24 | padding: 16px 20vw; 25 | border-bottom: 1px solid #ececec; 26 | display: flex; 27 | justify-content: space-between; 28 | align-items: center; 29 | } 30 | 31 | .App-header .username .ant-tabs-bar { 32 | margin: 0 0 0 0; 33 | border-bottom: 0 solid #e8e8e8; 34 | } 35 | 36 | .App-title { 37 | font-size: 1.5em; 38 | text-align: left; 39 | } 40 | 41 | .App-title a { 42 | color: #a5a4a4; 43 | text-decoration:none; 44 | } 45 | 46 | .App-intro { 47 | font-size: large; 48 | } 49 | 50 | .new { 51 | display: flex; 52 | align-items: center; 53 | flex-direction: column; 54 | } 55 | 56 | .new input { 57 | padding: 10px; 58 | margin: 20px; 59 | } 60 | 61 | .new label { 62 | font-size: 14px; 63 | } 64 | 65 | .new button { 66 | border: none; 67 | background-color: #2f54eb; 68 | font-size: 16px; 69 | padding: 6px; 70 | color: white; 71 | } 72 | 73 | .entry-list { 74 | margin: 75px; 75 | } 76 | 77 | @media (max-width: 767px) { 78 | .entry-list { 79 | margin: 0; 80 | } 81 | .entry { 82 | padding: 0; 83 | } 84 | } 85 | 86 | .entry { 87 | background: transparent; 88 | border: 1px solid #e8e8e8; 89 | padding: 12px 24px; 90 | border-radius: 2px 2px 0 0; 91 | zoom: 1; 92 | margin-bottom: -1px; 93 | min-width: 500px; 94 | font-size: 24px; 95 | display: flex; 96 | flex-direction: column; 97 | } 98 | 99 | .entry .article-title { 100 | cursor: pointer; 101 | text-decoration: none; 102 | color: cornflowerblue; 103 | } 104 | 105 | .entry .details { 106 | font-size: 12px; 107 | color: darkgray; 108 | text-align: center; 109 | } 110 | 111 | .entry .details a { 112 | text-decoration: none; 113 | color: #058deb; 114 | } 115 | 116 | .markdown-body { 117 | box-sizing: border-box; 118 | min-width: 200px; 119 | max-width: 980px; 120 | margin: 0 auto; 121 | padding: 45px; 122 | text-align: left; 123 | font-size: 14px; 124 | } 125 | 126 | @media (max-width: 767px) { 127 | .markdown-body { 128 | padding: 15px; 129 | } 130 | } 131 | 132 | .footer { 133 | width: 100%; 134 | min-width: 1200px; 135 | height: 160px; 136 | background-color: #000; 137 | color: #fff; 138 | position: relative; 139 | overflow: hidden; 140 | box-sizing: border-box; 141 | } 142 | 143 | .footer .license { 144 | color: rgba(255, 255, 255, 0.7); 145 | width: 100%; 146 | text-align: center; 147 | position: absolute; 148 | bottom: 45px; 149 | font-size: 14px; 150 | } 151 | 152 | .footer .license a { 153 | color: rgba(255, 255, 255, 0.7); 154 | padding-left: 5px; 155 | } 156 | 157 | .friend-link { 158 | color: rgba(255, 255, 255, 0.7); 159 | display: flex; 160 | justify-content: center; 161 | padding: 20px; 162 | } 163 | 164 | .friend-link a { 165 | text-decoration: none; 166 | color: #969696c7; 167 | padding-left: 10px; 168 | } 169 | 170 | .comment-content { 171 | display: flex; 172 | justify-content: center; 173 | } 174 | 175 | .suspension-panel { 176 | display: flex; 177 | flex-direction: column; 178 | align-items: center; 179 | justify-content: flex-end; 180 | position: fixed; 181 | right: 12rem; 182 | bottom: 2rem; 183 | z-index: 1000; 184 | } 185 | 186 | .suspension-panel .btn { 187 | margin: 1rem 0 0; 188 | padding: 0; 189 | width: 3.33rem; 190 | height: 3.33rem; 191 | line-height: 1; 192 | color: #909090; 193 | background-color: #fff; 194 | border: 1px solid #f1f1f1; 195 | border-radius: 50%; 196 | box-shadow: 0 0 5px rgba(0,0,0,.05); 197 | cursor: pointer; 198 | } 199 | 200 | .suspension-panel .to-top-btn { 201 | font-size: 16px; 202 | } 203 | 204 | i, cite, em, var, address, dfn { 205 | font-style: italic; 206 | } 207 | 208 | .ion-android-arrow-dropup:before { 209 | content: "Top"; 210 | } 211 | 212 | .contribute { 213 | display: flex; 214 | align-items: center; 215 | flex-direction: column; 216 | } 217 | 218 | .article-content { 219 | display: flex; 220 | flex-direction: column; 221 | align-items: center; 222 | padding: 30px; 223 | } 224 | 225 | pre { 226 | text-align: start; 227 | color: #333; 228 | background: #dedede; 229 | display: flex; 230 | justify-content: flex-start; 231 | } 232 | 233 | pre code { 234 | padding-left: 64px; 235 | background: #dedede; 236 | } 237 | 238 | code { 239 | padding-left: 10px; 240 | padding-right: 10px; 241 | background-color: blanchedalmond; 242 | } 243 | 244 | ul li { 245 | text-align: start; 246 | } 247 | -------------------------------------------------------------------------------- /client/src/App.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import './App.css'; 3 | import {BrowserRouter as Router, Route} from 'react-router-dom'; 4 | import New from "./component/New"; 5 | import Home from "./component/Home"; 6 | import IssueArticle from "./component/article/IssueArticle"; 7 | import Footer from "./component/Footer"; 8 | import Header from "./component/Header"; 9 | import RecordList from "./component/record/RecordList"; 10 | import Contribute from "./component/article/Contribute"; 11 | import Article from "./component/article/Article"; 12 | 13 | class App extends Component { 14 | 15 | render() { 16 | 17 | return ( 18 | 19 |
20 |
21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 |
30 |
31 |
32 | ); 33 | } 34 | } 35 | 36 | export default App; 37 | -------------------------------------------------------------------------------- /client/src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | ReactDOM.unmountComponentAtNode(div); 9 | }); 10 | -------------------------------------------------------------------------------- /client/src/api/articleApi.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Yuicon 3 | */ 4 | 5 | import {http} from "./base"; 6 | 7 | const articleBaseUrl = "https://api.saabisu.cn/article-service/common"; 8 | 9 | export const articleApi = { 10 | getArticles: (page = 0, size = 20) => { 11 | return http.get(articleBaseUrl + `/public?page=${page}&size=${size}`); 12 | }, 13 | getArticleById: (id) => { 14 | return http.get(articleBaseUrl + "/public/" + id); 15 | }, 16 | addArticle: (article) => { 17 | return http.post(articleBaseUrl, article); 18 | }, 19 | putArticle: (article) => { 20 | return http.put(articleBaseUrl, article); 21 | }, 22 | getArticlesByTag: (tid, page = 0, size = 20) => { 23 | return http.get(articleBaseUrl + `/tag/${tid}?page=${page}&size=${size}`); 24 | }, 25 | }; 26 | 27 | const commentBaseUrl = "https://api.saabisu.cn/article-service/comment"; 28 | 29 | export const commentApi = { 30 | 31 | findAll: (aid, current = 1, size = 100) => { 32 | return http.get(`${commentBaseUrl}/public?current=${current}&size=${size}&articleId=${aid}`); 33 | }, 34 | 35 | save: (comment) => { 36 | return http.post(`${commentBaseUrl}`, comment); 37 | } 38 | 39 | }; 40 | 41 | const tagBaseUrl = "https://api.saabisu.cn/article-service/tag"; 42 | 43 | export const tagApi = { 44 | 45 | save: (tag) => { 46 | return http.post(`${tagBaseUrl}`, tag); 47 | }, 48 | 49 | getTagsByArticle: (aid) => { 50 | return http.get(tagBaseUrl + `/article/${aid}`); 51 | }, 52 | 53 | findAll: (name, current = 1, size = 10) => { 54 | return http.get(`${tagBaseUrl}?current=${current}&size=${size}&name=${name}`); 55 | }, 56 | 57 | }; 58 | -------------------------------------------------------------------------------- /client/src/api/base.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Yuicon 3 | */ 4 | 5 | import {TOKEN_KEY} from "../constant"; 6 | 7 | function checkStatus(response) { 8 | if (response.ok) { 9 | return response.json() 10 | } 11 | const error = new Error(response.statusText); 12 | if (response.status === 401) { 13 | error.body = {success: false, message: "未登录或登录已过期", data: null}; 14 | localStorage.removeItem(TOKEN_KEY); 15 | localStorage.removeItem("username"); 16 | window.location.reload(); 17 | } else { 18 | try { 19 | error.body = response.json(); 20 | } catch (e) { 21 | error.body = {success: false, message: "未知错误", data: null}; 22 | } 23 | } 24 | throw error; 25 | } 26 | 27 | 28 | export const baseFetch = (url, data) => { 29 | return fetch(url, data).then(checkStatus).then(json => json).catch(error => { 30 | if (error.body) { 31 | return error.body; 32 | } 33 | return {success: false, message: "未知错误", data: null}; 34 | }); 35 | }; 36 | 37 | const get = (url) => { 38 | return baseFetch(url, { 39 | cache: 'no-cache', // *default, no-cache, reload, force-cache, only-if-cached 40 | headers: { 41 | 'content-type': 'application/json', 42 | 'token': localStorage.getItem(TOKEN_KEY), 43 | }, 44 | method: 'GET', // *GET, POST, PUT, DELETE, etc. 45 | }); 46 | }; 47 | 48 | const post = (url, body) => { 49 | return baseFetch(url, { 50 | body: JSON.stringify(body), // must match 'Content-Type' header 51 | cache: 'no-cache', // *default, no-cache, reload, force-cache, only-if-cached 52 | headers: { 53 | 'content-type': 'application/json', 54 | 'token': localStorage.getItem(TOKEN_KEY), 55 | }, 56 | method: 'POST', // *GET, POST, PUT, DELETE, etc. 57 | }); 58 | }; 59 | 60 | const put = (url, body) => { 61 | return baseFetch(url, { 62 | body: JSON.stringify(body), // must match 'Content-Type' header 63 | cache: 'no-cache', // *default, no-cache, reload, force-cache, only-if-cached 64 | headers: { 65 | 'content-type': 'application/json', 66 | 'token': localStorage.getItem(TOKEN_KEY), 67 | }, 68 | method: 'PUT', // *GET, POST, PUT, DELETE, etc. 69 | }); 70 | }; 71 | 72 | export const http = {get, post, put}; 73 | -------------------------------------------------------------------------------- /client/src/api/blogApi.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Yuicon 3 | */ 4 | 5 | import {baseFetch, http} from "./base"; 6 | 7 | const blogBaseUrl = "https://api.saabisu.cn/article-service/issue"; 8 | 9 | function getArticles(page = 0, size = 20) { 10 | return http.get(blogBaseUrl + `/public?page=${page}&size=${size}`); 11 | } 12 | 13 | function getArticleById(id) { 14 | return http.get(blogBaseUrl + "/public/" + id); 15 | } 16 | 17 | function getArticle(gitUserName, repositoryName, issueId) { 18 | return baseFetch(`https://api.github.com/repos/${gitUserName}/${repositoryName}/issues/${issueId}`); 19 | } 20 | 21 | function addArticle(gitUserName, repositoryName, issueId) { 22 | return http.post(blogBaseUrl + `/articles`, {gitUserName, repositoryName, issueId}); 23 | } 24 | 25 | function putArticle(id, title, body, createdAt, updatedAt, closedAt) { 26 | return http.put(blogBaseUrl + `/articles`, {id, title, body, createdAt, updatedAt, closedAt}); 27 | } 28 | 29 | export const blogApi = {}; 30 | 31 | blogApi.getArticles = getArticles; 32 | blogApi.addArticle = addArticle; 33 | blogApi.putArticle = putArticle; 34 | blogApi.getArticle = getArticle; 35 | blogApi.getArticleById = getArticleById; 36 | 37 | const baseUrl = "https://api.saabisu.cn/article-service/common"; 38 | 39 | export const commonArticleApi = { 40 | 41 | findAll: (current = 1, size = 10) => { 42 | return http.get(`${baseUrl}/public?current=${current}&size=${size}`); 43 | }, 44 | 45 | }; -------------------------------------------------------------------------------- /client/src/api/recordApi.js: -------------------------------------------------------------------------------- 1 | import {http} from "./base"; 2 | 3 | /** 4 | * @author Yuicon 5 | */ 6 | 7 | const recordBaseUrl = "https://api.saabisu.cn/record-service"; 8 | 9 | function list() { 10 | return http.get(recordBaseUrl + `/records`); 11 | } 12 | 13 | function items(rid) { 14 | return http.get(recordBaseUrl + `/items?recordId=${rid}`); 15 | } 16 | 17 | function insert(source, group) { 18 | return http.post(recordBaseUrl + `/records`, {"source": source, "group": group}); 19 | } 20 | 21 | function insertItem(recordId, label, value, sequence, kind) { 22 | return http.post(recordBaseUrl + `/items`, {recordId, label, value, sequence, kind}); 23 | } 24 | 25 | function updateItem(id, label, value, sequence, kind, state) { 26 | return http.put(recordBaseUrl + `/items`, {id, label, value, sequence, kind, state}); 27 | } 28 | 29 | export const recordApi = {}; 30 | 31 | recordApi.list = list; 32 | recordApi.insert = insert; 33 | recordApi.items = items; 34 | recordApi.insertItem = insertItem; 35 | recordApi.updateItem = updateItem; -------------------------------------------------------------------------------- /client/src/api/userApi.js: -------------------------------------------------------------------------------- 1 | import {http} from "./base"; 2 | 3 | /** 4 | * @author Yuicon 5 | */ 6 | 7 | const userBaseUrl = "https://api.saabisu.cn/user-service"; 8 | 9 | function login(email, password) { 10 | return http.post(userBaseUrl + `/public/login`, {email, password}); 11 | } 12 | 13 | function register(email, username, password, phone) { 14 | return http.post(userBaseUrl + `/public/register`, {email, username, password, phone}); 15 | } 16 | 17 | function activate(email, code) { 18 | return http.post(userBaseUrl + `/activate`, {email, code}); 19 | } 20 | 21 | function activateVerify() { 22 | return http.post(userBaseUrl + `/activate/verify`); 23 | } 24 | 25 | export const userApi = {}; 26 | 27 | userApi.login = login; 28 | userApi.register = register; 29 | userApi.activate = activate; 30 | userApi.activateVerify = activateVerify; -------------------------------------------------------------------------------- /client/src/component/Entry.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from "react"; 2 | 3 | /** 4 | * @author Yuicon 5 | */ 6 | 7 | class Entry extends Component { 8 | 9 | render() { 10 | 11 | return ( 12 |
13 | 20 |
21 | 创建于:{this.props.article.createTime} / 22 | 作者:{this.props.article.author} 23 |
24 |
25 | ); 26 | } 27 | } 28 | 29 | export default Entry; -------------------------------------------------------------------------------- /client/src/component/Footer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Yuicon 3 | */ 4 | import React, {Component} from "react"; 5 | 6 | class Footer extends Component { 7 | 8 | render() { 9 | 10 | return ( 11 |
12 |
13 |

友情链接:

14 | Debug客栈 15 |
16 |
17 | ©2020 Yuicon 18 | 浙ICP备18037558号-1 19 |
20 |
21 | ); 22 | } 23 | } 24 | 25 | export default Footer; 26 | -------------------------------------------------------------------------------- /client/src/component/Header.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Yuicon 3 | */ 4 | import React, {Component} from "react"; 5 | import UserInfo from "./UserInfo"; 6 | 7 | class Header extends Component { 8 | 9 | render() { 10 | return ( 11 |
12 | 15 | 16 |
17 | ); 18 | } 19 | } 20 | 21 | export default Header; 22 | -------------------------------------------------------------------------------- /client/src/component/Home.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from "react"; 2 | import {commonArticleApi} from "../api/blogApi"; 3 | import Entry from "./Entry"; 4 | import {Pagination, Spin} from 'antd'; 5 | 6 | /** 7 | * @author Yuicon 8 | */ 9 | 10 | class Home extends Component { 11 | 12 | constructor(props) { 13 | super(props); 14 | this.state = { 15 | articles: [], 16 | spinning: false, 17 | total: 0, 18 | current: 1 19 | } 20 | } 21 | 22 | componentDidMount() { 23 | this.getArticle(1, 10); 24 | } 25 | 26 | getArticle = async (page, pageSize) => { 27 | this.setState({spinning: true}); 28 | const body = await commonArticleApi.findAll(page, pageSize); 29 | this.setState({ 30 | articles: body.data.records || [], 31 | spinning: false, 32 | total: body.data.total, 33 | current: body.data.current, 34 | }); 35 | }; 36 | 37 | render() { 38 | 39 | const entryList = this.state.articles.map(article => ); 40 | 41 | return ( 42 |
43 | 44 | {entryList} 45 | 48 |
49 | ); 50 | } 51 | } 52 | 53 | export default Home; -------------------------------------------------------------------------------- /client/src/component/Login.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Yuicon 3 | */ 4 | import React, {Component} from "react"; 5 | import {Modal, Input, message} from "antd"; 6 | import {userApi} from "../api/userApi"; 7 | 8 | class Login extends Component { 9 | 10 | constructor(props) { 11 | super(props); 12 | this.state = { 13 | email: null, 14 | password: null 15 | }; 16 | } 17 | 18 | handleEmail = (e) => { 19 | this.setState({email: e.target.value}) 20 | }; 21 | 22 | handlePassword = (e) => { 23 | this.setState({password: e.target.value}) 24 | }; 25 | 26 | handleOk = async () => { 27 | const body = await userApi.login(this.state.email, this.state.password); 28 | if (body.success) { 29 | message.success(body.message); 30 | this.props.handleOk(body.data); 31 | } else { 32 | message.error(body.message); 33 | } 34 | }; 35 | 36 | render() { 37 | 38 | return ( 39 | 44 | 45 | 46 | 47 | ); 48 | } 49 | } 50 | 51 | export default Login; -------------------------------------------------------------------------------- /client/src/component/New.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from "react"; 2 | import {Tabs} from 'antd'; 3 | import Contribute from "./article/Contribute"; 4 | 5 | const {TabPane} = Tabs; 6 | 7 | /** 8 | * @author Yuicon 9 | */ 10 | 11 | class New extends Component { 12 | 13 | render() { 14 | 15 | return ( 16 |
17 | 18 | 19 | 20 | 21 | {/**/} 22 | {/* */} 23 | {/**/} 24 | 25 |
26 | ); 27 | } 28 | } 29 | 30 | export default New; -------------------------------------------------------------------------------- /client/src/component/Register.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Yuicon 3 | */ 4 | import React, {Component} from "react"; 5 | import {Modal, Input, message} from "antd"; 6 | import {userApi} from "../api/userApi"; 7 | 8 | class Register extends Component { 9 | 10 | constructor(props) { 11 | super(props); 12 | this.state = { 13 | email: null, 14 | password: null, 15 | username: null, 16 | phone: null 17 | }; 18 | } 19 | 20 | handleEmail = (e) => { 21 | this.setState({email: e.target.value}) 22 | }; 23 | 24 | handleUsername = (e) => { 25 | this.setState({username: e.target.value}) 26 | }; 27 | 28 | handlePhone = (e) => { 29 | this.setState({phone: e.target.value}) 30 | }; 31 | 32 | handlePassword = (e) => { 33 | this.setState({password: e.target.value}) 34 | }; 35 | 36 | handleOk = async () => { 37 | const body = await userApi.register(this.state.email, this.state.username, this.state.password, this.state.phone); 38 | if (body.success) { 39 | message.success(body.message); 40 | this.props.handleOk(body.data); 41 | } else { 42 | message.error(body.message); 43 | } 44 | }; 45 | 46 | render() { 47 | 48 | return ( 49 | 54 | 55 | 56 | 57 | 58 | 59 | ); 60 | } 61 | } 62 | 63 | export default Register; -------------------------------------------------------------------------------- /client/src/component/UserInfo.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Yuicon 3 | */ 4 | import React, {Component} from "react"; 5 | import Login from "./Login"; 6 | import {Button, Tabs} from 'antd'; 7 | import Register from "./Register"; 8 | import {withRouter} from "react-router-dom"; 9 | import {TOKEN_KEY} from "../constant"; 10 | 11 | const {TabPane} = Tabs; 12 | 13 | class UserInfo extends Component { 14 | 15 | handleOnLoginOk = (userData) => { 16 | localStorage.setItem("accessToken", userData.accessToken); 17 | localStorage.setItem("username", userData.username); 18 | this.setState({loginVisible: false, username: userData.username}); 19 | }; 20 | 21 | handleOnLoginCancel = () => { 22 | this.setState({loginVisible: false}); 23 | }; 24 | 25 | handleOnRegisterOk = () => { 26 | this.setState({registerVisible: false, loginVisible: true}); 27 | }; 28 | 29 | handleOnRegisterCancel = () => { 30 | this.setState({registerVisible: false}); 31 | }; 32 | 33 | login = () => { 34 | this.setState({loginVisible: true}); 35 | }; 36 | 37 | register = () => { 38 | this.setState({registerVisible: true}); 39 | }; 40 | callback = (key) => { 41 | const func = this.menuMap[key]; 42 | func(); 43 | console.log('click', key); 44 | }; 45 | 46 | constructor(props) { 47 | super(props); 48 | let defaultActiveKey; 49 | switch (window.location.pathname) { 50 | case "/record": 51 | defaultActiveKey = "record"; 52 | break; 53 | case "/article/new": 54 | defaultActiveKey = "upload"; 55 | break; 56 | default: 57 | defaultActiveKey = 'index'; 58 | } 59 | this.state = { 60 | username: localStorage.getItem("username"), 61 | loginVisible: false, 62 | registerVisible: false, 63 | defaultActiveKey: defaultActiveKey 64 | }; 65 | this.menuMap = { 66 | "upload": () => this.props.history.push("/article/new"), 67 | "index": () => this.props.history.push("/"), 68 | "logout": () => { 69 | localStorage.removeItem(TOKEN_KEY); 70 | localStorage.removeItem("username"); 71 | this.setState({username: localStorage.getItem("username")}); 72 | }, 73 | "record": () => this.props.history.push("/record"), 74 | } 75 | } 76 | 77 | render() { 78 | return ( 79 |
80 | 82 | 84 |

{this.state.username ? 85 |
86 |
87 | {this.state.username} 88 |
89 |
90 | 91 | 92 | 93 | 94 | 95 | 96 |
97 |
98 | : 99 |
  101 |
}

102 |
103 | ); 104 | } 105 | } 106 | 107 | export default withRouter(UserInfo); 108 | -------------------------------------------------------------------------------- /client/src/component/article/Article.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from "react"; 2 | import {message, Spin} from 'antd'; 3 | import {articleApi} from "../../api/articleApi"; 4 | import BraftArticle from "./BraftArticle"; 5 | import V2exArticle from "./V2exArticle"; 6 | import MarkDownArticle from "./MarkDownArticle"; 7 | 8 | /** 9 | * @author Yuicon 10 | */ 11 | 12 | export default class Article extends Component { 13 | 14 | constructor(props) { 15 | super(props); 16 | this.state = { 17 | article: {}, 18 | error: "", 19 | spinning: false, 20 | loadingComment: false 21 | }; 22 | } 23 | 24 | backTop = () => { 25 | window.scrollTo(0,0); 26 | }; 27 | 28 | async componentDidMount() { 29 | this.setState({spinning: true}); 30 | const body = await articleApi.getArticleById(this.props.match.params.id); 31 | if (!body.success) { 32 | message.error(body.message); 33 | return; 34 | } 35 | this.setState({article: body.data, spinning: false, loadingComment: true}); 36 | window._hmt.push(['_trackPageview', `/articles/${body.data.id}`]); 37 | } 38 | 39 | renderArticle = (type) => { 40 | if (type === undefined) { 41 | return
; 42 | } 43 | const map = { 44 | 1: , 45 | 2: , 46 | 3: , 47 | }; 48 | return map[type]; 49 | }; 50 | 51 | render() { 52 | 53 | return ( 54 |
55 | 56 |

{this.state.article.title}

57 |

{this.state.article.introduction}

58 |
59 | {this.renderArticle(this.state.article.articleType)} 60 |
61 |
62 | 65 |
66 | {/*
*/} 67 | {/*{this.state.loadingComment && }*/} 68 | {/*
*/} 69 |
70 | ); 71 | } 72 | } -------------------------------------------------------------------------------- /client/src/component/article/BraftArticle.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from "react"; 2 | import BraftEditor from 'braft-editor' 3 | 4 | /** 5 | * @author Yuicon 6 | */ 7 | 8 | export default class BraftArticle extends Component { 9 | 10 | renderContent = () => { 11 | const editorState = BraftEditor.createEditorState(this.props.article.body); 12 | const content = editorState.toHTML(); 13 | return {__html: content}; 14 | }; 15 | 16 | render() { 17 | return ( 18 |
19 | ); 20 | } 21 | } -------------------------------------------------------------------------------- /client/src/component/article/CommentBase.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Editor from "./Editor"; 3 | import {Button, Card, Comment, message, Modal, Tooltip} from "antd"; 4 | import {commentApi} from "../../api/articleApi"; 5 | import BraftEditor from "braft-editor"; 6 | import moment from "moment"; 7 | 8 | export default class CommentBase extends React.Component { 9 | 10 | 11 | constructor(props) { 12 | super(props); 13 | this.state = { 14 | content: '', 15 | comments: [], 16 | visible: false, 17 | quoteId: 0 18 | }; 19 | } 20 | 21 | componentDidMount() { 22 | this.getComments(); 23 | } 24 | 25 | getComments = async (e, current = 1, size = 100) => { 26 | const body = await commentApi.findAll(this.props.articleId, current, size); 27 | if (!body.success) { 28 | message.error(body.message); 29 | return; 30 | } 31 | const comments = body.data.records; 32 | const floors = {}; 33 | comments.filter(comment => comment.quoteId === 0).forEach(comment => { 34 | floors[comment.id] = [comment]; 35 | }); 36 | comments.filter(comment => comment.quoteId !== 0).reverse().forEach(comment => { 37 | floors[comment.id] = floors[comment.quoteId].concat(); 38 | floors[comment.id].push(comment); 39 | }); 40 | const commentList = []; 41 | comments.forEach(comment => { 42 | commentList.push(floors[comment.id]); 43 | }); 44 | this.setState({comments: commentList}); 45 | }; 46 | 47 | sentComment = async () => { 48 | if (this.state.content.length < 140) { 49 | message.warn("字数不足!"); 50 | return; 51 | } 52 | 53 | if (this.state.content.length > 5400) { 54 | message.warn("字数过多!"); 55 | return; 56 | } 57 | const body = await commentApi.save({ 58 | content: this.state.content, 59 | articleId: this.props.articleId, 60 | quoteId: this.state.quoteId, 61 | state: 10 62 | }); 63 | if (body.success && body.data) { 64 | this.setState({content: ''}); 65 | message.success("发送成功!"); 66 | this.getComments(); 67 | } else { 68 | message.error(body.message); 69 | } 70 | this.setState({visible: false}); 71 | }; 72 | 73 | replyHandle = (comment) => { 74 | this.setState({quoteId: comment.id, visible: true}); 75 | }; 76 | 77 | render() { 78 | 79 | return ( 80 |
81 | 发送评论}> 83 |
87 | this.setState({content: html})} 88 | controls={['bold', 'italic', 'underline', 'strike-through', 'emoji', 'text-color']} 89 | /> 90 |
91 |
92 | 刷新评论}> 94 | { 95 | this.state.comments.length > 0 &&
96 | { 97 | this.state.comments.map((commentList, i) => { 98 | return
99 | { 100 | commentList.map((comment, index) => { 101 | const editorState = BraftEditor.createEditorState(comment.content); 102 | const content = editorState.toHTML(); 103 | const style = {border: '1px solid #fff4f4b5', textAlign: "left"}; 104 | if (index !== (commentList.length - 1)) { 105 | style.backgroundColor = "#fff8dc82"; 106 | } 107 | return 回复]} 111 | key={comment.id} 112 | author={{`${comment.floor}L - ${comment.accountName}`}} 113 | content={
} 115 | datetime={ 116 | 117 | {moment(comment.createTime).fromNow()} 118 | 119 | } 120 | />; 121 | }) 122 | } 123 |
; 124 | }) 125 | } 126 |
127 | } 128 |
129 | this.setState({visible: false, quoteId: 0})} 134 | afterClose={() => this.setState({visible: false, quoteId: 0})} 135 | > 136 | this.setState({content: html})} 137 | controls={['bold', 'italic', 'underline', 'strike-through', 'emoji', 'text-color']}/> 138 | 139 |
140 | ) 141 | 142 | } 143 | 144 | } -------------------------------------------------------------------------------- /client/src/component/article/Contribute.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from "react"; 2 | import {Button, Input, message, Select, Spin} from 'antd'; 3 | import Editor from "./Editor"; 4 | import {articleApi, tagApi} from "../../api/articleApi"; 5 | 6 | const {Option} = Select; 7 | 8 | /** 9 | * @author Yuicon 10 | */ 11 | 12 | class Contribute extends Component { 13 | 14 | constructor(props) { 15 | super(props); 16 | this.state = { 17 | title: '', 18 | content: '', 19 | introduction: "", 20 | source: "self", 21 | articleType: 1, 22 | disabled: false, 23 | tags: [], 24 | fetching: false, 25 | selectTags: [] 26 | }; 27 | } 28 | 29 | componentDidMount() { 30 | this.fetchTag(""); 31 | } 32 | 33 | save = async () => { 34 | if (this.state.title.length < 5 || this.state.title.length > 26) { 35 | message.warn("标题过短或过长!"); 36 | return; 37 | } 38 | this.setState({disabled: true}); 39 | 40 | const body = await articleApi.addArticle({ 41 | title: this.state.title, 42 | body: this.state.content, 43 | introduction: this.state.introduction, 44 | source: this.state.source, 45 | articleType: this.state.articleType, 46 | articleTags: this.state.selectTags.map(tag => { 47 | return {tid: tag.key, tagName: tag.label}; 48 | }) 49 | }); 50 | if (body.success) { 51 | message.success("投稿成功"); 52 | } else { 53 | message.error(body.message); 54 | } 55 | this.setState({disabled: false}); 56 | window.location.reload(); 57 | }; 58 | 59 | fetchTag = async value => { 60 | this.setState({data: [], fetching: true}); 61 | const body = await tagApi.findAll(value); 62 | if (body.success) { 63 | const tags = body.data.records; 64 | this.setState({tags, fetching: false}); 65 | } else { 66 | message.error(body.message); 67 | } 68 | }; 69 | 70 | handleChange = value => { 71 | this.setState({ 72 | selectTags: value, 73 | fetching: false, 74 | }); 75 | }; 76 | 77 | render() { 78 | 79 | return ( 80 |
81 |
82 | this.setState({title: event.target.value})}/> 84 |
85 |
86 | this.setState({introduction: event.target.value})}/> 88 |
89 |
90 | 标签: 91 | 106 |
107 |
108 | this.setState({content: html})}/> 109 |
110 |
111 | 113 |
114 |
115 | ); 116 | } 117 | } 118 | 119 | export default Contribute; -------------------------------------------------------------------------------- /client/src/component/article/Editor.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import BraftEditor from 'braft-editor' 3 | import 'braft-editor/dist/index.css' 4 | 5 | export default class Editor extends React.Component { 6 | 7 | 8 | constructor(props) { 9 | super(props); 10 | this.state = { 11 | editorState: null, 12 | }; 13 | } 14 | 15 | submitContent = async () => { 16 | // 在编辑器获得焦点时按下ctrl+s会执行此方法 17 | // 编辑器内容提交到服务端之前,可直接调用editorState.toHTML()来获取HTML格式的内容 18 | this.props.onChange(this.state.editorState.toRAW()); 19 | }; 20 | 21 | handleEditorChange = (editorState) => { 22 | this.setState({editorState}, () => this.props.onChange(this.state.editorState.toRAW())) 23 | }; 24 | 25 | render () { 26 | 27 | const { editorState } = this.state; 28 | 29 | return ( 30 | 45 | ) 46 | 47 | } 48 | 49 | } -------------------------------------------------------------------------------- /client/src/component/article/IssueArticle.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from "react"; 2 | import {blogApi} from "../../api/blogApi"; 3 | import 'highlight.js/styles/github-gist.css'; 4 | import hljs from 'highlight.js'; 5 | import 'gitalk/dist/gitalk.css' 6 | import Gitalk from 'gitalk' 7 | import {Spin} from 'antd'; 8 | 9 | /** 10 | * @author Yuicon 11 | */ 12 | 13 | class IssueArticle extends Component { 14 | 15 | constructor(props) { 16 | super(props); 17 | this.state = { 18 | article: {}, 19 | error: "", 20 | spinning: false 21 | }; 22 | } 23 | 24 | backTop = () => { 25 | window.scrollTo(0, 0); 26 | }; 27 | 28 | async componentDidMount() { 29 | this.setState({spinning: true}); 30 | const article = await blogApi.getArticleById(this.props.match.params.id); 31 | this.setState({article: article.data, error: "

没有文章哦!

", spinning: false}, () => { 32 | const gitalk = new Gitalk({ 33 | clientID: '1f4bdb550130e267946f', 34 | clientSecret: '1b48fcde26285d26e4b71193bff6f1d809031a03', 35 | repo: article.data.repositoryName, 36 | owner: article.data.gitUserName, 37 | admin: [article.data.gitUserName], 38 | number: article.data.issueId, 39 | id: article.data.id, // Ensure uniqueness and length less than 50 40 | distractionFreeMode: true // Facebook-like distraction free mode 41 | }); 42 | 43 | gitalk.render('gitalk-container'); 44 | hljs.initHighlighting(); 45 | window._hmt.push(['_trackPageview', `/articles/${article.data.id}`]); 46 | }); 47 | } 48 | 49 | render() { 50 | 51 | return ( 52 |
53 | 54 |

{this.state.article.title}

55 |
57 |
58 |
59 | 62 |
63 |
64 |
65 | ); 66 | } 67 | } 68 | 69 | export default IssueArticle; -------------------------------------------------------------------------------- /client/src/component/article/IssueNew.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from "react"; 2 | import {blogApi} from "../../api/blogApi"; 3 | 4 | /** 5 | * @author Yuicon 6 | */ 7 | 8 | export default class IssueNew extends Component { 9 | 10 | constructor(props) { 11 | super(props); 12 | this.state = { 13 | gitUserName: "", 14 | repositoryName: "", 15 | issueId: 0 16 | }; 17 | this.handle = { 18 | "gitUserName": value => this.setState({gitUserName: value}), 19 | "repositoryName": value => this.setState({repositoryName: value}), 20 | "issueId": value => this.setState({issueId: value}), 21 | }; 22 | } 23 | 24 | handleOnChange = (event) => { 25 | this.handle[event.target.id](event.target.value); 26 | }; 27 | 28 | handleOnClick = async () => { 29 | console.log(this.state); 30 | if (this.state.gitUserName !== "" && this.state.repositoryName !== "") { 31 | const gitIssue = await blogApi.getArticle(this.state.gitUserName, this.state.repositoryName, this.state.issueId); 32 | if (gitIssue.message !== undefined) { 33 | alert(gitIssue.message); 34 | } else { 35 | const newArticle = await blogApi.addArticle(this.state.gitUserName, this.state.repositoryName, this.state.issueId); 36 | await blogApi.putArticle(newArticle.id, gitIssue.title, gitIssue.body, 37 | gitIssue.created_at, gitIssue.updated_at, gitIssue.closed_at); 38 | alert("新建成功"); 39 | } 40 | } else { 41 | alert("请输入"); 42 | } 43 | }; 44 | 45 | render() { 46 | 47 | return ( 48 |
49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 |
57 | ); 58 | } 59 | } -------------------------------------------------------------------------------- /client/src/component/article/MarkDownArticle.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from "react"; 2 | import 'highlight.js/styles/github-gist.css'; 3 | import hljs from 'highlight.js'; 4 | import 'gitalk/dist/gitalk.css' 5 | 6 | /** 7 | * @author Yuicon 8 | */ 9 | 10 | export default class MarkDownArticle extends Component { 11 | 12 | async componentDidMount() { 13 | hljs.initHighlighting(); 14 | } 15 | 16 | render() { 17 | 18 | return ( 19 |
20 | ); 21 | } 22 | } -------------------------------------------------------------------------------- /client/src/component/article/V2exArticle.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from "react"; 2 | 3 | /** 4 | * @author Yuicon 5 | */ 6 | 7 | export default class V2exArticle extends Component { 8 | 9 | render() { 10 | 11 | return ( 12 |
13 | ); 14 | } 15 | } -------------------------------------------------------------------------------- /client/src/component/record/ItemForm.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Yuicon 3 | */ 4 | import React, {Component} from "react"; 5 | import {Modal, Input, message, InputNumber, Switch} from "antd"; 6 | import {recordApi} from "../../api/recordApi"; 7 | 8 | class ItemForm extends Component { 9 | 10 | constructor(props) { 11 | super(props); 12 | this.state = { 13 | label: null, 14 | value: null, 15 | sequence: 0, 16 | kind: 0, 17 | state: 0, 18 | record: {} 19 | }; 20 | } 21 | 22 | static getDerivedStateFromProps(props, state) { 23 | if (props.record !== state.record) { 24 | return { 25 | label: props.record.label, 26 | value: props.record.value, 27 | sequence: props.record.sequence, 28 | kind: props.record.kind, 29 | state: props.record.state, 30 | record: props.record, 31 | } 32 | } 33 | return null; 34 | } 35 | 36 | handleLabelChange = (e) => { 37 | this.setState({label: e.target.value}) 38 | }; 39 | 40 | handleValueChange = (e) => { 41 | this.setState({value: e.target.value}) 42 | }; 43 | 44 | handleKindChange = (checked, event) => { 45 | this.setState({kind: checked ? 0 : 1}); 46 | }; 47 | 48 | handleStateChange = (checked, event) => { 49 | this.setState({state: checked ? 0 : 1}); 50 | }; 51 | 52 | handleSequenceChange = (value) => { 53 | this.setState({sequence: value}); 54 | }; 55 | 56 | handleOk = async () => { 57 | console.log(this.state); 58 | let body; 59 | if (this.props.record.id) { 60 | body = await recordApi.updateItem(this.props.record.id, this.state.label, this.state.value, 61 | this.state.sequence, this.state.kind, this.state.state) 62 | } else { 63 | body = await recordApi.insertItem(this.props.recordId, this.state.label, this.state.value, 64 | this.state.sequence, this.state.kind); 65 | } 66 | if (body.success) { 67 | message.success(body.message); 68 | this.props.handleOk(body.data); 69 | } else { 70 | message.error(body.message); 71 | } 72 | }; 73 | 74 | render() { 75 | 76 | return ( 77 | 82 | 83 | 84 | 85 | 86 | 顺序 87 | 88 | 89 | 90 | 92 | { 93 | this.props.record.id && 94 | 96 | } 97 | 98 | ); 99 | } 100 | } 101 | 102 | export default ItemForm; -------------------------------------------------------------------------------- /client/src/component/record/Record.css: -------------------------------------------------------------------------------- 1 | .record-list { 2 | padding-left: 100px; 3 | padding-top: 20px; 4 | display: flex; 5 | justify-content: space-around; 6 | } 7 | 8 | .item { 9 | flex: 1; 10 | margin: 10px; 11 | } 12 | 13 | .item button { 14 | display: block; 15 | margin-left: 5px; 16 | } -------------------------------------------------------------------------------- /client/src/component/record/RecordForm.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Yuicon 3 | */ 4 | import React, {Component} from "react"; 5 | import {Modal, Input, message} from "antd"; 6 | import {recordApi} from "../../api/recordApi"; 7 | 8 | class RecordForm extends Component { 9 | 10 | constructor(props) { 11 | super(props); 12 | this.state = { 13 | source: null, 14 | group: null, 15 | }; 16 | } 17 | 18 | handleChange = (e) => { 19 | this.setState({source: e.target.value}) 20 | }; 21 | 22 | handleGroupChange = (e) => { 23 | this.setState({group: e.target.value}) 24 | }; 25 | 26 | handleOk = async () => { 27 | const body = await recordApi.insert(this.state.source, this.state.group); 28 | if (body.success) { 29 | this.props.handleOk(body.data); 30 | } else { 31 | message.error(body.message); 32 | } 33 | }; 34 | 35 | render() { 36 | 37 | return ( 38 | 43 | 44 | 45 | 46 | ); 47 | } 48 | } 49 | 50 | export default RecordForm; -------------------------------------------------------------------------------- /client/src/component/record/RecordList.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Yuicon 3 | */ 4 | import React, {Component} from "react"; 5 | import {Button, message, PageHeader, Tag, Menu, Table} from "antd"; 6 | import {TOKEN_KEY} from "../../constant"; 7 | import {recordApi} from "../../api/recordApi"; 8 | import RecordForm from "./RecordForm"; 9 | import "./Record.css"; 10 | import ItemForm from "./ItemForm"; 11 | 12 | const SubMenu = Menu.SubMenu; 13 | 14 | class RecordList extends Component { 15 | 16 | constructor(props) { 17 | super(props); 18 | this.state = { 19 | records: [], 20 | items: [], 21 | rid: null, 22 | formVisible: false, 23 | itemFormVisible: false, 24 | item: {}, 25 | openKeys: [], 26 | }; 27 | this.groups = []; 28 | } 29 | 30 | componentDidMount() { 31 | if (!localStorage.getItem(TOKEN_KEY)) { 32 | this.props.history.push("/"); 33 | } 34 | this.list(); 35 | } 36 | 37 | list = async () => { 38 | this.setState({spinning: true}); 39 | const body = await recordApi.list(); 40 | this.groups = []; 41 | if (body.success) { 42 | this.setState({records: body.data}, () => { 43 | this.state.records.forEach((record, index) => { 44 | if (index === 0) { 45 | this.setState({openKeys: [record.group], rid: record.id}, () => this.itemList()); 46 | } 47 | if (!this.groups[record.group]) { 48 | this.groups[record.group] = []; 49 | } 50 | this.groups[record.group].push(record); 51 | }); 52 | }); 53 | } else { 54 | message.error(body.message); 55 | } 56 | this.setState({ 57 | spinning: false 58 | }); 59 | }; 60 | 61 | itemList = async () => { 62 | this.setState({spinning: true}); 63 | const body = await recordApi.items(this.state.rid); 64 | body.success ? this.setState({items: body.data}) : message.error(body.message); 65 | this.setState({ 66 | spinning: false 67 | }); 68 | }; 69 | 70 | handleOnInsertOk = () => { 71 | this.setState({formVisible: false}, this.list); 72 | }; 73 | 74 | handleOnItemInsertOk = () => { 75 | this.setState({itemFormVisible: false}, this.itemList); 76 | }; 77 | 78 | handleOnInsertCancel = () => { 79 | this.setState({formVisible: false, itemFormVisible: false, item: {}}); 80 | }; 81 | 82 | onClick = () => { 83 | this.setState({formVisible: true}); 84 | }; 85 | 86 | handleClick = async (e) => { 87 | const rid = e.key; 88 | this.setState({rid: rid}, this.itemList); 89 | }; 90 | 91 | handleItemClick = (item) => { 92 | this.setState({itemFormVisible: true, item: item}); 93 | }; 94 | 95 | handleItemDeleteClick = async (item) => { 96 | const body = await recordApi.updateItem(item.id, item.label, item.value, 97 | item.sequence, item.kind, 1); 98 | if (body.success) { 99 | message.success("删除成功"); 100 | this.itemList(); 101 | } else { 102 | message.error(body.message); 103 | } 104 | }; 105 | 106 | render() { 107 | 108 | const menus = Object.keys(this.groups).map((group, index) => { 109 | return {group}}> 110 | {this.groups[group].map(record => {record.source})} 111 | ; 112 | }); 113 | 114 | const dataSource = this.state.items; 115 | 116 | const columns = [{ 117 | title: '说明', 118 | dataIndex: 'label', 119 | key: 'label', 120 | }, { 121 | title: '数据', 122 | dataIndex: 'value', 123 | key: 'value', 124 | render: (text, record, index) => { 125 | if (record.kind === 1) { 126 | return "******" 127 | } 128 | return text; 129 | } 130 | }, { 131 | title: '操作', 132 | dataIndex: 'state', 133 | key: 'state', 134 | render: (text, record, index) => { 135 | return
136 | 137 | 139 |
140 | } 141 | }]; 142 | 143 | return ( 144 |
145 | 147 | 151 | window.history.back()} 153 | title="记录" 154 | subTitle="在这里,看到真实有趣的世界,找到自己感兴趣的人,也可以让世界发现真实有趣的自己。记录世界记录你。" 155 | tags={保密} 156 | extra={[ 157 | , 160 | ]} 161 | > 162 | 163 |
164 | this.setState({openKeys: openKeys})} 170 | > 171 | {menus} 172 | 173 |
174 | `Total ${total}`, 178 | defaultPageSize: 40 179 | }} 180 | rowKey={record => record.id} 181 | title={() => this.state.rid && 182 | } 183 | /> 184 | 185 | 186 | 187 | ); 188 | } 189 | } 190 | 191 | export default RecordList; -------------------------------------------------------------------------------- /client/src/constant.js: -------------------------------------------------------------------------------- 1 | export const TOKEN_KEY = "accessToken"; -------------------------------------------------------------------------------- /client/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: sans-serif; 5 | } 6 | -------------------------------------------------------------------------------- /client/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | 6 | 7 | ReactDOM.render(, document.getElementById('root')); 8 | 9 | -------------------------------------------------------------------------------- /client/src/registerServiceWorker.js: -------------------------------------------------------------------------------- 1 | // In production, we register a service worker to serve assets from local cache. 2 | 3 | // This lets the app load faster on subsequent visits in production, and gives 4 | // it offline capabilities. However, it also means that developers (and users) 5 | // will only see deployed updates on the "N+1" visit to a page, since previously 6 | // cached resources are updated in the background. 7 | 8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy. 9 | // This link also includes instructions on opting out of this behavior. 10 | 11 | const isLocalhost = Boolean( 12 | window.location.hostname === 'localhost' || 13 | // [::1] is the IPv6 localhost address. 14 | window.location.hostname === '[::1]' || 15 | // 127.0.0.1/8 is considered localhost for IPv4. 16 | window.location.hostname.match( 17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 18 | ) 19 | ); 20 | 21 | export default function register() { 22 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 23 | // The URL constructor is available in all browsers that support SW. 24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location); 25 | if (publicUrl.origin !== window.location.origin) { 26 | // Our service worker won't work if PUBLIC_URL is on a different origin 27 | // from what our page is served on. This might happen if a CDN is used to 28 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374 29 | return; 30 | } 31 | 32 | window.addEventListener('load', () => { 33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 34 | 35 | if (isLocalhost) { 36 | // This is running on localhost. Lets check if a service worker still exists or not. 37 | checkValidServiceWorker(swUrl); 38 | 39 | // Add some additional logging to localhost, pointing developers to the 40 | // service worker/PWA documentation. 41 | navigator.serviceWorker.ready.then(() => { 42 | console.log( 43 | 'This web app is being served cache-first by a service ' + 44 | 'worker. To learn more, visit https://goo.gl/SC7cgQ' 45 | ); 46 | }); 47 | } else { 48 | // Is not local host. Just register service worker 49 | registerValidSW(swUrl); 50 | } 51 | }); 52 | } 53 | } 54 | 55 | function registerValidSW(swUrl) { 56 | navigator.serviceWorker 57 | .register(swUrl) 58 | .then(registration => { 59 | registration.onupdatefound = () => { 60 | const installingWorker = registration.installing; 61 | installingWorker.onstatechange = () => { 62 | if (installingWorker.state === 'installed') { 63 | if (navigator.serviceWorker.controller) { 64 | // At this point, the old content will have been purged and 65 | // the fresh content will have been added to the cache. 66 | // It's the perfect time to display a "New content is 67 | // available; please refresh." message in your web app. 68 | console.log('New content is available; please refresh.'); 69 | } else { 70 | // At this point, everything has been precached. 71 | // It's the perfect time to display a 72 | // "Content is cached for offline use." message. 73 | console.log('Content is cached for offline use.'); 74 | } 75 | } 76 | }; 77 | }; 78 | }) 79 | .catch(error => { 80 | console.error('Error during service worker registration:', error); 81 | }); 82 | } 83 | 84 | function checkValidServiceWorker(swUrl) { 85 | // Check if the service worker can be found. If it can't reload the page. 86 | fetch(swUrl) 87 | .then(response => { 88 | // Ensure service worker exists, and that we really are getting a JS file. 89 | if ( 90 | response.status === 404 || 91 | response.headers.get('content-type').indexOf('javascript') === -1 92 | ) { 93 | // No service worker found. Probably a different app. Reload the page. 94 | navigator.serviceWorker.ready.then(registration => { 95 | registration.unregister().then(() => { 96 | window.location.reload(); 97 | }); 98 | }); 99 | } else { 100 | // Service worker found. Proceed as normal. 101 | registerValidSW(swUrl); 102 | } 103 | }) 104 | .catch(() => { 105 | console.log( 106 | 'No internet connection found. App is running in offline mode.' 107 | ); 108 | }); 109 | } 110 | 111 | export function unregister() { 112 | if ('serviceWorker' in navigator) { 113 | navigator.serviceWorker.ready.then(registration => { 114 | registration.unregister(); 115 | }); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /git-author-rewrite.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | git filter-branch --env-filter ' 4 | OLD_EMAIL="wangpenglei@tan66.com" 5 | CORRECT_NAME="Yuicon" 6 | CORRECT_EMAIL="910722178@qq.com" 7 | if [ "$GIT_COMMITTER_EMAIL" = "$OLD_EMAIL" ] 8 | then 9 | export GIT_COMMITTER_NAME="$CORRECT_NAME" 10 | export GIT_COMMITTER_EMAIL="$CORRECT_EMAIL" 11 | fi 12 | if [ "$GIT_AUTHOR_EMAIL" = "$OLD_EMAIL" ] 13 | then 14 | export GIT_AUTHOR_NAME="$CORRECT_NAME" 15 | export GIT_AUTHOR_EMAIL="$CORRECT_EMAIL" 16 | fi 17 | ' --tag-name-filter cat -- --branches --tags 18 | -------------------------------------------------------------------------------- /server/.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | !.mvn/wrapper/maven-wrapper.jar 3 | application-product.properties 4 | 5 | ### STS ### 6 | .apt_generated 7 | .classpath 8 | .factorypath 9 | .project 10 | .settings 11 | .springBeans 12 | .sts4-cache 13 | 14 | ### IntelliJ IDEA ### 15 | .idea 16 | *.iws 17 | *.iml 18 | *.ipr 19 | *.log 20 | 21 | ### NetBeans ### 22 | /nbproject/private/ 23 | /build/ 24 | /nbbuild/ 25 | /dist/ 26 | /nbdist/ 27 | /.nb-gradle/ -------------------------------------------------------------------------------- /server/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM openjdk:8-jdk-alpine 2 | MAINTAINER Yuicon <910722178@qq.com> 3 | VOLUME /tmp 4 | ARG JAR_FILE 5 | COPY ${JAR_FILE} app.jar 6 | ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"] -------------------------------------------------------------------------------- /server/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | services: 3 | web: 4 | image: registry.cn-hangzhou.aliyuncs.com/yuicon/article-service:1.0.0-SNAPSHOT 5 | ports: 6 | - "18120:18120" 7 | 8 | -------------------------------------------------------------------------------- /server/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | com.yuicon 7 | blog-server 8 | 1.0.0-SNAPSHOT 9 | jar 10 | 11 | blog-server 12 | blog project for Spring Boot 13 | 14 | 15 | org.springframework.boot 16 | spring-boot-starter-parent 17 | 2.0.4.RELEASE 18 | 19 | 20 | 21 | 22 | UTF-8 23 | UTF-8 24 | 1.8 25 | 0.34.18 26 | 27 | 28 | 29 | 30 | wang.penglei 31 | common 32 | 1.1-SNAPSHOT 33 | 34 | 35 | com.vladsch.flexmark 36 | flexmark 37 | ${flexmark.version} 38 | 39 | 40 | com.vladsch.flexmark 41 | flexmark-util 42 | ${flexmark.version} 43 | 44 | 45 | com.vladsch.flexmark 46 | flexmark-ext-tables 47 | ${flexmark.version} 48 | 49 | 50 | org.springframework.boot 51 | spring-boot-starter-data-redis-reactive 52 | 53 | 54 | org.springframework.boot 55 | spring-boot-starter-webflux 56 | 57 | 58 | org.mybatis.spring.boot 59 | mybatis-spring-boot-starter 60 | 1.3.2 61 | 62 | 63 | 64 | mysql 65 | mysql-connector-java 66 | runtime 67 | 68 | 69 | org.springframework.boot 70 | spring-boot-starter-test 71 | test 72 | 73 | 74 | io.projectreactor 75 | reactor-test 76 | test 77 | 78 | 79 | org.springframework.boot 80 | spring-boot-starter-actuator 81 | 82 | 83 | org.springframework.cloud 84 | spring-cloud-starter-netflix-eureka-client 85 | 86 | 87 | 88 | 89 | 90 | 91 | org.springframework.cloud 92 | spring-cloud-dependencies 93 | Finchley.SR2 94 | pom 95 | import 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | org.springframework.boot 104 | spring-boot-maven-plugin 105 | 106 | 107 | 108 | com.spotify 109 | dockerfile-maven-plugin 110 | 1.4.9 111 | 112 | 113 | default 114 | 115 | build 116 | 117 | 118 | 119 | 120 | 121 | registry.cn-hangzhou.aliyuncs.com/yuicon/article-service 122 | ${project.version} 123 | 124 | ./target/${project.build.finalName}.jar 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | -------------------------------------------------------------------------------- /server/src/main/java/com/yuicon/blogserver/BlogServerApplication.java: -------------------------------------------------------------------------------- 1 | package com.yuicon.blogserver; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | /** 7 | * @author Yuicon 8 | */ 9 | @SpringBootApplication 10 | public class BlogServerApplication { 11 | 12 | public static void main(String[] args) { 13 | SpringApplication.run(BlogServerApplication.class, args); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /server/src/main/java/com/yuicon/blogserver/config/RedisConfig.java: -------------------------------------------------------------------------------- 1 | package com.yuicon.blogserver.config; 2 | 3 | import com.yuicon.blogserver.model.Article; 4 | import org.springframework.beans.factory.annotation.Qualifier; 5 | import org.springframework.beans.factory.annotation.Value; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | import org.springframework.context.annotation.Primary; 9 | import org.springframework.data.redis.connection.ReactiveRedisConnectionFactory; 10 | import org.springframework.data.redis.connection.RedisPassword; 11 | import org.springframework.data.redis.connection.RedisStandaloneConfiguration; 12 | import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration; 13 | import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; 14 | import org.springframework.data.redis.core.ReactiveRedisTemplate; 15 | import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; 16 | import org.springframework.data.redis.serializer.RedisSerializationContext; 17 | import org.springframework.data.redis.serializer.StringRedisSerializer; 18 | 19 | /** 20 | * @author Yuicon 21 | */ 22 | @Configuration 23 | public class RedisConfig { 24 | 25 | @Value("${spring.redis.host}") 26 | private String host; 27 | 28 | @Value("${spring.redis.port}") 29 | private int port; 30 | 31 | @Value("${spring.redis.password}") 32 | private String password; 33 | 34 | @Bean 35 | @Primary 36 | public ReactiveRedisConnectionFactory lettuceConnectionFactory() { 37 | LettuceClientConfiguration clientConfig = LettuceClientConfiguration.builder() 38 | .build(); 39 | RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration(host, port); 40 | redisStandaloneConfiguration.setPassword(RedisPassword.of(password)); 41 | return new LettuceConnectionFactory(redisStandaloneConfiguration, clientConfig); 42 | } 43 | 44 | @Bean(name = "reactiveRedisTemplate") 45 | public ReactiveRedisTemplate reactiveRedisTemplate( 46 | @Qualifier("lettuceConnectionFactory") ReactiveRedisConnectionFactory connectionFactory) { 47 | 48 | Jackson2JsonRedisSerializer
serializer = new Jackson2JsonRedisSerializer<>(Article.class); 49 | 50 | RedisSerializationContext.RedisSerializationContextBuilder builder = 51 | RedisSerializationContext.newSerializationContext(new StringRedisSerializer()); 52 | 53 | RedisSerializationContext serializationContext = builder 54 | .value(serializer) 55 | .hashValue(serializer) 56 | .build(); 57 | return new ReactiveRedisTemplate<>(connectionFactory, serializationContext); 58 | } 59 | // 60 | // @Bean(name = "reactiveJsonPornographicPicturesRedisTemplate") 61 | // public ReactiveRedisTemplate reactiveJsonPornographicPicturesRedisTemplate( 62 | // @Qualifier("lettuceConnectionFactory") ReactiveRedisConnectionFactory connectionFactory) { 63 | // Jackson2JsonRedisSerializer serializer = 64 | // new Jackson2JsonRedisSerializer<>(PornographicPictures.class); 65 | // 66 | // RedisSerializationContext.RedisSerializationContextBuilder builder = 67 | // RedisSerializationContext.newSerializationContext(new StringRedisSerializer()); 68 | // 69 | // RedisSerializationContext serializationContext = builder 70 | // .value(serializer) 71 | // .hashValue(serializer) 72 | // .build(); 73 | // return new ReactiveRedisTemplate<>(connectionFactory, serializationContext); 74 | // } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /server/src/main/java/com/yuicon/blogserver/config/WebConfig.java: -------------------------------------------------------------------------------- 1 | package com.yuicon.blogserver.config; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | import org.springframework.web.reactive.config.CorsRegistry; 5 | import org.springframework.web.reactive.config.EnableWebFlux; 6 | import org.springframework.web.reactive.config.WebFluxConfigurer; 7 | 8 | /** 9 | * @author Yuicon 10 | */ 11 | @Configuration 12 | @EnableWebFlux 13 | public class WebConfig implements WebFluxConfigurer { 14 | 15 | @Override 16 | public void addCorsMappings(CorsRegistry registry) { 17 | registry.addMapping("/*/**").allowedMethods("*"); 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /server/src/main/java/com/yuicon/blogserver/controller/ArticleController.java: -------------------------------------------------------------------------------- 1 | package com.yuicon.blogserver.controller; 2 | 3 | import com.yuicon.blogserver.model.Article; 4 | import com.yuicon.blogserver.service.ArticleService; 5 | import org.springframework.data.domain.PageImpl; 6 | import org.springframework.http.ResponseEntity; 7 | import org.springframework.web.bind.annotation.*; 8 | import reactor.core.publisher.Mono; 9 | 10 | /** 11 | * @author Yuicon 12 | */ 13 | @RestController 14 | public class ArticleController { 15 | 16 | private final ArticleService articleService; 17 | 18 | public ArticleController(ArticleService articleService) { 19 | this.articleService = articleService; 20 | } 21 | 22 | @PostMapping 23 | public Mono
insert(@RequestBody Article article) { 24 | return articleService.save(article); 25 | } 26 | 27 | @PutMapping 28 | public Mono
update(@RequestBody Article article) { 29 | return articleService.put(article); 30 | } 31 | 32 | @GetMapping("public") 33 | public Mono findAll(@RequestParam(value = "page", defaultValue = "0", required = false) int page, 34 | @RequestParam(value = "size", defaultValue = "20", required = false) int size) { 35 | return articleService.findByPage(page, size); 36 | } 37 | 38 | @GetMapping("public/{id}") 39 | public Mono findById(@PathVariable("id") int id) { 40 | return articleService.findById(id); 41 | } 42 | 43 | 44 | } 45 | -------------------------------------------------------------------------------- /server/src/main/java/com/yuicon/blogserver/mapper/ArticleMapper.java: -------------------------------------------------------------------------------- 1 | package com.yuicon.blogserver.mapper; 2 | 3 | import com.yuicon.blogserver.model.Article; 4 | import org.apache.ibatis.annotations.*; 5 | import org.springframework.stereotype.Repository; 6 | 7 | import java.util.List; 8 | 9 | /** 10 | * @author Yuicon 11 | */ 12 | @Mapper 13 | @Repository 14 | public interface ArticleMapper { 15 | 16 | /** 17 | * 新建文章 18 | * 19 | * @param article 文章实体 20 | * @return 新建数量 21 | */ 22 | @Insert("INSERT INTO tb_article " + 23 | "(gitUserName, repositoryName, issueId) " + 24 | "VALUES (#{article.gitUserName}, #{article.repositoryName}, #{article.issueId})") 25 | int insert(@Param("article") Article article); 26 | 27 | 28 | /** 29 | * 修改文章 30 | * 31 | * @param article 文章实体 32 | * @return 修改数量 33 | */ 34 | @Update("UPDATE tb_article " + 35 | "SET title=#{article.title}, createdAt=#{article.createdAt}, updatedAt=#{article.updatedAt}, closedAt=#{article.closedAt}, body=#{article.body} " + 36 | "WHERE id = #{article.id}") 37 | int update(@Param("article") Article article); 38 | 39 | /** 40 | * 根据id查询文章 41 | * 42 | * @param id 文章id 43 | * @return 文章 44 | */ 45 | @Select("SELECT * FROM tb_article WHERE id = #{id}") 46 | Article findById(@Param("id") int id); 47 | 48 | /** 49 | * 查询唯一的文章 50 | * 51 | * @param article 文章 52 | * @return 文章 53 | */ 54 | @Select("SELECT * FROM tb_article WHERE gitUserName = #{article.gitUserName} AND repositoryName = #{article.repositoryName} AND issueId = #{article.issueId}") 55 | Article findByUniqe(@Param("article") Article article); 56 | 57 | /** 58 | * 分页查询文章 59 | * 60 | * @param offset 偏移量 61 | * @param size 一页的数量 62 | * @return 文章列表 63 | */ 64 | @Select("SELECT id, title, gitUserName, repositoryName, issueId, createdAt, updatedAt, closedAt FROM tb_article ORDER BY createdAt DESC LIMIT #{offset}, #{size}") 65 | List
findByPage(@Param("offset") int offset, @Param("size") int size); 66 | 67 | /** 68 | * 查询文章总数 69 | * 70 | * @return 文章总数 71 | */ 72 | @Select("SELECT count(1) FROM tb_article") 73 | int count(); 74 | 75 | 76 | } 77 | -------------------------------------------------------------------------------- /server/src/main/java/com/yuicon/blogserver/model/Article.java: -------------------------------------------------------------------------------- 1 | package com.yuicon.blogserver.model; 2 | 3 | import com.vladsch.flexmark.ast.Node; 4 | import com.vladsch.flexmark.ext.tables.TablesExtension; 5 | import com.vladsch.flexmark.html.HtmlRenderer; 6 | import com.vladsch.flexmark.parser.Parser; 7 | import com.vladsch.flexmark.util.options.MutableDataSet; 8 | 9 | import java.util.Collections; 10 | import java.util.Objects; 11 | 12 | /** 13 | * @author Yuicon 14 | */ 15 | public class Article { 16 | 17 | private int id; 18 | 19 | private String gitUserName; 20 | 21 | private String repositoryName; 22 | 23 | private int issueId; 24 | 25 | private String title; 26 | 27 | private String createdAt; 28 | 29 | private String updatedAt; 30 | 31 | private String closedAt; 32 | 33 | private String body; 34 | 35 | private int userId; 36 | 37 | public String githubUrl() { 38 | return "/repos/" + getGitUserName() + "/" + getRepositoryName() + "/" + "issues" + "/" + getIssueId(); 39 | } 40 | 41 | /** 42 | * markdown转html 43 | */ 44 | public Article toHtml() { 45 | if (body == null || Objects.equals(body, "")) { 46 | return this; 47 | } 48 | MutableDataSet options = new MutableDataSet(); 49 | options.set(Parser.EXTENSIONS, Collections.singletonList(TablesExtension.create())); 50 | Parser parser = Parser.builder(options).build(); 51 | HtmlRenderer renderer = HtmlRenderer.builder(options).build(); 52 | Node document = parser.parse(body.replaceAll("\\\\r", "\r").replaceAll("\\\\n", "\n").replaceAll("\\\\t", "\t")); 53 | this.body = renderer.render(document); 54 | return this; 55 | } 56 | 57 | public int getUserId() { 58 | return userId; 59 | } 60 | 61 | public void setUserId(int userId) { 62 | this.userId = userId; 63 | } 64 | 65 | public int getId() { 66 | return id; 67 | } 68 | 69 | public void setId(int id) { 70 | this.id = id; 71 | } 72 | 73 | public String getGitUserName() { 74 | return gitUserName; 75 | } 76 | 77 | public void setGitUserName(String gitUserName) { 78 | this.gitUserName = gitUserName; 79 | } 80 | 81 | public String getRepositoryName() { 82 | return repositoryName; 83 | } 84 | 85 | public void setRepositoryName(String repositoryName) { 86 | this.repositoryName = repositoryName; 87 | } 88 | 89 | public int getIssueId() { 90 | return issueId; 91 | } 92 | 93 | public void setIssueId(int issueId) { 94 | this.issueId = issueId; 95 | } 96 | 97 | public String getTitle() { 98 | return title; 99 | } 100 | 101 | public void setTitle(String title) { 102 | this.title = title; 103 | } 104 | 105 | public String getCreatedAt() { 106 | return createdAt; 107 | } 108 | 109 | public void setCreatedAt(String createdAt) { 110 | this.createdAt = createdAt; 111 | } 112 | 113 | public String getUpdatedAt() { 114 | return updatedAt; 115 | } 116 | 117 | public void setUpdatedAt(String updatedAt) { 118 | this.updatedAt = updatedAt; 119 | } 120 | 121 | public String getClosedAt() { 122 | return closedAt; 123 | } 124 | 125 | public void setClosedAt(String closedAt) { 126 | this.closedAt = closedAt; 127 | } 128 | 129 | public String getBody() { 130 | return body; 131 | } 132 | 133 | public void setBody(String body) { 134 | this.body = body; 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /server/src/main/java/com/yuicon/blogserver/service/ArticleService.java: -------------------------------------------------------------------------------- 1 | package com.yuicon.blogserver.service; 2 | 3 | import com.yuicon.blogserver.mapper.ArticleMapper; 4 | import com.yuicon.blogserver.model.Article; 5 | import model.vo.JsonResponse; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.data.domain.PageImpl; 8 | import org.springframework.data.domain.PageRequest; 9 | import org.springframework.data.redis.core.ReactiveRedisTemplate; 10 | import org.springframework.http.ResponseEntity; 11 | import org.springframework.stereotype.Service; 12 | import reactor.core.publisher.Mono; 13 | 14 | import java.time.LocalDateTime; 15 | import java.time.format.DateTimeFormatter; 16 | import java.util.List; 17 | 18 | /** 19 | * @author Yuicon 20 | */ 21 | @Service 22 | public class ArticleService { 23 | 24 | private final ArticleMapper articleMapper; 25 | private final ReactiveRedisTemplate reactiveRedisTemplate; 26 | private final String ARTICLE_KEY = "ARTICLE:"; 27 | private final String ARTICLES_KEY = "ARTICLES:"; 28 | 29 | @Autowired 30 | public ArticleService(ArticleMapper articleMapper, ReactiveRedisTemplate reactiveRedisTemplate) { 31 | this.articleMapper = articleMapper; 32 | this.reactiveRedisTemplate = reactiveRedisTemplate; 33 | } 34 | 35 | public static void main(String[] args) { 36 | System.out.println(LocalDateTime.parse("2017-08-13T07:09:00Z", DateTimeFormatter.ISO_INSTANT)); 37 | } 38 | 39 | public Mono
put(Article article) { 40 | try { 41 | articleMapper.update(article); 42 | } catch (Exception e) { 43 | e.printStackTrace(); 44 | } 45 | return Mono.just(articleMapper.findById(article.getId())); 46 | } 47 | 48 | public Mono
save(Article article) { 49 | try { 50 | articleMapper.insert(article); 51 | } catch (Exception e) { 52 | e.printStackTrace(); 53 | } 54 | return Mono.just(articleMapper.findByUniqe(article)); 55 | } 56 | 57 | public Mono findByPage(int page, int size) { 58 | List
articles = articleMapper.findByPage(page * size, size); 59 | return Mono.just(new PageImpl<>(articles, PageRequest.of(page, size), articleMapper.count())); 60 | } 61 | 62 | public Mono findById(int id) { 63 | Article article = articleMapper.findById(id); 64 | if (article == null) { 65 | article = new Article(); 66 | } 67 | return Mono.justOrEmpty(ResponseEntity.ok(JsonResponse.success(article.toHtml()))); 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /server/src/main/resources/application-dev.properties: -------------------------------------------------------------------------------- 1 | spring.datasource.url=jdbc:mysql://localhost:3306/db_blog?useUnicode=true&characterEncoding=UTF-8 2 | spring.datasource.username=root 3 | spring.datasource.password=root 4 | spring.datasource.driver-class-name=com.mysql.jdbc.Driver -------------------------------------------------------------------------------- /server/src/main/resources/application.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yuicon/blog/f2a1e6379188d191f9e1b2ca2c3f31fcbe2b8581/server/src/main/resources/application.properties -------------------------------------------------------------------------------- /server/src/test/java/com/yuicon/blogserver/BlogServerApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.yuicon.blogserver; 2 | 3 | import org.junit.Test; 4 | import org.junit.runner.RunWith; 5 | import org.springframework.boot.test.context.SpringBootTest; 6 | import org.springframework.test.context.junit4.SpringRunner; 7 | 8 | @RunWith(SpringRunner.class) 9 | @SpringBootTest 10 | public class BlogServerApplicationTests { 11 | 12 | @Test 13 | public void contextLoads() { 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /server/steup.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE tb_article ( 2 | id BIGINT(20) NOT NULL AUTO_INCREMENT, 3 | gitUserName VARCHAR(255) NOT NULL, 4 | repositoryName VARCHAR(255) NOT NULL, 5 | issueId BIGINT(20) NOT NULL, 6 | title VARCHAR(255), 7 | createdAt VARCHAR(255), 8 | updatedAt VARCHAR(255), 9 | closedAt VARCHAR(255), 10 | body TEXT, 11 | UNIQUE (gitUserName, repositoryName, issueId), 12 | PRIMARY KEY (id) 13 | ) 14 | ENGINE = InnoDB 15 | AUTO_INCREMENT = 1 16 | DEFAULT CHARSET = utf8; 17 | -------------------------------------------------------------------------------- /server/zsh.exe.stackdump: -------------------------------------------------------------------------------- 1 | Exception: STATUS_ACCESS_VIOLATION at eip=61138D8C 2 | eax=FFE897C9 ebx=0000009A ecx=FFEA0001 edx=FFEA0000 esi=FFE84BF6 edi=FFE897C9 3 | ebp=0060BF88 esp=0060BE10 program=C:\Users\Yuicon\.babun\cygwin\bin\zsh.exe, pid 9208, thread main 4 | cs=0023 ds=002B es=002B fs=0053 gs=002B ss=002B 5 | Stack trace: 6 | Frame Function Args 7 | 0060BF88 61138D8C (0060BFF8, 0060C030, 00000000, 2063F4A8) 8 | End of stack trace 9 | --------------------------------------------------------------------------------