资讯动态

Weaviate 授权调用全图谱:解析 auth_calls.md 的 79 个 Authorize 调用点与 RBAC 实现

发布时间:2026/9/14 4:22:24 来源:尧图企业网站定制
Weaviate 授权调用全图谱解析 auth_calls.md 的 79 个 Authorize 调用点与 RBAC 实现【免费下载链接】weaviateWeaviate is an open-source vector database that stores both objects and vectors, allowing for the combination of vector search with structured filtering with the fault tolerance and scalability of a cloud-native database​.项目地址: https://gitcode.com/GitHub_Trending/we/weaviate导读usecases/auth/authorization/docs/auth_calls.md是 Weaviate 代码库中所有授权调用Authorization Calls的自动生成清单它以一张函数 × 文件 × Verb → Resources总表完整刻画了 GraphQL、gRPC、REST 三大 API 入口以及 Backup、Classification、Objects、Schema 等核心业务用例中的每一次Authorize鉴权点。本文以该清单为骨架结合usecases/auth/authorization下的源码与测试逐层拆解 Weaviate 的 RBAC基于角色的访问控制模型动词与资源体系、Casbin 策略引擎、资源过滤与审计日志并给出清单的重新生成方法。读完本文你将能读懂并定位任意一条授权调用理解Authorize(ctx, principal, verb, resources...)背后的完整决策链路并掌握为新增功能接入授权检查的标准做法。一、这份文档是什么代码库的授权调用全景图auth_calls.md不是一个手写的概念说明而是一份由 Go 程序扫描源码 AST抽象语法树自动生成的全量清单。它遍历整个仓库的非测试 Go 源文件找出每一个调用名为Authorize的函数调用表达式记录其所在函数、所在文件以及调用参数中的动词verb与资源resource最后汇总成一张表。文档开头的统计信息是生成时刻的快照Total files found:23162Files processed:1937Total Authorize calls found:79Files processed 远小于 Files found是因为生成器按规则跳过了目录与文件详见下文。而79 个调用是按函数内 verb→resource 组合计数得到的——同一个函数里的多次授权调用会合并到一行用br分隔因此最终表格行数少于 79。如何重新生成这份清单文档自身给出了复现步骤在仓库根目录执行cd usecases/auth/authorization/docs go run generator.go生成器即同目录下的 generator.go。它的工作流程值得拆解因为理解了它你就理解了这份清单的口径与边界从仓库根mainDirPath ../../../../开始filepath.Walk遍历全部文件跳过目录.git、vendor、node_modules、dist、build见shouldSkipDir跳过文件非.go文件、含/vendor/的路径、_test.go测试文件因此测试中的授权断言不计入清单对每个 Go 文件用go/parser解析为 AST遍历所有*ast.CallExpr调用表达式命中SelectorExpr中Sel.Name Authorize的调用通过 AST 定位该调用所属的*ast.FuncDecl函数跳过Test*前缀的测试函数提取call.Args[1]动词与call.Args[2]资源去掉、authorization.前缀后归一化按函数 文件聚合同一函数内多个 verb→resource 组合追加到同一行最后写入auth_calls.md。结论清单统计的是非测试代码中显式调用Authorize的地方。凡是经过AuthorizeSilent静默鉴权不产生审计日志的调用以及通过FilterAuthorizedResources批量过滤的调用都不在这张表里——它们是同一鉴权体系的不同入口。二、前置知识Verb 与 Resource 体系要读懂总表里READ → CollectionsMetadata、CREATE → ShardsData这类条目必须先理解 Weaviate 授权模型的两个核心概念动词Verb与资源Resource。2.1 四个动词在 types.go 中动词被定义为单字母常量对应 REST 语义常量值含义CREATEC创建新资源READR读取资源UPDATEU更新已有资源DELETED删除资源另外还有两个非 CRUD 动词ROLE_SCOPE_ALLALL与ROLE_SCOPE_MATCHMATCH用于角色权限的范围限定USER_AND_GROUP_ASSIGN_AND_REVOKEA用于用户/组的角色指派与撤销。VerbWithScope(verb, scope)会把这些动词拼成{verb}_{scope}形式的动作字符串。2.2 领域Domain与资源路径授权操作面向的资源是一串带路径结构的字符串其第一段是领域domain。types.go中定义了全部领域常量GroupsDomain groups UsersDomain users RolesDomain roles ClusterDomain cluster NodesDomain nodes BackupsDomain backups SchemaDomain schema CollectionsDomain collections TenantsDomain tenants DataDomain data McpDomain mcp ReplicateDomain replicate AliasesDomain aliases NamespacesDomain namespaces资源路径由一组生成函数产出清单中的CollectionsMetadata、ShardsData、CollectionsData、Objects等既是资源名也是函数名。这些函数集中在 types.go 中核心几个如下Objects(class, shard, id)→ 形如data/collections/{class}/shards/{shard}/objects/{id}空参数替换为*通配符这是最底层的对象级资源。CollectionsData(classes...)→ 对每个类生成data/collections/{Class}/shards/*/objects/*即某类的全部数据。CollectionsMetadata(classes...)→schema/collections/{Class}/shards/##表示仅做集合级检查collection-only check。Collections(classes...)CollectionsDataCollectionsMetadata即数据与元数据都授权。ShardsData(class, shards...)/ShardsMetadata(class, shards...)→ 分片级的数据/元数据资源。Backups(classes...)→backups/collections/{Class}Roles(roles...)→roles/{role}Users→users/{user}Groups→groups/{type}/*Nodes(verbosity, classes...)→nodes/verbosity/{minimal|verbose}/...Aliases(class, aliases...)→aliases/collections/{Class}/aliases/{alias}Replications(class, shard)→replicate/collections/{Class}/shards/{shard}Namespaces(names...)→namespaces/{ns}Mcp()→mcp。注意两点实现细节类名会经schema.UppercaseClassesNames统一转为大写空参/*参数会被折叠为对应领域的通配资源如roles/*。建议阅读 types_test.go 中针对Objects、CollectionsMetadata、Backups等的断言用例可以快速确认每个生成函数的精确输出格式例如Objects(, , )返回collections/*/shards/*/objects/*这种带通配符的路径。2.3 授权调用的标准形态所有调用点的最终形态都收敛到Authorizer接口authorizer.gotype Authorizer interface { Authorize(ctx context.Context, principal *models.Principal, verb string, resources ...string) error AuthorizeSilent(ctx context.Context, principal *models.Principal, verb string, resources ...string) error FilterAuthorizedResources(ctx context.Context, principal *models.Principal, verb string, resources ...string) ([]string, error) }Authorize做是/否决策失败返回错误并记录审计日志AuthorizeSilent同样做决策但跳过审计日志供内部高频路径使用FilterAuthorizedResources尽力而为地过滤出被允许的资源子集配合列表类接口做行级可见性控制。接口注释明确指出底层究竟用 RBAC、adminlist 还是其他策略对调用方透明——这是清单能覆盖多种入口而调用方式始终一致的根本原因。未启用任何鉴权时默认注入DummyAuthorizer它无条件放行Authorize恒返回nil等价于无授权。三、完整授权调用清单79 个调用点以下表格原样继承自 auth_calls.md列为生成时快照如需最新请按上文方法重新生成FunctionFileVerb → ResourcesresolveAggregateadapters/handlers/graphql/local/aggregate/resolver.goREAD → ShardsDataauthorizePathadapters/handlers/graphql/local/common_filters/authz.goREAD → CollectionsDataAuthorizePropertyadapters/handlers/graphql/local/common_filters/authz.goREAD → CollectionsDataresolveExploreadapters/handlers/graphql/local/explore/concepts_resolver.goREAD → CollectionsDataresolveGetadapters/handlers/graphql/local/get/class_builder_fields.goREAD → ShardsDatabatchDeleteadapters/handlers/grpc/v1/service.goDELETE → ShardsDatabatchObjectsadapters/handlers/grpc/v1/service.goUPDATE → ShardsDataCREATE → ShardsDataclassGetterWithAuthzFuncadapters/handlers/grpc/v1/service.goREAD → CollectionscreateRoleadapters/handlers/rest/authz/handlers_authz.goCREATE → RolesaddPermissionsadapters/handlers/rest/authz/handlers_authz.goUPDATE → RolesremovePermissionsadapters/handlers/rest/authz/handlers_authz.goUPDATE → RoleshasPermissionadapters/handlers/rest/authz/handlers_authz.goREAD → RolesgetRolesadapters/handlers/rest/authz/handlers_authz.goREAD → RolesgetRoleadapters/handlers/rest/authz/handlers_authz.goREAD → RolesdeleteRoleadapters/handlers/rest/authz/handlers_authz.goDELETE → RolesassignRoleadapters/handlers/rest/authz/handlers_authz.goUPDATE → RolesgetRolesForUseradapters/handlers/rest/authz/handlers_authz.goREAD → RolesgetUsersForRoleadapters/handlers/rest/authz/handlers_authz.goREAD → RolesrevokeRoleadapters/handlers/rest/authz/handlers_authz.goUPDATE → RolessetupGraphQLHandlersadapters/handlers/rest/handlers_graphql.goREAD → CollectionsMetadata, CollectionsBackupusecases/backup/scheduler.goCREATE → BackupsRestoreusecases/backup/scheduler.goCREATE → BackupsCancelusecases/backup/scheduler.goDELETE → BackupsScheduleusecases/classification/classifier.goUPDATE → CollectionsMetadatavalidateFilterusecases/classification/classifier.goREAD → CollectionsMetadataGetusecases/classification/classifier.goREAD → CollectionsMetadataGetNodeStatususecases/nodes/handler.goREAD → NodesGetNodeStatisticsusecases/nodes/handler.goREAD → ClusterAddObjectusecases/objects/add.goCREATE → ShardsDataREAD → CollectionsMetadataautoSchemausecases/objects/auto_schema.goCREATE → CollectionsMetadataUPDATE → CollectionsMetadataAddObjectsusecases/objects/batch_add.goUPDATE → ShardsDataCREATE → ShardsDataDeleteObjectsusecases/objects/batch_delete.goDELETE → ShardsDataclassGetterFuncusecases/objects/batch_delete.goREAD → CollectionsAddReferencesusecases/objects/batch_references_add.goUPDATE → pathsDataREAD → pathsMetadataaddReferencesusecases/objects/batch_references_add.goREAD → shardsDataPathsDeleteObjectusecases/objects/delete.goREAD → CollectionsMetadataDELETE → ObjectsGetObjectusecases/objects/get.goREAD → ObjectsGetObjectsusecases/objects/get.goREAD → ObjectsGetObjectsClassusecases/objects/get.goREAD → ObjectsHeadObjectusecases/objects/head.goREAD → Objects, CollectionsMetadataMergeObjectusecases/objects/merge.goUPDATE → ObjectsREAD → CollectionsMetadataQueryusecases/objects/query.goREAD → CollectionsMetadataAddObjectReferenceusecases/objects/references_add.goUPDATE → ShardsDataREAD → CollectionsMetadata, CollectionsDeleteObjectReferenceusecases/objects/references_delete.goUPDATE → ShardsDataREAD → CollectionsMetadata, CollectionsDataUpdateObjectReferencesusecases/objects/references_update.goUPDATE → ShardsDataREAD → CollectionsMetadata, CollectionsUpdateObjectusecases/objects/update.goUPDATE → ObjectsREAD → CollectionsMetadataValidateObjectusecases/objects/validate.goREAD → ObjectsGetClassusecases/schema/class.goREAD → CollectionsMetadataGetConsistentClassusecases/schema/class.goREAD → CollectionsMetadataGetCachedClassusecases/schema/class.goREAD → CollectionsMetadataAddClassusecases/schema/class.goCREATE → CollectionsMetadataREAD → CollectionsMetadataDeleteClassusecases/schema/class.goDELETE → CollectionsMetadataREAD → CollectionsMetadataUpdateClassusecases/schema/class.goUPDATE → CollectionsMetadataGetSchemausecases/schema/handler.goREAD → CollectionsMetadataGetConsistentSchemausecases/schema/handler.goREAD → CollectionsMetadataUpdateShardStatususecases/schema/handler.goUPDATE → ShardsMetadataShardsStatususecases/schema/handler.goREAD → ShardsMetadataAddClassPropertyusecases/schema/property.goUPDATE → CollectionsMetadataREAD → CollectionsMetadataDeleteClassPropertyusecases/schema/property.goUPDATE → CollectionsMetadataAddTenantsusecases/schema/tenant.goCREATE → ShardsMetadataUpdateTenantsusecases/schema/tenant.goUPDATE → ShardsMetadataDeleteTenantsusecases/schema/tenant.goDELETE → ShardsMetadataGetConsistentTenantsusecases/schema/tenant.goREAD → ShardsMetadataConsistentTenantExistsusecases/schema/tenant.goREAD → ShardsMetadatavalidateFiltersusecases/traverser/traverser_get.goREAD → CollectionsMetadata四、调用点分组解读每个入口在守卫什么清单按函数 文件聚合可以归纳为几大功能域。以下按域解读其鉴权语义并给出源码佐证。4.1 GraphQL 查询入口adapters/handlers/graphqlGraphQL 层有 4 个授权点全部是READ类resolveAggregate聚合查询Aggregate守卫ShardsData——聚合属于对数据的读取authorizePath 与AuthorizePropertycommon_filters下对查询路径/属性的按属性鉴权守卫CollectionsData即要读数据就得有对应类的数据读权限resolveExploreExplore 近邻探索查询同样要求READ → CollectionsDataresolveGetGet 查询守卫ShardsData。setupGraphQLHandlers在 handlers_graphql.go则在 REST 层初始化 GraphQL 处理器时为整个 GraphQL 端点设置READ → CollectionsMetadata, Collections的前置检查。4.2 gRPC 入口adapters/handlers/grpc/v1/service.gogRPC 服务v1 协议有 3 个调用点batchDelete批量删除DELETE → ShardsDatabatchObjects批量写入同时需要UPDATE与CREATE两个动词作用于ShardsData——批量接口一次调用可能同时包含新增与覆盖两类对象classGetterWithAuthzFunc按类取数前的类读取守卫READ → Collections数据 元数据。4.3 REST 授权管理端点adapters/handlers/rest/authz/handlers_authz.go这是管理角色Roles本身的入口共 10 个调用点全部作用于Roles资源端点行为函数动词创建角色createRoleCREATE给角色加权限 / 删权限addPermissions / removePermissionsUPDATE检查权限hasPermissionREAD列出角色 / 取单角色getRoles / getRoleREAD删除角色deleteRoleDELETE指派角色assignRoleUPDATE查用户角色 / 角色用户getRolesForUser / getUsersForRoleREAD撤销角色revokeRoleUPDATE可见管理角色本身也是一个受控资源只有持有Roles领域对应动词权限的主体才能操作角色形成了授权系统的自保护。4.4 备份与恢复usecases/backup/scheduler.goBackup/RestoreCREATE → Backups创建备份/恢复任务CancelDELETE → Backups取消任务。对应资源由Backups(classes...)生成backups/collections/*或按类限定。4.5 分类任务usecases/classification/classifier.goSchedule调度分类任务UPDATE → CollectionsMetadata分类会改写集合元数据状态validateFilter/Get校验过滤器与读取分类状态READ → CollectionsMetadata。4.6 节点与集群信息usecases/nodes/handler.goGetNodeStatusREAD → NodesGetNodeStatisticsREAD → ClusterCluster()返回cluster/*。节点与集群属于运维级operator-only领域在启用了命名空间namespaces的集群上命名空间内的主体对这些域一律无权见下文operatorOnlyResource。4.7 对象 CRUDusecases/objects对象域是调用最密集的区域覆盖单对象与批量操作且普遍遵循写数据之前先确认元数据权限的双重检查模式。以 add.go 为例if err : m.authorizer.Authorize(ctx, principal, authorization.CREATE, authorization.ShardsData(className, object.Tenant)...); err ! nil { return nil, err }即新增对象要求CREATE → ShardsData对目标类、目标租户的分片数据拥有创建权。AddObject同时还需要READ → CollectionsMetadata说明新增前要先能读集合元数据。关键映射总结操作函数动词与资源新增单对象AddObjectCREATE → ShardsDataREAD → CollectionsMetadata自动建 SchemaautoSchemaCREATE / UPDATE → CollectionsMetadata批量新增AddObjectsUPDATE、CREATE → ShardsData批量删除DeleteObjectsDELETE → ShardsData批量删前的类读取classGetterFuncREAD → Collections批量加引用AddReferences / addReferencesUPDATE → pathsDataREAD → pathsMetadata / shardsDataPaths删除对象DeleteObjectREAD → CollectionsMetadataDELETE → Objects获取对象GetObject / GetObjects / GetObjectsClassREAD → Objects对象 HEADHeadObjectREAD → Objects, CollectionsMetadata合并对象MergeObjectUPDATE → ObjectsREAD → CollectionsMetadata查询QueryREAD → CollectionsMetadata引用增删改AddObjectReference / DeleteObjectReference / UpdateObjectReferencesUPDATE → ShardsDataREAD → CollectionsMetadata (Collections)更新对象UpdateObjectUPDATE → ObjectsREAD → CollectionsMetadata校验对象ValidateObjectREAD → Objects值得注意的语义细节单对象操作Get/Head/Merge/Update/Delete在动词作用于Objects后通常还需额外READ → CollectionsMetadata确保主体对该集合的元数据集合是否存在、分片布局等可见DeleteObject的删除动词只作用于Objects对象级而集合/类本身的删除是DeleteClassDELETE → CollectionsMetadata两者权限可分离引用reference操作的资源路径与对象操作不同AddReferences使用pathsData/pathsMetadata、addReferences使用shardsDataPaths对应引用跨集合的路径式授权模型。4.8 Schema 管理usecases/schemaSchema 域集中管理集合Class与分片Shard的元数据操作函数动词与资源读集合GetClass / GetConsistentClass / GetCachedClassREAD → CollectionsMetadata增集合AddClassCREATE READ → CollectionsMetadata删集合DeleteClassDELETE READ → CollectionsMetadata改集合UpdateClassUPDATE → CollectionsMetadata读全部 SchemaGetSchema / GetConsistentSchemaREAD → CollectionsMetadata更新分片状态UpdateShardStatusUPDATE → ShardsMetadata分片状态查询ShardsStatusREAD → ShardsMetadata加属性AddClassPropertyUPDATE READ → CollectionsMetadata删属性DeleteClassPropertyUPDATE → CollectionsMetadata增/改/删租户AddTenants / UpdateTenants / DeleteTenantsCREATE / UPDATE / DELETE → ShardsMetadata租户查询GetConsistentTenants / ConsistentTenantExistsREAD → ShardsMetadata模式很清晰集合级元数据操作含先读后写的加类、删类、加属性统一守卫CollectionsMetadata分片/租户级操作统一守卫ShardsMetadata。CollectionsMetadata与ShardsMetadata的差别在于路径末段schema/collections/{Class}/shards/#集合级检查vsschema/collections/{Class}/shards/{shard}具体分片。4.9 查询校验usecases/traverser/traverser_get.govalidateFiltersREAD → CollectionsMetadata——对遍历查询的过滤器做校验前先确认主体对集合元数据的读权限。五、清单背后的决策链路从调用点到策略引擎清单里的每一行Authorize最终都会落入 RBAC 管理器。以下是链路的关键环节均可与源码对应。5.1 入口与适配Authorizer 接口所有调用方都依赖Authorizer接口authorizer.go生产环境中注入的是 RBAC 的Manager未配置鉴权时注入DummyAuthorizer。接口把用哪种技术做决策完全隐藏这是 GraphQL / gRPC / REST 三层入口能共用同一套调用形态的原因。5.2 RBAC 管理器casbin 策略引擎核心实现在 rbac/model.go 与 rbac/manager.go。模型定义MODEL常量是理解一切的钥匙[request_definition] r sub, obj, act, ns [policy_definition] p sub, obj, act, dom [role_definition] g _, _ [policy_effect] e some(where (p.eft allow)) [matchers] m g(r.sub, p.sub) namespaceAwareMatcher(r.obj, p.obj, r.ns) regexMatch(r.act, p.act)要点请求是sub主体/ obj资源/ act动词/ ns命名空间四元组策略是sub / obj / act / dom匹配规则 角色层级继承g∧ 命名空间感知的资源匹配namespaceAwareMatcher∧ 动作正则匹配效果是任一允许即允许。策略持久化在{policyPath}/rbac/policy.csv通过fileadapter加载Init会创建SyncedCachedEnforcer并启用 1 小时 TTL 的缓存保证高并发下鉴权性能。启动时若检测到策略版本低于1.29.0DEFAULT_POLICY_VERSION会自动执行升级迁移upgradePoliciesFrom129/upgradeGroupingsFrom129。applyPredefinedRoles会在每次启动时重建四个内置角色root、admin、viewer、read-only的绑定其中root/read-only仅能通过环境变量配置RootUsers、RootGroups、ReadOnlyGroups等见 rbacconf/config.goadmin/viewer则同时支持 API 动态管理。BuiltInRoles定义于 types.go。5.3 命名空间感知匹配namespaceAwareMatchermodel.go 中的namespaceAwareMatcher是 Weaviate 多租户/多命名空间授权的核心算法它把资源路径中的类名与调用者命名空间绑定命名空间调用者ns ! 策略中未限定的段被特化为ns:segment已限定的段必须严格以ns:开头否则跨命名空间拒绝全局调用者ns 未限定段用[^/:]:通配符放宽可匹配任意命名空间前缀operator-only 领域backups、nodes、replicate、cluster、namespaces、groups对命名空间调用者一律拒绝operatorOnlyResource集合级请求不得匹配到租户级策略/shards/#vs/shards/.*的 carve-out见weaviateKeyMatch。5.4 审计与错误语义rbac/authorizer.go 的authorize实现了决策 审计主体为空 →errors.NewUnauthenticated()user is not authenticated资源列表为空 → at least 1 resource is required每个资源独立走checkPermissions任一失败即返回errors.NewForbidden(principal, verb, resources)错误信息形如authorization, forbidden action: user xxx has insufficient permissions to READ ...见 errors/errors.go审计日志字段包含actionauthorize、user、request_action动词、audit_mode、source_ip可通过IpInAuditDisabled关闭、client_identifier、groups以及命名空间集群上的namespace/global_operator标记审计版本号AuditLogVersion 2AuthorizeSilent走同一决策路径但skipAudittrueFilterAuthorizedResources则逐资源判定并返回允许集合用于列表接口的可见性过滤。5.5 批量资源的行级过滤filter/filter.go 提供ResourceFilter[T].Filter用于对一批候选对象做授权裁剪若所有项属于同一父资源通配路径相同只做一次鉴权即可全量放行否则退化为逐项过滤配合FilterAuthorizedResources得到允许子集。这是 GraphQL / 列表接口在不泄漏未授权数据前提下保持性能的关键设施。六、如何阅读与维护这份清单给开发者的三条指引新增 API 时必须同步新增授权点。对照清单可见每个公开入口GraphQL resolver、gRPC handler、REST handler、usecases 业务方法都有对应Authorize。新增端点时应在usecases层而非 adapter 层完成鉴权以authorization.CREATE/READ/UPDATE/DELETE 合适的资源生成函数Objects、ShardsData、CollectionsMetadata、Roles、Backups等调用m.authorizer.Authorize(...)参考 usecases/objects/add.go 的写法。选择正确的资源粒度。清单体现了三种粒度对象级Objects单条数据、分片级ShardsData/ShardsMetadata面向多租户的分片数据、集合级CollectionsMetadata仅元数据检查路径末段用#。粒度越细权限模型越精确但调用点也越多、审计日志越冗长需按业务取舍。新增动词/领域时同步三处types.go中的动词常量与领域常量、availableWeaviateActions以便纳入内置角色的预置权限代码注释明确提醒add it to availableWeaviateActions、以及 rbac/model.go 中若涉及命名空间特化则需补充namespaceAwareMatcher的路径段逻辑。修改鉴权后重新生成清单在仓库根执行cd usecases/auth/authorization/docs go run generator.go让文档与代码保持同步提交时请一并检查清单统计数字Total files / Processed / Calls的变动是否符合预期。七、结语auth_calls.md用一张总表浓缩了 Weaviate 全部 79 个授权调用点是理解这个向量数据库数据安全边界的最佳入口。配合usecases/auth/authorization下的源码阅读可以串起一条完整链路API 入口 →Authorizer.Authorize→ RBAC Manager → casbin 策略含命名空间感知匹配→ 审计日志与错误返回。无论是排查insufficient permissions类问题、设计多租户权限模型还是为自定义模块接入鉴权这张表与它背后的实现都值得作为起点反复研读。【免费下载链接】weaviateWeaviate is an open-source vector database that stores both objects and vectors, allowing for the combination of vector search with structured filtering with the fault tolerance and scalability of a cloud-native database​.项目地址: https://gitcode.com/GitHub_Trending/we/weaviate创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价