hyapp-server/docs/login-region-risk-architecture.md
2026-05-09 21:47:33 +08:00

24 KiB
Raw Blame History

Login Region Risk Architecture

本文档定义登录地区、语言和时区限制的当前推荐方案。方案按当前产品取舍设计:不引入 pending_risk 会话,不让各玩法服务分别判断风险状态;用户在未知 IP 首次登录时可以进入 App后台异步 IP 风控命中不支持国家后撤销 session 并踢下线,后续同一 IP 再登录会在入口直接拒绝。

Scope

Capability Owner Rule
真实入口 IP 提取 gateway-service 从可信上游 header 或 RemoteAddr 提取,不信任前端 JSON 里的 IP
语言和时区快速判断 gateway-service 只做入口级弱信号拦截,命中明显不支持配置时直接拒绝登录
IP 风控缓存读取 gateway-service 登录前读取 Redis IP 决策缓存,blocked 直接拒绝,allowed/unknown/miss 放行并触发复核
登录、session 和 token user-service 仍是 session owner登录成功后创建 auth_sessions 并签发 token
异步 IP 风控任务 cron-service 调度,user-service 执行 按优先级顺序查询 IP Geo 源;当前一个不可用时才降级到下一个,写持久审计和 Redis 缓存
session 撤销 user-service 命中不支持国家后撤销 session写撤销原因和审计
登录态接口统一拦截 gateway-service JWT 校验后统一检查 session 是否已撤销,避免玩法服务散落判断
房间/IM 踢下线 gateway-service 编排,room-service/腾讯云 IM 执行 只能走 Room Cell 命令或腾讯云 IM 服务端接口,不能直接改 room Redis
后台登录日志 hyapp-admin-server 只在用户管理一级菜单下新增二级菜单“登录日志”,不单独做风控策略后台

Non-Goals

Non-goal Reason
不做 pending_risk session 当前阶段要降低接入成本,避免设计白名单接口和受限权限集
不在玩法服务判断地区风险 房间、礼物、钱包、VIP、任务都不应该各自写 risk_status 判断,容易漏
不把 IP Geo 查询接口放进同步登录路径 外部接口慢、超时或不稳定会拖慢登录
不信任前端传过来的 IP 客户端字段可伪造,只能作为调试或兼容字段记录
不靠“清客户端 token”完成踢下线 JWT 已签发后客户端仍可能继续使用,必须服务端撤销 session 并由 gateway 拦截
不把 Redis 当唯一事实来源 Redis 只做快查缓存;审计、任务、明文 IP 和撤销原因必须能在 MySQL 追溯
不做独立风控后台 当前后台只提供登录日志查看不提供策略编辑、provider 管理或人工改判

Core Decision

采用“入口快拦 + 登录后异步复核 + session 统一撤销”的模型:

flowchart LR
  C["Client"] --> G["gateway-service"]
  G --> LangTZ["language/timezone fast guard"]
  G --> Cache["Redis IP decision cache"]
  Cache -->|blocked| Deny["reject login"]
  Cache -->|allowed/miss/unknown| U["user-service Auth"]
  U --> Session[("auth_sessions")]
  U --> Resp["token response"]
  U --> Job[("login_ip_risk_jobs / outbox")]
  Job --> RiskBatch["cron triggered IP risk batch"]
  RiskBatch --> IP1["IP Geo A"]
  IP1 -. "unavailable/timeout" .-> IP2["IP Geo B"]
  IP2 -. "unavailable/timeout" .-> IP3["IP Geo C"]
  IP3 -. "unavailable/timeout" .-> IP4["IP Geo D"]
  RiskBatch --> Decision[("risk decision + Redis cache")]
  RiskBatch -->|blocked| Revoke["revoke session"]
  Revoke --> Kick["IM/Room kick"]

这个方案接受一个明确边界:未知 IP 第一次可能短暂进入 App。通过 Redis 缓存和 session 撤销,同一个 IP 后续不能反复完整登录。

Login Flow

Password Login

