简介:阿里云短信接口的 .NET 开发示例包,面向需要在业务系统中接入用户验证、通知推送等短信能力的 C#/.NET 开发者,覆盖 AccessKey 鉴权、Action/PhoneNumbers/SignName/TemplateCode/TemplateParam 参数组装、发送请求与响应处理的完整调用链路,并提示签名模板审核及发送频率限制等注意事项。压缩包共 92 个文件,以 68 个 C# 源码、18 个动态链接库为主,搭配工程文件、说明文档、配置文件和示例截图,整体仅 485KB,解压后可在 Visual Studio 中直接打开工程跟进发送、接收等模块。目录将 API 示例、消息接收示例和 SDK 核心动态库分开组织,便于快速定位发送短信与接收回执的代码段,也适合对照源码理解阿里云短信服务的签名机制和 REST 请求构造。已有 580 人学习/下载,对后端开发者而言,这是比官方文档更直观的落地参考,既能快速复用封装好的发送逻辑,也能参考其中的错误处理与重试思路,减少接入时的踩坑成本。
1. 从“验证码发不出去”的课设现场说起:短信接口方案到底难在哪
一次课设验收现场,A同学演示用户注册模块:输入手机号、点击获取验证码,短信迟迟不来。排错半小时,最终发现根本不是代码问题——账号用的是主账号密钥,短信服务开通了但没申请正式签名,模板还在审核中,接口却在走线上链路。这是短信接口开发最典型的起点:代码本身不难,难在把账号、签名、模板、接口调用这几件事串成一条完整链路。
这篇方案以云厂商短信服务为例,从需求背景一路讲到落地验收,覆盖账号体系、签名模板申请、验证码发送实现、回执状态跟踪、频率控制和答辩技巧,适合正在做课设或毕设、需要给系统接入短信验证码的同学,也适合要给业务系统补充短信通知能力的开发者参考。
2. 总体架构设计:短信能力在业务系统中的定位与调用链路
2.1 短信服务在系统里的位置:独立能力层而不是业务模块
很多同学会把验证码发送逻辑直接写在用户Service里,比如注册方法内部直接new一个SmsClient。代码少、流程短,一旦要给系统加通知、告警,就要复制一堆重复代码。更麻烦的是,所有业务的发送记录会堆到同一个地方,出问题根本没法排查。所以我会把短信能力单独抽成一个服务模块,任何业务要发短信,只跟这个模块打交道。这个模块对外暴露四个能力:发送验证码、发送通知、查询回执、频率校验。对内它只依赖数据库、缓存和短信服务商的OpenAPI,不依赖业务模块的任何实体,注册登录、密码找回、管理端告警接进来成本都是一致的。
| 子模块 | 职责 | 关键依赖 |
|---|---|---|
| SmsController | 对外提供REST入口,只做参数校验和结果包装 | Spring MVC |
| SmsService | 场景码映射、模板参数组装、落库、频控判断 | MySQL / Redis |
| SmsClient | 计算签名、发起HTTP请求、解析返回值 | HTTP客户端 |
| SmsQueryService | 主动拉取发送回执、更新记录状态 | 定时任务 |
实际项目里我还会在SmsService外面加一层接口拦截器,把手机号格式校验、IP限流这些横切逻辑放到拦截器里。这样划分之后,答辩时也能讲得更完整:“我把短信能力做成了独立模块,业务通过场景码对接”,比“我在登录里写了一个发短信的方法”听起来有价值得多。
2.2 调用链路设计:三段式请求与回执状态机
短信发送的调用链路可以抽象成三段:业务层、服务层、OpenAPI层。业务层只传手机号和场景码,比如REGISTER场景传入phone=13800000000和scene=REGISTER;服务层拿场景码去查模板配置,生成验证码、组装参数,把请求交给SmsClient;到了OpenAPI层才真正发起网络请求。这里最容易被忽略的一点是:OpenAPI返回“请求成功”不代表手机已经收到短信,只表示服务商受理了这条消息,最终是否送达要看回执状态。
所以设计时要单独定义状态机。通常做法是准备四个状态:PENDING(待发送)、SUCCESS(已送达)、FAILED(发送失败)、UNKNOWN(未知,等待回执)。请求提交成功落PENDING,服务商推送或主动查询到成功就更新成SUCCESS,失败记录错误码并进入重试判断。不要把“受理成功”和“发送成功”混成一个状态,否则会出现记录显示成功、用户却收不到短信的情况,这种问题排查起来很玄学。
这里给一份场景码到模板的映射表,后续实现可以直接照搬:
| 场景码 | 业务含义 | 模板内容示例 | 过期时间 |
|---|---|---|---|
| REGISTER | 注册验证码 | 您的注册验证码为${code},5分钟内有效 | 5分钟 |
| LOGIN | 登录验证码 | 您的登录验证码为${code},5分钟内有效 | 5分钟 |
| RESET_PWD | 找回密码 | 您的密码重置验证码为${code},10分钟内有效 | 10分钟 |
模板内容不要写死在前端,也不要散落在业务代码里,统一存到sms_template表中。业务只传场景码,服务商侧模板有变动时只改配置,不用重新发版。
2.3 数据结构设计:发送记录表与幂等防重字段
发送记录是整个短信方案的“黑匣子”,出了问题全靠它回溯。我一般会建一张sms_send_record表,核心字段如下。
CREATE TABLE `sms_send_record` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', `biz_id` varchar(64) NOT NULL COMMENT '业务幂等键,由调用方生成', `phone` varchar(20) NOT NULL COMMENT '接收手机号', `scene_code` varchar(32) NOT NULL COMMENT '场景码,如REGISTER', `template_code` varchar(64) NOT NULL COMMENT '服务商侧模板编号', `content` varchar(255) NOT NULL COMMENT '实际发送的短信内容', `status` tinyint NOT NULL DEFAULT '0' COMMENT '状态:0等待 1成功 2失败 3未知', `request_id` varchar(64) DEFAULT NULL COMMENT '服务商返回的请求ID', `error_code` varchar(64) DEFAULT NULL COMMENT '失败时服务商返回的错误码', `send_time` datetime NOT NULL COMMENT '发送时间', `callback_time` datetime DEFAULT NULL COMMENT '回执时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_biz_id` (`biz_id`), KEY `idx_phone_time` (`phone`, `send_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='短信发送记录表';biz_id是幂等防重字段的关键。发送前由调用方生成,可以是UUID或“场景码-手机号-时间戳”拼出的字符串,重复提交时唯一索引会直接拦截,避免用户狂点按钮导致一条短信发两次。phone和send_time联合索引服务查询接口,按手机号拉近期记录时不会全表扫描。status字段配合后面的回执模块使用,答辩时要说清楚:从0到1是正常送达,从0到2说明被服务商拒了,长时间停在0或3就需要补偿重试。
3. 接入准备与技术选型:账号体系、签名模板与客户端封装
3.1 账号体系与权限分配:子账号才能把风险关进笼子
接入短信服务商的第一步不是写代码,而是理清账号体系。很多同学图省事,把主账号的AccessKey直接写进配置文件。调试阶段看着没问题,但这相当于把整个账号的钥匙挂在项目里——代码一旦传到公开仓库,别人拿到密钥不仅能发短信,还能操作主账号下的其他云资源,风险非常大。
通常做法是先创建一个访问控制子账号,只给它分配短信服务的发送和查询权限,不授予任何其他资源权限;如果服务商支持IP白名单,再把部署机器的出口IP绑上。主账号只用来做总览和权限管理,不参与业务运行。这样即使子账号密钥泄露,影响范围也只在短信发送,不至于牵连整个云资源。
| 对比项 | 主账号密钥 | 短信子账号密钥 |
|---|---|---|
| 权限范围 | 账号下所有资源 | 仅短信发送与查询 |
| 泄露影响 | 整个账号被控制 | 只能发短信(仍需频控兜底) |
| 适用场景 | 不推荐用于业务代码 | 测试、生产环境均推荐 |
我的习惯是测试环境和生产环境各建一个子账号,密钥分开管理。配置文件里不写明文,从环境变量或配置中心读取。答辩时被问“安全怎么考虑的”,你能答出“最小权限加密钥分离”,这比单纯说“我用的是服务商给的Key”高出一个层次。
3.2 签名与模板申请:审核不通过,代码写得再好也白搭
云短信服务有一个反直觉的约束:不是有了AccessKey就能发短信,必须先通过“签名”和“模板”的审核。签名就是短信开头的【某某】,比如应用名称、公司名称或网站域名;模板是短信正文的骨架。两者都要经过服务商人工审核才能在线上使用,审核规则值得提前研究。
签名的申请材料和命名强相关:个人开发者通常用应用名称,并提交应用商店展示链接;公司主体可以用公司名称,附上营业执照信息。签名内容要和材料对得上,申请一个跟应用名完全无关的签名,大概率被驳回。模板这边最常见的坑是变量太多,正文里塞四五个${},服务商对模板变量数量有限制,一般建议控制在3个以内。变量名尽量用code、link这类英文,不要用中文变量名,部分系统里会出诡异问题。
调试阶段有个便利项:多数服务商提供测试签名和测试模板给开发者调通链路,比如【测试】签名加固定格式正文。先用测试资源把代码跑通,正式签名和模板审核通过后再切换,避免正式资源没下来阻塞开发进度。
3.3 客户端选型:用通用HTTP方式封装,不绑死厂商SDK
接短信服务商时通常要做选择题:用官方SDK还是自己写HTTP客户端。官方SDK按文档能快速跑起来,但版本迭代快,不同大版本方法名不一样,还会把依赖带重。课设或中小型项目里,我更推荐直接用HTTP客户端封装,把签名逻辑握在自己手里,出问题可以直接调试请求报文,换服务商时改动也小。
云厂商短信接口的通用认证思路是:公共参数和业务参数按字典序拼接,再用密钥计算签名,签名放进请求。下面是自研SmsClient的骨架,核心是参数排序和签名计算。
public class SmsClient { private String accessKeyId; private String accessKeySecret; private String endpoint; // 例如 https://sms-api.example.com/send public SmsResult send(String phone, String templateCode, Map<String, String> templateParams) { // 1. 组装公共参数,TreeMap保证字典序 Map<String, String> params = new TreeMap<>(); params.put("AccessKeyId", accessKeyId); params.put("PhoneNumbers", phone); params.put("TemplateCode", templateCode); // 模板变量需要JSON序列化,例如用Jackson的ObjectMapper params.put("TemplateParam", new ObjectMapper().writeValueAsString(templateParams)); params.put("Timestamp", formatIso8601(new Date())); params.put("SignatureNonce", UUID.randomUUID().toString()); // 2. 计算签名:真实环境请使用所对接服务商要求的签名算法 String stringToSign = buildStringToSign(params); String signature = hmacSha256(stringToSign, accessKeySecret); // 3. 发起POST请求并解析结果 String body = HttpUtils.post(endpoint, params, signature); return parseResult(body); } }几个容易忽略的点:参数必须用TreeMap保证字典序,拼接时换行符或编码不一样都会导致签名不合法,这是新手最常见的翻车原因;SignatureNonce每次请求都要随机生成,防止重放攻击,不能复用;Timestamp建议用ISO8601格式,并且和服务商服务器的时间偏差在容差范围内,本机时钟不对会直接鉴权失败。这套方法不依赖厂商SDK,换服务商只需要改endpoint和签名算法,业务代码完全不用动。
3.4 环境隔离:通过配置切换测试与生产资源
签名模板有了,密钥分好了,下一步是环境隔离。最简单有效的方式是Spring的Profile机制,dev和prod各放一套配置。
# application-dev.yml sms: endpoint: https://sms-test.example.com/send access-key-id: ${SMS_TEST_AK} access-key-secret: ${SMS_TEST_SK} sign-name: 【测试签名】 template-code: SMS_TEST_TPL_001 enabled: true # application-prod.yml sms: endpoint: https://sms-api.example.com/send access-key-id: ${SMS_PROD_AK} access-key-secret: ${SMS_PROD_SK} sign-name: 【正式签名】 template-code: SMS_PROD_TPL_001 enabled: true密钥没有硬编码在yml里,通过环境变量引用,代码仓库里永远不出现真实密钥。enabled开关是总开关:联调阶段如果不想真发短信,设为false时SmsService直接返回模拟成功,本地开发不产生费用。这个开关建议保留到生产环境,服务商故障需要紧急下线短信能力时,改配置比改代码快得多。
环境隔离最典型的坑是:测试环境误用了正式签名和正式模板,调试时少量发送问题不大,压测时可能把服务商侧的流控阈值打满。所以测试环境固定用测试签名和测试模板,上线前人工确认prod环境的签名、模板、密钥三者全换成正式资源,这条写进验收清单。
4. 核心实现流程:验证码发送、状态回执与频率控制的落地代码
4.1 验证码发送:从场景码到出网请求的完整方法
有了客户端封装,业务层代码就清爽多了。发送验证码的核心思路:先做频控检查,再生成验证码,写Redis,调SmsClient发送,最后落库。
@Service public class SmsServiceImpl implements SmsService { private static final String CODE_PREFIX = "sms:code:"; public SmsSendResult sendVerifyCode(String sceneCode, String phone) { // 1. 频控检查:60秒内不能重复发 if (!frequencyLimiter.tryAcquire(sceneCode, phone)) { return SmsSendResult.fail("请求太频繁,请稍后再试"); } // 2. 生成6位随机验证码,去掉容易混淆的0和1 String code = generateCode(6); // 3. 先写Redis,key带场景和手机号,TTL为过期时间 String cacheKey = CODE_PREFIX + sceneCode + ":" + phone; stringRedisTemplate.opsForValue().set(cacheKey, code, 5, TimeUnit.MINUTES); // 4. 组装模板参数,调客户端发送 Map<String, String> templateParam = Collections.singletonMap("code", code); SmsResponse response = smsClient.send(phone, fetchTemplateCode(sceneCode), templateParam); // 5. 落库,bizId作为幂等键 smsRecordMapper.insert(buildRecord(sceneCode, phone, code, response)); return SmsSendResult.success(response.getRequestId()); } }generateCode有个容易被忽视的细节:直接random.nextInt(999999)可能生成“012345”这种带0开头的码,读起来容易混淆。我一般用排除0和1的字符集,比如“23456789”,循环6次随机取字符。fetchTemplateCode负责按场景码查模板编号,避免模板Code硬编码在业务方法里。发送顺序也有讲究:先写Redis再发HTTP,如果反过来,Redis写入失败时用户收到了验证码却永远校验不通过,这类问题排查起来极其耗时。
4.2 存储与幂等:Redis缓存加数据库双写配合
验证码存储分缓存和持久化两层,各管各的事。Redis管校验:验证码只能活几分钟,TTL自动实现过期逻辑,不需要定时任务清理。数据库管审计:每条发送记录保留,用于排查问题和核对费用。原则是缓存先行,落库不阻塞主流程。
public boolean verifyCode(String sceneCode, String phone, String inputCode) { String cacheKey = CODE_PREFIX + sceneCode + ":" + phone; String savedCode = stringRedisTemplate.opsForValue().get(cacheKey); if (StringUtils.isEmpty(savedCode)) { return false; // 验证码不存在或已过期 } if (!savedCode.equals(inputCode)) { // 连续错误3次,直接删除key,避免被暴力穷举 if (attemptLimiter.incrementAndCheck(sceneCode, phone) >= 3) { stringRedisTemplate.delete(cacheKey); } return false; } // 验证成功后主动删除key,验证码一次性使用 stringRedisTemplate.delete(cacheKey); return true; }验证成功后立即删除,保证一个验证码只能用一次。连续错误3次也删除,防止破解者在5分钟内反复尝试。严格场景下“先比较再删除”需要原子性,并发量大时可以用Lua脚本实现compare-and-delete,课设阶段用上面这段代码足够。
4.3 状态回执:主动拉取与异步回调的取舍
短信发送接口返回成功只代表受理,不代表送达。拿到最终状态有两种途径:配置回调,服务商把结果POST到Webhook;轮询查询接口,按时间窗口主动拉取。对课设和中小型项目,推荐主动拉取:回调要配外网可达的地址还要做安全校验,本地开发没法联调;主动查询只要一个定时任务,逻辑直观,演示方便。
@Component public class SmsStatusSyncer { public void syncStatus() { // 只查最近10分钟、状态为“等待”的记录 List<SmsSendRecord> pendingList = smsRecordMapper.selectPendingSince(now() - 10 * 60); for (SmsSendRecord record : pendingList) { SmsQueryResult result = smsClient.query(record.getRequestId(), record.getPhone()); // status可能是SUCCESS/FAILED/UNKNOWN if ("SUCCESS".equals(result.getStatus())) { smsRecordMapper.updateStatus(record.getId(), SmsStatus.SUCCESS, result.getErrorCode()); } else if ("FAILED".equals(result.getStatus())) { smsRecordMapper.updateStatus(record.getId(), SmsStatus.FAILED, result.getErrorCode()); } // UNKNOWN保持等待,下次再查 } } }这个定时任务用Spring的@Scheduled注解,固定频率如每30秒跑一次。服务商返回的错误码要原样存进errorCode字段,不要自己翻译成“用户没开机”这种描述性文本,翻译错了会误导排查。时间窗口要特别注意:回执查询接口一般只支持查最近24小时内数据,定时任务太慢会漏数据,查询范围宁可重叠也不要留空隙。
4.4 频率控制:限流、黑名单与兜底策略
频控属于短信模块的重点。服务商侧有流控阈值,但那是针对整个账号的,单用户高频请求必须自己做。频率控制分三个维度:单手机号、单IP、全局。单手机号维度用Redis的INCR加EXPIRE组合。
public boolean tryAcquire(String sceneCode, String phone) { String key = "sms:freq:" + sceneCode + ":" + phone; Long count = stringRedisTemplate.opsForValue().increment(key); // 第一次请求时设置过期时间 if (count != null && count == 1) { stringRedisTemplate.expire(key, 60, TimeUnit.SECONDS); } // 60秒内同一个手机号最多1次 return count != null && count <= 1; }注意INCR和EXPIRE不是天然原子操作,需要严格保证时可以用Lua脚本组合,大多数项目里这个写法已经够用。单IP维度看整体防护:一个IP在1小时内触发超过5次不同手机号的验证码请求,直接把IP放入黑名单,缓存有效期1小时。全局维度设每日发送上限,比如一天最多10万条,超过后所有发送直接拒绝并发告警。
提示:短信按条计费,频控不只是业务体验问题,更是成本控制手段。生产环境务必配置费用阈值告警,账单异常时能第一时间发现。
接口层还要做一层兜底:前端按钮加倒计时只是体验优化,不是防刷防线。服务端限流要同时基于手机号和IP判断,两者缺一不可。我见过只有手机号限流、没有IP维度的系统,攻击者换个手机号参数继续刷,这种漏洞答辩时很容易被问倒。
5. 短信接口开发避坑指南:最容易翻车的五个环节
5.1 本地调通线上报“签名不合法”
现象:本地调试一切正常,部署到服务器后第一次发短信就报签名不合法。
原因通常有三个:签名串拼接时参数顺序错了;本机和服务器时间不一致,Timestamp偏差超过容差;配置文件的AccessKey或Secret里带有多余空格。
解决方法是把签名字符串打印出来,对照服务商文档逐项核对,确认服务器时间已同步,配置文件不要有多余空白符。从那以后我的SmsClient里都会加一个“打印签名串”的开关,临时排错才打开,避免生产环境刷日志。
5.2 接口被刷,一晚欠费几千
现象:夜间流量上来后,短信接口被脚本循环调用,验证码发了一整晚,第二天账单飙升。
原因就是只做了发送成功校验,没做频控,密钥也没做权限收敛。攻击者用主账号密钥直接调发送接口,费用全算在账号上。
解决方法是把第4章的频控全部落地:单手机号60秒1条、单IP每小时5条、全局每日限额;密钥切换到短信子账号,并配置费用阈值告警。这个坑很适合写进答辩材料,直接证明你考虑过成本问题。
5.3 模板变量里的中文变成乱码
现象:短信发出去了,中文参数显示成问号或乱码,用户反馈“短信看不懂”。
原因一般是HTTP请求编码或模板参数编码不是UTF-8。
解决分三步:项目文件统一UTF-8编码;调用SmsClient时请求体显式设置Content-Type为text/html;charset=UTF-8,参数值发送前做trim;变量内容不要放换行符,换行在部分服务商模板里会被当作非法字符拦截。
5.4 回执状态一直是“未知”,明明收到验证码
现象:验证码能正常收到,后台查询发送记录,status一直停在PENDING,问服务商也说没有记录。
原因往往是查询时间窗口不对,或者查询时传的requestId不是发送接口返回的那个请求ID,查出来“无记录”被代码翻译成了“未知”。
解决方法是把发送接口返回的requestId原样落库,查询时严格按最近N分钟时间窗口拉数,UNKNOWN状态只保留有限次数,超过3次更新成FAILED并记录“回执查询失败”,不要无限停在未知状态。否则数据越积越多,后台看板全是不明状态,后续排查也没法下手。
5.5 换个手机号调试,验证码发到上次那个号码
现象:本地开发时换了个手机号,收到验证码的还是旧号码,看起来像缓存玄学。
原因是有同学把验证码存到内存变量,全局只有一个code变量,第二个手机号请求时覆盖了前一个;或者发送接口参数顺序写反,phone和templateCode传串了。
解决方法是验证码必须以“场景码+手机号”作为key存储,任何读取验证码的地方都要同时校验传入的phone与key中的phone一致;接口方法参数用对象封装,不要传一长串基本类型,能减少大部分传参错位问题。代码评审时盯住这两个点,基本能杜绝这类问题。
6. 方案验收与答辩:从功能测试到防刷验证的完整路径
6.1 功能验收清单:六类场景逐项过
验收不要只测“验证码能发出来”,建议按这张表逐项过,每项记录实际结果:
| 编号 | 测试场景 | 操作步骤 | 预期结果 |
|---|---|---|---|
| 1 | 正常发送 | 输入有效手机号,点击获取验证码 | 返回“发送成功”,手机收到验证码 |
| 2 | 手机号非法 | 输入非数字或长度不合法号码 | 接口返回参数错误,不产生费用 |
| 3 | 重复请求 | 60秒内再次点击获取 | 提示“请求太频繁”,不发第二条 |
| 4 | 验证码过期 | 等待5分钟后输入旧验证码 | 校验失败,提示重新获取 |
| 5 | 错误验证码3次 | 连续输错3次 | 缓存被删除,再次校验失败 |
| 6 | 验证码复用 | 成功后再次提交同一验证码 | 校验失败,一次性有效 |
表里的第4项演示时太慢,可以临时把过期时间调短到1分钟展示过期效果,演示完再改回去。验收记录单做成表格,注明测试时间、测试人、结果,答辩时放在PPT附页。
6.2 安全验收:用脚本模拟高频请求
防刷逻辑写没写,写了对不对,一个脚本就能验证。下面这段bash模拟15秒内连续请求20次验证码接口,观察被限流的比例。
#!/bin/bash for i in $(seq 1 20); do RESP=$(curl -s -X POST http://localhost:8080/sms/code/register \ -H "Content-Type: application/json" \ -d '{"phone":"13900000000"}') echo "$RESP" | grep -o '"message":"[^"]*"' sleep 0.5 done如果频控生效,前几次请求返回成功,后续请求会看到类似“请求太频繁”的提示。这个脚本能一次验证三个点:发送链路是通的、频率限制确实生效、错误信息能被接口正常包装返回。答辩现场如果网络允许,当场跑一次脚本比十页PPT都有效。
6.3 答辩技巧:从状态机讲起,把“实现了”说成“设计并验证了”
短信模块在答辩时最容易讲散。建议开场不要先说“我用SpringBoot实现了短信验证码”,而是从设计切入,三句话讲完:第一句讲我把短信拆成三层——业务层对接场景码、服务层做频控和落库、客户端层管签名和回执;第二句说状态流转——从PENDING到SUCCESS或FAILED,未知状态有补偿机制;第三句给验证数据——用高频脚本验证过限流,用回执任务验证过状态更新,整个过程没有漏发,也没有欠费告警。这三句话把方案完整度、工程意识和安全意识全讲到了。
最后说点个人习惯。课设翻车之后,我每次接短信功能都会强制走一遍自查清单:签名模板是不是正式资源、密钥是不是子账号、签名字符串能不能打印、单机频控和全局限额有没有配、回执状态有没有兜底,五项全过才敢联调。后来靠这套流程拦下过好几类上线前就能发现的问题。如果你想把这份方案直接改成自己的课设或毕设,资源包里的代码骨架和验收表格可以直接拿走,对照第2章的表结构改数据库,再改第3章的配置项就能跑起来,希望帮到你。
本文还有配套的精品资源,点击获取