目前StarRocks根据摄入数据和实际存储数据之间的映射关系,分为明细模型(Duplicate key)、聚合模型(Aggregate key)、更新模型(Unique key)和主键模型(Primary key)。

四种模型分别对应不同业务场景

明细模型

StarRocks建表默认采用明细模型,排序列使用稀疏索引,可以快速过滤数据。明细模型用于保存所有历史数据,并且用户可以考虑将过滤条件中频繁使用的维度列作为排序键,比如用户经常需要查看某一时间,可以将事件时间和事件类型作为排序键。

  • 建表,在建表时指定模型和排序键
    1
    2
    3
    4
    5
    6
    7
    8
    9
    CREATE TABLE IF NOT EXISTS detail (
    event_time DATETIME NOT NULL COMMENT "datetime of event",
    event_type INT NOT NULL COMMENT "type of event",
    user_id INT COMMENT "id of user",
    device_code INT COMMENT "device of ",
    channel INT COMMENT ""
    )
    DUPLICATE KEY ( event_time, event_type )
    DISTRIBUTED BY HASH ( user_id );
  • 插入测试数据
    1
    2
    3
    4
    5
    INSERT INTO detail VALUES('2021-11-18 12:00:00.00',1,1001,1,1);
    INSERT INTO detail VALUES('2021-11-17 12:00:00.00',2,1001,1,1);
    INSERT INTO detail VALUES('2021-11-16 12:00:00.00',3,1001,1,1);
    INSERT INTO detail VALUES('2021-11-15 12:00:00.00',1,1001,1,1);
    INSERT INTO detail VALUES('2021-11-14 12:00:00.00',2,1001,1,1);
  • 查询
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    SELECT *FROM detail;

    +---------------------+------------+---------+-------------+---------+
    | event_time | event_type | user_id | device_code | channel |
    +---------------------+------------+---------+-------------+---------+
    | 2021-11-18 12:00:00 | 1 | 1001 | 1 | 1 |
    | 2021-11-17 12:00:00 | 2 | 1001 | 1 | 1 |
    | 2021-11-16 12:00:00 | 3 | 1001 | 1 | 1 |
    | 2021-11-15 12:00:00 | 1 | 1001 | 1 | 1 |
    | 2021-11-14 12:00:00 | 2 | 1001 | 1 | 1 |
    +---------------------+------------+---------+-------------+---------+

聚合模型

在数据分析中,很多场景需要基于明细数据进行统计和汇总,这个时候就可以使用聚合模型了。比如:统计app访问流量、用户访问时长、用户访问次数、展示总量、消费统计等等场景。
适合聚合模型来分析的业务场景有以下特点:

  • 业务方进行查询为汇总类查询,比如sum、count、max
  • 不需要查看原始明细数据
  • 老数据不会被频繁修改,只会追加和新增

  • 建表,指定聚合模型
    1
    2
    3
    4
    5
    6
    7
    8
    9
    CREATE TABLE IF NOT EXISTS aggregate_tbl (
    site_id LARGEINT NOT NULL COMMENT "id of site",
    date DATE NOT NULL COMMENT "time of event",
    city_code VARCHAR ( 20 ) COMMENT "city_code of user",
    pv BIGINT SUM DEFAULT "0" COMMENT "total page views",
    mt BIGINT MAX
    )
    AGGREGATE KEY(site_id, date, city_code)
    DISTRIBUTED BY HASH (site_id);
  • 插入测试数据
    1
    2
    3
    4
    5
    6
    7
    8
    INSERT INTO aggregate_tbl VALUES(1001,'2021-11-18 12:00:00.00',100,1,5);
    INSERT INTO aggregate_tbl VALUES(1001,'2021-11-18 12:00:00.00',100,1,10);
    INSERT INTO aggregate_tbl VALUES(1001,'2021-11-18 12:00:00.00',100,1,15);
    INSERT INTO aggregate_tbl VALUES(1001,'2021-11-18 12:00:00.00',100,1,100);
    INSERT INTO aggregate_tbl VALUES(1001,'2021-11-18 12:00:00.00',100,1,20);
    INSERT INTO aggregate_tbl VALUES(1002,'2021-11-18 12:00:00.00',100,1,5);
    INSERT INTO aggregate_tbl VALUES(1002,'2021-11-18 12:00:00.00',100,3,25);
    INSERT INTO aggregate_tbl VALUES(1002,'2021-11-18 12:00:00.00',100,1,15);
  • 查询测试数据,可以看到pv是sum累计的值,mt是明细中最大的值。如果只需要查看聚合后的指标,那么使用此种模型将会大大减少存储的数据量。
    1
    2
    3
    4
    5
    6
    7
    8
    select *from aggregate_tbl;

    +---------+------------+-----------+------+------+
    | site_id | date | city_code | pv | mt |
    +---------+------------+-----------+------+------+
    | 1001 | 2021-11-18 | 100 | 5 | 100 |
    | 1002 | 2021-11-18 | 100 | 5 | 25 |
    +---------+------------+-----------+------+------+

更新模型