sequenceDiagram
  participant App
  participant Gateway
  participant User as user-service
  participant Redis
  participant Risk as cron-service/user-service batch

  App->>Gateway: POST /api/v1/auth/account/login
  Gateway->>Gateway: extract trusted client_ip
  Gateway->>Gateway: check language/timezone config
  Gateway->>Redis: GET ip decision
  alt ip blocked
    Gateway-->>App: AUTH_LOGIN_BLOCKED
  else allowed/miss/unknown
    Gateway->>User: LoginPassword(meta.client_ip, language, timezone)
    User->>User: verify password and create session
    User->>User: enqueue login_ip_risk_job
    User-->>Gateway: token(session_id)
    Gateway-->>App: token
    Risk->>Risk: async resolve IP by provider fallback chain
    alt unsupported country
      Risk->>User: RevokeSession(session_id, reason)
      Risk->>Redis: SET ip decision = blocked
      Risk->>Gateway: trigger kick orchestration
    else allowed country
      Risk->>Redis: SET ip decision = allowed
    end
  end

Third Party Login

三方登录和密码登录使用同一套入口风控,只是 user-service 在创建或复用用户 session 后写风险任务。三方首次注册时,客户端提交的 country 仍然只是用户选择国家IP 国家由服务端 IP Geo 结果决定,不能用 country 替代风控国家。

Gateway Fast Guard

gateway 只做三件事:

  1. 提取可信入口 IP。
  2. languagetimezone 做简单配置判断。
  3. 查询 Redis IP 决策缓存,命中 blocked 时拒绝登录。

Trusted IP

可信 IP 来源顺序:

trusted edge header -> X-Forwarded-For first public IP -> RemoteAddr

前端提交的 ip 字段不参与判断。旧客户端如果还传 IP只能写入审计扩展字段不能作为风控依据。

Language And Timezone

语言和时区是弱信号,因为客户端可以修改。它们只能做入口快速拦截:

login_risk:
  blocked_languages:
    - zh-CN
  blocked_language_primary_tags:
    - zh
  blocked_timezones:
    - Asia/Shanghai
  blocked_timezone_prefixes:
    - Asia/

规则:

  • language 按 BCP 47 轻量格式校验,例如 en-USzh-CN
  • timezone 必须是 IANA 名称,例如 America/Los_Angeles
  • 命中阻断配置时gateway 返回 AUTH_LOGIN_BLOCKED
  • 未传、格式异常或不可信时,不直接推断国家;只记录审计。

IP Decision Cache

Redis key 建议不要保存明文 IP

login_risk:ip:{app_code}:{sha256(ip)}

Value

{
  "decision": "blocked",
  "country": "CN",
  "source": "geo_provider_chain",
  "checked_at_ms": 1710000000000,
  "expires_at_ms": 1710604800000
}

决策值:

Decision Gateway Action
blocked 登录前直接拒绝
allowed 放行登录,可低频复核
unknown 放行登录,必须异步复核
miss 放行登录,必须异步复核

TTL 建议:

Decision TTL
blocked 7 到 30 天
allowed 1 到 7 天
unknown 10 到 60 分钟

可用性策略Redis 不可用时放行登录,只记录错误和告警;不能因为 Redis 故障阻断登录。

Async IP Risk Check

异步 IP 风控 batch 按优先级顺序调用 IP Geo 源,不阻塞登录响应。一次任务只使用一个可用 provider 的结果;只有当前 provider 超时、不可用或返回不可解析结果时,才请求下一个 provider。

Job

风险任务需要包含:

Field Rule
job_id 幂等任务 ID
app_code 多 App 隔离
session_id 命中后撤销的 session
user_id 审计和踢下线目标
client_ip 明文入口 IP后台登录日志直接展示
client_ip_hash 缓存 key、索引和日志关联字段避免普通日志输出明文 IP
channel 登录渠道:account/google/facebook/twitter/tiktok
platform 客户端平台:android/ios
language 登录时客户端语言
timezone 登录时客户端时区
request_id 串联登录日志
status pending/running/completed/failed/skipped
created_at_ms 任务创建时间
updated_at_ms 任务更新时间

任务可以落 MySQL 表 login_ip_risk_jobs,也可以由 user outbox 派生。首版如果没有统一 MQ优先用 MySQL 任务表 + cron-service 调度的 batch claim避免 Redis list 丢失导致永远不复核。MySQL 写任务失败时不影响登录结果,但必须记录错误和告警。

Provider Fallback Chain

