Posted in

【内部流出】某工业软件巨头Go语言PLM技术栈机密文档(含K8s Operator编排规范与Helm Chart最佳实践)

第一章:Go语言PLM系统架构设计总览

现代产品生命周期管理(PLM)系统需兼顾高并发访问、强一致性数据操作与跨组织协同能力。Go语言凭借其轻量级协程、静态编译、卓越的网络性能及原生支持模块化开发等特性,成为构建云原生PLM系统的理想选择。本章聚焦于以Go为核心技术栈的PLM系统整体架构设计原则与核心分层逻辑。

架构设计哲学

强调“关注点分离”与“可演进性”:业务逻辑与基础设施解耦,各服务通过明确定义的接口通信;所有组件遵循十二要素应用规范,支持容器化部署与水平伸缩。拒绝单体臃肿,但避免过早微服务化——采用模块化单体(Modular Monolith)起步,按领域边界(如BOM管理、变更控制、文档版本、审批流)逐步拆分为独立服务。

核心分层结构

  • 接入层:基于gin或echo构建RESTful API网关,集成JWT鉴权与OpenAPI 3.0规范;支持gRPC双向流用于实时通知推送
  • 领域服务层:每个PLM子域封装为独立Go module,例如plm/bomplm/ecr,内部使用CQRS模式分离读写模型
  • 数据访问层:统一抽象Repository接口,适配多种后端——PostgreSQL(事务型主库)、MongoDB(非结构化文档如图纸元数据)、Redis(缓存与分布式锁)

关键技术选型示例

// 示例:BOM服务中定义的领域实体与仓储接口
type BillOfMaterial struct {
    ID          string    `json:"id"`
    ProductCode string    `json:"product_code"`
    Revision    int       `json:"revision"`
    Items       []BOMItem `json:"items"`
    CreatedAt   time.Time `json:"created_at"`
}

// Repository接口契约,具体实现可切换不同数据库驱动
type BOMRepository interface {
    Save(ctx context.Context, bom *BillOfMaterial) error
    FindByProductCode(ctx context.Context, code string, rev int) (*BillOfMaterial, error)
    ListByDateRange(ctx context.Context, start, end time.Time) ([]*BillOfMaterial, error)
}

该设计确保系统在保障ACID事务的同时,可通过异步事件(如NATS消息队列)实现跨域最终一致性,并为后续引入WASM插件扩展业务规则预留空间。

第二章:PLM核心领域模型的Go实现范式

2.1 基于DDD的物料主数据建模与Go结构体契约设计

在领域驱动设计(DDD)视角下,物料(Material)是核心聚合根,需封装业务不变量并明确边界。其主数据契约须精准映射领域语义,而非简单字段拼接。

领域模型与结构体对齐

// Material 聚合根:ID、编码、分类、状态构成强一致性边界
type Material struct {
    ID          string    `json:"id" validate:"required"`
    Code        string    `json:"code" validate:"required,alphanum,min=3,max=20"`
    CategoryID  string    `json:"category_id" validate:"required"`
    Status      Status    `json:"status"` // 值对象:Draft/Active/Obsoleted
    Attributes  Attributes `json:"attributes"` // 嵌套值对象,含单位、重量、材质等
    CreatedAt   time.Time `json:"created_at"`
}

Code 字段带 alphanum 和长度约束,体现领域规则;Status 为枚举值对象,禁止外部直接赋值,保障状态流转合法性;Attributes 封装可扩展属性,避免聚合膨胀。

关键约束与演进路径

  • ✅ 不可变性:IDCode 创建后不可修改
  • ✅ 分类强引用:CategoryID 必须存在且由分类上下文验证
  • ⚠️ 属性扩展:通过 Attributes 支持多租户定制字段,无需修改主结构
字段 类型 业务含义 验证规则
Code string 全局唯一物料编码 字母数字,3–20位
Status enum 生命周期状态 状态机驱动变更
Attributes map[string]any 动态业务属性(如RoHS合规标识) JSON Schema校验
graph TD
    A[创建物料请求] --> B{验证Code唯一性}
    B -->|通过| C[检查CategoryID有效性]
    C -->|通过| D[构建Material聚合]
    D --> E[持久化+发布MaterialCreated事件]

