Posted in

从零搭建符合ASPICE L2要求的Go车载中间件:CI/CD流水线、MISRA-Go静态检查、ASAM XIL测试集成全闭环

第一章:Go语言在智能电动汽车中间件领域的技术适配性与行业价值

智能电动汽车对中间件提出严苛要求:低延迟通信、高可靠性运行、跨ECU异构部署能力,以及快速迭代的OTA升级支持。Go语言凭借其原生协程(goroutine)、无锁通道(channel)、静态链接可执行文件及卓越的交叉编译能力,在该领域展现出独特优势。

并发模型契合车载实时通信场景

Go的轻量级goroutine(内存开销仅2KB)可轻松支撑数百个CAN/FlexRay/以太网通信任务并行处理,远超传统C/C++线程模型的资源消耗。例如,以下代码片段实现多路CAN帧接收与协议解析解耦:

// 启动独立goroutine监听CAN总线,避免阻塞主控逻辑
go func() {
    for frame := range canBus.Receiver() { // 非阻塞通道接收
        select {
        case parsed := <-protocolStack.Decode(frame): // 异步协议解析
            telemetryChannel <- parsed // 转发至中央数据总线
        default:
            log.Warn("Protocol stack busy, dropping frame")
        }
    }
}()

静态链接与嵌入式友好性

Go编译生成单一二进制文件,无需依赖外部glibc或动态库,天然适配ARM64/AARCH32车载SoC(如NVIDIA Orin、高通SA8295)。交叉编译指令示例:

GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build -o vehicle-middleware-arm64 .

CGO_ENABLED=0确保零C依赖,满足ASIL-B功能安全认证中对第三方库调用的限制。

生态工具链加速中间件开发闭环

工具 车载场景价值
go test -race 检测ECU间共享内存访问竞态,规避传感器数据错乱
pprof 实时分析ADAS模块CPU/内存热点,定位实时性瓶颈
go mod vendor 锁定确定性依赖版本,满足车规级软件可追溯性要求

Go标准库对HTTP/2、gRPC、MQTT 5.0的原生支持,使V2X通信、云端协同训练等新型架构落地成本显著降低。

第二章:ASPICE L2合规性落地的Go工程化实践体系

2.1 ASPICE L2过程域映射到Go中间件开发生命周期的理论框架与代码示例

ASPICE L2强调受控过程,核心在于可追溯性、工作产品评审与变更控制。在Go中间件开发中,该要求自然映射为:配置驱动的生命周期钩子、结构化日志链路、以及版本化中间件注册机制。

数据同步机制

通过middleware.Chain实现可审计的中间件装配,每个组件携带元数据(来源、版本、评审状态):

// MiddlewareWithTrace 包装中间件并注入ASPICE L2追踪字段
func MiddlewareWithTrace(name, version, reviewedBy string, fn http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // 记录L2要求的可追溯上下文
        log.WithFields(log.Fields{
            "middleware": name,
            "version":    version,
            "reviewed_by": reviewedBy,
            "trace_id":   r.Header.Get("X-Trace-ID"),
        }).Debug("L2-compliant middleware executed")
        fn.ServeHTTP(w, r)
    })
}

逻辑分析:MiddlewareWithTrace将ASPICE L2的“工作产品标识”与“评审证据”内化为运行时日志字段;name标识过程域(如SWE.4需求实现),version对应基线化配置,reviewedBy满足L2对评审记录的强制要求。

过程域映射表

ASPICE L2过程域 Go中间件生命周期对应点 实现方式
SWE.2 需求分析 config/middleware.yaml 解析 结构化配置驱动中间件加载顺序
SUP.8 配置管理 Git-tagged middleware/registry.go 每次go run自动校验SHA256哈希
graph TD
    A[需求文档 v1.2] --> B[config/middleware.yaml]
    B --> C{Registry.Load()}
    C --> D[MiddlewareWithTrace]
    D --> E[CI流水线触发评审检查]

2.2 基于Go Module与语义化版本的可追溯配置管理实践(含SCM配置项标识与基线控制)

Go Module 天然支持语义化版本(SemVer)约束,结合 go.mod 中的 replaceexcluderequire 指令,可实现配置项的精确溯源与基线锁定。

SCM配置项标识规范