batch 行为:

  1. 先查 Redis 是否已有新鲜决策;已有 blocked/allowed 可以跳过外部查询。
  2. miss 时按配置顺序调用第一个 provider。
  3. 如果 provider 在 timeout 内返回有效国家码,立即停止查询后续 provider。
  4. 如果 provider 超时、不可用、限流、返回空国家或返回不可解析结果,记录失败原因并降级到下一个 provider。
  5. 所有 provider 都失败时,输出 unknown,写短 TTL 缓存并等待后续任务重试;如果缓存也不可用,只记录日志和告警。
  6. 有效国家码命中不支持国家时输出 blocked;有效国家码未命中不支持国家时输出 allowed
  7. 写 MySQL 决策记录和 Redis 缓存。
  8. 如果命中不支持国家,撤销当前 session 并触发踢下线。

该模型没有“结果冲突”问题,因为同一个任务不会同时采信多个 provider。provider 顺序就是信任优先级:

Case Decision
第一个可用 provider 返回不支持国家 blocked
第一个可用 provider 返回支持国家 allowed
provider 超时、不可用或返回无效结果 请求下一个 provider
所有 provider 都失败 unknown,短 TTL后续重试

不支持国家列表必须来自配置,不应硬编码在风控逻辑里;当前后台不提供策略编辑能力。

Session Revocation

命中不支持国家时,撤销 session 是核心动作。

auth_sessions.revoked_at_ms = now
auth_sessions.revoked_reason = GEO_POLICY_BLOCKED

当前表只有 revoked_at_ms,实现时建议补充:

Field Reason
revoked_reason 区分用户 logout、refresh 轮换、后台封禁、IP 风控撤销
revoked_request_id 串联风险任务和审计
revoked_by risk_workeruser_logoutadmin

撤销成功后写 Redis denylist

auth:revoked_session:{app_code}:{session_id} = GEO_POLICY_BLOCKED
TTL = access token remaining ttl + safety window

Gateway Session Guard

如果 access token 是 JWT只校验签名不够。用户已经拿到 access token 后,即使 auth_sessions 被撤销JWT 在过期前仍然能访问接口。

因此 gateway 必须增加统一 session guard

Authorization JWT valid
+ sid exists
+ auth:revoked_session:{app_code}:{sid} not exists

首版可以先只查 Redis denylist不对每个请求同步查 MySQL

  • 登录成功时不需要写 active cache。
  • 风控撤销 session 时写 denylist。
  • logout、refresh 轮换、后台封禁同样写 denylist。
  • access token TTL 到期后 denylist 自然失效。

后续如果需要更强一致性,再增加 ValidateSession RPC 或 auth:active_session cache。

所有 App 登录态接口都走 gateway middleware因此玩法服务不需要感知风险状态

flowchart TD
  Req["Business Request"] --> JWT["Verify JWT"]
  JWT --> SID["Read sid claim"]
  SID --> Deny["Check Redis revoked_session"]
  Deny -->|revoked| Fail["SESSION_REVOKED"]
  Deny -->|active| Handler["Business handler"]

Kick Offline

踢下线分三层,按能做到的程度逐步接入:

Layer Action Required
API gateway session guard 返回 SESSION_REVOKED 必须
IM 调腾讯云 IM 服务端接口踢下线或发系统消息通知客户端清 token 建议
Room 如果用户在房间,通过 room-service Room Cell 命令踢出或触发离房 建议

不能直接删除 room-service Redis presence 或麦位缓存。房间状态 owner 是 Room Cell风险踢人必须走 room-service 的命令入口;如果首版没有踢人命令,至少依靠 session guard 阻止后续 HTTP 操作,并让房间 heartbeat/断线补偿释放状态。

Error Codes

建议新增稳定业务码:

Code HTTP gRPC Meaning
AUTH_LOGIN_BLOCKED 403 PermissionDenied 登录入口命中地区、语言、时区或 IP 风控策略
SESSION_REVOKED 401 Unauthenticated session 已撤销,客户端必须清登录态

客户端行为:

  • AUTH_LOGIN_BLOCKED:停留登录页,展示地区不支持类提示。
  • SESSION_REVOKED:清本地 token退出房间/IM回登录页。
  • 普通 5xx 或网络失败不能清 token。

Audit And Observability

必须记录:

