案例 26:订单防刷单风控校验
本案例对应 JQuick-Java 仓库 USE-CASES 场景 26「订单防刷单风控校验」,属于电商风控领域。在秒杀、满减、发券等营销活动期间,对每笔活动订单做实时防刷单决策:六个维度独立评分汇总为 0-100 的风险总分,再叠加 Java 侧两条硬规则前置短路,最终输出放行(PASS)、人工复核(REVIEW)或拦截(REJECT)。
大促期间黑产常使用批量注册的新号、设备池/IP 池聚集账户、秒杀脚本高频下单等手段套取优惠券、补贴与限量商品。平台需要在订单链路上对每笔营销活动订单实时采集风险要素,并在毫秒内给出处置结论。
评分口径拆成六个互相独立的维度,各有分值上限:账户龄风险(0-20)、下单频次风险(0-25,含 1 小时档与 24 小时档两段叠加)、设备/IP 聚集风险(0-20,设备档与 IP 档叠加)、身份与地址风险(0-15)、券单与空包风险(0-10,用券订单档与历史空包率档叠加)、客单价偏离风险(0-10)。六维得分相加并在 Service 侧封顶到 100 分。
除软评分外,Service 还在 Java 侧维护两条不可被分数稀释的硬规则:未实名账户且 24 小时内同设备关联账户数达到 5 个,判定为「黑产设备池」;1 小时内下单达到 5 单且本单客单价偏离历史水平达到 400%,判定为「秒杀脚本特征」。命中任一硬规则,或总分达到 70 分则拦截;40 至 69 分转人工复核;其余放行。
决策输出物为 CampaignFraudDecision:风险总分、处置动作、是否命中硬规则、六个维度的逐条得分明细(scoreDetails),以及按要素阈值收集的风险标签(riskTags,如「注册未满 7 天」「同设备关联多账户」「历史空包率高」等),供风控运营复核与对客解释使用。
- 大促攻防以周甚至天为单位变化,黑产换一批账号与设备特征,评分档位和硬规则阈值若硬编码在交易系统里,每次调参都要走全量发版,赶不上黑产节奏。
- 六个风险维度需要独立调参、灰度甚至按活动差异化配置(如券单维度在发券日临时收紧),写在同一段
if-else中无法单独维护与回溯。 - 硬否决与软评分必须分层:秒杀脚本哪怕只有 30 分也必须拦截,传统把所有条件混在一张决策表里的做法,很难解释「分数不高为什么也被拦」。
- 商家或用户申诉时,需要逐维度还原得分依据与命中标签,黑盒式风控结论无法审计、无法举证。
- 老交易中台多运行在 JDK 8 与信创配套 JDK 上,难以引入需要独立部署、带重型仓储与控制台的规则平台。
- 设备指纹、IP 聚集、空包率等信号来自不同数据域,聚合后要求在下单链路毫秒级返回,不能承受跨进程规则服务的网络开销。
本案例采用「入参模型 + 决策模型 + 规则接口 + XML 规则脚本 + Service 编排 + JUnit 测试」六件套结构:规则接口只声明六维评分口径,阈值分档全部外置在 XML,Service 负责汇总、封顶、硬规则短路与标签组装。
| 层次 | 职责 | 对应文件 |
|---|---|---|
| 领域模型层 | 承载营销订单刷单风险要素,输出分数、动作、明细与风险标签 | CampaignOrder.java、CampaignFraudDecision.java |
| 规则接口层 | 声明六个评分方法,入参以 @Param 按名绑定 |
CampaignFraudRules.java |
| 规则脚本层 | 六维阈值分档与分值全部外置,支持热更新 | campaign-fraud.xml |
| 编排与测试层 | 汇总评分、100 分封顶、硬规则前置短路、标签装配;6 组订单形态回归 | CampaignFraudService.java、CampaignFraudServiceTest.java |
规则方法拆分
Section titled “规则方法拆分”| 规则方法 | 口径 | 分值/输出 |
|---|---|---|
| scoreAccountAge | 按账户注册天分档:不满 1 天 20 分;不满 7 天 14 分;不满 30 天 8 分;不满 90 天 3 分;其余 0 分 | 0-20 |
| scoreFrequency | 1 小时档(≥5 单加 18、≥3 单加 12、≥1 单加 5)与 24 小时档(≥15 单加 7、≥8 单加 4、≥3 单加 2)相加 | 0-25 |
| scoreDevice | 同设备账户档(≥5 加 14、≥3 加 8、≥2 加 4)与同 IP 账户档(≥10 加 6、≥5 加 4、≥3 加 2)相加 | 0-20 |
| scoreIdentity | 未实名加 8 分;付款人与下单人不一致加 4 分;虚拟/集中代收地址加 3 分 | 0-15 |
| scoreCoupon | 7 天用券订单档(≥10 加 6、≥5 加 4、≥2 加 2)与历史空包率档(≥50% 加 4、≥20% 加 2)相加 | 0-10 |
| scoreDeviation | 客单价偏离率 ≥400% 加 10、≥200% 加 6、≥100% 加 3,否则 0 | 0-10 |
代码实现(完整可运行)
Section titled “代码实现(完整可运行)”将下列文件按原始包名放入 JQuick-Java 测试工程:两个模型放入 src/test/java/com/github/paohaijiao/demo/model/,规则接口与测试类放入 src/test/java/com/github/paohaijiao/demo/,Service 放入 src/test/java/com/github/paohaijiao/demo/service/,XML 脚本放入 src/test/resources/demo/。在工程根目录执行 mvn test -Dtest=CampaignFraudServiceTest 即可运行。
CampaignOrder.java
Section titled “CampaignOrder.java”package com.github.paohaijiao.demo.model;
import lombok.AllArgsConstructor;import lombok.Data;import lombok.NoArgsConstructor;
/** * 营销活动订单的刷单风险要素 */@Data@NoArgsConstructor@AllArgsConstructorpublic class CampaignOrder {
/** * 订单号 */ private String orderId;
/** * 下单用户 */ private String userId;
/** * 活动编号 */ private String campaignId;
/** * 账户注册天数 */ private int accountAgeDays;
/** * 近 1 小时下单数 */ private int orderCount1h;
/** * 近 24 小时下单数 */ private int orderCount24h;
/** * 24 小时内同设备关联账户数 */ private int deviceAccountCount24h;
/** * 24 小时内同 IP 关联账户数 */ private int ipAccountCount24h;
/** * 7 天内使用优惠券的订单数 */ private int couponOrderCount7d;
/** * 历史空包率(整数百分比) */ private int emptyPackageRate;
/** * 本单客单价相对历史均单的偏离率(整数百分比,如 400 表示高出 4 倍) */ private int amountDeviationPercent;
/** * 是否已实名认证 */ private boolean realName;
/** * 付款人与下单人实名是否一致 */ private boolean paymentConsistent;
/** * 收货地址是否为虚拟/集中代收点 */ private boolean virtualAddress;}CampaignFraudDecision.java
Section titled “CampaignFraudDecision.java”package com.github.paohaijiao.demo.model;
import lombok.Data;
import java.util.ArrayList;import java.util.List;
/** * 营销刷单风控决策 */@Datapublic class CampaignFraudDecision {
/** * 订单号 */ private String orderId;
/** * 风险总分(0-100,越高越危险) */ private int riskScore;
/** * 处置动作:PASS 放行 / REVIEW 复核 / REJECT 拦截 */ private String action;
/** * 是否命中硬规则 */ private boolean hardRuleHit;
/** * 各维度得分明细 */ private List<String> scoreDetails = new ArrayList<String>();
/** * 命中的风险标签 */ private List<String> riskTags = new ArrayList<String>();
public void addDetail(String detail) { this.scoreDetails.add(detail); }
public void addRiskTag(String tag) { this.riskTags.add(tag); }}CampaignFraudRules.java
Section titled “CampaignFraudRules.java”package com.github.paohaijiao.demo;
import com.github.paohaijiao.xml.param.Param;
/** * 场景 26:营销活动订单防刷单六维风险评分 */public interface CampaignFraudRules {
/** * 账户龄风险(0-20) */ int scoreAccountAge(@Param("days") int days);
/** * 下单频次风险(0-25):1 小时档 + 24 小时档 */ int scoreFrequency(@Param("count1h") int count1h, @Param("count24h") int count24h);
/** * 设备/IP 聚集风险(0-20) */ int scoreDevice(@Param("deviceAccounts") int deviceAccounts, @Param("ipAccounts") int ipAccounts);
/** * 身份与地址风险(0-15) */ int scoreIdentity(@Param("realName") boolean realName, @Param("paymentConsistent") boolean paymentConsistent, @Param("virtualAddress") boolean virtualAddress);
/** * 券单与空包风险(0-10) */ int scoreCoupon(@Param("couponOrders") int couponOrders, @Param("emptyRate") int emptyRate);
/** * 客单价偏离风险(0-10) */ int scoreDeviation(@Param("deviationPercent") int deviationPercent);}campaign-fraud.xml
Section titled “campaign-fraud.xml”<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE javas PUBLIC "-//PAOHAIJIAO//DTD API JAVA 1.0//EN" "classpath:paohaijiao/dtd/Jquick-java.dtd"><javas namespace="com.github.paohaijiao.demo.CampaignFraudRules">
<java name="scoreAccountAge" returnClass="int"> <![CDATA[ int def scoreAccountAge(int:days) { if (days < 1) { return 20; } else if (days < 7) { return 14; } else if (days < 30) { return 8; } else if (days < 90) { return 3; } else { return 0; } } ]]> </java>
<java name="scoreFrequency" returnClass="int"> <![CDATA[ int def scoreFrequency(int:count1h, int:count24h) { int score = 0; if (count1h >= 5) { score = score + 18; } else if (count1h >= 3) { score = score + 12; } else if (count1h >= 1) { score = score + 5; } if (count24h >= 15) { score = score + 7; } else if (count24h >= 8) { score = score + 4; } else if (count24h >= 3) { score = score + 2; } return score; } ]]> </java>
<java name="scoreDevice" returnClass="int"> <![CDATA[ int def scoreDevice(int:deviceAccounts, int:ipAccounts) { int score = 0; if (deviceAccounts >= 5) { score = score + 14; } else if (deviceAccounts >= 3) { score = score + 8; } else if (deviceAccounts >= 2) { score = score + 4; } if (ipAccounts >= 10) { score = score + 6; } else if (ipAccounts >= 5) { score = score + 4; } else if (ipAccounts >= 3) { score = score + 2; } return score; } ]]> </java>
<java name="scoreIdentity" returnClass="int"> <![CDATA[ int def scoreIdentity(boolean:realName, boolean:paymentConsistent, boolean:virtualAddress) { int score = 0; if (realName) { } else { score = score + 8; } if (paymentConsistent) { } else { score = score + 4; } if (virtualAddress) { score = score + 3; } return score; } ]]> </java>
<java name="scoreCoupon" returnClass="int"> <![CDATA[ int def scoreCoupon(int:couponOrders, int:emptyRate) { int score = 0; if (couponOrders >= 10) { score = score + 6; } else if (couponOrders >= 5) { score = score + 4; } else if (couponOrders >= 2) { score = score + 2; } if (emptyRate >= 50) { score = score + 4; } else if (emptyRate >= 20) { score = score + 2; } return score; } ]]> </java>
<java name="scoreDeviation" returnClass="int"> <![CDATA[ int def scoreDeviation(int:deviationPercent) { if (deviationPercent >= 400) { return 10; } else if (deviationPercent >= 200) { return 6; } else if (deviationPercent >= 100) { return 3; } else { return 0; } } ]]> </java>
</javas>CampaignFraudService.java
Section titled “CampaignFraudService.java”package com.github.paohaijiao.demo.service;
import com.github.paohaijiao.demo.CampaignFraudRules;import com.github.paohaijiao.demo.model.CampaignFraudDecision;import com.github.paohaijiao.demo.model.CampaignOrder;
/** * 场景 26:营销活动订单防刷单风控 * <p> * 六维风险评分(账户龄/频次/设备聚集/身份/券单空包/客单偏离)叠加硬规则拦截。 */public class CampaignFraudService {
private final CampaignFraudRules rules;
public CampaignFraudService(CampaignFraudRules rules) { this.rules = rules; }
public CampaignFraudDecision check(CampaignOrder order) { CampaignFraudDecision d = new CampaignFraudDecision(); d.setOrderId(order.getOrderId());
int ageScore = rules.scoreAccountAge(order.getAccountAgeDays()); int freqScore = rules.scoreFrequency(order.getOrderCount1h(), order.getOrderCount24h()); int deviceScore = rules.scoreDevice(order.getDeviceAccountCount24h(), order.getIpAccountCount24h()); int identityScore = rules.scoreIdentity(order.isRealName(), order.isPaymentConsistent(), order.isVirtualAddress()); int couponScore = rules.scoreCoupon(order.getCouponOrderCount7d(), order.getEmptyPackageRate()); int deviationScore = rules.scoreDeviation(order.getAmountDeviationPercent());
d.addDetail("账户龄: " + ageScore + " / 20"); d.addDetail("下单频次: " + freqScore + " / 25"); d.addDetail("设备与IP聚集: " + deviceScore + " / 20"); d.addDetail("身份与地址: " + identityScore + " / 15"); d.addDetail("券单与空包: " + couponScore + " / 10"); d.addDetail("客单价偏离: " + deviationScore + " / 10");
int total = ageScore + freqScore + deviceScore + identityScore + couponScore + deviationScore; if (total > 100) { total = 100; }
boolean devicePoolHit = !order.isRealName() && order.getDeviceAccountCount24h() >= 5; boolean scriptHit = order.getOrderCount1h() >= 5 && order.getAmountDeviationPercent() >= 400; boolean hardHit = devicePoolHit || scriptHit; d.setHardRuleHit(hardHit); if (devicePoolHit) { d.addRiskTag("黑产设备池:未实名账户聚集同一设备"); } if (scriptHit) { d.addRiskTag("秒杀脚本特征:1 小时高频下单且客单价严重偏离"); }
collectSoftTags(d, order);
if (hardHit || total >= 70) { d.setAction("REJECT"); } else if (total >= 40) { d.setAction("REVIEW"); } else { d.setAction("PASS"); } d.setRiskScore(total); return d; }
private void collectSoftTags(CampaignFraudDecision d, CampaignOrder o) { if (o.getAccountAgeDays() < 1) { d.addRiskTag("新注册账户(不满 1 天)"); } else if (o.getAccountAgeDays() < 7) { d.addRiskTag("注册未满 7 天"); } if (o.getOrderCount1h() >= 5) { d.addRiskTag("1 小时内高频下单"); } if (o.getOrderCount24h() >= 15) { d.addRiskTag("24 小时下单频次异常"); } if (o.getDeviceAccountCount24h() >= 5) { d.addRiskTag("同设备关联多账户"); } if (o.getIpAccountCount24h() >= 10) { d.addRiskTag("同 IP 关联多账户"); } if (!o.isRealName()) { d.addRiskTag("账户未实名认证"); } if (!o.isPaymentConsistent()) { d.addRiskTag("付款人与下单人不一致"); } if (o.isVirtualAddress()) { d.addRiskTag("虚拟/集中代收收货地址"); } if (o.getCouponOrderCount7d() >= 10) { d.addRiskTag("7 天用券订单异常集中"); } if (o.getEmptyPackageRate() >= 50) { d.addRiskTag("历史空包率高"); } if (o.getAmountDeviationPercent() >= 400) { d.addRiskTag("客单价严重偏离历史水平"); } }}CampaignFraudServiceTest.java
Section titled “CampaignFraudServiceTest.java”package com.github.paohaijiao.demo;
import com.github.paohaijiao.demo.model.CampaignFraudDecision;import com.github.paohaijiao.demo.model.CampaignOrder;import com.github.paohaijiao.demo.service.CampaignFraudService;import com.github.paohaijiao.xml.JQuickJava;import org.junit.Assert;import org.junit.Before;import org.junit.Test;
/** * 场景 26:营销活动订单防刷单 * <p> * 覆盖 6 组订单形态:正常老客放行、新号高频临界 40 分复核、黑产满分拦截、 * 秒杀脚本硬规则拦截、疑似刷券 54 分复核、70 分纯评分拦截边界。 */public class CampaignFraudServiceTest {
private CampaignFraudService service;
@Before public void setUp() { CampaignFraudRules rules = JQuickJava.create() .createApi(CampaignFraudRules.class, "demo/campaign-fraud.xml"); this.service = new CampaignFraudService(rules); }
/** * 资深实名老客、行为平稳 → 0 分放行 */ @Test public void shouldPassNormalLoyalCustomer() { CampaignOrder o = base("ORD001"); o.setAccountAgeDays(400); o.setOrderCount24h(1);
CampaignFraudDecision d = service.check(o);
Assert.assertEquals(0, d.getRiskScore()); Assert.assertEquals("PASS", d.getAction()); Assert.assertFalse(d.isHardRuleHit()); }
/** * 注册 3 天新号、4 单/小时、10 单/日、设备 2 账户、3 张券 → 恰好 40 分复核 */ @Test public void shouldReviewNewAccountAtScoreForty() { CampaignOrder o = base("ORD002"); o.setAccountAgeDays(3); o.setOrderCount1h(4); o.setOrderCount24h(10); o.setDeviceAccountCount24h(2); o.setIpAccountCount24h(4); o.setCouponOrderCount7d(5); o.setAmountDeviationPercent(50);
CampaignFraudDecision d = service.check(o);
Assert.assertEquals(40, d.getRiskScore()); Assert.assertEquals("REVIEW", d.getAction()); Assert.assertFalse(d.isHardRuleHit()); Assert.assertTrue(d.getScoreDetails().contains("账户龄: 14 / 20")); Assert.assertTrue(d.getRiskTags().contains("注册未满 7 天")); }
/** * 黑产设备池全套特征 → 100 分并命中硬规则拦截 */ @Test public void shouldRejectFullFraudProfile() { CampaignOrder o = base("ORD003"); o.setAccountAgeDays(0); o.setOrderCount1h(8); o.setOrderCount24h(20); o.setDeviceAccountCount24h(6); o.setIpAccountCount24h(12); o.setCouponOrderCount7d(12); o.setEmptyPackageRate(60); o.setAmountDeviationPercent(500); o.setRealName(false); o.setPaymentConsistent(false); o.setVirtualAddress(true);
CampaignFraudDecision d = service.check(o);
Assert.assertEquals(100, d.getRiskScore()); Assert.assertEquals("REJECT", d.getAction()); Assert.assertTrue(d.isHardRuleHit()); Assert.assertTrue(d.getRiskTags().contains("黑产设备池:未实名账户聚集同一设备")); Assert.assertTrue(d.getRiskTags().contains("同设备关联多账户")); Assert.assertTrue(d.getRiskTags().contains("历史空包率高")); }
/** * 其余特征干净,但 1 小时 6 单且客单偏离 450% → 秒杀脚本硬规则拦截 */ @Test public void shouldRejectScriptPatternByHardRule() { CampaignOrder o = base("ORD004"); o.setAccountAgeDays(500); o.setOrderCount1h(6); o.setOrderCount24h(6); o.setAmountDeviationPercent(450);
CampaignFraudDecision d = service.check(o);
Assert.assertEquals(30, d.getRiskScore()); Assert.assertEquals("REJECT", d.getAction()); Assert.assertTrue(d.isHardRuleHit()); Assert.assertTrue(d.getRiskTags().contains("秒杀脚本特征:1 小时高频下单且客单价严重偏离")); }
/** * 注册 5 天、设备 3 账户 IP 5 个、5 张券空包率 25%、偏离 220% → 54 分复核 */ @Test public void shouldReviewCouponAbuseAtScoreFiftyFour() { CampaignOrder o = base("ORD005"); o.setAccountAgeDays(5); o.setOrderCount1h(3); o.setOrderCount24h(9); o.setDeviceAccountCount24h(3); o.setIpAccountCount24h(5); o.setCouponOrderCount7d(5); o.setEmptyPackageRate(25); o.setAmountDeviationPercent(220);
CampaignFraudDecision d = service.check(o);
Assert.assertEquals(54, d.getRiskScore()); Assert.assertEquals("REVIEW", d.getAction()); Assert.assertTrue(d.getScoreDetails().contains("设备与IP聚集: 12 / 20")); Assert.assertTrue(d.getScoreDetails().contains("券单与空包: 6 / 10")); }
/** * 恰好 70 分:未命中任何硬规则,纯评分线拦截 */ @Test public void shouldRejectAtExactScoreSeventy() { CampaignOrder o = base("ORD006"); o.setAccountAgeDays(5); o.setOrderCount1h(6); o.setOrderCount24h(20); o.setDeviceAccountCount24h(2); o.setIpAccountCount24h(4); o.setCouponOrderCount7d(5); o.setEmptyPackageRate(0); o.setAmountDeviationPercent(220); o.setRealName(false); o.setPaymentConsistent(false); o.setVirtualAddress(true);
CampaignFraudDecision d = service.check(o);
Assert.assertEquals(70, d.getRiskScore()); Assert.assertEquals("REJECT", d.getAction()); Assert.assertFalse(d.isHardRuleHit()); Assert.assertTrue(d.getRiskTags().contains("付款人与下单人不一致")); Assert.assertTrue(d.getRiskTags().contains("虚拟/集中代收收货地址")); }
private CampaignOrder base(String orderId) { CampaignOrder o = new CampaignOrder(); o.setOrderId(orderId); o.setUserId("U10001"); o.setCampaignId("CAMP2026"); o.setAccountAgeDays(365); o.setOrderCount1h(0); o.setOrderCount24h(0); o.setDeviceAccountCount24h(1); o.setIpAccountCount24h(1); o.setCouponOrderCount7d(0); o.setEmptyPackageRate(0); o.setAmountDeviationPercent(0); o.setRealName(true); o.setPaymentConsistent(true); o.setVirtualAddress(false); return o; }}在工程根目录执行:
mvn test -Dtest=CampaignFraudServiceTest测试覆盖形态(共 6 个 @Test 方法):
| 测试方法 | 业务形态 | 关键断言/期望决策 |
|---|---|---|
| shouldPassNormalLoyalCustomer | 400 天资深实名老客、24 小时仅 1 单,行为平稳 | riskScore=0、action=PASS、hardRuleHit=false |
| shouldReviewNewAccountAtScoreForty | 注册 3 天新号、1 小时 4 单、24 小时 10 单、设备 2 账户、5 张券、偏离 50% | riskScore=40、action=REVIEW、未命中硬规则;明细含「账户龄: 14 / 20」;标签含「注册未满 7 天」 |
| shouldRejectFullFraudProfile | 0 天新号、8 单/小时、设备 6 账户、IP 12 账户、12 张券、空包率 60%、偏离 500%、未实名/不一致/虚拟地址 | riskScore=100、action=REJECT、hardRuleHit=true;含「黑产设备池」「同设备关联多账户」「历史空包率高」标签 |
| shouldRejectScriptPatternByHardRule | 其余特征干净(500 天老客),仅 1 小时 6 单且客单偏离 450% | riskScore=30 但 action=REJECT、hardRuleHit=true;标签含「秒杀脚本特征:1 小时高频下单且客单价严重偏离」 |
| shouldReviewCouponAbuseAtScoreFiftyFour | 注册 5 天、设备 3 账户、IP 5 账户、5 张券、空包率 25%、偏离 220% | riskScore=54、action=REVIEW;明细含「设备与IP聚集: 12 / 20」「券单与空包: 6 / 10」 |
| shouldRejectAtExactScoreSeventy | 恰好 70 分但不满足任何硬规则组合 | action=REJECT、hardRuleHit=false,验证纯评分边界;标签含「付款人与下单人不一致」「虚拟/集中代收收货地址」 |
- 规则外置热更新:六维阈值档位全部写在 campaign-fraud.xml,大促期间调分值、加档位只需改 XML 重建代理,交易系统无需重新发版。
- 一方法一口径、可独立调参与审计:六个评分方法各管一个维度,Service 把每维得分写入 scoreDetails(如「设备与IP聚集: 12 / 20」),决策链路逐维可回溯。
- Java 侧封顶与硬否决前置:总分 clamp 到 100;黑产设备池、秒杀脚本两条硬规则在评分之外短路,30 分也能直接 REJECT,硬否决与软评分分层清晰。
- 双档交叉相乘/叠加口径:频次(1 小时档 + 24 小时档)、设备聚集(设备档 + IP 档)、券单(券单档 + 空包档)均在一个方法内两段独立判档后相加,复合风险信号不丢档。
- 正向分支规避取反:scoreIdentity 脚本以「命中正向条件走空分支、else 加分」的写法表达未实名/不一致扣分,避免在脚本中书写
!取反,口径直白不易写错。 @Param按名绑定 + 代理复用:参数名即 XML 形参名,签名顺序与脚本解耦;规则接口由 ASM 动态代理实现,无独立进程与重型依赖,JDK 8 遗留系统与信创环境可直接嵌入。