Posted in

【最后窗口期】H3C即将停用Telnet协议(2025Q2起),Go开发者必须掌握的SSH+REST+NETCONF迁移路线图

第一章:H3C网络设备协议演进与Go语言适配背景

H3C作为国内主流网络设备厂商,其管理协议栈历经从私有CLI脚本解析、SNMPv2c/v3基础监控,到Comware V7/V9平台全面支持RESTful API、NETCONF over SSH及Telemetry gRPC的演进。早期运维工具多依赖Python+Paramiko模拟SSH会话,存在连接复用率低、并发能力弱、TLS握手开销大等问题;而SNMP虽标准化程度高,但缺乏对动态配置变更、YANG模型驱动和结构化数据推送的支持。

随着云网融合场景深化,运营商与大型企业对自动化编排、秒级故障感知和轻量级Agent部署提出更高要求。Go语言凭借其原生协程调度、静态链接二进制、零依赖部署及对HTTP/2、gRPC、JSON Schema的内置支持,成为构建新一代网络管控组件的理想选择。H3C自Comware V9.5起正式开放基于YANG 1.1定义的RESTCONF接口,并提供OpenConfig兼容的模块化数据模型,为Go生态集成奠定语义基础。

H3C协议能力关键演进节点

  • CLI时代:仅支持SSH交互式命令,需正则解析非结构化输出(如display interface brief)
  • SNMP时代:MIB-II扩展受限,私有MIB维护成本高,无法表达嵌套配置(如QoS策略树)
  • RESTCONF/NETCONF时代:支持RFC 8040标准,可直接PUT/PATCH /restconf/data/huawei-device:device-config 路径
  • Telemetry时代:通过gRPC订阅/h3c-telemetry:telemetry-stream,实现毫秒级接口丢包率、CPU利用率流式推送

Go客户端对接H3C RESTCONF示例

// 初始化带Bearer Token认证的HTTP客户端(需提前通过OAuth2获取token)
client := &http.Client{
    Transport: &http.Transport{
        TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, // 生产环境应校验证书
    },
}
req, _ := http.NewRequest("GET", "https://192.168.1.1/restconf/data/h3c-ifm:interfaces/interface=Ten-GigabitEthernet1/0/1", nil)
req.Header.Set("Accept", "application/yang-data+json")
req.Header.Set("Authorization", "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...")

resp, err := client.Do(req)
if err != nil {
    log.Fatal(err) // 实际项目中应使用结构化错误处理
}
defer resp.Body.Close()
// 响应体为标准RFC 8040格式JSON,可直接Unmarshal至Go struct

第二章:Go语言SSH客户端深度实践

2.1 SSH协议原理与H3C交换机SSH服务配置验证

SSH(Secure Shell)基于TCP 22端口,采用非对称加密(RSA/ECDSA)完成密钥交换与身份认证,后续会话使用对称加密(AES-128-CBC)保障传输机密性。

SSH连接建立流程

# H3C交换机启用SSH服务的关键配置
[SW] public-key local create rsa      # 生成本地RSA主机密钥(2048位)
[SW] ssh server enable                # 全局启用SSH服务器
[SW] user-interface vty 0 4
[SW-ui-vty0-4] authentication-mode scheme  # 启用AAA认证模式
[SW-ui-vty0-4] protocol inbound ssh        # 仅允许SSH接入

该配置强制VTY线路使用SSH协议,并依赖AAA模块完成用户名/密码或公钥认证;public-key local create rsa是SSH服务启动前提,缺失将导致ssh server enable静默失败。

认证方式对比

方式 安全性 管理复杂度 适用场景
密码认证 中 低 临时运维、小规模
DSA/RSA公钥 高 中 生产环境推荐

协议交互时序

graph TD
    A[客户端发起TCP连接] --> B[服务端发送公钥指纹]
    B --> C[客户端验证并生成会话密钥]
    C --> D[加密协商参数+用户认证]
    D --> E[建立加密Shell通道]

2.2 使用golang.org/x/crypto/ssh构建安全连接池与会话复用

SSH 连接建立开销大,频繁新建连接易引发认证延迟与资源耗尽。golang.org/x/crypto/ssh 原生不提供连接池,需基于 ssh.Client 和 sync.Pool 自行封装。