Event Fields
login_fast_guard_blocked request_id/app_code/client_ip_hash/language/timezone/block_reason
login_ip_risk_job_created request_id/app_code/session_id/user_id/client_ip/client_ip_hash/channel/platform
login_ip_geo_provider_attempt job_id/provider/order/country/status/duration_ms/error_code
login_ip_risk_decision job_id/session_id/user_id/decision/country/confidence
login_session_revoked_by_risk job_id/session_id/user_id/reason/country
kick_offline_result session_id/user_id/im_result/room_result

敏感规则:

  • 不记录明文 access token、refresh token、三方 credential。
  • 明文 IP 可以落 MySQL 业务表,供后台登录日志查看;普通服务日志仍只记录 client_ip_hash,避免日志系统大范围暴露明文 IP。
  • provider 返回原始报文不进日志,只记录本次尝试的顺序、标准化国家码、耗时和错误码。

Data Model

login_ip_risk_jobs

CREATE TABLE login_ip_risk_jobs (
  app_code VARCHAR(32) NOT NULL,
  job_id VARCHAR(96) NOT NULL,
  session_id VARCHAR(96) NOT NULL,
  user_id BIGINT NOT NULL,
  client_ip VARCHAR(64) NOT NULL,
  client_ip_hash VARCHAR(128) NOT NULL,
  channel VARCHAR(32) NOT NULL,
  platform VARCHAR(16) NOT NULL,
  language VARCHAR(32) NOT NULL DEFAULT '',
  timezone VARCHAR(64) NOT NULL DEFAULT '',
  request_id VARCHAR(96) NOT NULL DEFAULT '',
  status VARCHAR(32) NOT NULL,
  decision VARCHAR(32) NOT NULL DEFAULT '',
  country_code VARCHAR(3) NOT NULL DEFAULT '',
  failure_reason VARCHAR(128) NOT NULL DEFAULT '',
  locked_by VARCHAR(128) NULL,
  locked_until_ms BIGINT NULL,
  attempt_count INT NOT NULL DEFAULT 0,
  created_at_ms BIGINT NOT NULL,
  updated_at_ms BIGINT NOT NULL,
  PRIMARY KEY (app_code, job_id),
  KEY idx_login_ip_risk_jobs_status (app_code, status, updated_at_ms),
  KEY idx_login_ip_risk_jobs_session (app_code, session_id),
  KEY idx_login_ip_risk_jobs_user (app_code, user_id, created_at_ms),
  KEY idx_login_ip_risk_jobs_ip (app_code, client_ip_hash, updated_at_ms)
);

login_ip_risk_decisions

CREATE TABLE login_ip_risk_decisions (
  app_code VARCHAR(32) NOT NULL,
  decision_id VARCHAR(96) NOT NULL,
  client_ip VARCHAR(64) NOT NULL,
  client_ip_hash VARCHAR(128) NOT NULL,
  decision VARCHAR(32) NOT NULL,
  country_code VARCHAR(3) NOT NULL DEFAULT '',
  confidence INT NOT NULL DEFAULT 0,
  provider_attempts_json JSON NOT NULL,
  decided_at_ms BIGINT NOT NULL,
  expires_at_ms BIGINT NOT NULL,
  created_at_ms BIGINT NOT NULL,
  PRIMARY KEY (app_code, decision_id),
  KEY idx_login_ip_risk_decisions_ip (app_code, client_ip_hash, decided_at_ms),
  KEY idx_login_ip_risk_decisions_country (app_code, country_code, decided_at_ms),
  KEY idx_login_ip_risk_decisions_expires (app_code, expires_at_ms)
);

login_audit Additions

现有 login_audit 已记录登录类型、provider、结果和失败原因。为了支撑后台登录日志需要把登录入口展示字段补齐

ALTER TABLE login_audit
  ADD COLUMN channel VARCHAR(32) NOT NULL DEFAULT '',
  ADD COLUMN platform VARCHAR(16) NOT NULL DEFAULT '',
  ADD COLUMN client_ip VARCHAR(64) NOT NULL DEFAULT '',
  ADD COLUMN ip_country_code VARCHAR(3) NOT NULL DEFAULT '',
  ADD COLUMN blocked TINYINT(1) NOT NULL DEFAULT 0,
  ADD COLUMN block_reason VARCHAR(64) NOT NULL DEFAULT '',
  ADD KEY idx_login_audit_user_created (app_code, user_id, created_at_ms),
  ADD KEY idx_login_audit_blocked (app_code, blocked, created_at_ms),
  ADD KEY idx_login_audit_ip_country (app_code, ip_country_code, created_at_ms);

