白筱汐

想都是问题,做都是答案

0%

介绍

Jenkins 是一个开源的自动化服务器,广泛用于持续集成(CI)和持续交付(CD)的自动化流程。

本教程基于 docker 安装 Jenkins,需要掌握 docker 相关基础知识,且已经安装好 docker。

在 docker 上安装 Jenkins

  1. 打开终端,创建 bridge network

    1
    docker network create jenkins
  2. 为了在 Jenkins 节点内执行 Docker 命令,请使用以下 docker run 命令下载并运行 docker:dind Docker 镜像

    1
    docker run --name jenkins-docker --detach --privileged --network jenkins --network-alias docker --env DOCKER_TLS_CERTDIR=/certs --volume jenkins-docker-certs:/certs/client --volume jenkins-data:/var/jenkins_home --publish 2376:2376 docker:dind --storage-driver overlay2
  3. 自定义官方 Jenkins docker 镜像,通过以下步骤

  • 创建一个 Dockerfile,加入下面的内容
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    FROM jenkins/jenkins:latest-jdk21
    USER root
    RUN apt-get update && apt-get install -y lsb-release ca-certificates curl && \
    install -m 0755 -d /etc/apt/keyrings && \
    curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc && \
    chmod a+r /etc/apt/keyrings/docker.asc && \
    echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] \
    https://download.docker.com/linux/debian $(. /etc/os-release && echo \"$VERSION_CODENAME\") stable" \
    | tee /etc/apt/sources.list.d/docker.list > /dev/null && \
    apt-get update && apt-get install -y docker-ce-cli && \
    apt-get clean && rm -rf /var/lib/apt/lists/*
    USER jenkins
    RUN jenkins-plugin-cli --plugins "blueocean docker-workflow json-path-api"
    我这里使用了最新的 jenkins/jenkins:latest-jdk21,你也可以使用其他版本,不过 jenkins 目前只支持 jdk17 和 jdk21。

如果这里镜像拉取失败,可以提前使用 docker pull 把需要的 jenkins/jenkins:latest-jdk21 拉取下来.

服务器上你可以使用阿里云的docker镜像加速,本地你可能需要科学上网了。

  • 使用 docker build 基于上面的 Dockerfile 创建一个镜像,名称为 myjenkins-blueocean
    1
    docker build -t myjenkins-blueocean .
  1. 使用下面的命令,将刚刚创建的 docker 镜像 myjenkins-blueocean 运行为容器
    1
    docker run --name jenkins-blueocean --restart=on-failure --detach --network jenkins --env DOCKER_HOST=tcp://docker:2376 --env DOCKER_CERT_PATH=/certs/client --env DOCKER_TLS_VERIFY=1 --publish 8080:8080 --publish 50000:50000 --volume jenkins-data:/var/jenkins_home --volume jenkins-docker-certs:/certs/client:ro myjenkins-blueocean

完成 Jenkins 基础配置

解锁 Jenkins

首次访问 http://localhost:8081/ 会出现一个需要解锁 Jenkins的页面,里面需要你输入管理员密码。

使用

1
docker logs jenkins-blueocean

查看运行日志,在两行*号之前有类似 7a3f82b6c4884ee19c8d6f48370a232c 这样的密码。

或者直接用命令提取

1
docker exec jenkins-blueocean cat /var/jenkins_home/secrets/initialAdminPassword

安装插件

作为新手,直接点击安装推荐的插件就行,然后等待它完成。

创建第一个管理员用户

需要创建一个管理员账户进行登录,需要账户、密码、邮箱,其他你可以不写。

创建完成直接保存,然后一直下一步,直到界面出现 欢迎来到 Jenkins!, 至此 Jenkins 基础配置就完成了。

配置前端项目

在 Jenkins 主页,点击 “新建Item”, 输入项目名称,选择 流水线(Pipeline) 类型。

点击配置,General 里面的 “This project is parameterized”, 给项目配置环境参数。

Choice Parameter 选项,配置变量名称 “BUILD_ENV”, 选项配置如下

1
2
3
development
testing
production

写上中文描述,”请选择打包环境”。

在下面找到 **流水线(Pipeline)**,选择 “Pipeline Script”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
pipeline {
agent {
docker {
image 'node:lts-buster-slim'
}
}

parameters {
choice(name: 'BUILD_ENV', choices: ['development', 'testing', 'production'], description: '请选择打包环境')
}

stages {
stage('Checkout') {
steps {
git 'https://github.com/你的代码仓库地址/simple-node-js-react-npm-app.git'
}
}
stage('Install') {
steps {
sh 'npm install'
}
}
stage('Build') {
steps {
script {
if (params.BUILD_ENV == 'development') {
sh 'npm run build:dev'
} else if (params.BUILD_ENV == 'testing') {
sh 'npm run build:test'
} else if (params.BUILD_ENV == 'production') {
sh 'npm run build:prod'
}
}
}
}
}
}

保存配置,找到之前配置的项目名称,如下图

Jenkins

选择对应的环境,点击 “Build”。

点击左下角构建历史,绿色表示构建成功,红色表示构建失败。点击最近一次构建的记录,然后点击 “Console Output”, 查看详情内容。

一般最后出现 “Finished: SUCCESS” 就是构建成功了。

构建的时候,Pipeline Script 通过 docker:bind (Docker in Docker) 安装了 node 环境,执行 npm install,最后选择对应的环境执行 npm run build:dev等。

至此,前端的项目就构建完成,接下来,需要把打包的项目传到 nginx 静态目录就可以了。

Android平台签名证书(.keystore)生成指南

前提条件,需要配置jdk(推荐使用 openJdk),需求配置好安卓开发环境。

适用于 UniApp 离线打包、Android 原生项目打包、发布到应用市场等场景。

一、使用 keytool 生成 .keystore 文件

命令格式

keytool -genkey -v
-keystore your-key.keystore
-alias your-alias
-keyalg RSA
-keysize 2048
-validity 36500

参数说明

参数 说明
-keystore 指定生成的 keystore 文件名
-alias 密钥条目的别名(alias)
-keyalg 加密算法(Android 推荐用 RSA)
-keysize 密钥长度(推荐 2048)
-validity 有效期,单位为天(36500 约等于 100 年)
-v 输出详细过程(verbose)

示例

1
keytool -genkey -alias testalias -keyalg RSA -keysize 2048 -validity 36500 -keystore test.keystore

执行后会提示输入:
• keystore 密码(storePassword)
• 证书信息(姓名、组织、城市等)
• alias 密码(keyPassword,可与 storePassword 一样)

使用以下命令查看证书信息

1
2
keytool -list -v -keystore test.keystore  
Enter keystore password: //输入密码,回车

二、在 Gradle 项目中配置签名

编辑 app/build.gradle(或 simpleDemo/build.gradle)添加:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
android {
signingConfigs {
release {
storeFile file("test.keystore") // 指向你的 keystore 文件
storePassword "你的storePassword"
keyAlias "testalias"
keyPassword "你的keyPassword"
}
}

buildTypes {
release {
signingConfig signingConfigs.release
minifyEnabled false
shrinkResources false
zipAlignEnabled true
}
debug {
signingConfig signingConfigs.release
}
}
}

注意: storePassword 和 keyPassword 必须和生成时输入的一致。

三、打包 APK 并签名

命令行打包,生成 release 包

1
./gradlew assembleRelease

输出文件位置:
app/build/outputs/apk/release/app-release.apk

或使用 Android Studio:
Build > Build Bundle(s) / APK(s) > Build APK(s)

四、注意事项

  • .keystore 一旦丢失,将无法更新已发布应用。
  • 强烈建议备份 .keystore 文件和密码信息。
  • 不同应用商店对签名一致性有严格要求。
  • 使用 Google Play App Signing 功能可减轻密钥遗失风险(建议开启)。

五、直接使用 Android Studio 生成 证书

SQL 综合练习

– 创建 customers 表,用于存储客户信息
CREATE TABLE IF NOT EXISTS customers (
id int(11) NOT NULL AUTO_INCREMENT COMMENT ‘客户ID,自增长’,
name varchar(255) NOT NULL COMMENT ‘客户姓名,非空’,
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT=’客户信息表’;

– 创建 orders 表,用于存储订单信息
CREATE TABLE IF NOT EXISTS orders (
id int(11) NOT NULL AUTO_INCREMENT COMMENT ‘订单ID,自增长’,
customer_id int(11) NOT NULL COMMENT ‘客户ID,非空’,
order_date date NOT NULL COMMENT ‘订单日期,非空’,
total_amount decimal(10,2) NOT NULL COMMENT ‘订单总金额,非空’,
PRIMARY KEY (id),
FOREIGN KEY (customer_id) REFERENCES customers (id) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT=’订单信息表’;

– 创建 order_items 表,用于存储订单商品信息
CREATE TABLE IF NOT EXISTS order_items (
id int(11) NOT NULL AUTO_INCREMENT COMMENT ‘商品ID,自增长’,
order_id int(11) NOT NULL COMMENT ‘订单ID,非空’,
product_name varchar(255) NOT NULL COMMENT ‘商品名称,非空’,
quantity int(11) NOT NULL COMMENT ‘商品数量,非空’,
price decimal(10,2) NOT NULL COMMENT ‘商品单价,非空’,
PRIMARY KEY (id),
FOREIGN KEY (order_id) REFERENCES orders (id) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT=’订单商品信息表’;

– 向 customers 表插入数据
INSERT INTO customers (name)
VALUES
(‘张丽娜’),(‘李明’),(‘王磊’),(‘赵静’),(‘钱伟’),
(‘孙芳’),(‘周涛’),(‘吴洋’),(‘郑红’),(‘刘华’),
(‘陈明’),(‘杨丽’),(‘王磊’),(‘张伟’),(‘李娜’),
(‘刘洋’),(‘陈静’),(‘杨阳’),(‘王丽’),(‘张强’);

SELECT * FROM customers;

– 向 orders 表插入数据
INSERT INTO orders (customer_id, order_date, total_amount)
VALUES
(1, ‘2022-01-01’,100.00),(1, ‘2022-01-02’,200.00),
(2, ‘2022-01-03’,300.00),(2, ‘2022-01-04’,400.00),
(3, ‘2022-01-05’,500.00),(3, ‘2022-01-06’,600.00),
(4, ‘2022-01-07’,700.00),(4, ‘2022-01-08’,800.00),
(5, ‘2022-01-09’,900.00),(5, ‘2022-01-10’,1000.00);

SELECT * FROM orders;

– 向 order_items 表插入数据
INSERT INTO order_items (order_id, product_name, quantity, price)
VALUES
(1, ‘耐克篮球鞋’,1,100.00),
(1, ‘阿迪达斯跑步鞋’,2,50.00),
(2, ‘匡威帆布鞋’,3,100.00),
(2, ‘万斯板鞋’,4,50.00),
(3, ‘新百伦运动鞋’,5,100.00),
(3, ‘彪马休闲鞋’,6,50.00),
(4, ‘锐步经典鞋’,7,100.00),
(5, ‘亚瑟士运动鞋’,10,50.00),
(5, ‘帆布鞋’,1,100.00),
(1, ‘苹果手写笔’,2,50.00),
(2, ‘电脑包’,3,100.00),
(3, ‘苹果手机’,4,50.00),
(4, ‘苹果耳机’,5,100.00),
(5, ‘苹果平板’,7,100.00);

SELECT * FROM order_items;

– 需求1:查询每个客户的订单总金额
– 分析:客户的订单存在订单表里,可能有多个订单,这里需求 JOIN ON 关联2个表,然后用 GROUP BY 根据客户id 分组,再通过 SUM 函数计算价格总和。
SELECT
customers.NAME,
SUM(orders.total_amount) AS total_amount
FROM
customers
INNER JOIN orders ON customers.id = orders.customer_id
GROUP BY
customers.id
ORDER BY
total_amount DESC
LIMIT 0,
3;

– 需求2:查询每个客户的订单总金额,并计算其占比
– 分析:每个客户的总金额的需求上面已经实现了,这里需求算占比,就需要通过一个子查询来计算全部订单的总金额,然后相除
SELECT
customers.NAME,
SUM(orders.total_amount) AS total_amount,
SUM(orders.total_amount) / (SELECT SUM(total_amount) FROM orders) AS percentage
FROM
customers
INNER JOIN orders ON customers.id = orders.customer_id
GROUP BY
customers.id;

– 需求3:查询每个客户的订单总金额,并列出每个订单的商品清单
– 分析:这里在总金额的基础上,多了订单项的查询,需求多关联一个表
SELECT
customers.NAME,
orders.order_date,
orders.total_amount,
order_items.product_name,
order_items.quantity,
order_items.price
FROM
customers
JOIN orders ON customers.id = orders.customer_id
JOIN order_items ON orders.id = order_items.order_id
ORDER BY
customers.name,
orders.order_date;

– 需求4:查询每个客户的订单总金额,并列出每个订单的商品清单,同时只显示客户名字姓 “张” 的客户记录
– 分析:总金额和商品清单的需求前面实现过了,现在只需要加一个 WHERE 来过滤只查询姓张的客户
SELECT
customers.NAME,
orders.order_date,
orders.total_amount,
order_items.product_name,
order_items.quantity,
order_items.price
FROM
customers
JOIN orders ON customers.id = orders.customer_id
JOIN order_items ON orders.id = order_items.order_id
WHERE
name LIKE ‘张%’
ORDER BY
customers.NAME,
orders.order_date;
– 需求5:查询每个客户的订单总金额,并列出每个订单的商品清单,同时只显示订单日期在 2022-01-01 至 2022-01-03 之间的记录。
– 分析:通过 BETWEEN AND 来过滤时间范围。
SELECT
customers.NAME,
orders.order_date,
orders.total_amount,
order_items.product_name,
order_items.quantity,
order_items.price
FROM
customers
JOIN orders ON customers.id = orders.customer_id
JOIN order_items ON orders.id = order_items.order_id
WHERE
orders.order_date BETWEEN ‘2022-01-01’
AND ‘2022-01-03’
ORDER BY
customers.NAME,
orders.order_date;

– 需求6:查询每个客户的订单总金额,并计算商品数量,只包含商品名称包含 “鞋” 的商品,商品名用 - 连接,显示前3条记录
– 分析:查询订单总金额和商品总数量都需要用 GROUP BY 根据 customer.id 分组,过滤出只包含鞋的商品 %鞋%,把分组的多条商品名连接需要用 GROUP_CONCAT 函数,SEPARATOR 只能用于 GROUP_CONCAT函数,指定拼接字符串之间的分隔符
SELECT
c.NAME AS cutomer_name,
SUM(o.total_amount) total_amount,
COUNT(oi.id) total_quantity,
GROUP_CONCAT(oi.product_name SEPARATOR ‘-‘) product_names
FROM
customers c
JOIN orders o ON c.id = o.customer_id
JOIN order_items oi ON o.id = oi.order_id
WHERE
oi.product_name LIKE ‘%鞋%’
GROUP BY
c.id
ORDER BY
total_amount DESC
LIMIT 3;

– 查询存在订单的客户
– 分析:使用子查询和 EXISTS 实现
SELECT
*
FROM
customers
WHERE
EXISTS (SELECT 1 FROM orders WHERE orders.customer_id = customers.id);
– 查询没有下单过的客户
SELECT
*
FROM
customers c
WHERE
NOT EXISTS (SELECT 1 FROM orders o WHERE o.customer_id = c.id);

– 需求8:将王磊的订单总金额打九折
– 分析:update 更新订单金额,子查询查 王磊的订单

– 查询王磊的订单
SELECT * FROM orders
JOIN customers ON orders.customer_id = customers.id
WHERE customers.name = ‘王磊’;

– 订单可能有多条,使用 in 指定一个集合
UPDATE orders o SET o.total_amount = o.total_amount * 0.9
WHERE o.customer_id IN (SELECT id FROM customers WHERE name = ‘王磊’);

– 还原订单价格
UPDATE orders o
JOIN customers c ON o.customer_id = c.id
SET o.total_amount = o.total_amount / 0.9
WHERE c.name = ‘王磊’;

子查询和 EXISTS

SELECT * FROM student;

– 查询最高分
SELECT MAX(score) FROM student;

– 再查询这个分数位最分的学生
SELECT name, class FROM student WHERE score = 95;

– 合并 sql 成为子查询
SELECT name, class FROM student WHERE score = (SELECT MAX(score) FROM student);

– 查询成绩高于全校平均成绩的学生记录
SELECT * FROM student WHERE score > (SELECT AVG(score) FROM student);

– 查询部门表数据
SELECT * FROM department;

– 查询员工表的数据
SELECT * FROM employee;

– EXISTS 查询有员工的部门
SELECT name FROM department WHERE EXISTS (SELECT * FROM employee WHERE department.id = employee.department_id);

– NOT EXISTS 查询所有没有员工的部门
SELECT name FROM department WHERE NOT EXISTS (SELECT * FROM employee WHERE department.id = employee.department_id);

– 新建产品表
CREATE TABLE product (
id INT PRIMARY KEY,
name VARCHAR(50),
price DECIMAL(10,2),
category VARCHAR(50),
stock INT
);

– 插入数据
INSERT INTO product (id, NAME, price, category, stock)
VALUES
(1, ‘iPhone12’, 6999.00, ‘手机’, 100),
(2, ‘iPad Pro’, 7999.00, ‘平板电脑’, 50),
(3, ‘MacBook Pro’, 12999.00, ‘笔记本电脑’, 30),
(4, ‘AirPods Pro’, 1999.00, ‘耳机’, 200),
(5, ‘Apple Watch’, 3299.00, ‘智能手表’, 80);

– 查询价格最高的产品信息
– 通过一个子查询查最高的价格,然后外层查询价格为最高价格的产品
SELECT name, price FROM product WHERE price = (SELECT MAX(price) FROM product);

– 把每个产品分类的分类名、平均价格查出来放入另一个 avg_price_by_category 表
CREATE TABLE avg_price_by_category (
id INT AUTO_INCREMENT,
category VARCHAR(50) NOT NULL,
avg_price DECIMAL(10,2) NOT NULL,
PRIMARY KEY (id)
);

– 把产品表里的分类和平均价格查询出来插入这个表
INSERT INTO avg_price_by_category (category,avg_price)
SELECT category, AVG(price) FROM product GROUP BY category;

SELECT category, AVG(price) FROM product GROUP BY category

SELECT * FROM avg_price_by_category;

– 查询名字等于技术部的 department 的 id,然后更新 department_id 为这个 id 的所有 employee 的名字为 CONCAT(“技术-“, name)

UPDATE employee SET name = CONCAT(‘技术-‘,name) WHERE department_id = (SELECT id FROM department WHERE name = ‘技术部’);

SELECT * FROM employee;

– 删除所有技术部的员工
DELETE FROM employee WHERE department_id = (SELECT id FROM department WHERE name = ‘技术部’);

总结:
sql 和 sql 可以组合来完成更复杂的功能,这种语法叫做子查询。
它还有个特有的关键字 EXISTS(和 NOT EXISTS),当子查询有返回结果的时候成立,没有返回结果的时候不成立。
子查询不止 select 可用,在 update、insert、delete 里也可以用。
灵活运用子查询,能写出功能更强大的 sql.

mysql学习指南 - 一对多、多对多关系表设计

– 一对多关系,一个部门有多个员工
– 新建 department 表
CREATE TABLE department (
id INT NOT NULL AUTO_INCREMENT,
name VARCHAR (45) NOT NULL,
PRIMARY KEY (id)
);

– 新建 employee 表
CREATE TABLE employee (
id INT NOT NULL AUTO_INCREMENT,
name VARCHAR(45) NOT NULL,
department_id INT NULL,
PRIMARY KEY(id),
INDEX department_id_idx (department_id),
CONSTRAINT department_id
FOREIGN KEY(department_id)
REFERENCES department(id)
ON DELETE SET NULL
ON UPDATE SET NULL
);

– 插入数据
INSERT INTO department (id, name)
VALUES
(1, ‘人事部’),
(2, ‘财务部’),
(3, ‘市场部’),
(4, ‘技术部’),
(5, ‘销售部’),
(6, ‘客服部’),
(7, ‘采购部’),
(8, ‘行政部’),
(9, ‘品控部’),
(10, ‘研发部’);

SELECT * FROM department;

INSERT INTO employee (id, name, department_id)
VALUES
(1, ‘张三’, 1),
(2, ‘李四’, 2),
(3, ‘王五’, 3),
(4, ‘赵六’, 4),
(5, ‘钱七’, 5),
(6, ‘孙八’, 5),
(7, ‘周九’, 5),
(8, ‘吴十’, 8),
(9, ‘郑十一’, 9),
(10, ‘王十二’, 10);

SELECT * FROM employee;

– 通过 join on 关联查询 id 为 5 的部门的所有的员工
SELECT * FROM department JOIN employee ON department.id = employee.department_id WHERE department.id = 5;

SELECT * FROM department LEFT JOIN employee ON department.id = employee.department_id;

– 所有员工都有部门,所以和 inner join 结果一样
select * from department RIGHT join employee on department.id = employee.department_id;

– 多对多关系,文章和标签
– 文章一个表、标签一个表、这两个表都不保存外键,然后添加一个中间表来保存双方的外键

– 创建 article 表
CREATE TABLE article (
id INT NOT NULL AUTO_INCREMENT,
title VARCHAR(50) NOT NULL,
content TEXT NOT NULL,
PRIMARY KEY (id)
);

INSERT INTO article (title, content)
VALUES
(‘文章1’, ‘这是文章1的内容。’),
(‘文章2’, ‘这是文章2的内容。’),
(‘文章3’, ‘这是文章3的内容。’),
(‘文章4’, ‘这是文章4的内容。’),
(‘文章5’, ‘这是文章5的内容。’);

– 创建 tag 表
CREATE TABLE tag (
id INT NOT NULL AUTO_INCREMENT,
name VARCHAR(50) NOT NULL,
PRIMARY KEY (id)
);

INSERT INTO tag (name)
VALUES
(‘标签1’),
(‘标签2’),
(‘标签3’),
(‘标签4’),
(‘标签5’);

– 创建中间表 article_tag
CREATE TABLE article_tag (
article_id INT NOT NULL,
tag_id INT NOT NULL,
PRIMARY KEY (article_id,tag_id),
INDEX tag_id_idx (tag_id),
CONSTRAINT article_id
FOREIGN KEY (article_id)
REFERENCES article (id)
ON DELETE CASCADE
on UPDATE CASCADE,
CONSTRAINT tag_id
FOREIGN KEY (tag_id)
REFERENCES tag (id)
ON DELETE CASCADE
on UPDATE CASCADE
);

– 插入数据
INSERT INTO article_tag (article_id, tag_id)
VALUES
(1, 1),
(1, 2),
(1, 3),
(2, 2),
(2, 3),
(2, 4),
(3, 3),
(3, 4),
(3, 5),
(4, 4),
(4, 5),
(4, 1),
(5, 5),
(5, 1),
(5, 2);

– as 作为表面或者列名可以 省略, 别名含特殊字符时建议加引号
SELECT * FROM article a JOIN article_tag at ON a.id = at.article_id
JOIN tag t ON t.id = at.tag_id
WHERE a.id = 1;

– 多对多 关联查询
SELECT
t.NAME AS 标签名,
a.title AS 文章标题
FROM
article a
JOIN article_tag at ON a.id = at.article_id
JOIN tag t ON t.id = at.tag_id
WHERE
a.id = 1;

一对一、join 查询、级联方式

– 新建 user 表
CREATE TABLE hello-mysql.user (
id INT NOT NULL AUTO_INCREMENT COMMENT ‘id’,
name VARCHAR(45) NOT NULL COMMENT ‘名字’,
PRIMARY KEY (id)
);

– 新建 id_card 表
CREATE TABLE id_card (
id int NOT NULL AUTO_INCREMENT COMMENT ‘id’,
card_name varchar(45) NOT NULL COMMENT ‘身份证号’,
user_id int DEFAULT NULL COMMENT ‘用户 id’,
PRIMARY KEY (id),
INDEX card_id_idx (user_id),
CONSTRAINT user_id FOREIGN KEY (user_id) REFERENCES user (id)
) CHARSET=utf8mb4;

– 插入数据
INSERT INTO user (name)
VALUES
(‘张三’),
(‘李四’),
(‘王五’),
(‘赵六’),
(‘孙七’),
(‘周八’),
(‘吴九’),
(‘郑十’),
(‘钱十一’),
(‘陈十二’);

– 查询
SELECT * FROM user;

– 插入 id_card 表数据
INSERT INTO id_card (card_name, user_id)
VALUES
(‘110101199001011234’,1),
(‘310101199002022345’,2),
(‘440101199003033456’,3),
(‘440301199004044567’,4),
(‘510101199005055678’,5),
(‘330101199006066789’,6),
(‘320101199007077890’,7),
(‘500101199008088901’,8),
(‘420101199009099012’,9),
(‘610101199010101023’,10);

SELECT * FROM id_card;

– 一对一查询 join on 其实默认是 inner join on,只返回2个表中能关联上的数据
SELECT * FROM user JOIN id_card ON user.id = id_card.user_id;

SELECT user.id,name,id_card.id as card_id, card_name FROM user INNER JOIN id_card ON user.id = id_card.user_id;

– left join 是额外返回左表中没有关联上的数据
– right join 是额外返回右表中没有关联上的数据
– 在 FROM 后的左表, JOIN 后表是右表。
SELECT user.id, name, id_card.id as card_id,card_name FROM user RIGHT JOIN id_card ON user.id = id_card.user_id;

SELECT user.id, name, id_card.id as card_id,card_name FROM user LEFT JOIN id_card ON user.id = id_card.user_id;

– MySQL 中的外键约束定义了当主表(被引用表)发生更新或删除操作时,从表(引用表)应该如何响应
– CASCADE: 主表主键更新,从表关联记录的外键跟着更新,主表记录删除,从表关联记录删除
– SET NULL:主表主键更新或者主表记录删除,从表关联记录的外键设置为 null
– RESTRICT:只有没有从表的关联记录时,才允许删除主表记录或者更新主表记录的主键 id
– NO ACTION: 同 RESTRICT,只是 sql 标准里分了 4 种,但 mysql 里 NO ACTION 等同于 RESTRICT。

SQL 查询语法以及函数

SELECT * FROM student;

SELECT name,score FROM student;

SELECT name as 姓名,score as 分数 FROM student;

SELECT name as 姓名,age as 年龄 FROM student WHERE age >= 18;

SELECT name as 姓名, class as 班级, score as 分数 FROM student WHERE gender=’男’ and score >= 90;

– 查询名字以“王”开头的学生
SELECT * from student WHERE name like ‘王%’;

SELECT * from student WHERE class IN (‘一班’,’二班’);

SELECT * from student WHERE class NOT IN (‘一班’,’二班’);

SELECT * from student WHERE age BETWEEN 18 AND 20;

SELECT * FROM student LIMIT 0,5;

SELECT * FROM student LIMIT 5;

– 跳过5条,限制展示 5 条
SELECT * FROM student LIMIT 5,5;

– order by 指定根据 score 升序排列,如果 score 相同再根据 age 降序排列
SELECT name,score,age FROM student ORDER BY score ASC, age DESC;

– 统计每个班级的平均成绩
SELECT class as 班级, AVG(score) AS 平均成绩 FROM student GROUP BY class ORDER BY 平均成绩 DESC;

– 查询每个班级的学生人数
SELECT class, COUNT(*) as count from student GROUP BY class;

SELECT class, AVG(score) AS avg_score FROM student GROUP BY class HAVING avg_score > 90;

– DISTINC 去重
SELECT DISTINCT class from student;

– 聚合函数:AVG,COUNT,SUM,MIN, MAX
SELECT class AS 班级, AVG(score) AS 平均成绩,COUNT(*) AS 人数,SUM(score) AS 总成绩,MIN(score) AS 最低分,MAX(score) AS 最高分 FROM student GROUP BY class;

– 字符串处理函数: CONCAT,SUBSTR,LENGTH,UPPER,LOWER
– substr 第二个参数表示开始的下标(mysql 下标从 1 开始)
SELECT CONCAT(‘xx’,name,’yy’), SUBSTR(name,2,3),LENGTH(name),UPPER(‘aa’),LOWER(‘TT’) FROM student;

– 数值函数:ROUND 四舍五入、CEIL 向上取整、FLOOR 向下取整、ABS 绝对值、MOD 取模
SELECT ROUND(1.234567,2),CEIL(1.234567),FLOOR(1.234567),ABS(-1.233),MOD(5,2);

– 日期函数:DATE,TIME,YEAR,MONTH,DAY
SELECT YEAR(‘2006-01-02 15:04:05’),MONTH(‘2006-01-02 15:04:05’),DAY(‘2006-01-02 15:04:05’),DATE(‘2006-01-02 15:04:05’),TIME(‘2006-01-02 15:04:05’);

– 条件函数:根据条件是否成立返回不同的值。比如 if case
SELECT name, IF(score >= 60,’及格’,’不及格’) from student;

SELECT name,
score,
CASE
WHEN score >= 90 THEN
‘优秀’
WHEN score >= 60 THEN
‘及格’
ELSE
‘差’
END AS ‘档次’
FROM
student;

– 系统函数:用于获取系统信息,比如 VERIOSN,DATABASE,USER
SELECT VERSION(),DATABASE(),USER();

– 其他函数:NULLIF,COALESCE,GREATEST,LEAST
– NULLIF,如果相等返回第一个值,不相等返回null
SELECT NULLIF(1,1),NULLIF(1,2);

– COALESCE,返回第一个非 null 的值
SELECT COALESCE(null,1), COALESCE(null,null,2);

– GREATEST、LEAST:返回几个值中最大 最小的。
SELECT GREATEST(1,2,3),LEAST(1,2,3,4);

– 类型转换函数:转换类型为另一种,比如 CAST、CONVERT、DATE_FORMAT、STR_TO_DAT

– 将 ‘123’ 转化为 整型3
SELECT GREATEST(1,CONVERT(‘123’,signed),3);

SELECT GREATEST(1,CAST(‘123’ AS signed),3);

SELECT DATE_FORMAT(‘2022-01-01’,’%y年%m月%d日’);

SELECT STR_TO_DATE(‘2025-06-06’,’%Y-%m-%d’);

介绍

因为最近项目中遇到合并表格行的需求,所以用这篇文章记录下实现思路。

看下实现效果:

合并单元格

实现逻辑

element plus 实现 table 的合并行或者列需要实现 span-method 方法。 官网地址element-plus-table

案例中的代码是实现行的合并,假设接口返回的数据是嵌套的模式,父级就是需要合并的列表,内层的数据长度就是需要合并的行数。当然如果嵌套的层次比较深,就需要遍历内层的数量统计最外层需要合并的数量。

案例代码是三层的结构,使用简单的嵌套循环,一般项目中也够用了,更多层级的数据结构也是一样的道理,多加n 次循环,此时建议使用递归来实现了。

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
<script setup>
import { ref } from "vue";

// 模拟数据
const testData = [
{
fromMajorCity: 'BFN',
data: [
{
level: 1,
toMajorCity: 'BFN,CPT',
data: [
{
weightGt: 0,
weightLte: 15,
basic_weight: 5,
starting_price: 10,
thereafter_price: 4,
per_nkg: 1
},
{
weightGt: 15,
weightLte: null,
basic_weight: 5,
starting_price: 9.5,
thereafter_price: 4,
per_nkg: 1
}
]
},
{
level: 2,
toMajorCity: 'DUR,ELS,KIM,WLK.Other',
data: [
{
weightGt: 0,
weightLte: null,
basic_weight: 5,
starting_price: 10,
thereafter_price: 5,
per_nkg: 1
}
]
}
]
}
]

// 扁平化数据并且计算合并行的 rowspan
const flattenDataWithRowspan = (nestData) => {
const flatData = []

nestData.forEach((row) => {
let firstRowSpan = 0 // 最外层合并行数
row.data?.forEach((item) => {
const secondRowSpan = item.data.length // 第二层合并行数
item.secondRowSpan = secondRowSpan
firstRowSpan += secondRowSpan
})
row.firstRowSpan = firstRowSpan
})

nestData.forEach((row) => {
row.data?.forEach((item, i) => {
item.data?.forEach((val, index) => {
flatData.push({
fromMajorCity: row.fromMajorCity,
level: item.level,
toMajorCity: item.toMajorCity,
...val,
firstRowSpan: (index === 0 && i == 0) ? row.firstRowSpan : 0,
secondRowSpan: index === 0 ? item.secondRowSpan : 0,
})
})
})
})

return flatData
}

// 合并行的方法
function spanMethod({ row, columnIndex, column }) {
if (column.label === 'From Major City' || column.label === 'No' || column.label === 'Action') {
// 如果有值需要合并
if (row.firstRowSpan > 0) {
return {
rowspan: row.firstRowSpan,
colspan: 1,
}
} else {
return {
rowspan: 0,
colspan: 0,
}
}
}
if (column.label === 'Level' || column.label === 'To Major City') {
// 如果有值需要合并
if (row.secondRowSpan > 0) {
return {
rowspan: row.secondRowSpan,
colspan: 1,
}
} else {
return {
rowspan: 0,
colspan: 0,
}
}
}
}

const tableData = ref(flattenDataWithRowspan(testData))
</script>

<template>
<div>
<el-table :data="tableData" :span-method="spanMethod" border>
<el-table-column label="No" type="index" width="55" align="center"></el-table-column>
<el-table-column label="From Major City" prop="fromMajorCity" width="200" align="center"></el-table-column>
<el-table-column label="Level" prop="level" width="100" align="center"></el-table-column>
<el-table-column label="To Major City" prop="toMajorCity" width="200" align="center"></el-table-column>
<el-table-column label="Weight (>)" prop="weightGt" width="150" align="center"></el-table-column>
<el-table-column label="Weight (<=)" prop="weightLte" width="150" align="center"></el-table-column>
<el-table-column label="Basic Weight (kg)" prop="basic_weight" width="150" align="center"></el-table-column>
<el-table-column label="Starting Price" prop="starting_price" width="150" align="center"></el-table-column>
<el-table-column label="Thereafter Price" prop="thereafter_price" width="150" align="center"></el-table-column>
<el-table-column label="per N Kg" prop="perNkg" width="150" align="center"></el-table-column>
<el-table-column label="Action" width="200" align="center" fixed="right">
<template #default>
<el-button type="primary">Edit</el-button>
<el-button type="primary">Copy</el-button>
</template>
</el-table-column>
</el-table>
</div>
</template>

<style scoped></style>

react 状态管理库 Jotai

Jotai 是一个轻量级、原子化的状态管理库,专为 React 应用设计。与其他状态管理库(如 Redux、MobX 或 Recoil)相比,Jotai 提供了一种更简洁的方式来管理和共享状态,同时保持良好的性能和灵活性。

安装 Jotai

1
pnpm add jotai

基本用法

定义原子状态

1
2
// 定义一个原子
const counterAtom = atom(0)

使用原子状态

1
2
3
4
5
6
7
8
9
10
11
12
13
export default function Counter() {
// 使用原子
const [count, setCount] = useAtom(counterAtom)

return (
<div>
<h1>
{count}
<button onClick={() => setCount((c) => c + 1)}>add</button>
</h1>
</div>
)
}

派生原子

1
const doubleCounterAtom = atom((get) => get(counterAtom) * 2)

异步行为

1
2
3
4
5
6
7
8
9
10
const numAtom = atom(1)
const asyncReadCountAtom = atom(
// 异步读取 返回 promise
async (get) => {
await new Promise((reslove) => {
setTimeout(reslove, 1000)
});
return get(numAtom)
},
)
1
2
3
4
// 异步写
const asyncWriteCountAtom = atom(null, async (get, set) => {
set(numAtom, get(numAtom) + 1);
})

持久化存储

1
2
3
4
5
6
7
8
9
const theme = atomWithStorage('dark', false)

function ComponentUsingAsyncAtoms() {
const [asyncCount] = useAtom(asyncReadCountAtom)

return (
<p>async num: {asyncCount}</p>
)
}

react hooks 使用教程

本教程使用简单代码介绍了 react 的 useState,useEffect,useContext,useReducer, useRef,useMemo,useCallback 以及自定义hooks 的用法。

useState

向组件添加一个 状态变量,进行状态管理。

1
const [state, setState] = useState(initialState)
  • initialState: 你希望 state 初始化的值。它可以是任何类型的值,但对于函数有特殊的行为。在初始渲染后,此参数将被忽略。
    如果传递函数作为 initialState,则它将被视为 初始化函数。它应该是纯函数,不应该接受任何参数,并且应该返回一个任何类型的值。当初始化组件时,React 将调用你的初始化函数,并将其返回值存储为初始状态。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function App() {
const [count, setCount] = useState(0)

const handlePlus = () => {
setCount(count + 1)
}

// setCount 传入函数,第一个参数为 useState 的旧值
const handleMinus = () => {
setCount(count => count - 1)
}

return (
<>
<div>{count}</div>
<button onClick={handlePlus}>+</button>
<button onClick={handleMinus}>-</button>
</>

)
}

useEffect

useEffect(setup, dependencies?)

dependencies 传入空数组只会在组件更新时触发一次回调函数, setup 函数 在每次依赖项变更重新渲染后, setup 函数可以选择性返回一个
清理(cleanup) 函数,React 将首先使用旧值运行 cleanup 函数

1
2
3
useEffect(() => {
console.log('useEffect')
}, []);

数组中传入具体的状态,当状态变化时触发回调函数

1
2
3
useEffect(() => {
console.log('useEffect')
}, [count]);

setup 函数返回的函数称为 cleanup 清理函数

1
2
3
4
5
6
7
useEffect(() => {
console.log('useEffect')

return () => {
console.log('clear')
}
}, [count]);

useContext

通过父组件给后代组件传递数据时,如果嵌套的层级太深,可以通过在组件的最顶级调用 useContext 来读取和订阅 context。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 创建 context
const ThemeContext = createContext(null)

function App() {
return (
// 顶层组件传递 context 数据
<ThemeContext.Provider value={{color: 'green'}}>
<ChildComponent></ChildComponent>
</ThemeContext.Provider>
)
}

function ChildComponent() {
// 内部组件获取 context 数据
const theme = useContext(ThemeContext)
console.log(theme) // {color: 'green'}

return (
<div>Child Component</div>
)
}

以上代码演示了如何通过在顶层组件设置 context 并且传递数据,内部的组件如何获取数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 创建 context
const ThemeContext = createContext(null)

function App() {
const [theme, setTheme] = useState('green')

const switchTheme = () => {
setTheme(theme === 'green' ? 'pink' : 'green')
}

return (
// 传递 context 数据
<ThemeContext.Provider value={{color: theme}}>
<div>
<button onClick={ switchTheme }>switch theme</button>
</div>
<ChildComponent></ChildComponent>
</ThemeContext.Provider>
)
}

function ChildComponent() {
const theme = useContext(ThemeContext)
console.log(theme)

return (
<div style={theme}>Child Component</div>
)
}

通过结合 useState 可以更新 context 并且传递给后代,这里演示了如何 通过 context 全局更新主题颜色。

useReducer

对于复杂的状态设置和管理。

1
const [state, dispatch] = useReducer(reducer, initialArg, init?)
  • reducer: 用于更新 state 的纯函数。参数为 state 和 action,返回值是更新后的 state。state 与 action 可以是任意合法值。
  • initialArg: 用于初始化 state 的任意值。初始值的计算逻辑取决于接下来的 init 参数。
  • 可选参数 init: 用于计算初始值的函数。如果存在,使用 init(initialArg) 的执行结果作为初始值,否则使用 initialArg

通过不同的 reducer type 执行不同的操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
const initialState = {
count: 0,
age: 18,
// 一些其他属性
}

const countReducer = (state, action) => {
switch (action.type) {
case 'increment':
return {
...state,
count: state.count + 1,
}
case 'decrement':
return {
...state,
count: state.count - 1,
}
default:
return state;
}
}

export function CountProvider() {

const [state, dispatch] = useReducer(countReducer, initialState)

const increment = () => {
dispatch({type: 'increment'})
}

const decrement = () => {
dispatch({type: 'decrement'})
}

return (
<div>
<h1>{state.count}</h1>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</div>
)
}

useRef

用于在重新渲染中,去存储易变数据。数据存储在 current 属性上,常用于 DOM 访问。(修改 ref.current 不会触发重新渲染)

1
const ref = useRef(initialValue)
  • initialValue: ref 对象的 current 属性的初始值。可以是任意类型的值。这个参数在首次渲染后被忽略。

返回值: useRef 返回一个只有一个属性的对象:

  • current:初始值为传递的 initialValue。之后可以将其设置为其他值。如果将 ref 对象作为一个 JSX 节点的 ref 属性传递给 React,React 将为它设置 current 属性。

在后续的渲染中,useRef 将返回同一个对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// useRef 操作 dom 案例
export function RefDemo() {
const inputRef = useRef(null)

const handleClick = () => {
inputRef.current.focus()
}

return (
<div>
<input ref={inputRef}/>
<button onClick={handleClick}>
聚焦输入框
</button>
</div>
)
}

useMemo

在每次重新渲染的时候能够缓存计算的结果

1
const cachedValue = useMemo(calculateValue, dependencies)
  • calculateValue:要缓存计算值的函数。它应该是一个没有任何参数的纯函数,并且可以返回任意类型。React 将会在首次渲染时调用该函数;在之后的渲染中,如果 dependencies 没有发生变化,React 将直接返回相同值。否则,将会再次调用 calculateValue 并返回最新结果,然后缓存该结果以便下次重复使用。
  • dependencies:所有在 calculateValue 函数中使用的响应式变量组成的数组。响应式变量包括 props、state 和所有你直接在组件中定义的变量和函数。如果你在代码检查工具中 配置了 React,它将会确保每一个响应式数据都被正确地定义为依赖项。依赖项数组的长度必须是固定的并且必须写成 [dep1, dep2, dep3] 这种形式。React 使用 Object.is 将每个依赖项与其之前的值进行比较。
1
2
3
4
5
6
7
// 省略部分代码。。。
const [count, setCount] = useState(0)

const doubleCount = useMemo(() => {
console.log('in use Memo')
return count * 2
}, [count])

useMemo 在多次重新渲染中缓存了 calculation 函数计算的结果直到依赖项的值发生变化。

另外你可以使用 memo 来优化你的代码,它允许你的组件在 props 没有改变的情况下跳过重新渲染。

1
const MemoizedComponent = memo(SomeComponent, arePropsEqual?)
1
2
3
4
5
const Greeting = memo(function Greeting({ name }) {
return <h1>Hello, {name}!</h1>;
});

export default Greeting;

useCallback

允许你在多次渲染中缓存函数。

1
const cachedFn = useCallback(fn, dependencies)
  • fn:想要缓存的函数。此函数可以接受任何参数并且返回任何值。在初次渲染时,React 将把函数返回给你(而不是调用它!)。当进行下一次渲染时,如果 dependencies 相比于上一次渲染时没有改变,那么 React 将会返回相同的函数。否则,React 将返回在最新一次渲染中传入的函数,并且将其缓存以便之后使用。React 不会调用此函数,而是返回此函数。你可以自己决定何时调用以及是否调用。

  • dependencies:有关是否更新 fn 的所有响应式值的一个列表。响应式值包括 props、state,和所有在你组件内部直接声明的变量和函数。如果你的代码检查工具 配置了 React,那么它将校验每一个正确指定为依赖的响应式值。依赖列表必须具有确切数量的项,并且必须像 [dep1, dep2, dep3] 这样编写。React 使用 Object.is 比较每一个依赖和它的之前的值。(简言之就是依赖项列表中的变量需要被想要缓存的函数内部使用)

  • 返回值:
    在初次渲染时,useCallback 返回你已经传入的 fn 函数
    在之后的渲染中, 如果依赖没有改变,useCallback 返回上一次渲染中缓存的 fn 函数;否则返回这一次渲染传入的 fn。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
export function UseCallbackDemo() {
const [state, setState] = useState('on')
const [count, setCount] = useState(0)

const handleAdd = () => {
setCount((count) => count + 1)
}

const cacheFn = useCallback(() => {
console.log(state)
}, [state]);

const handleChange = () => {
setState(state === 'on' ? 'off' : 'on')
}

return (
<div>
<button onClick={handleAdd}>add</button>
<h3>{count}</h3>
<button onClick={handleChange}>change</button>
<Child handleClick={cacheFn}></Child>
</div>
)
}

// eslint-disable-next-line react/prop-types
function Child({handleClick}) {
return (
<div>
<button onClick={() => handleClick()}>click</button>
</div>
)
}

点击 ‘add’ 会触发父组件的重新渲染,但点击子组件的 Child 的 ‘Click’ 发现打印的内容没有变化。点击 ‘Change’ 才会改变子组件的 handleClick 的打印结果。因为 useCallback 的依赖项里面包含 state 状态。

自定义 hook

自定义一个 useLocalStorage,该 Hook 用于将状态与 localStorage 同步,实现数据的持久化存储

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
export const useLocalStorage = (key, initialValue) => {
const [stateValue, setStateValue] = useState(() => {
try {
const item = localStorage.getItem(key);
// 如果存在则解析,否则判断 initialValue 是否为函数
return item ? JSON.parse(item) : (typeof initialValue === 'function' ? initialValue() : initialValue);
} catch (error) {
console.log(error)
return (typeof initialValue === 'function' ? initialValue() : initialValue);
}
})

const setStorage = (value) => {
let valueToStore;
if (typeof value === 'function') {
valueToStore = value(stateValue)
} else {
valueToStore = value
}
setStateValue(valueToStore)
localStorage.setItem(key, JSON.stringify(valueToStore))
}

return [stateValue, setStorage]
}
1
2
3
4
5
6
const useToggler = initialState => {
const [value, setValue] = React.useState(initialState);
const toggleValue = React.useCallback(() => setValue(prev => !prev), []);

return [value, toggleValue];
};