连接池核心结构

type SSHPool struct {
    pool *sync.Pool
    cfg  *ssh.ClientConfig
}

func NewSSHPool(cfg *ssh.ClientConfig) *SSHPool {
    return &SSHPool{
        cfg: cfg,
        pool: &sync.Pool{
            New: func() interface{} {
                return &sshSession{client: nil} // 懒初始化 client
            },
        },
    }
}

sync.Pool 复用 *sshSession 实例,避免重复分配;ClientConfig 预置密钥、超时与主机验证策略,确保每次复用均满足安全约束。

会话复用关键约束

复用维度 是否支持 说明
TCP 连接 ✅ 底层 net.Conn 可复用(需保持活跃)
SSH 会话(session) ❌ 每次 client.NewSession() 创建独立通道,但可并发复用同一 client
认证上下文 ✅ ClientConfig 一次性加载私钥,无状态复用
graph TD
    A[获取连接] --> B{Pool 中有空闲 client?}
    B -->|是| C[复用已认证 client]
    B -->|否| D[新建 TCP + SSH handshake + auth]
    C & D --> E[调用 client.NewSession]
    E --> F[执行命令/传输文件]

2.3 基于SSH的CLI命令交互封装:结构化输出解析与错误注入测试

封装核心类设计

使用 paramiko 构建可复用的 SSH 会话管理器,支持命令超时、编码自动协商与结构化响应建模:

class SSHEngine:
    def __init__(self, host, user, key_path=None):
        self.client = paramiko.SSHClient()
        self.client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
        self.client.connect(host, username=user, key_filename=key_path, timeout=10)

    def execute(self, cmd, parse_fn=json.loads):  # 默认尝试 JSON 解析
        stdin, stdout, stderr = self.client.exec_command(cmd)
        out = stdout.read().decode('utf-8').strip()
        err = stderr.read().decode('utf-8').strip()
        return {"output": out, "error": err, "parsed": parse_fn(out) if out else None}

逻辑分析:parse_fn 可动态注入(如 lambda x: yaml.safe_load(x)),实现协议无关的结构化解析;timeout=10 防止阻塞,AutoAddPolicy 仅用于测试环境。

错误注入测试策略

通过预设异常响应模拟网络中断、权限拒绝等场景:

注入类型 触发方式 预期行为
PermissionDenied chmod 000 /bin/ls stderr 非空,parsed=None
Timeout sleep 15 && echo ok 抛出 socket.timeout

输出解析流程

graph TD
    A[执行CLI命令] --> B{stdout非空?}
    B -->|是| C[调用parse_fn解析]
    B -->|否| D[返回空parsed]
    C --> E[验证schema一致性]
    D --> E
    E --> F[返回结构化结果]

2.4 多厂商兼容性抽象:H3C vs Cisco vs Juniper SSH行为差异处理

网络自动化脚本常因厂商SSH交互细节不同而中断:H3C默认禁用pty分配,Cisco在enable后需等待#提示符,Juniper则要求显式发送\n触发%提示符。

关键差异速查表