字段口径:

Field Rule
channel account/google/facebook/twitter/tiktok,三方 provider 必须归一成这几个展示值
platform android/ios,来自客户端请求体或可信 header
client_ip 明文入口 IP来自 gateway 提取结果
ip_country_code IP Geo 解析出的国家码,未解析时为空
blocked 当前登录是否被语言、时区、IP cache 或异步 IP 风控阻断
block_reason language/timezone/ip_cache/ip_async 等稳定原因

auth_sessions Additions

ALTER TABLE auth_sessions
  ADD COLUMN revoked_reason VARCHAR(64) NOT NULL DEFAULT '',
  ADD COLUMN revoked_request_id VARCHAR(96) NOT NULL DEFAULT '',
  ADD COLUMN revoked_by VARCHAR(64) NOT NULL DEFAULT '';

Admin Login Logs

后台管理当前只做查询和展示,不做风控配置编辑。入口位置:

用户管理
  └── 登录日志

List Page

列表按登录事件展示,默认按 created_at_ms DESC 分页。

展示字段:

Column Source
用户 ID login_audit.user_id
短号 join users.current_display_user_id
昵称 join users.username
国家/区域 join users.country/region_id,只作用户资料快照展示
登录 IP login_audit.client_ip
IP 所在国家 login_audit.ip_country_code
渠道 login_audit.channel,值为 account/google/facebook/twitter/tiktok
平台 login_audit.platform,值为 android/ios
结果 success/failed/blocked/revoked
屏蔽原因 login_audit.block_reason
登录时间 login_audit.created_at_ms

交互规则:

  • blocked=truefailure_code=AUTH_LOGIN_BLOCKED 的表格行背景显示红色。
  • 如果异步 IP 风控在登录成功后撤销 session该次登录日志也要更新为屏蔽态行背景显示红色。
  • 点击任意用户行,进入该用户历史登录日志侧栏或详情页。
  • 列表只读,不提供解除屏蔽、修改策略、重试 provider 或手动踢下线按钮。

筛选条件:

Filter Rule
用户 ID / 短号 精确搜索,短号需要先解析成 user_id
IP 精确搜索明文 IP
IP 国家 ip_country_code
渠道 account/google/facebook/twitter/tiktok
平台 android/ios
是否屏蔽 all/blocked/not_blocked
时间范围 created_at_ms

User History

点击用户行后展示该用户历史登录日志:

Field Rule
用户基础信息 user_id、短号、昵称、头像、当前国家、当前区域
最近登录列表 按时间倒序展示该用户所有登录事件
风控结果 展示 IP 国家、是否屏蔽、屏蔽原因、撤销 session 时间
渠道和平台 展示每次登录的 channel/platform

用户历史页只读。若后续需要手动封禁、解封或策略配置,必须走独立后台能力和审计,不混在登录日志里。

Configuration

首版配置放 gateway、user-service 和 cron-service 的 YAML。当前后台只做登录日志查看不管理不支持国家、provider 顺序或语言/时区策略:

login_risk:
  enabled: true
  fast_guard:
    blocked_languages: []
    blocked_language_primary_tags: []
    blocked_timezones: []
    blocked_timezone_prefixes: []
  ip:
    unsupported_countries:
      - CN
    blocked_ttl_sec: 2592000
    allowed_ttl_sec: 604800
    unknown_ttl_sec: 1800
    provider_timeout_ms: 800
    providers:
      - primary_geo
      - fallback_geo_a
      - fallback_geo_b
      - fallback_geo_c

# services/cron-service/configs/config*.yaml
tasks:
  login_ip_risk:
    enabled: true
    interval: 5s
    timeout: 3s
    lock_ttl: 30s
    batch_size: 100

配置原则:

  • gateway 快速判断和 user-service IP 风控策略的不支持国家列表必须同源,避免 gateway 放行但后台风控用另一套规则。
  • provider 顺序必须显式配置,顺序代表信任优先级;不要在代码里随机切换。
  • 线上配置不要写 provider secretprovider key 走密钥系统或环境变量。
  • 多 App 时按 app_code 覆盖策略。

Failure Policy