有些分析场景之下,数据需要进行更新比如拉链表,StarRocks则采用更新模型来满足这种需求,比如电商场景中,订单的状态经常会发生变化,每天的订单更新量可突破上亿。这种业务场景下,如果只靠明细模型下通过delete+insert的方式,是无法满足频繁更新需求的,因此,用户需要使用更新模型(唯一键来判断唯一性)来满足分析需求。但是如果用户需要更加实时/频繁的更新操作,建议使用主键模型。
使用更新模型的场景特点:

  • 已经写入的数据有大量的更新需求
  • 需要进行实时数据分析

  • 建表,指定更新模型
    1
    2
    3
    4
    5
    6
    7
    8
    CREATE TABLE IF NOT EXISTS update_detail (
    create_time DATE NOT NULL COMMENT "create time of an order",
    order_id BIGINT NOT NULL COMMENT "id of an order",
    order_state INT COMMENT "state of an order",
    total_price BIGINT COMMENT "price of an order"
    )
    UNIQUE KEY ( create_time, order_id )
    DISTRIBUTED BY HASH ( order_id );
  • 插入测试数据,注意:现在是指定create_time和order_id为唯一键,那么相同日期相同订单的数据会进行覆盖操作
    1
    2
    3
    4
    5
    INSERT INTO update_detail VALUES('2011-11-18',1001,1,1000);
    INSERT INTO update_detail VALUES('2011-11-18',1001,2,2000);
    INSERT INTO update_detail VALUES('2011-11-17',1001,2,500);
    INSERT INTO update_detail VALUES('2011-11-18',1002,3,3000);
    INSERT INTO update_detail VALUES('2011-11-18',1002,4,4500);
  • 查询结果,可以看到如果日期和订单相同则会进行覆盖操作。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    select *from update_detail;

    +-------------+----------+-------------+-------------+
    | create_time | order_id | order_state | total_price |
    +-------------+----------+-------------+-------------+
    | 2011-11-17 | 1001 | 2 | 500 |
    | 2011-11-18 | 1001 | 2 | 2000 |
    | 2011-11-18 | 1002 | 4 | 4500 |
    +-------------+----------+-------------+-------------+

主键模型

相比较更新模型,主键模型可以更好地支持实时/频繁更新的功能。虽然更新模型也可以实现实时对数据的更新,但是更新模型采用Merge on Read读时合并策略会大大限制查询功能,在主键模型更好地解决了行级的更新操作。配合Flink-connector-starrocks可以完成Mysql CDC实时同步的方案。
需要注意的是:由于存储引擎会为主键建立索引,导入数据时会把索引加载到内存中,所以主键模型对内存的要求更高,所以不适合主键模型的场景还是比较多的。
目前比较适合使用主键模型的场景有这两种:

  • 数据冷热特征,比如最近几天的数据才需要修改,老的冷数据很少需要修改,比如订单数据,老的订单完成后就不在更新,并且分区是按天进行分区的,那么在导入数据时历史分区的数据的主键就不会被加载,也就不会占用内存了,内存中仅会加载近几天的索引。
  • 大宽表(数百列数千列),主键只占整个数据的很小一部分,内存开销比较低。比如用户状态/画像表,虽然列非常多,但总的用户数量不大(千万-亿级别),主键索引内存占用相对可控。

原理:由于更新模型采用Merge策略,使得谓词无法下推和索引无法使用,严重影响查询性能。所以主键模型通过主键约束,保证同一个主键仅存一条数据的记录,这样就规避了Merge操作。StarRocks收到对某记录的更新操作时,会通过主键索引找到该条数据的位置,并对其标记为删除,再插入一条数据,相当于把update改写为delete+insert

  • 建表,指定主键模型

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    CREATE TABLE IF NOT EXISTS users (
    user_id BIGINT NOT NULL,
    name STRING NOT NULL,
    email STRING NULL,
    address STRING NULL,
    age TINYINT NULL,
    sex TINYINT NULL
    )
    PRIMARY KEY ( user_id )
    DISTRIBUTED BY HASH (user_id);
  • 插入测试数据,和更新模型类似,当user_id相同发送冲突时会进行覆盖

    1
    2
    3
    4
    INSERT INTO users VALUES(1001,'张三','zhang@qq.com','address1',17,'0');
    INSERT INTO users VALUES(1001,'李四','li@qq.com','address2',18,'1');
    INSERT INTO users VALUES(1002,'alice','alice@qq.com','address3',18,'0');
    INSERT INTO users VALUES(1002,'peter','peter@qq.com','address4',18,'1');
  • 查询数据

    1
    2
    3
    4
    5
    6
    7
    select *from users;
    +---------+--------+--------------+----------+------+------+
    | user_id | name | email | address | age | sex |
    +---------+--------+--------------+----------+------+------+
    | 1001 | 李四 | li@qq.com | address2 | 18 | 1 |
    | 1002 | peter | peter@qq.com | address4 | 18 | 1 |
    +---------+--------+--------------+----------+------+------+

其他

StarRocks入门教程

存算分离是真香

StarRocks 存算分离集群采用了存储计算分离架构,特别为云存储设计。在存算分离的模式下,StarRocks 将数据存储在对象存储(例如 AWS S3、GCS、OSS、Azure Blob 以及 MinIO)或 HDFS 中,而本地盘作为热数据缓存,用以加速查询。通过存储计算分离架构,您可以降低存储成本并且优化资源隔离。除此之外,集群的弹性扩展能力也得以加强。在查询命中缓存的情况下,存算分离集群的查询性能与存算一体集群性能一致。

在 v3.1 版本及更高版本中,StarRocks 存算分离集群由 FE 和 CN 组成。CN 取代了存算一体集群中的 BE。

相对存算一体架构,StarRocks 的存储计算分离架构提供以下优势:

  • 廉价且可无缝扩展的存储。
  • 弹性可扩展的计算能力。由于数据不存储在 CN 节点中,因此集群无需进行跨节点数据迁移或 Shuffle 即可完成扩缩容。
  • 热数据的本地磁盘缓存,用以提高查询性能。
  • 可选异步导入数据至对象存储,提高导入效率。

背景

本例背景以腾讯云COS作为存储卷,starrocks-3.2.2,1fe1cn进行部署

腾讯云服务器:172.16.0.4,4C8G,确保有jdk11环境