厂商 默认PTY分配 特权模式进入方式 提示符匹配模式
H3C False super 15 [>\]]
Cisco True enable [#>]
Juniper True cli [%#>]

自适应连接初始化逻辑

def init_session(device):
    # 根据vendor动态配置pty和prompt regex
    pty = device.vendor != "h3c"  # H3C需显式关闭pty避免阻塞
    prompt = r"[#%>]" if device.vendor == "juniper" else r"[#>]"
    return paramiko.SSHClient().connect(
        hostname=device.ip,
        username=device.user,
        password=device.passwd,
        allow_agent=False,
        look_for_keys=False,
        pty=pty  # 关键:H3C必须设为False
    )

参数说明:pty=False防止H3C因伪终端未就绪挂起;prompt正则覆盖Juniper特有的%提示符;look_for_keys=False规避厂商SSH密钥协商差异。

行为适配流程

graph TD
    A[建立SSH连接] --> B{识别vendor}
    B -->|H3C| C[禁用PTY,发送super命令]
    B -->|Cisco| D[启用PTY,等待#提示符]
    B -->|Juniper| E[启用PTY,发送\\n触发CLI]
    C --> F[执行命令]
    D --> F
    E --> F

2.5 生产级SSH连接管理:超时控制、密钥轮换与审计日志集成

连接超时防护

避免空闲会话长期占用资源,需在 /etc/ssh/sshd_config 中配置:

ClientAliveInterval 300     # 每5分钟发送一次保活探测
ClientAliveCountMax 3      # 连续3次无响应则断开
TCPKeepAlive yes           # 启用底层TCP保活(辅助作用)

ClientAliveInterval 从应用层触发心跳,比 TCPKeepAlive 更可控;CountMax=3 确保网络抖动不误断,兼顾健壮性与安全性。

密钥生命周期自动化

推荐使用 ssh-keygen -t ed25519 -C "prod-$(date +%Y%m%d)" 生成带时间戳标识的密钥,并纳入CI/CD流水线执行轮换。关键策略包括:

  • 密钥有效期 ≤ 90 天
  • 轮换前自动推送新公钥至目标主机 authorized_keys
  • 旧密钥保留7天灰度期后强制清理

审计日志集成方案

日志源 输出目标 关键字段
sshd 服务日志 Syslog + ELK pam_unix(sshd:session)
用户命令审计 auditd 规则 execve, pty 事件
graph TD
    A[SSH登录请求] --> B{PAM认证}
    B --> C[auditd捕获session_open]
    B --> D[syslog记录AUTH_SUCCESS]
    C --> E[ELK聚合分析]
    D --> E

审计链路覆盖身份验证、会话建立与命令执行三层,满足等保三级日志留存要求。

第三章:REST API对接H3C Comware V7/V9设备

3.1 H3C RESTful API权限模型与Token认证流程Go实现

H3C设备RESTful API采用基于OAuth 2.0精简模型的Bearer Token认证机制,需先获取access_token,再在后续请求中携带于Authorization: Bearer <token>头。

认证核心流程

  • 向/api/v1/login POST用户名密码,获取含access_token、expires_in和refresh_token的JSON响应
  • Token有效期通常为3600秒,过期后需用refresh_token续期或重新登录
  • 所有受保护接口必须校验JWT签名及scope字段(如network:read)

Go客户端Token管理示例

// 获取并缓存Token(线程安全)
func (c *H3CClient) Login() error {
    resp, err := c.httpClient.Post(
        c.baseURL + "/api/v1/login",
        "application/json",
        strings.NewReader(fmt.Sprintf(`{"username":"%s","password":"%s"}`, c.user, c.pass)),
    )
    if err != nil { return err }
    defer resp.Body.Close()

    var tokenRes struct {
        AccessToken string `json:"access_token"`
        ExpiresIn   int    `json:"expires_in"` // 秒级有效期
        RefreshToken string `json:"refresh_token"`
    }
    if err := json.NewDecoder(resp.Body).Decode(&tokenRes); err != nil {
        return err
    }

    // 使用time.Now().Add(time.Second * time.Duration(tokenRes.ExpiresIn))计算过期时间
    c.token = tokenRes.AccessToken
    c.expiresAt = time.Now().Add(time.Second * time.Duration(tokenRes.ExpiresIn))
    return nil
}

该实现封装了凭证交换逻辑,ExpiresIn用于本地时效判断,避免频繁重登录;access_token后续通过http.Header.Set("Authorization", "Bearer "+c.token)注入请求头。

Token校验关键字段对照表

字段名 类型 说明 示例
access_token string JWT签名令牌 eyJhbGciOiJIUzI1NiIs...
expires_in int 有效秒数 3600
scope string 权限范围 network:read network:write
graph TD
    A[客户端发起Login请求] --> B[服务端验证凭据]
    B --> C{验证成功?}
    C -->|是| D[签发JWT Token+refresh_token]
    C -->|否| E[返回401 Unauthorized]
    D --> F[客户端缓存Token及expires_at]
    F --> G[后续请求自动注入Authorization头]

3.2 使用net/http+json构建设备资源CRUD操作DSL

设计原则

以 RESTful 风格为基石,将设备资源抽象为 /api/devices/{id} 路径,动词映射标准 HTTP 方法(GET/POST/PUT/DELETE),请求体与响应体统一采用 JSON 序列化。

核心路由注册

func setupDeviceRoutes(mux *http.ServeMux) {
    mux.HandleFunc("GET /api/devices", listDevices)      // 查询全部
    mux.HandleFunc("POST /api/devices", createDevice)    // 创建
    mux.HandleFunc("GET /api/devices/{id}", getDevice)   // 单查
    mux.HandleFunc("PUT /api/devices/{id}", updateDevice) // 全量更新
    mux.HandleFunc("DELETE /api/devices/{id}", deleteDevice) // 删除
}

{id} 由 http.StripPrefix + 路径解析提取,避免依赖第三方路由器;所有处理器接收 http.ResponseWriter 和 *http.Request,通过 json.Decode/Encode 实现双向序列化。

设备结构体契约

字段 类型 必填 说明
ID string 是 UUID 格式主键
Name string 是 设备名称
Status string 否 online/offline

数据同步机制

graph TD
    A[HTTP Client] -->|POST /api/devices| B[createDevice]
    B --> C[JSON Decode → Device struct]
    C --> D[Validate & Store]
    D -->|201 Created| E[JSON Encode Response]

3.3 REST接口幂等性设计与批量配置提交的事务语义模拟

幂等性实现核心:唯一操作标识(Idempotency-Key)

客户端在请求头中携带 Idempotency-Key: <uuid>,服务端基于该键缓存响应状态(200/409)及结果体,避免重复执行。

# 幂等操作装饰器示例
def idempotent_handler(key_func=lambda r: r.headers.get("Idempotency-Key")):
    def decorator(f):
        def wrapper(request):
            key = key_func(request)
            if not key:
                return {"error": "Missing Idempotency-Key"}, 400
            cached = redis.get(f"idemp_{key}")  # TTL=24h
            if cached:
                return json.loads(cached), 200
            result = f(request)  # 实际业务逻辑
            redis.setex(f"idemp_{key}", 86400, json.dumps(result))
            return result, 201
        return wrapper
    return decorator

逻辑分析:key_func 提取标识,Redis 缓存响应(含状态码与body),TTL 防止键无限膨胀;201 表示首次成功创建,200 返回缓存结果体现幂等性。

批量提交的事务语义模拟

操作类型 是否原子 回滚机制 状态一致性
单资源PUT 是 自然幂等 强一致
批量PATCH 否 客户端补偿 最终一致

状态机驱动的批量执行流程

graph TD
    A[接收Batch Request] --> B{校验所有id存在?}
    B -->|否| C[返回422 + 错误明细]
    B -->|是| D[预占资源锁]
    D --> E[逐项执行并记录step_result]
    E --> F{全部成功?}
    F -->|是| G[提交并释放锁 → 200]
    F -->|否| H[触发补偿写入failed_steps → 207]

补偿式事务的关键字段

  • x-batch-id: 全局批次标识,用于追踪与重试
  • x-step-index: 当前子操作序号,支撑部分失败定位
  • retry-after: 失败时建议重试间隔(秒)

第四章:NETCONF over SSH协议的Go原生支持方案

4.1 NETCONF 1.1协议栈解析:、、消息建模

NETCONF 1.1(RFC 6241)通过三层消息结构实现设备可编程控制:会话建立、操作请求与响应反馈。

<hello> 消息:能力协商起点

<hello xmlns="urn:ietf:params:xml:ns:netconf:base:1.0">
  <capabilities>
    <capability>urn:ietf:params:netconf:base:1.1</capability>
    <capability>urn:ietf:params:netconf:capability:writable-running:1.0</capability>
  </capabilities>
</hello>

该消息在SSH会话建立后立即交换,<capability>元素声明支持的协议特性(如writable-running表示可直接写入运行配置),是后续操作合法性的前提。

<rpc> 与 <rpc-reply>:原子操作闭环

元素 作用 约束
<rpc> 封装操作指令(如 <get-config>) 必含 message-id 属性
<rpc-reply> 返回执行结果或错误 必须与请求 message-id 匹配
graph TD
  A[客户端发送<rpc message-id=“101”>] --> B[服务器验证并执行]
  B --> C{成功?}
  C -->|是| D[<rpc-reply message-id=“101”><data>…</data></rpc-reply>]
  C -->|否| E[<rpc-reply message-id=“101”><rpc-error>…</rpc-error></rpc-reply>]

4.2 基于go-netconf库实现Capabilities协商与YANG数据模型绑定

NETCONF会话启动时,客户端与服务端需通过<hello>消息交换并协商双方支持的capabilities(如urn:ietf:params:netconf:capability:yang:1.1),以确定YANG模型解析边界。

Capabilities协商流程

// 构建带能力声明的Hello消息
hello := netconf.Hello{
    Capabilities: []string{
        "urn:ietf:params:netconf:capability:base:1.1",
        "urn:ietf:params:netconf:capability:yang:1.1",
        "urn:ietf:params:netconf:capability:with-defaults:1.0",
    },
}

该代码显式声明客户端支持YANG 1.1及默认值处理能力;go-netconf自动将其序列化为标准<hello>XML,并在连接建立后首帧发送。服务端响应的<hello>将返回其实际支持的能力集,库据此启用对应解析器。

YANG模型绑定机制

绑定方式 适用场景 工具链支持
ygot生成Go结构 静态编译时强类型校验 ✅ ygen
gnmi动态映射 运行时路径灵活订阅 ⚠️ 需额外适配层
graph TD
    A[NETCONF Session] --> B[Hello Exchange]
    B --> C{Capabilities Match?}
    C -->|Yes| D[Load YANG Schema]
    C -->|No| E[Fail or Fallback]
    D --> F[Bind to ygot-generated structs]

4.3 使用ietf-yang-schema生成Go结构体并执行原子操作

YANG Schema到Go结构体的自动化映射

使用 yang2go 工具(基于 ietf-yang-schema)可将 YANG 模块(如 ietf-interfaces.yang)一键生成类型安全的 Go 结构体:

yang2go -path ./yang -output ./gen/interfaces.go ietf-interfaces.yang

逻辑说明:-path 指定YANG依赖路径,-output 控制生成目标,工具自动解析 container/list/leaf 并映射为嵌套 struct、切片与基本类型,支持 xml.Name 标签以匹配 NETCONF XML 命名空间。

构建并提交原子 edit-config 请求

生成结构体后,通过 gnmi 或 netconf 客户端序列化为 <edit-config> XML:

字段 作用 示例值
Target 配置目标数据库 "candidate"
Operation CRUD动作 "merge"
Data 序列化后的Go结构体 &interfaces.Interfaces{...}
req := &netconf.EditConfig{
    Target: "candidate",
    Config: interfaces.NewInterfaces().WithInterface("eth0").Up(),
}
client.EditConfig(ctx, req)

参数说明:WithInterface("eth0") 返回链式构建器,.Up() 设置 admin-status = "up";EditConfig 自动注入命名空间并确保 <config> 内容符合 YANG schema 约束。

数据一致性保障机制

graph TD
    A[Go Struct] -->|JSON/YAML/Proto| B[Schema Validation]
    B --> C[XML Marshaling]
    C --> D[NETCONF <edit-config>]
    D --> E[Server端YANG验证]
    E --> F[原子提交或回滚]

4.4 错误恢复机制:session重连、RPC超时退避与状态追踪

三重保障设计哲学

当网络抖动或服务端短暂不可用时,客户端需避免雪崩式失败。本机制融合 session 级连接韧性、RPC 智能退避、以及 commit 状态的幂等确认。

session 自愈重连

# 基于指数退避的 session 重建
def reconnect_with_backoff():
    delay = min(10.0, 0.5 * (2 ** attempt))  # 初始0.5s,上限10s
    time.sleep(delay)
    return create_session(timeout=30)  # 新 session 绑定新 TCP 连接

逻辑分析:attempt 从0开始递增,防止重试风暴;timeout=30 确保新建会话不阻塞主线程;退避上限避免过长等待影响 SLA。

RPC 超时分级策略

场景 初始超时 最大重试 退避因子
读请求(GET) 800ms 2 1.5
写请求(POST) 2500ms 1 —

状态追踪流程

graph TD
    A[客户端发起 commit] --> B{服务端返回 202 Accepted?}
    B -->|是| C[启动异步轮询 /status]
    B -->|否| D[立即失败并触发重试]
    C --> E[收到 COMMITTED/ABORTED]
    E --> F[本地状态机更新]

幂等性关键约束

  • 每次 commit 请求携带唯一 request_id
  • 服务端对重复 request_id 直接返回历史结果,不二次执行

第五章:面向2025Q2停服的全链路迁移工程落地建议

识别停服影响范围与关键依赖路径

2025年第二季度,阿里云经典网络ECS、腾讯云CVM旧版API网关及AWS EC2-Classic实例将统一终止服务。某省级政务云平台在2024年10月完成资产扫描后确认:17个核心业务系统(含社保征缴、不动产登记、医保结算)均直接调用已标记为“Deprecated”的v1.2版认证中间件,该中间件依赖EC2-Classic底层网络栈。通过curl -X GET https://api.gov-cloud/v1/health?detail=true接口探针验证,32%的API请求在模拟停服环境下返回503 Service Unavailable。

制定分阶段灰度迁移节奏

采用“三周冲刺+双周观察”滚动周期:第一阶段(2025-Q1-W1至W3)完成测试环境全链路切流;第二阶段(Q1-W4至Q2-W2)在生产环境按业务权重分批切换——优先迁移非实时类系统(如档案查询),最后攻坚高并发交易链路(如电子发票签发)。下表为首批迁移系统优先级矩阵:

系统名称 日均TPS 数据一致性要求 依赖停服组件 预估迁移耗时
公共信用查询 842 最终一致 ✅ 3人日
工程项目监管 2156 强一致 ✅✅ 11人日
政策文件库 198 最终一致 ❌ 2人日

构建自动化验证流水线

在GitLab CI中嵌入三项强制检查:① 每次提交触发kubectl get pods -n legacy | grep "classic"检测残留组件;② 使用Prometheus指标比对新旧链路P95延迟偏差(阈值≤50ms);③ 执行基于OpenAPI 3.0规范的契约测试,校验迁移后接口响应字段完整性。示例验证脚本片段:

# 验证JWT令牌兼容性
curl -H "Authorization: Bearer $(cat token_v1.jwt)" \
     https://api.new-gov.gov/v2/user/profile | jq '.id, .version' \
     | grep -q "v2" && echo "✅ v1 token accepted by v2 API"

建立跨厂商灾备通道

当发现某地市政务外网专线在迁移窗口期出现≥3分钟抖动时,立即启用预置的混合云应急通道:通过华为云Stack与天翼云SD-WAN建立BGP路由冗余,将流量经由上海IDC节点绕行。该通道已在2024年12月实测承载峰值12.8Gbps流量,RTO控制在47秒内。

设计熔断降级兜底策略

在API网关层部署Sentinel规则,当新链路错误率超15%持续60秒时自动触发降级:将用户请求重定向至本地缓存静态页(含业务说明与客服入口),同时向运维告警群推送结构化事件:

{
  "event": "MIGRATION_FALLBACK_ACTIVATED",
  "service": "e-invoice-core",
  "fallback_type": "cache_redirect",
  "impact_scope": ["shanghai", "nanjing"]
}

开展真实场景压力演练

2025年3月15日联合三大运营商开展“停服倒计时72小时”实战压测:模拟10万终端设备同步发起电子证照下载请求,暴露Redis集群Key倾斜问题(热点Key cert:2025Q2:batch_07 QPS达42k)。通过动态分片策略将该Key拆分为cert:2025Q2:batch_07:shard_{0..3},最终达成单节点负载均衡。

启用双向流量镜像监控

在Envoy代理配置中开启traffic_mirror功能,将10%生产流量同步转发至影子集群,对比新旧链路在SQL慢查询、HTTP状态码分布、TLS握手耗时三个维度的差异。Mermaid流程图展示关键监控路径:

graph LR
A[生产流量] --> B[Envoy Sidecar]
B --> C{镜像分流}
C -->|10%| D[影子集群]
C -->|90%| E[新生产集群]
D --> F[Prometheus Metrics Diff]
E --> G[APM链路追踪]
F & G --> H[自动化差异报告]

组织跨团队联合值守机制

成立由开发、SRE、网络、安全四组组成的“迁移作战室”,实行每日18:00-22:00联合值班。值班表采用轮换制,每班次配备1名架构师+2名运维工程师+1名DBA,所有操作必须执行“双人确认+录屏存档”,操作指令需经堡垒机审计日志留存至少180天。

记录分布式系统搭建过程,从零到一,步步为营。

发表回复

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