每个配置模块需独立发布为 Git 仓库,并遵循 vX.Y.Z+scm.<commit-hash> 格式打标签(如 v1.2.0+scm.7a2f1e3),确保版本号携带 SCM 上下文。

基线控制示例

// go.mod 片段:显式锁定配置基线
require (
    github.com/org/app-config v1.2.0+scm.7a2f1e3
)
replace github.com/org/app-config => ./internal/config // 本地调试时启用

v1.2.0+scm.7a2f1e3 同时表达功能版本与确切提交,规避“幽灵变更”;
replace 仅作用于构建期,不影响生产环境 go.sum 校验链。

配置维度 生产基线 开发快照
版本标识 v1.2.0+scm.7a2f1e3 v1.2.1-0.20240520143211-9b8c4a2d3f1e
检出方式 git checkout tags/v1.2.0+scm.7a2f1e3 git checkout 9b8c4a2d3f1e
graph TD
    A[go get github.com/org/app-config@v1.2.0+scm.7a2f1e3] --> B[解析tag元数据]
    B --> C[校验go.sum中SHA256哈希]
    C --> D[加载config/v1/schema.json作为基线配置契约]

2.3 Go项目需求-设计-实现双向追溯矩阵构建(结合OpenAPI/SysML导出与Go注释解析)

双向追溯矩阵需打通需求(SysML Requirement)、接口契约(OpenAPI v3)与实现代码(Go handler/struct)三端映射。

数据同步机制

采用统一元数据ID(req-001, api-/users/GET, pkg/handler.UserList)作为跨域锚点,通过go:generate触发双路扫描:

  • sysml2json 解析 .xmi 提取 <Requirement id="req-001" text="用户可分页查询"/>
  • openapi2go 提取 paths./users.get.operationId: UserListHandler
  • godox 扫描 // @req req-001// @api /users/GET 注释

追溯关系表

需求ID OpenAPI路径 Go符号 覆盖状态
req-001 /users/GET handler.UserList
req-002 /users/POST handler.UserCreate ⚠️(缺失注释)

注释解析核心逻辑

// ParseGoComments scans // @req and // @api markers in Go files
func ParseGoComments(fset *token.FileSet, file *ast.File) map[string][]string {
    annotations := make(map[string][]string)
    ast.Inspect(file, func(n ast.Node) bool {
        if cmt, ok := n.(*ast.CommentGroup); ok {
            for _, c := range cmt.List {
                if strings.Contains(c.Text, "@req") {
                    // Extract "req-001" from "// @req req-001"
                    id := strings.Fields(c.Text)[1] // param[1]: requirement ID
                    annotations["req"] = append(annotations["req"], id)
                }
            }
        }
        return true
    })
    return annotations
}

该函数利用go/ast遍历AST注释节点,按空格切分提取@req后首个标识符作为需求ID,确保轻量无依赖;fset提供源码位置信息用于后续定位溯源。

graph TD
    A[SysML .xmi] -->|sysml2json| B(Requirement DB)
    C[OpenAPI.yaml] -->|openapi2go| B
    D[Go source] -->|godox + AST| B
    B --> E[Trace Matrix CSV/HTML]

2.4 Go中间件变更影响分析与回归策略(基于AST解析的依赖图谱与增量编译验证)

AST驱动的影响边界识别

使用go/ast遍历中间件注册点(如router.Use()调用),提取函数字面量或变量引用,构建调用上下文。关键路径需捕获http.Handler实现体与装饰器链式调用。

// 解析 middleware.Register() 调用节点
func visitCallExpr(n *ast.CallExpr) bool {
    if ident, ok := n.Fun.(*ast.Ident); ok && ident.Name == "Register" {
        if len(n.Args) > 0 {
            // 参数为 func(http.ResponseWriter, *http.Request),即影响域起点
            return true
        }
    }
    return false
}

该逻辑定位所有注册入口,参数n.Args[0]即待分析中间件函数节点,是依赖图谱的根节点。

增量验证流程

graph TD
    A[修改中间件源码] --> B{AST差异检测}
    B -->|新增/删除调用| C[更新依赖子图]
    B -->|签名变更| D[触发关联Handler重编译]
    C & D --> E[仅运行受影响e2e测试]

回归验证矩阵