启动FE节点

  • 1、创建元数据存储路径

    1
    mkdir -p /opt/downloads/StarRocks-3.2.2/fe/meta

    在配置项 meta_dir 中指定元数据路径

    1
    meta_dir = /opt/downloads/StarRocks-3.2.2/fe/meta
  • 2、增加存算分离配置

    fe.conf 增加 run_mode,将原来的默认:shared_nothing 变更为 shared_data
    其他的不用变更,会有默认值如:
    cloud_native_meta_port 默认 6090,
    enable_load_volume_from_conf 默认 true
    cloud_native_storage_type 默认 S3
    我们按照官方示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    run_mode = shared_data
    cloud_native_meta_port = <meta_port>
    cloud_native_storage_type = S3

    # 如 testbucket/subpath
    aws_s3_path = <s3_path>
    # 例如:ap-beijing
    aws_s3_region = <region>
    # 例如:https://cos.ap-beijing.myqcloud.com
    aws_s3_endpoint = <endpoint_url>
    aws_s3_access_key = <access_key>
    aws_s3_secret_key = <secret_key>

    结合我们实际最终在fe.conf添加如下配置

    1
    2
    3
    4
    5
    6
    7
    8
    meta_dir = /opt/downloads/StarRocks-3.2.2/fe/meta

    run_mode = shared_data
    aws_s3_path = dev-files-1253767413/starrocks
    aws_s3_region = ap-guangzhou
    aws_s3_endpoint = https://cos.ap-guangzhou.myqcloud.com
    aws_s3_access_key = <access_key>
    aws_s3_secret_key = <secret_key>
  • 3、启动 FE 节点

    1
    ./fe/bin/start_fe.sh --daemon
  • 4、查看 FE 日志,检查 FE 节点是否启动成功。

    1
    cat fe/log/fe.log | grep thrift

    如果日志打印以下内容,则说明该 FE 节点启动成功:
    “2024-01-13 16:12:29,911 INFO (UNKNOWN x.x.x.x_9010_1660119137253(-1)|1) [FeServer.start():52] thrift server started with port 9020.”

启动 CN 服务

Compute Node(CN)是一种无状态的计算服务,本身不存储数据。您可以通过添加 CN 节点为查询提供额外的计算资源。您可以使用 BE 部署文件部署 CN 节点。

因为使用默认端口配置,所以我们无需修改任何CN配置,如果需要变更,可到be/conf/cn.conf进行变更端口

  • 1、启动 CN 节点
    1
    ./be/bin/start_cn.sh --daemon
  • 查看 CN 日志,检查 CN 节点是否启动成功
    1
    cat be/log/cn.INFO | grep heartbeat
    如果日志打印以下内容,则说明该 CN 节点启动成功:
    “I0313 15:03:45.820030 412450 thrift_server.cpp:375] heartbeat has started listening port on 9050”

搭建集群

我们下面通过 MySQL 客户端来连接 Starrocks FE,下载免安装的 MySQL 客户端

  • 1、通过 MySQL 客户端连接到 StarRocks。您需要使用初始用户 root 登录,密码默认为空。
    1
    2
    # mysql -h <fe_address> -P<query_port> -uroot
    mysql -uroot -P9030 -h127.0.0.1
  • 2、执行以下 SQL 查看 Leader FE 节点状态。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    mysql> SHOW PROC '/frontends'\G
    *************************** 1. row ***************************
    Name: 172.16.0.4_9010_1705155779443
    IP: 172.16.0.4
    EditLogPort: 9010
    HttpPort: 8030
    QueryPort: 9030
    RpcPort: 9020
    Role: LEADER
    ClusterId: 66234781
    Join: true
    Alive: true
    ReplayedJournalId: 1895
    LastHeartbeat: 2024-01-14 00:07:14
    IsHelper: true
    ErrMsg:
    StartTime: 2024-01-13 22:23:08
    Version: 3.2.2-269e832
    1 row in set (0.04 sec)
    • 如果字段 Alive 为 true,说明该 FE 节点正常启动并加入集群。
    • 如果字段 Role 为 FOLLOWER,说明该 FE 节点有资格被选为 Leader FE 节点。
    • 如果字段 Role 为 LEADER,说明该 FE 节点为 Leader FE 节点。
  • 3、添加 CN 节点至集群。
    1
    ALTER SYSTEM ADD COMPUTE NODE "172.16.0.4:9050"
  • 4、执行以下 SQL 查看 CN 节点状态。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    mysql> SHOW PROC '/compute_nodes'\G
    *************************** 1. row ***************************
    ComputeNodeId: 11083
    IP: 172.16.0.4
    HeartbeatPort: 9050
    BePort: 9060
    HttpPort: 8040
    BrpcPort: 8060
    LastStartTime: 2024-01-13 23:24:02
    LastHeartbeat: 2024-01-14 00:09:59
    Alive: true
    SystemDecommissioned: false
    ClusterDecommissioned: false
    ErrMsg:
    Version: 3.2.2-269e832
    CpuCores: 4
    NumRunningQueries: 0
    MemUsedPct: 1.61 %
    CpuUsedPct: 0.2 %
    HasStoragePath: true
    StarletPort: 9070
    WorkerId: 1
    1 row in set (0.00 sec)
    • 如果字段 Alive 为 true,说明该 CN 节点正常启动并加入集群。

其他

  • 停止 FE 节点。
    1
    ./fe/bin/stop_fe.sh --daemon
  • 停止 BE 节点。
    1
    ./be/bin/stop_be.sh --daemon
  • 停止 CN 节点。
    1
    ./be/bin/stop_cn.sh --daemon
  • 重置root密码
    1
    SET PASSWORD for root = PASSWORD('xxxxxx');

概述

相信使用过pv和pvc的肯定会想到很多问题,比如每次申请 pvc 都需要手动添加pv,这岂不是太不方便了。那我们如何实现类似于公有云或者私有云的共享存储模式呢?kubernetes 提供了 storageclass 的概念,接下来我们来一探究竟。

先上一张图大家就比较清楚了:
v354K4

环境

k8s集群环境

Node(宿主机上)都要安装nfs

1
2
[root@node-1 ~]# yum -y install nfs-utils
[root@node-2 ~]# yum -y install nfs-utils