Failure Policy
Redis 不可用 放行登录;记录错误和告警,不读写 IP decision cachesession denylist 失效属于踢下线实时性下降,不能阻断登录
MySQL 写登录日志或风险任务失败 放行登录;记录 error 和告警,后续依赖下次登录或缓存命中补偿
所有 IP provider 都失败 放行登录;能写缓存则写 unknown 短 TTL不能写缓存也不阻塞
前置 provider 不可用 自动降级到下一个 provider如果全部不可用则放行
session 已经 logout 后台风控撤销幂等成功,不重复踢
用户已换新 session 只撤销命中的 session_id;如果 IP 已 blocked后续登录会被 gateway 拒绝
踢 IM 失败 session 已撤销仍然生效;记录补偿任务或等待客户端下一次 API 返回 SESSION_REVOKED
房间踢出失败 不直接改 Redis记录补偿依赖 Room Cell/heartbeat 兜底

Implementation Sequence

  1. 增加 AUTH_LOGIN_BLOCKED 错误码,并确认 gateway 对 SESSION_REVOKED 的客户端语义。
  2. gateway 登录入口增加 language/timezone fast guard 和 IP decision cache 读取。
  3. 登录请求把 client_ip/language/timezone 透传到 user-service密码登录也需要补这两个字段。
  4. user-service 登录成功后创建 login_ip_risk_jobs,任务至少包含 session_id/user_id/client_ip/client_ip_hash/channel/platform/request_id
  5. 扩展 login_audit,保存明文 IP、IP 国家、渠道、平台、是否屏蔽和屏蔽原因。
  6. 实现 UserCronService.ProcessLoginIPRiskBatchMySQL claim 任务,按优先级顺序调用 provider fallback chain写决策和 Redis cache。
  7. user-service 增加按 session_id 撤销 session 的内部能力,并写撤销原因。
  8. gateway 增加统一 session denylist middlewareJWT 校验后查 auth:revoked_session:{app_code}:{sid}
  9. 风控撤销 session 时写 Redis denylist并把对应登录审计更新为屏蔽态。
  10. 接入腾讯云 IM 踢下线或系统消息通知。
  11. 增加 room-service 踢人命令或复用现有 Room Cell 管理命令,不直接操作 room Redis。
  12. admin 在用户管理下新增二级菜单“登录日志”,支持列表、筛选、红色屏蔽行和用户历史登录日志。
  13. 增加日志、指标和告警provider 超时率、unknown 比例、blocked 数、撤销数、踢下线失败数。

Test Cases

Case Expected
language 命中阻断 gateway 直接返回 AUTH_LOGIN_BLOCKED,不调用 user-service 登录
timezone 命中阻断 gateway 直接返回 AUTH_LOGIN_BLOCKED
IP cache blocked gateway 直接返回 AUTH_LOGIN_BLOCKED
IP cache miss 登录成功,创建风险任务
后台风控判定 allowed 写 allowed cache不撤销 session
后台风控判定 blocked 撤销 session写 blocked cache写 denylist触发踢下线
blocked 后同 IP 再登录 gateway 登录前拒绝
已撤销 session 调业务接口 gateway middleware 返回 SESSION_REVOKED
provider 全失败 放行,能写缓存则写 unknown cache不踢下线
第一个 provider 不可用、第二个 provider 返回 blocked country 写 blocked cache撤销 session
登录日志列表含 blocked 行 该行背景为红色
点击用户登录日志行 展示该用户历史登录日志
refresh 使用已撤销 session user-service 返回 SESSION_REVOKED

Open Decisions

Decision Recommendation
Redis/MySQL/IP 查询不可用时是否阻断登录 当前方案统一 fail-open全部放行只记录错误和告警
第一个可用 provider 命中不支持国家是否踢 当前方案直接踢provider 顺序代表信任优先级
明文 IP 是否落库 明文 IP 落 MySQL普通服务日志仍只打 hash
是否做后台管理页面 只做用户管理下的“登录日志”二级菜单,不做策略编辑后台

Final Rule

当前阶段最重要的工程约束是:地区风险只在 gateway 入口和 user-service session 层处理,玩法服务不感知风险状态。只要 gateway 统一检查 session denylist后台异步命中后撤销 session 就能对房间、钱包、礼物、VIP、消息等所有 HTTP 业务统一生效。