变更类型 影响范围判定依据 验证动作
函数体修改 AST节点Hash变化 执行本包+下游HTTP测试
接口方法新增 go/types检查方法集扩展 启动集成沙箱环境
注册顺序调整 调用链拓扑序偏移 校验中间件执行时序日志

2.5 Go单元测试覆盖率强制门禁与ASPICE工作产品交付物自动化归档(go test -json + custom reporter)

覆盖率采集与结构化输出

go test -json -coverprofile=coverage.out ./... 生成事件流式 JSON,每行含 Action, Test, Elapsed, Cover 等字段。关键在于 Action: "output" 中嵌套的覆盖率百分比文本需解析提取。

go test -json -coverprofile=coverage.out -covermode=count ./... | \
  go run coverage-reporter/main.go

coverage-reporter/main.go 是自定义 reporter:监听 Action=="pass" 事件,从 Output 字段正则提取 coverage: \d+\.\d+%,聚合模块级覆盖率并写入 report/coverage-summary.json

ASPICE交付物自动归档流程

graph TD
A[go test -json] –> B[Custom Reporter]
B –> C{覆盖率 ≥ 80%?}
C –>|Yes| D[生成 ISO 26262 兼容 XML 报告]
C –>|No| E[CI 失败并阻断 PR]
D –> F[归档至 PLM 系统 /aspice/deliverables/v1.2.0/]

归档元数据规范

字段 示例值 说明
work_product_id SW-UNIT-TEST-001 ASPICE V&V 工作产品编号
tool_version go1.22.3+coverage-reporter-v0.4.1 可追溯工具链版本
timestamp 2024-05-22T09:15:33Z UTC 时间戳,满足 ASPICE 审计要求

第三章:MISRA-Go静态规则引擎的定制化集成与裁剪

3.1 MISRA-C 2012向Go语言的安全编码原则迁移方法论与规则映射表

MISRA-C 2012的143条规则聚焦于C语言未定义行为、内存安全与可移植性,而Go通过内存模型、类型系统和运行时机制天然规避了其中62%的核心风险(如指针算术、裸指针解引用)。

核心迁移策略

  • 静态约束前移:用Go的-gcflags="-m"分析逃逸行为,替代MISRA-C Rule 18.4(动态内存分配限制)
  • 显式所有权建模:以sync.Pool+runtime.SetFinalizer模拟RAII资源生命周期管理

关键规则映射示例