nfs 环境

  • 搭建nfs服务端
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    yum -y install rpcbind nfs-utils

    systemctl start rpcbind
    systemctl start nfs
    systemctl enable rpcbind
    systemctl enable nfs

    mkdir /home/nfsfile
    chmod -R 777 /home/nfsfile
    cd /home/nfsfile
    echo "This is a test file" > /nfsfile/test.txt
    vi /etc/exports
    1
    /home/nfsfile  *(rw,sync,root_squash,insecure)
    这行代码的意思是把共享目录 /home/nfsfile 共享给 * 这个客户端ip,后面括号里的内容是权限参数,其中:
    • rw 表示设置目录可读写。
    • sync 表示数据会同步写入到内存和硬盘中,相反 rsync 表示数据会先暂存于内存中,而非直接写入到硬盘中。
    • no_root_squash NFS客户端连接服务端时如果使用的是root的话,那么对服务端分享的目录来说,也拥有root权限。
    • no_all_squash 不论NFS客户端连接服务端时使用什么用户,对服务端分享的目录来说都不会拥有匿名用户权限。
      showmount -e localhost
      1
      2
      Export list for localhost:
      /home/nfsfile *
  • 客户端验证nfs
    我们在客户端执行以下命令:
    showmount -e 10.8.111.153
    1
    2
    Exports list on 10.8.111.153:
    /home/nfsfile *
    客户端开始挂载共享目录:
    1
    2
    mkdir nfsfile  # 客户端新建挂载点
    mount -t nfs 10.8.111.153:/home/nfsfile /root/nfsfile  # 挂载服务端共享目录到新创建的挂载点
    客户端验证是否挂载成功:
    1
    2
    cd /root/nfsfile  # 进入该目录后,将会看到之前在服务端创建的 test.txt 文件
    cat test.txt  # 打开后,发现文件内容与服务端文件内容的一致。说明本次 nfs 共享文件系统搭建成功!
    最后,如果需要永久挂载该共享目录(即实现开机自动挂载),则可以通过如下方式实现:
    1
    echo "mount -t nfs 10.8.111.153:/home/nfsfile /root/nfsfile" >> /etc/rc.d/rc.local  # 将挂载命令写入 rc.local

直接pod挂载nfs

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
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
labels:
name: nginx
spec:
replicas: 1
selector:
matchLabels:
name: nginx
template:
metadata:
labels:
name: nginx
spec:
containers:
- name: nginx
image: nginx:latest
ports:
- containerPort: 80
volumeMounts:
- name: wwwroot
mountPath: /usr/share/nginx/html
volumes:
- name: wwwroot
nfs:
server: 10.8.111.153
path: "/home/nfsfile/www"

使用storageClass、pv、pvc

rbac.yaml

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
apiVersion: v1
kind: ServiceAccount
metadata:
name: nfs-client-provisioner
# replace with namespace where provisioner is deployed
namespace: storages
---
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: nfs-client-provisioner-runner
rules:
- apiGroups: [""]
resources: ["persistentvolumes"]
verbs: ["get", "list", "watch", "create", "delete"]
- apiGroups: [""]
resources: ["persistentvolumeclaims"]
verbs: ["get", "list", "watch", "update"]
- apiGroups: ["storage.k8s.io"]
resources: ["storageclasses"]
verbs: ["get", "list", "watch"]
- apiGroups: [""]
resources: ["events"]
verbs: ["create", "update", "patch"]
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: run-nfs-client-provisioner
subjects:
- kind: ServiceAccount
name: nfs-client-provisioner
# replace with namespace where provisioner is deployed
namespace: storages
roleRef:
kind: ClusterRole
name: nfs-client-provisioner-runner
apiGroup: rbac.authorization.k8s.io
---
kind: Role
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: leader-locking-nfs-client-provisioner
# replace with namespace where provisioner is deployed
namespace: storages
rules:
- apiGroups: [""]
resources: ["endpoints"]
verbs: ["get", "list", "watch", "create", "update", "patch"]
---
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: leader-locking-nfs-client-provisioner
# replace with namespace where provisioner is deployed
namespace: storages
subjects:
- kind: ServiceAccount
name: nfs-client-provisioner
# replace with namespace where provisioner is deployed
namespace: storages
roleRef:
kind: Role
name: leader-locking-nfs-client-provisioner
apiGroup: rbac.authorization.k8s.io

nfs-subdir-external-provisioner.yaml

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
kind: Deployment
apiVersion: apps/v1
metadata:
name: nfs-client-provisioner
spec:
replicas: 1
selector:
matchLabels:
app: nfs-client-provisioner
strategy:
type: Recreate
template:
metadata:
labels:
app: nfs-client-provisioner
spec:
serviceAccountName: nfs-client-provisioner
containers:
- name: nfs-client-provisioner
# image: registry.k8s.io/sig-storage/nfs-subdir-external-provisioner:v4.0.2
image: k8s.dockerproxy.com/sig-storage/nfs-subdir-external-provisioner:v4.0.2
volumeMounts:
- name: nfs-client-root
mountPath: /persistentvolumes
env:
- name: PROVISIONER_NAME
value: k8s-sigs.io/nfs-subdir-external-provisioner
- name: NFS_SERVER
# value: <YOUR NFS SERVER HOSTNAME>
value: 10.8.111.153
- name: NFS_PATH
# value: /var/nfs
value: /home/nfsfile
volumes:
- name: nfs-client-root
nfs:
# server: <YOUR NFS SERVER HOSTNAME>
server: 10.8.111.153
path: /home/nfsfile

nfs-storage-class.yaml

1
2
3
4
5
6
7
8
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: nfs-client
provisioner: k8s-sigs.io/nfs-subdir-external-provisioner # or choose another name, must match deployment's env PROVISIONER_NAME'
parameters:
pathPattern: "${.PVC.namespace}/${.PVC.annotations.nfs.io/storage-path}" # 此处也可以使用 "${.PVC.namespace}/${.PVC.name}" 来使用pvc的名称作为nfs中真实目录名称
onDelete: delete

nfs-test-pvc.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: test-pvc
annotations:
nfs.io/storage-path: "test-path" # not required, depending on whether this annotation was shown in the storage class description
spec:
storageClassName: nfs-client
accessModes:
- ReadWriteMany
resources:
requests:
storage: 5Gi

nfs-test-nginx-pod.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
apiVersion: v1
kind: Pod
metadata:
name: test-nginx-pod
spec:
containers:
- name: nginx
image: nginx:latest
volumeMounts:
- name: nginx-data
mountPath: /usr/share/nginx/html
volumes:
- name: nginx-data
persistentVolumeClaim:
claimName: test-pvc