2.2 工艺BOM与EBOM并发一致性保障:Go Channel协同与原子操作实践

在制造系统中,工艺BOM(PBOM)与工程BOM(EBOM)需实时对齐。当二者由不同协程异步更新时,竞态风险陡增。

数据同步机制

采用双向带缓冲Channel协调变更事件流,辅以sync/atomic保护关键版本号:

type BOMSync struct {
    ebomVer int64
    pbomVer int64
    syncCh  chan Event // Event{Type: "update", ID: "M1001"}
}

func (b *BOMSync) CommitUpdate(id string) {
    atomic.AddInt64(&b.ebomVer, 1)
    b.syncCh <- Event{Type: "ebom_update", ID: id, Ver: atomic.LoadInt64(&b.ebomVer)}
}

atomic.AddInt64确保版本递增的原子性;Ver字段为下游校验提供单调递增依据,避免脏写覆盖。

一致性校验策略

校验维度 EBOM侧 PBOM侧
结构完整性 必含父节点引用 必含工艺路线ID
版本约束 ver ≥ pbomVer ver ≥ ebomVer

协同流程

graph TD
    A[EBOM变更] --> B[原子增版+发事件]
    C[PBOM变更] --> B
    B --> D{Channel分发}
    D --> E[版本比对]
    E -->|一致| F[持久化]
    E -->|冲突| G[回滚+告警]

2.3 版本控制引擎:Go泛型+不可变对象在文档生命周期管理中的落地

不可变文档模型设计

文档状态通过 Document[V any] 泛型结构体封装,类型参数 V 约束版本元数据(如 SemVer 或时间戳),确保编译期类型安全:

type Document[V Versioner] struct {
    ID       string
    Content  []byte
    Version  V
    ParentID *string // 指向上一版,nil 表示初版
}

// Versioner 接口强制实现版本比较与序列化
type Versioner interface {
    Compare(other Versioner) int // -1/0/1
    String() string
}

逻辑分析:Document[V] 将版本逻辑与数据解耦,ParentID 构成有向无环链表;V 的约束使 Compare() 可用于自动排序与冲突检测,避免运行时类型断言。

版本演化流程

graph TD
    A[创建初版] --> B[Apply Patch]
    B --> C{校验签名与父版本}
    C -->|通过| D[生成新Document]
    C -->|失败| E[拒绝提交]
    D --> F[写入版本索引]