MISRA-C 2012 Rule Go等效实践 安全目标
Rule 1.3 (no #undef) 禁用//go:cgo且不导出C符号 防止预处理污染
Rule 11.9 (no void*) 使用unsafe.Pointer需配//lint:ignore注释 显式标记不安全边界
// 示例:Rule 17.7(函数返回值必须使用)
func parseConfig() (cfg Config, err error) {
    cfg, err = loadYAML("config.yaml")
    if err != nil {
        return Config{}, fmt.Errorf("invalid config: %w", err) // 强制错误链封装
    }
    return cfg, nil // 不允许裸return
}

该函数强制调用方处理返回值,避免MISRA-C中“忽略返回值导致状态丢失”问题;fmt.Errorf%w动词保障错误溯源能力,对应Rule 22.5(错误处理完整性)。

graph TD
    A[MISRA-C Rule] --> B{Go语言特性适配层}
    B --> C[编译器检查]
    B --> D[静态分析工具]
    B --> E[运行时防护]
    C --> F[govet + staticcheck]
    D --> G[misrago linter]
    E --> H[panic recovery + context timeout]

3.2 基于golang.org/x/tools/go/analysis的轻量级MISRA-Go检查器开发与车载场景规则裁剪

核心架构设计

采用 analysis.Analyzer 接口实现静态检查器,避免 AST 重复遍历,天然适配 goplsgo vet 生态。

规则裁剪策略

车载嵌入式场景聚焦三类高危子集:

  • 禁止动态内存分配(new/make 在非初始化函数中)
  • 强制显式错误处理(忽略 error 返回值即告警)
  • 限定循环结构(禁用 for range 在实时任务 goroutine 中)

示例检查逻辑

var MisraGo123Analyzer = &analysis.Analyzer{
    Name: "misrago123",
    Doc:  "禁止在非init函数中调用 new/make",
    Run: func(pass *analysis.Pass) (interface{}, error) {
        for _, file := range pass.Files {
            ast.Inspect(file, func(n ast.Node) bool {
                if call, ok := n.(*ast.CallExpr); ok {
                    if fun, ok := call.Fun.(*ast.Ident); ok && 
                        (fun.Name == "new" || fun.Name == "make") {
                        if !isInitFunc(pass, call) { // 自定义上下文判断
                            pass.Reportf(call.Pos(), "MISRA-Go Rule 12.3 violation: dynamic allocation outside init")
                        }
                    }
                }
                return true
            })
        }
        return nil, nil
    },
}

该分析器通过 pass.Files 获取已解析 AST,ast.Inspect 深度遍历;isInitFunc 辅助函数基于 passFunction 栈回溯当前作用域,确保仅对 init() 或包级变量初始化上下文放行。参数 pass 提供类型信息与源码位置,支撑精准定位。

规则ID 车载裁剪依据 检查粒度
MISRA-Go 5.1 实时线程栈空间受限 函数级
MISRA-Go 12.3 防止堆碎片影响确定性调度 表达式级
graph TD
    A[Go源码] --> B[go/parser.ParseFile]
    B --> C[golang.org/x/tools/go/analysis]
    C --> D{Rule 12.3?}
    D -->|Yes| E[报告违规位置]
    D -->|No| F[继续其他规则]

3.3 静态检查结果与Jira/DOORS需求ID自动关联及DevOps看板可视化

数据同步机制

静态分析工具(如SonarQube、Coverity)输出的JSON报告需提取ruleIdlineseverity及注释中嵌入的需求标识(如REQ-123DOORS-4567)。通过正则匹配与元数据映射表实现双向绑定。

import re
# 从源码注释提取需求ID(支持Jira/DOORS双格式)
pattern = r'(REQ-\d+|DOORS-\d+|JRA-\d+)'
with open("src/module.py") as f:
    content = f.read()
req_ids = re.findall(pattern, content)  # ['REQ-204', 'DOORS-8891']

逻辑分析:该脚本在编译前扫描源码,捕获硬编码需求ID;pattern覆盖主流ID命名变体;返回列表供后续API调用注入Jira Issue Link字段。

DevOps看板集成

字段 来源 DevOps看板映射
Status SonarQube severity Blocker → “Blocked”
Requirement 提取的REQ-ID Jira Epic Link
Assignee Git author 自动分配至模块Owner

可视化流程

graph TD
    A[静态扫描] --> B{解析注释中的REQ-ID}
    B -->|匹配成功| C[调用Jira REST API创建Issue Link]
    B -->|失败| D[标记为Unlinked并告警]
    C --> E[更新Azure DevOps工作项状态]

第四章:ASAM XIL驱动的车载中间件全链路自动化测试闭环

4.1 ASAM XIL 3.0协议栈在Go中的原生实现与ECU信号仿真适配层设计

ASAM XIL 3.0 是面向ECU测试的跨平台接口标准,其核心在于抽象硬件通信、信号映射与生命周期管理。Go语言凭借原生协程、强类型接口与零依赖二进制部署能力,成为构建轻量级XIL协议栈的理想选择。

核心组件分层设计

  • Transport Layer:基于 net/rpc + 自定义二进制帧(含CRC16校验)实现低延迟XIL消息传输
  • Signal Adapter:动态信号描述符(SignalDef{ID, BitOffset, Length, Endian, Scale, Offset})驱动运行时解包逻辑
  • ECU Simulator Bridge:通过内存映射IO模拟ECU寄存器空间,支持实时读写触发回调

数据同步机制

// SignalBuffer 实现原子信号快照与版本戳
type SignalBuffer struct {
    data   []byte          // 原始字节缓冲(按XIL bit-layout填充)
    version uint64         // CAS递增版本号,供消费者检测更新
    mu     sync.RWMutex
}

func (b *SignalBuffer) ReadAtVersion(ver uint64) ([]byte, bool) {
    b.mu.RLock()
    defer b.mu.RUnlock()
    if b.version != ver { return nil, false } // 防止脏读
    return append([]byte(nil), b.data...), true // 深拷贝防并发修改
}

该设计确保仿真器与测试用例间信号状态严格一致;version 字段为XIL 3.0要求的“信号一致性标记”,ReadAtVersion 支持无锁快照语义,避免传统互斥锁导致的测试时序抖动。

组件 Go类型 XIL 3.0对应实体
XilSession struct IXilSession COM interface
SignalMapper interface{} ISignalMapping
SimulatorProxy *ecu.SimNode IEcuSimulator
graph TD
    A[Go Test Case] -->|XIL::WriteSignal| B[XilSession]
    B --> C[SignalAdapter]
    C --> D[ECU Memory-Mapped Buffer]
    D -->|Hardware Trigger| E[Simulated ECU Logic]
    E -->|Async Notify| C
    C -->|XIL::ReadSignal| A

4.2 Go中间件XIL测试用例自动生成(基于AUTOSAR XML与CAN DBC双向转换)

核心流程概览

graph TD
    A[AUTOSAR XML] --> B[解析ECU/Signal/Port]
    C[CAN DBC] --> B
    B --> D[语义对齐引擎]
    D --> E[生成XIL测试用例 YAML]

关键数据结构映射

AUTOSAR 元素 CAN DBC 对应项 XIL测试字段
I-SIGNAL BO_ + SG_ signal_name
SW-BASE-TYPE VAL_ + bit width data_type, size
PORT-PROTOTYPE Message ID can_id, frame_type

示例:信号级测试用例生成

// 从DBC解析出信号后构造XIL断言
testcase := &XILTestCase{
    SignalName: "BrakePedalPosition",
    CanID:      0x2A1,
    ExpectedValue:  uint16(1023), // 满行程ADC值
    TimeoutMs:      50,
    ValidationType: "range", // 支持 range / enum / checksum
}

该结构将DBC中SG_ BrakePedalPosition : 0|10@0+ (1,0) [0|1023] ""的物理范围映射为XIL可执行断言;TimeoutMs源自AUTOSAR TIMING-CONSTRAINTS,确保时序合规性。

4.3 XIL测试执行器与CI/CD流水线深度集成(支持Testbench启动、信号注入、时序断言与故障注入)

XIL测试执行器通过标准化REST API与Jenkins/GitLab CI解耦集成,实现FPGA验证流程自动化闭环。

核心能力矩阵

功能 实现方式 触发时机
Testbench启动 xil-run --tb=uart_tb.v 编译成功后
信号注入 JSON描述符驱动波形注入 仿真前200ns
时序断言 SVA嵌入+XIL动态使能开关 运行时实时监控
故障注入 inject -signal rst_n -type glitch -width 5ns 随机化阶段

CI触发配置示例

# .gitlab-ci.yml 片段
test_fpga:
  stage: test
  script:
    - xil-executor --config xil-ci.yaml --mode=full

xil-ci.yaml 中定义了信号注入点、断言敏感边沿及故障注入概率分布;--mode=full 启用全路径验证(含时序违例回溯)。

执行流图

graph TD
    A[CI触发] --> B[XIL解析testplan.yaml]
    B --> C{启动Questa/ModelSim}
    C --> D[加载Testbench + 注入信号]
    D --> E[启用SVA断言监控]
    E --> F[按策略注入故障]
    F --> G[生成覆盖率+违例报告]

4.4 测试报告符合ISO 26262 ASIL-B证据包要求(含TC-ID、执行环境快照、覆盖率数据嵌入)

为满足ASIL-B级可追溯性与完整性要求,测试报告需结构化嵌入三类强制证据:

  • TC-ID:唯一标识每个测试用例(如 TC-ABS-CTRL-047),支持双向追溯至安全需求(SR)与软件架构设计(SAD);
  • 执行环境快照:含编译器版本、目标ECU固件哈希、CANoe仿真配置CRC32;
  • 覆盖率数据:行覆盖(≥90%)、MC/DC覆盖(≥100%)以XML格式内联嵌入报告。
<!-- 示例:内联覆盖率片段 -->
<coverage tc-id="TC-ABS-CTRL-047" asil="B">
  <environment hash="a1b2c3d4" tool="Vector CANoe 15.0.5"/>
  <metrics lines-covered="92.3%" mc-dc-covered="100%"/>
</coverage>

该XML片段在生成PDF报告前由CI流水线注入,tc-id 与需求管理工具(如Polarion)ID严格对齐;hash 确保环境可复现;mc-dc-covered 值经Klocwork静态分析+Tessy动态验证双校验。

数据同步机制

证据链完整性校验

字段 来源系统 验证方式
TC-ID TestRail REST API响应码+正则校验
环境哈希 Jenkins构建日志 SHA256比对
MC/DC覆盖率 Tessy + LDRA JSON Schema校验
graph TD
  A[执行测试] --> B[采集环境元数据]
  B --> C[运行覆盖率工具]
  C --> D[生成带TC-ID的XML证据块]
  D --> E[嵌入PDF报告并签名]

第五章:面向SOA演进的Go车载中间件可持续演进路径

在某头部新能源车企的域控制器平台升级项目中,原有基于C++的CAN/LIN通信中间件已无法支撑智能座舱与智驾域间高频、低延迟、可订阅的服务调用需求。团队以Go语言重构中间件核心,历时14个月完成从单体通信代理到轻量级SOA运行时的渐进式演进,覆盖3类ECU(座舱主控、智驾域控、网关模块)和27个微服务端点。

架构分层治理策略

采用四层解耦设计:协议适配层(支持CAN FD、Ethernet AVB、SOME/IP over UDP)、服务抽象层(通过service.yaml声明接口契约)、运行时调度层(基于Go channel+worker pool实现异步请求分发)、生命周期管理层(集成OpenTelemetry trace ID透传与gRPC-Web兼容的健康探针)。所有层均通过接口隔离,替换CAN驱动仅需重写CanTransport接口实现,不影响上层服务注册逻辑。

版本灰度发布机制

通过Kubernetes ConfigMap动态加载服务路由规则,实现按ECU型号、固件版本、VIN前缀三维度灰度。例如,对搭载高通SA8295P芯片的车型,自动启用ZeroMQ backend替代默认TCP;对OTA升级至V2.3.1的网关节点,将诊断服务超时阈值从300ms降至180ms。下表为2024年Q2灰度配置片段:

维度类型 匹配规则 启用特性 生效服务
VIN前缀 LSVCN2E4 TLS 1.3双向认证 OTA-Proxy
固件版本 ≥3.1.0 eBPF流量整形 Diag-Service
ECU型号 TC397 内存池预分配 CAN-Gateway

服务契约自动化验证

利用Go的go:generate工具链,在CI流水线中自动生成契约测试桩。当vehicle_control.api文件变更时,触发以下流程:

go run github.com/uber-go/atomic@v1.10.0 -f vehicle_control.api \
  -o pkg/contract/vehicle_control_test.go \
  --validator=soa-contract-validator

该工具解析Protobuf定义,注入127种边界条件(如负速度指令、非法档位组合),并生成对应Mock Server。实测发现某次API变更导致制动服务在-40℃环境模拟下出现goroutine泄漏,提前拦截缺陷。

跨域安全通信加固

在车载以太网环境中,采用双通道TLS+IPSec混合加密:控制面信令走mTLS(证书绑定ECU唯一HSM密钥),数据面流媒体采用IPSec ESP隧道(SPI由TPM2.0动态派生)。所有密钥轮换操作通过UDS诊断服务触发,轮换日志实时上报至车云协同安全审计平台。

持续可观测性建设

集成Prometheus指标体系,暴露23类关键指标,包括middleware_service_latency_seconds_bucket{service="ac_control",le="0.1"}middleware_worker_queue_length{ecu="adcu"}。使用Mermaid绘制服务依赖热力图,实时反映各域控制器间调用频次与错误率:

flowchart LR
    subgraph Seat_Control
        AC[空调服务] -->|avg RT: 42ms| HVAC[HVAC控制器]
        Seat[座椅调节] -->|error rate: 0.03%| SeatECU[座椅ECU]
    end
    subgraph ADAS_Domain
        ADAS[ADAS服务] -.->|SOME/IP| SensorFusion
        SensorFusion -->|CAN FD| Radar[Radar ECU]
    end
    Seat_Control <-->|gRPC+TLS| ADAS_Domain

该演进路径已在量产车型中稳定运行超800万车公里,平均服务发现延迟降低至23ms,跨域调用成功率从92.7%提升至99.995%。

热爱 Go 语言的简洁与高效,持续学习,乐于分享。

发表回复

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