其他

https://blog.51cto.com/u_16175526/6718397
https://blog.51cto.com/u_16213459/7344688
https://blog.csdn.net/qq_30051761/article/details/131055705

用GO搭建物联网平台一段时间了,还是依然在指针上习惯性误用,汇总一下

指针地址和指针类型

一个指针变量可以指向任何一个值的内存地址,它所指向的值的内存地址在 32 和 64 位机器上分别占用 4 或 8 个字节,占用字节的大小与所指向的值的大小无关。当一个指针被定义后没有分配到任何变量时,它的默认值为 nil。指针变量通常缩写为 ptr。
每个变量在运行时都拥有一个地址,这个地址代表变量在内存中的位置。Go语言中使用在变量名前面添加&操作符(前缀)来获取变量的内存地址(取地址操作),格式如下:

1
ptr := &v    // v 的类型为 T

其中 v 代表被取地址的变量,变量 v 的地址使用变量 ptr 进行接收,ptr 的类型为T,称做 T 的指针类型,代表指针。
指针实际用法,可以通过下面的例子了解:

1
2
3
4
5
6
7
8
9
package main
import (
"fmt"
)
func main() {
var cat int = 1
var str string = "banana"
fmt.Printf("%p %p", &cat, &str)
}

运行结果:

1
0xc042052088 0xc0420461b0

代码说明如下:

  • 第 8 行,声明整型变量 cat。
  • 第 9 行,声明字符串变量 str。
  • 第 10 行,使用 fmt.Printf 的动词%p打印 cat 和 str 变量的内存地址,指针的值是带有0x十六进制前缀的一组数据。

提示:变量、指针和地址三者的关系是,每个变量都拥有地址,指针的值就是地址。

从指针获取指针指向的值

当使用&操作符对普通变量进行取地址操作并得到变量的指针后,可以对指针使用*操作符,也就是指针取值,代码如下

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
package main

import (
"fmt"
)

func main() {

// 准备一个字符串类型
var house = "Malibu Point 10880, 90265"

// 对字符串取地址, ptr类型为*string
ptr := &house

// 打印ptr的类型
fmt.Printf("ptr type: %T\n", ptr)

// 打印ptr的指针地址
fmt.Printf("address: %p\n", ptr)

// 对指针进行取值操作
value := *ptr

// 取值后的类型
fmt.Printf("value type: %T\n", value)

// 指针取值后就是指向变量的值
fmt.Printf("value: %s\n", value)

}

运行结果:

1
2
3
4
ptr type: *string
address: 0xc0420401b0
value type: string
value: Malibu Point 10880, 90265

代码说明如下:

  • 第 10 行,准备一个字符串并赋值。
  • 第 13 行,对字符串取地址,将指针保存到变量 ptr 中。
  • 第 16 行,打印变量 ptr 的类型,其类型为 *string。
  • 第 19 行,打印 ptr 的指针地址,地址每次运行都会发生变化。
  • 第 22 行,对 ptr 指针变量进行取值操作,变量 value 的类型为 string。
  • 第 25 行,打印取值后 value 的类型。
  • 第 28 行,打印 value 的值。

取地址操作符&和取值操作符是一对互补操作符,&取出地址,根据地址取出地址指向的值。

变量、指针地址、指针变量、取地址、取值的相互关系和特性如下:

  • 对变量进行取地址操作使用&操作符,可以获得这个变量的指针变量。
  • 指针变量的值是指针地址。
  • 对指针变量进行取值操作使用*操作符,可以获得指针变量指向的原变量的值。

使用指针修改值

通过指针不仅可以取值,也可以修改值。

前面已经演示了使用多重赋值的方法进行数值交换,使用指针同样可以进行数值交换,代码如下:

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
package main

import "fmt"

// 交换函数
func swap(a, b *int) {

// 取a指针的值, 赋给临时变量t
t := *a

// 取b指针的值, 赋给a指针指向的变量
*a = *b

// 将a指针的值赋给b指针指向的变量
*b = t
}

func main() {

// 准备两个变量, 赋值1和2
x, y := 1, 2

// 交换变量值
swap(&x, &y)

// 输出变量值
fmt.Println(x, y)
}

运行结果:

1
2 1

代码说明如下:

  • 第 6 行,定义一个交换函数,参数为 a、b,类型都为 *int 指针类型。
  • 第 9 行,取指针 a 的值,并把值赋给变量 t,t 此时是 int 类型。
  • 第 12 行,取 b 的指针值,赋给指针 a 指向的变量。注意,此时*a的意思不是取 a 指针的值,而是“a 指向的变量”。
  • 第 15 行,将 t 的值赋给指针 b 指向的变量。
  • 第 21 行,准备 x、y 两个变量,分别赋值为 1 和 2,类型为 int。
  • 第 24 行,取出 x 和 y 的地址作为参数传给 swap() 函数进行调用。
  • 第 27 行,交换完毕时,输出 x 和 y 的值。

操作符作为右值时,意义是取指针的值,作为左值时,也就是放在赋值操作符的左边时,表示 a 指针指向的变量。其实归纳起来,操作符的根本意义就是操作指针指向的变量。当操作在右值时,就是取指向变量的值,当操作在左值时,就是将值设置给指向的变量。

如果在 swap() 函数中交换操作的是指针值,会发生什么情况?可以参考下面代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
package main

import "fmt"

func swap(a, b *int) {
b, a = a, b
}

func main() {
x, y := 1, 2
swap(&x, &y)
fmt.Println(x, y)
}

运行结果:

1
1 2

结果表明,交换是不成功的。上面代码中的 swap() 函数交换的是 a 和 b 的地址,在交换完毕后,a 和 b 的变量值确实被交换。但和 a、b 关联的两个变量并没有实际关联。这就像写有两座房子的卡片放在桌上一字摊开,交换两座房子的卡片后并不会对两座房子有任何影响。