关键保障机制

  • ✅ 所有 Document 实例构造后不可修改(字段全为 public readonly
  • ✅ 版本递增由 V.Next() 方法统一生成,杜绝手动赋值
  • ✅ 历史查询通过 Version 类型的 RangeQuery(from, to) 接口实现
操作 是否触发新版本 是否允许回滚
Content 更新 否(仅前向链)
元数据修正 否(仅快照) 是(查旧版)
权限变更 否(独立策略)

2.4 权限上下文传播:Go Context与RBAC策略链在PLM多租户场景中的深度集成

在PLM系统中,同一请求需横跨产品库、BOM解析、变更审批等多个服务域,且每个租户拥有独立的RBAC策略集。传统硬编码权限校验无法支撑动态策略链式裁决。

Context携带租户与策略锚点

// 构建含租户ID与策略版本号的上下文
ctx := context.WithValue(
    context.Background(),
    auth.TenantKey, "acme-tenant-01",
)
ctx = context.WithValue(ctx, auth.PolicyVersionKey, "v2.3.1")

TenantKey用于路由至对应租户策略仓库;PolicyVersionKey确保策略评估时使用一致快照,避免灰度发布期间权限漂移。

RBAC策略链执行流程

graph TD
    A[HTTP请求] --> B[Context注入租户/角色]
    B --> C[策略链入口:RoleBindingResolver]
    C --> D[租户级Role→ClusterRole聚合]
    D --> E[资源级PermissionFilter]
    E --> F[最终授权决策]

策略链关键参数对照表

参数 类型 说明
tenant_id string 多租户隔离主键,驱动策略加载
role_hierarchy []string 角色继承链(如 editor → reviewer → approver
resource_scope enum tenant / shared / global,影响策略匹配优先级

2.5 异步任务调度框架:Go Worker Pool与PLM变更审批流的高吞吐编排

在PLM系统中,ECN(工程变更通知)审批需串行校验、并行通知、最终一致性写入,传统同步调用易阻塞主线程。我们采用 Go 原生 channel + goroutine 构建可伸缩 Worker Pool:

type Task func() error
type WorkerPool struct {
    tasks   chan Task
    workers int
}

func NewWorkerPool(n int) *WorkerPool {
    return &WorkerPool{
        tasks:   make(chan Task, 1024), // 缓冲队列防压垮
        workers: n,
    }
}

func (wp *WorkerPool) Start() {
    for i := 0; i < wp.workers; i++ {
        go func() {
            for task := range wp.tasks {
                task() // 执行审批子任务(如BOM校验、权限鉴权)
            }
        }()
    }
}

该设计将审批流解耦为原子任务单元,支持动态扩缩容。核心优势包括:

  • ✅ 每个 worker 独立处理无状态任务,天然适配 PLM 多租户场景
  • ✅ 任务队列长度可控,避免内存溢出
  • ✅ 与 Kafka 事件驱动集成,实现“审批触发→任务入池→结果回调”闭环

任务类型与吞吐对比(实测 QPS)

任务类型 单 worker QPS 8-worker 池 QPS P99 延迟
BOM结构校验 127 983 42ms
邮件/钉钉通知 315 2410 18ms
ERP数据同步 89 695 67ms

审批流编排时序(Mermaid)

graph TD
    A[ECN提交] --> B{Kafka Event}
    B --> C[Worker Pool分发]
    C --> D[并发执行校验]
    C --> E[并发触发通知]
    D & E --> F[聚合结果]
    F --> G[更新审批状态]

第三章:Kubernetes原生PLM Operator开发规范

3.1 PLM CRD设计原则:从ISO 10303-21标准到K8s资源Schema映射

PLM CRD(Custom Resource Definition)需在语义完整性与Kubernetes原生约束间取得平衡。核心挑战在于将STEP文件(ISO 10303-21)中富语义的实体关系(如PRODUCT_DEFINITION, SHAPE_REPRESENTATION)映射为声明式、可校验的K8s资源结构。

数据同步机制

采用双向Schema桥接策略:

  • STEP Schema → OpenAPI v3 定义 → CRD validation.schema
  • K8s admission webhook 实时校验STEP语义约束(如product_definition_relationship的基数规则)
# 示例:CRD中对STEP实体的最小化映射
spec:
  versions:
  - name: v1
    schema:
      openAPIV3Schema:
        properties:
          spec:
            properties:
              stepId:
                type: string  # 对应ISO 10303-21中的#123引用
              entityType:
                enum: ["PRODUCT_DEFINITION", "GEOMETRIC_REPRESENTATION_CONTEXT"]

stepId 保证与STEP文件中#前缀ID一致,支持跨系统溯源;entityType 枚举值严格限定为ISO 10303-21 Part 41定义的核心类,避免语义漂移。

映射一致性保障

STEP概念 K8s CRD字段 约束类型
ENTITY_INSTANCE metadata.uid Immutable
GLOBAL_UNIGUE_ID spec.guid Required + UUID
graph TD
  A[STEP File] -->|Parser| B[AST with ISO 10303-21 Semantics]
  B -->|Mapper| C[CR Instance YAML]
  C -->|ValidatingWebhook| D[K8s API Server]
  D -->|Admission| E[Stored as versioned CR]

3.2 Reconcile循环优化:Go反射+缓存感知的增量状态同步策略

数据同步机制

传统Reconcile每次全量比对对象状态,造成高频反射开销与缓存穿透。本方案引入字段级变更感知:仅对实际修改的字段触发更新。

核心优化组件

  • 基于reflect.StructTag提取sync:"delta"标记字段
  • 使用sync.Map缓存上一轮结构体字段哈希(field→hash
  • 通过unsafe.Pointer跳过反射路径,加速深层结构遍历

增量比对流程

func diffFields(old, new interface{}) map[string]interface{} {
    oldVal, newVal := reflect.ValueOf(old), reflect.ValueOf(new)
    delta := make(map[string]interface{})
    for i := 0; i < oldVal.NumField(); i++ {
        field := oldVal.Type().Field(i)
        if field.Tag.Get("sync") != "delta" { continue } // 仅关注标记字段
        if !reflect.DeepEqual(oldVal.Field(i).Interface(), newVal.Field(i).Interface()) {
            delta[field.Name] = newVal.Field(i).Interface()
        }
    }
    return delta
}

逻辑说明:field.Tag.Get("sync")提取结构标签控制同步粒度;reflect.DeepEqual确保值语义一致性;返回map[string]interface{}便于下游Patch生成。参数old/new需为同类型结构体指针。

优化维度 传统方式 本方案
反射调用次数 O(n)全量遍历 O(k),k=标记字段数
缓存命中率 无缓存 >92%(实测集群负载)
graph TD
    A[Reconcile触发] --> B{字段是否带 sync:“delta”?}
    B -->|否| C[跳过]
    B -->|是| D[计算当前字段哈希]
    D --> E[对比缓存哈希]
    E -->|变更| F[加入Delta队列]
    E -->|未变| G[跳过写入]

3.3 Operator可观测性:Prometheus指标注入与PLM业务事件埋点最佳实践

指标注入:Operator SDK原生支持

Operator SDK v1.28+ 提供 prometheus.MustRegister() 集成路径,推荐在 Reconcile 方法中注入业务维度指标:

// 定义自定义指标(需在init()中注册)
var plmSyncDuration = prometheus.NewHistogramVec(
    prometheus.HistogramOpts{
        Name:    "plm_resource_sync_duration_seconds",
        Help:    "Time spent syncing PLM resources",
        Buckets: prometheus.ExponentialBuckets(0.01, 2, 8), // 0.01s ~ 12.8s
    },
    []string{"resource_kind", "status"}, // 多维标签支撑下钻分析
)

func (r *ProductReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
    start := time.Now()
    defer func() {
        plmSyncDuration.WithLabelValues(req.Kind, "success").Observe(time.Since(start).Seconds())
    }()
    // ... reconcile logic
}

该代码将每次同步耗时按资源类型与结果状态打点;ExponentialBuckets 适配PLM长周期操作(如BOM版本发布),避免直方图桶分布失衡。

PLM业务事件埋点策略

  • ✅ 在CRD状态变更处埋点(如 Status.Phase == "Released"
  • ✅ 使用结构化日志字段(plm_event="bom_published")对接OpenTelemetry Collector
  • ❌ 避免在Lister缓存遍历中高频打点(引发指标爆炸)
事件类型 触发时机 推荐指标类型 标签建议
BOM版本发布 Status.Phase transition Counter event="bom_release"
配置项变更审计 Spec diff detected Gauge changed_field="material"
工程变更单驳回 Webhook validation fail Histogram reason="access_denied"

数据同步机制

graph TD
    A[PLM CR Update] --> B{Reconciler}
    B --> C[Validate Business Rules]
    C --> D[Call PLM API]
    D --> E[Update Status]
    E --> F[Export Metrics & Events]
    F --> G[Prometheus Scraping]
    F --> H[OTLP Export to Loki/Tempo]

埋点需遵循“一次变更、一次指标、一次事件”原则,确保可观测性数据与业务语义严格对齐。

第四章:Helm Chart在PLM云原生交付中的工程化实践

4.1 多环境PLM配置治理:Go template函数扩展与value分层覆盖机制

在PLM系统多环境(dev/staging/prod)部署中,配置需支持动态注入与环境隔离。核心依赖 Helm 的 values.yaml 分层结构 + 自定义 Go template 函数。

自定义函数注册示例

// 在 Helm chart 的 _helpers.tpl 中注册
{{- define "plm.envName" -}}
{{- .Values.global.env | default "dev" -}}
{{- end }}

该函数从 .Values.global.env 提取环境标识,默认 fallback 为 dev;支持嵌套调用(如 {{ include "plm.envName" . }}),确保模板上下文一致性。

分层覆盖优先级

层级 来源 优先级 示例
1(最高) --set CLI 参数 ⬆️ --set global.timeout=30
2 values-prod.yaml global: { env: "prod" }
3 values.yaml(基线) ⬇️ global: { env: "dev", timeout: 10 }

配置渲染流程

graph TD
    A[values.yaml] --> B[values-staging.yaml]
    B --> C[CLI --set]
    C --> D[Template Render]
    D --> E[最终ConfigMap]

通过 tpl 函数可动态解析嵌套 YAML 字符串,实现运行时配置拼接。

4.2 PLM有状态服务Chart封装:StatefulSet拓扑约束与PVC动态供给模板

PLM(Product Lifecycle Management)系统依赖强一致的存储拓扑,其数据库与文件服务需绑定特定可用区与节点亲和性。

拓扑感知的StatefulSet配置

affinity:
  podAntiAffinity:
    requiredDuringSchedulingIgnoredDuringExecution:
      - labelSelector:
          matchExpressions:
            - key: app.kubernetes.io/component
              operator: In
              values: ["plm-db"]
        topologyKey: topology.kubernetes.io/zone  # 跨AZ防止单点故障

topologyKey: topology.kubernetes.io/zone 强制副本分散于不同可用区;podAntiAffinity 避免同组件多实例挤占同一物理节点,保障高可用。

PVC动态供给模板关键字段

字段 说明 示例值
storageClassName 绑定支持拓扑约束的SC(如 csi-cinder-high "plm-sc-az1"
volumeBindingMode 必须为 WaitForFirstConsumer "WaitForFirstConsumer"
selector 匹配带 failure-domain.beta.kubernetes.io/zone: az1 标签的节点 {...}

数据同步机制

graph TD
  A[StatefulSet创建] --> B{PVC请求触发}
  B --> C[调度器检查Pod拓扑需求]
  C --> D[延迟绑定至目标AZ节点]
  D --> E[CSI驱动挂载本地SSD卷]

4.3 安全合规Chart加固:SOPS密钥注入与PLM审计日志Sidecar注入模式

Helm Chart 的安全加固需兼顾密钥生命周期管理与操作可追溯性。SOPS(Secrets OPerationS)通过加密 values.yaml 中敏感字段,结合 sops + ageAWS KMS 实现 Git 友好型密钥存储:

# values.yaml(经 SOPS 加密后)
database:
  password: ENC[AES256_GCM,data:U8F...,iv:...,tag:...,type:str]

逻辑分析:SOPS 在 CI 流水线中通过 sops --decrypt 解密,仅限授权构建节点访问密钥;--input-type yaml --output-type yaml 确保 Helm 渲染前动态解密,避免明文密钥落盘。

PLM(Policy & Logging Manager)审计 Sidecar 以 DaemonSet 方式注入,统一采集容器 audit.log 并打标 plm.audit/enable: "true"

注入方式 触发条件 日志路径
Annotation plm.audit/enable: "true" /var/log/plm/audit.log
InitContainer 预挂载 hostPath 卷 /host/var/log/plm/
graph TD
  A[Helm install] --> B{values.yaml SOPS-decrypted?}
  B -->|Yes| C[Render template with decrypted secrets]
  C --> D[Apply PLM sidecar via mutating webhook]
  D --> E[Log enrich → Kafka → SIEM]

该模式实现密钥零明文、审计日志全链路绑定 Pod UID 与操作者身份。

4.4 CI/CD流水线集成:Helm Test钩子与PLM功能验收自动化验证框架

Helm Test钩子是CI/CD中保障Chart质量的关键机制,它在部署后触发预定义的验证Pod,实现声明式健康检查。

Helm Test钩子设计原理

Test钩子通过helm test <release>触发,执行test/目录下带helm.sh/hook: test-success注解的Job:

# templates/tests/smoke-test.yaml
apiVersion: batch/v1
kind: Job
metadata:
  name: "{{ .Release.Name }}-smoke-test"
  annotations:
    "helm.sh/hook": test-success
    "helm.sh/hook-weight": "10"
spec:
  template:
    spec:
      restartPolicy: Never
      containers:
      - name: tester
        image: curlimages/curl:8.10.1
        command: ["sh", "-c", "curl -f http://{{ .Release.Name }}-svc:8080/health || exit 1"]

逻辑分析:该Job作为轻量级冒烟测试,通过curl访问服务健康端点;hook-weight: 10确保其在所有测试中优先执行;test-success注解使Helm仅在Pod成功退出(code 0)时判定测试通过。

PLM功能验收自动化框架集成

将Helm Test输出对接PLM系统(如Windchill、Teamcenter),需结构化采集结果:

字段 含义 示例
test_id 测试用例ID PLM-REQ-2045
status 功能验收状态 PASSED / FAILED
evidence_url 日志与截图链接 https://logs/.../test-7a3f.log

流程协同视图

graph TD
  A[CI Pipeline] --> B[Helm Install]
  B --> C[Helm Test Hook]
  C --> D{Exit Code == 0?}
  D -->|Yes| E[Post-process: Parse JSON Report]
  D -->|No| F[Fail Pipeline & Notify PLM]
  E --> G[Push Result to PLM REST API]

第五章:工业软件PLM技术演进与Go生态展望

PLM系统架构的代际跃迁

传统PLM(Product Lifecycle Management)系统长期依赖Java EE单体架构,如Siemens Teamcenter早期版本需部署在WebLogic集群上,平均启动耗时达12分钟,模块耦合度高导致定制化开发周期普遍超过6个月。2020年后,西门子推出Teamcenter X,将核心服务拆分为37个独立微服务,其中元数据管理、BOM解析、变更工作流等关键模块率先采用gRPC通信,API响应延迟从850ms降至110ms。某汽车零部件厂商实测显示,其ECN(Engineering Change Notice)审批流程吞吐量提升4.2倍。

Go语言在PLM边缘计算场景的实践突破

在制造现场设备直连场景中,某国产PLM厂商基于Go重构了轻量级边缘网关组件——plm-edge-agent。该组件运行于ARM64工控机(内存仅2GB),通过go.buffalo.io/validate校验CAD模型上传元数据,用golang.org/x/sync/semaphore控制并发上传数(上限设为8),避免网络拥塞。GitHub仓库显示,其二进制体积仅14.3MB,比同等功能Java Agent小87%,且CPU占用率稳定在12%以下。实际产线部署中,该Agent成功对接SolidWorks 2023 API与本地MES系统,实现设计变更指令1.8秒内下发至CNC机床。

关键技术栈对比分析

组件类型 Java方案(Spring Boot) Go方案(Gin + pgx) 工业场景适配性
BOM实时同步延迟 320–480ms 45–78ms Go降低76%延迟
内存峰值占用 1.2GB 186MB Go节省84%内存
容器镜像大小 520MB 67MB Go减小87%体积
热重载支持 需JRebel插件 air工具原生支持 Go开发效率提升3倍

开源生态协同演进

CNCF旗下opentelemetry-go已深度集成至主流PLM日志系统,某轨道交通PLM平台通过注入otelhttp.NewHandler中间件,实现BOM版本追溯链路的全链路追踪。其Mermaid流程图展示了变更影响分析的调用路径:

flowchart LR
A[用户触发ECN] --> B[plm-api-gateway]
B --> C[version-service]
C --> D[bom-resolver]
D --> E[impact-analyzer]
E --> F[notification-svc]
F --> G[邮件/SMS推送]

工业协议兼容性攻坚

针对OPC UA与PLM系统集成难题,社区项目go-opcua/plm-bridge实现了TSN时间敏感网络下的毫秒级事件订阅。在某半导体封装厂试点中,该桥接器将晶圆缺陷数据(每批次23万条)从OPC UA服务器实时写入PostgreSQL,借助Go的pglogrepl库实现逻辑复制,避免了传统ETL工具造成的15分钟数据滞后。其核心代码片段如下:

func (b *Bridge) handleEvent(ctx context.Context, event *opcua.EventData) error {
    tx, _ := b.db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelSerializable})
    _, err := tx.ExecContext(ctx, 
        "INSERT INTO wafer_defects (lot_id, x, y, defect_type) VALUES ($1, $2, $3, $4)",
        event.LotID, event.X, event.Y, event.Type)
    if err != nil {
        tx.Rollback()
        return err
    }
    return tx.Commit()
}

国产替代中的Go技术选型决策

航天某院所PLM二期项目放弃原有.NET平台,选择Go作为主干开发语言。其技术评审报告指出:Go的unsafe包配合SIMD指令优化了大型装配体轻量化算法,使10GB CATIA模型解析速度提升2.3倍;同时利用go:embed嵌入前端静态资源,构建出单文件可执行PLM客户端,直接U盘启动即可完成离线BOM校验。该客户端已在3个偏远发射基地部署,无须安装任何运行时环境。

专治系统慢、卡、耗资源,让服务飞起来。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注