磁盘情况

  • 查看扩容前的磁盘容量
    1
    2
    3
    4
    5
    6
    7
    8
    [root@k8s-node2 ~]# df -h
    Filesystem Size Used Avail Use% Mounted on
    devtmpfs 2.9G 0 2.9G 0% /dev
    tmpfs 2.9G 0 2.9G 0% /dev/shm
    tmpfs 2.9G 279M 2.7G 10% /run
    tmpfs 2.9G 0 2.9G 0% /sys/fs/cgroup
    /dev/mapper/centos-root 17G 13G 4.5G 75% / <-- 17G
    /dev/sda1 1014M 187M 828M 19% /boot
  • 查看磁盘分区情况
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    [root@k8s-node2 ~]# fdisk -l

    Disk /dev/sda: 107.4 GB, 107374182400 bytes, 209715200 sectors <-- 107.4 GB
    Units = sectors of 1 * 512 = 512 bytes
    Sector size (logical/physical): 512 bytes / 512 bytes
    I/O size (minimum/optimal): 512 bytes / 512 bytes
    Disk label type: dos
    Disk identifier: 0x000a6a43

    Device Boot Start End Blocks Id System
    /dev/sda1 * 2048 2099199 1048576 83 Linux
    /dev/sda2 2099200 41943039 19921920 8e Linux LVM

    Disk /dev/mapper/centos-root: 18.2 GB, 18249416704 bytes, 35643392 sectors
    Units = sectors of 1 * 512 = 512 bytes
    Sector size (logical/physical): 512 bytes / 512 bytes
    I/O size (minimum/optimal): 512 bytes / 512 bytes


    Disk /dev/mapper/centos-swap: 2147 MB, 2147483648 bytes, 4194304 sectors
    Units = sectors of 1 * 512 = 512 bytes
    Sector size (logical/physical): 512 bytes / 512 bytes
    I/O size (minimum/optimal): 512 bytes / 512 bytes

对扩容的磁盘分区操作

  • 磁盘分区命令
    1
    fdisk /dev/sda
    eOFJrd
  • 分区设置分区格式,在Fdisk命令处输入:t,分区号用默认 3(或回车),Hex代码输入:8e (代表适用Linux LVM分区类型),最后写入分区表,在Fdisk命令位置输入:w
    FjBB1t
  • fdisk -l 查看我们新创建的dev/sda3分区了,分区格式为Linux LVM类型。
    HUBP0A
  • 不重启的情况下重读分区,马上生效,格式化新增磁盘并分区
    1
    2
    partprobe /dev/sda
    mkfs.ext3 /dev/sda3
    WuzwSV
  • 进入lvm中合并磁盘
    1
    2
    3
    4
    5
    6
    #进入lvm
    lvm
    #初始化/dev/sda3
    pvcreate /dev/sda3
    #将新分区添加进系统默认的Volume group,centOS的默认Volume group为centos
    vgextend centos /dev/sda3
    deaNOu
    1
    2
    3
    4
    5
    #查看一下当前的Volume卷详情
    vgdisplay -v
    #将系统盘/dev/mapper/centos-root与sda3的5119空余容量合并,输入如下命令:
    lvextend -l +20479 /dev/mapper/centos-root
    quit
    IVbZMN
  • 最后查看扩容及磁盘状态
    1
    2
    #文件系统进行扩容,以让系统识别,输入如下命令(只适用于CentOS7)
    xfs_growfs /dev/mapper/centos-root
    h61lCm
    1
    fdisk -l
    s72ybS
  • 查看系统容量
    1
    df -h
    6U5k7Z

idurrT

目标

为chatGpt聊天窗口添加支持本地持久化,操作方式分为:

1、新增聊天项,即在chat数据库增加数据 (聊天项列表)

1
2
3
4
5
6
7
CREATE TABLE "main"."chat" (
"sessionId" INTEGER,
"chatId" TEXT,
"content" TEXT,
"isChatgpt" INTEGER,
"createTime" REAL
);

2、新增会话,即在session数据库中增加数据 (会话列表)

1
2
3
4
5
6
CREATE TABLE "main"."session" (
"sessionId" INTEGER,
"title" TEXT,
"createTime" REAL
);

可以类比使用微信的过程,数据进行的本地化存储持久化。

实践

  • 封装文件目录操作
    我们先封装一下对文件目录的操作
    QFileManage.swift

    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
    import Foundation
    struct QFileManage {
    /// 创建文件夹
    static func createDirectory(at path: String) {
    let isExisted = FileManager.default.fileExists(atPath: path)
    guard !isExisted else { return }
    let url = URL(fileURLWithPath: path)
    do {
    try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true, attributes: nil)
    } catch let error {
    debugPrint("创建文件夹失败!Path: \(path), Error: \(error.localizedDescription)")
    }
    }
    }

    extension QFileManage {
    /// 库目录
    static func libraryDirectory() -> String {
    return NSSearchPathForDirectoriesInDomains(.libraryDirectory, .userDomainMask, true).last!
    }

    /// 数据库目录
    static func databaseDirectory() -> String {
    let path = libraryDirectory() + "/database"
    createDirectory(at: path)
    return path
    }

    /// 文档目录
    static func documentsDirectory() -> String {
    return NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).last!
    }

    /// 图片目录
    static func imagesDirectory() -> String {
    let path = documentsDirectory() + "/images"
    createDirectory(at: path)
    return path
    }
    }

  • 模型绑定
    聊天模型:ChatDbModel.swift

    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
    import Foundation
    import WCDBSwift

    final class ChatDbModel: TableCodable {
    static var tableName: String { "chat" }

    var sessionId: Int64 = 0
    var chatId: String = ""
    var content: String = ""
    var isChatgpt: Bool = false
    var createTime: Date? = nil

    enum CodingKeys: String, CodingTableKey {
    typealias Root = ChatDbModel
    static let objectRelationalMapping = TableBinding(CodingKeys.self)

    case sessionId
    case chatId
    case content
    case isChatgpt
    case createTime
    }

    init(sessionId: Int64, chatId: String, content: String, isChatgpt: Bool, createTime: Date) {
    self.sessionId = sessionId
    self.chatId = chatId
    self.content = content
    self.isChatgpt = isChatgpt
    self.createTime = createTime
    }
    }

    会话模型:SessionDbModel.swift

    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
    import Foundation
    import WCDBSwift

    final class SessionDbModel: TableCodable {
    static var tableName: String { "session" }

    var sessionId: Int64 = 0
    var title: String? = nil
    var createTime: Date? = nil

    enum CodingKeys: String, CodingTableKey {
    typealias Root = SessionDbModel
    static let objectRelationalMapping = TableBinding(CodingKeys.self)
    case sessionId
    case title
    case createTime
    }

    init(sessionId: Int64, title: String? = nil, createTime: Date? = nil) {
    self.sessionId = sessionId
    self.title = title
    self.createTime = createTime
    }
    }
    extension SessionDbModel {
    static func insert(objects: [SessionDbModel]) {
    do {
    try db?.insert(objects, intoTable: SessionDbModel.tableName)
    } catch let error {
    debugPrint("插入session失败 ->\n\(error.localizedDescription)")
    }
    }
    }

  • 启动时,创建数据库

    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
    import SwiftUI
    import WCDBSwift

    var db: Database?

    @main
    struct tableViewApp: App {
    @AppStorage("appearance") var appearance: String = "system"
    var body: some Scene {
    WindowGroup {
    ContentView().preferredColorScheme(appearance == "system" ? nil : (appearance == "dark" ? .dark : .light))
    }
    }
    init() {
    // 创建数据库
    let path = QFileManage.databaseDirectory() + "/chatgpt.db"
    debugPrint("数据库路径:\(path)")
    db = Database(at:path)
    do {
    // 建表
    try db?.run(transaction: {_ in
    try db?.create(table: SessionDbModel.tableName, of: SessionDbModel.self)
    try db?.create(table: ChatDbModel.tableName, of: ChatDbModel.self)
    })
    } catch let error {
    debugPrint("创建数据库失败!Error: \(error.localizedDescription)")
    }
    }
    }

操作数据库 TODO: viewModel方式操作

  • 新增对话
    1
    SessionDbModel.insert(objects: [SessionDbModel(sessionId: 2, title: "111", createTime: Date())])

v53hLf
SAP MM 组织结构 PDF

总揽

MM 模块的总揽图如下所示:
Dzdh7D

SAP MM 中的主数据

在 MM 模块中,需要维护和管理的主数据包括:

  • 物料主数据管理:将企业所有的物料数据集成在单一的物料数据库中,消除了数据冗余的问题,并实现各部门对数据的共享
  • 供应商主数据管理:将为企业进行供应的供应商的信息集成在供应商主数据库中予以统一管理和维护,并实现共享
  • 采购主数据管理:包含与采购活动相关的各类主数据:交易信息记录、货源清单、配额协议、框架协议、合同、订供货时间表、价格条件等

SAP MM 中的主要业务流程

MM 模块主要分为两部分业务功能:采购管理库存管理

  • 采购管理:重点关注物料或是服务的采购,货源的确定,采购订单到货与应付款的监控等;

    首先是要有需求,有了需求后,就要去找货源,有了货源后就要去选择供应商,通过一系统列的询、报价后,进行价格等方面的比较,选择适合的供应商,选择好供应商后就可以下单给供应商,然后监控这张订单和仓库的回货情况,等仓库全部收完货后,作发票校验。做完后财务就可以付款给供应商。

rgZLhE

  • 库存管理:重点关注物料的移动、物料数量与金额的管理以及库存盘点等。

SAP MM 最常用的业务流程事务代码

MM01 【创建物料主数据】
VD01 【创建客户主数据】
FD01 【维护客户财务数据】
VD51 【创建客户-物料信息记录】
FD32 【更改客户信贷管理】
MK01 【创建供应商主数据】
FK01 【维护供应商财务数据】
ME11 【创建采购信息记录】
ME01 【创建货源清单】
CS01 【创建物料清单BOM】

4VO1By

背景

最近在推进我的一款mac应用产品,需使用到menuBar,以前都使用swift appDelegate,现在使用新的MenuBarExtras验证下。

实践

在 macOS 13 Ventura 中,Apple 终于提供了一种实现MenuBarExtras SwiftUI 方式的方法。它首次在 WWDC 演讲“为您的 SwiftUI 应用程序带来多个窗口”中引入,并使在 Swift UI 中编写实用程序应用程序变得轻而易举。

现在MenuBarExtras可以直接将您的应用程序主体与您的Windows或WindowGroups.

1
2
3
4
5
6
7
8
9
@main 
struct UtilityApp : App {
var body: some Scene {
MenuBarExtra ( "UtilityApp" , systemImage: "hammer" ) { ... }

WindowGroup { ... }
}
}

MenuBarExtras大多数时候采用三个参数:

TitleKey: 标识它的字符串。很可能是您的应用程序的名称
Image:菜单栏中显示的符号。最好是,一个SFSymbol。通过这种方式,您可以开箱即用地获得浅色和深色主题行为。
Content: 这几乎可以是任何东西。不过,这取决于所选样式的呈现方式。
有两种预定义样式MenuBarExtras

菜单

两者中较容易的是.menu。它是默认样式,将 Menu Bar Extra 的内容呈现为标准菜单。
62CM3J

1
2
3
4
5
6
7
8
9
10
@main 
struct UtilityApp : App {
var body: some Scene {
MenuBarExtra ( "UtilityApp" , systemImage: "hammer" ) {
AppMenu ()
}

WindowGroup { ... }
}
}

当然你也可以显性的使用 .menuBarExtraStyle(.menu)

窗户

窗口样式允许您将任何类型的内容呈现到的MenuBarExtra弹出窗口中,并且可用于需要更多自定义控件(如滑块或开关)的应用程序。
oZjjne

为了启用窗口样式,将.menuBarExtraStyle修饰符添加到MenuBarExtra并将其设置为.window。

1
2
3
4
5
6
7
8
9
10
@main 
struct UtilityApp : App {
var body: some Scene {
MenuBarExtra ( "UtilityApp" , systemImage: "hammer" ) {
AppMenu ()
}.menuBarExtraStyle(.window)

WindowGroup { ... }
}
}

从 Dock 隐藏应用程序

如果您的应用仅包含一个MenuBarExtra并且不需要额外的窗口,您可以WindowGroup完全删除该窗口。在这些情况下,您很可能也不希望您的应用出现在 Dock 中。

这可以通过UIElement在您的应用程序的info.plist. 代理应用程序不会出现在用户的 Dock 中
OOXQjc

实践

我们来实践一下使用MVVM模式,并使用async await进行网络请求。

XfOeyo

Library.swift

1
2
3
4
5
import Foundation
struct Common: Codable,Hashable {
var act: String = ""
var prompt: String = ""
}

LibraryViewModel.swift

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import Foundation
@MainActor
class LibraryViewModel: ObservableObject {
@Published var commons: [Common] = []
@Published var commonSelected:Common = Common()
func getCommons() async {
guard let url = URL(string: "https://carteclip.com/api/v1/ama/prompts") else {
print("URL无效,请检查输入~")
return
}
do {
var request = URLRequest(url: url)
request.setValue("zh", forHTTPHeaderField: "prefer-language")
let (data, _) = try await URLSession.shared.data(
for: request
)
if let decodedResponse = try? JSONDecoder().decode([Common].self, from: data) {
self.commons = decodedResponse
}
} catch {
print("数据解码失败,请检查~")
}
}
}

LibraryView.swift

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
import SwiftUI
struct LibraryRow: View {
var common: Common
var body: some View {
VStack(alignment: .leading) {
Spacer()
Text(common.act)
.listRowSeparator(.hidden)
.font(.system(size: 18))
.frame(maxWidth: .infinity, alignment: .leading)

Spacer()
Text(common.prompt)
.frame(maxWidth: .infinity, alignment: .leading)
.font(.system(size: 15))
.lineLimit(3)
Spacer()
}
}

}
struct LibraryView: View {
@State var presentSheetKey = false
@State var cur_common:Common = Common(act: "", prompt: "")
@ObservedObject var viewModel: LibraryViewModel
var body: some View {
NavigationStack {
List(viewModel.commons,id: \.self) { common in
Section{
Button {
viewModel.commonSelected = common
presentSheetKey = true
} label: {
LibraryRow(common:common).listRowSeparator(.hidden)
}.buttonStyle(.plain)

}
}.task {
await viewModel.getCommons()
}.refreshable {
await viewModel.getCommons()
}
}.sheet(isPresented: $presentSheetKey) {
VStack(alignment: .leading,spacing: 20) {
Text(viewModel.commonSelected.act).font(.system(size: 17)).foregroundColor(.primary)
Text(viewModel.commonSelected.prompt).font(.system(size: 15)).foregroundColor(.secondary).lineSpacing(8)
Spacer()
HStack(alignment: .bottom,spacing: 20){
Spacer()
Button {
presentSheetKey = false
} label: {
Text("取消").foregroundColor(.primary)
}
Button {

} label: {
Text("开始对话").foregroundColor(.primary)
}
}


}.padding()
.presentationDetents([.fraction(0.5),.medium])
.presentationBackground(.thinMaterial)
.presentationCornerRadius(20)
.presentationDragIndicator(.visible)
}

}
}
struct LibraryView_Previews: PreviewProvider {
static var previews: some View {
LibraryView( viewModel: LibraryViewModel())
}
}

LibraryDetailView.swift

1
2
3
4
5
6
7
8
9
10
11

import Foundation
import SwiftUI

struct LibraryDetailView: View {
var common:Common
@Environment(\.dismiss) var dismiss
var body: some View {
Text(common.prompt)
}
}

Pjqg7f

我们以暗黑模式的适配来实践EnvironmentObject

暗黑模式适配

定义AppSetting,设置 @Published var darkModeSettings

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class AppSetting: ObservableObject {
@Published var darkModeSettings: Int = UserDefaults.standard.integer(forKey: "darkMode") {
didSet {
UserDefaults.standard.set(self.darkModeSettings, forKey: "darkMode")
let scenes = UIApplication.shared.connectedScenes
let windowScene = scenes.first as? UIWindowScene
let window = windowScene?.windows.first
switch self.darkModeSettings {
case 0:
window?.overrideUserInterfaceStyle = .unspecified
case 1:
window?.overrideUserInterfaceStyle = .light
case 2:
window?.overrideUserInterfaceStyle = .dark
default:
window?.overrideUserInterfaceStyle = .unspecified
}
}
}
}

传入 .environmentObject(AppSetting())

1
2
3
4
5
6
7
8
@main
struct tableViewApp: App {
var body: some Scene {
WindowGroup {
ContentView().environmentObject(AppSetting())
}
}
}

使用@EnvironmentObject

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

struct SettingsView: View {
@EnvironmentObject var appSettings: AppSetting

var body: some View {
NavigationStack {
List {
Section {
HStack{
Button {
appSettings.darkModeSettings = 0
} label: {
Text("跟随系统").foregroundColor(.primary)
}
Spacer()
if(appSettings.darkModeSettings == 0){
Image(systemName: "checkmark")
}
}
HStack{
Button {
appSettings.darkModeSettings = 1
} label: {
Text("白").foregroundColor(.primary)
}
Spacer()
if(appSettings.darkModeSettings == 1){
Image(systemName: "checkmark")
}
}
HStack{
Button {
appSettings.darkModeSettings = 2
} label: {
Text("暗黑").foregroundColor(.primary)
}
Spacer()
if(appSettings.darkModeSettings == 2){
Image(systemName: "checkmark")
}

}

} header: {
Text("主题设置")
.textCase(nil)
}
}
.listStyle(.insetGrouped)
.navigationTitle("设置")
}
}
}

相关链接阅读

SwiftUI: 全局状态管理

0%