Node.js接入支付宝(蚂蚁金服)支付
最近项目(Android和Ios)中需要接入付费功能(支付宝和微信),下面就先来介绍下接入支付宝的流程。文章主要分为三大块:
第一块是如何在蚂蚁金服的开放平台创建一个应用并且配置开发选项。
第二块是node端接入支付功能生成前端支付需要的参数(私钥签名)。
第三块是node端对支付结果异步通知的验签(公钥验签)。
蚂蚁金服开放平台创建一个应用
一、登陆:进入开放平台登录账号后,进入开发者中心-网页&移动应用栏目,点击创建应用中的支付接入
二、创建应用:使用场景选择“自用型应用”,并且给你的应用取一个响亮的名字(应用名称和应用图标会在授权、分享的场景中露出)
三、创建完成:这时候在我的应用里面可以看到我们刚刚创建的应用了,这时候点击“查看”按钮开始配置应用
四、添加功能:进入之后需要添加我们需要的功能选项(手机网站支付、app支付、授权等),很多功能是需要签约的,按照签约的提示填写即可。添加完毕后就可以开始开发配置了
五、开发配置:开发配置分为3步,第1步设置应用公钥,第2步设置应用公关,第3步设置授权回调地址,接下来详细介绍
第2步:设置应用网关,这个地址也是很重要的,我们之后的支付结果支付宝都会通过异步的post请求这个到该地址上(用户付钱有没有成功就是依据他啦)。具体的请求参数参考:https://docs.open.alipay.com/204/105301/
6.提交审核:填写完上述信息就可以提交审核了,经过我们几次开发,发现支付宝审核非常快,白天几十分钟就会审核完毕了,在这个过程中我们也不要等着了,可以开始coding咯。
Node.js实现支付参数的生成
下面就以app支付为例子进行分析:
app端发起一个支付请求,需要一个参数(orderInfo),这个参数是从后台生成,如果我们后台(node)能够生成一个正确的参数,app端就可以成功的唤起支付宝,并且完成支付。
后台具体需要拼接哪些参数,请参考https://docs.open.alipay.com/204/105465/,文档中标记必填的参数我们也必须要填写。
看过请求参数的文档之后我们就可以正式开始组成app端需要的参数了,我们按照文档中的步骤进行构建参数,总共分为三步:
第一步:把所有必填的参数以及我们自己业务需要的参数组成key-value对象。
第二步:在第一步中有一个参数是最复杂,也是支付宝用来校验请求的合法性。就是sign(签名)这个参数,我们无法直接填写,需要通过应用的私钥去签名得到,我们第二步就是为了生成这个参数。
第三步:对我们参数中所有的value进行编码(encodeURIComponent),并且将参数转换成字符串返回给客户端即可;
第一步:生成基础参数
let params = new Map();
params.set('app_id', this.accountSettings.APP_ID);
params.set('method', 'alipay.trade.app.pay');
params.set('charset', 'utf-8');
params.set('sign_type', 'RSA2');
params.set('timestamp', moment().format('YYYY-MM-DD HH:mm:ss'));
params.set('version', '1.0');
params.set('notify_url', this.accountSettings.APP_GATEWAY_URL);
params.set('biz_content', this._buildBizContent('商品名称xxxx', '商户订单号xxxxx', '商品金额8.88'));
_buildBizContent()这个方法是用来生成参数biz_content的,这个参数用来传递一些附加参数,具体参数请参考文档中的业务参数
/*** 生成业务请求参数的集合* @param subject 商品的标题/交易标题/订单标题/订单关键字等。* @param outTradeNo 商户网站唯一订单号* @param totalAmount 订单总金额,单位为元,精确到小数点后两位,取值范围[0.01,100000000]* @returns {string} json字符串* @private*/
_buildBizContent(subject, outTradeNo, totalAmount) {let bizContent = {subject: subject,out_trade_no: outTradeNo,total_amount: totalAmount,product_code: 'QUICK_MSECURITY_PAY',};return JSON.stringify(bizContent);
}
第二步:生成签名
通过第一步,我们已经生成了基础参数存放在了params对象中,但是params中还缺少非常核心的一个参数就是“sign”下面我们就来说说如何生成sign,这次先看代码吧!(生成签名的官方文档在此)
/*** 根据参数构建签名* @param paramsMap Map对象* @returns {number|PromiseLike<ArrayBuffer>}* @private*/
_buildSign(paramsMap) {//1.获取所有请求参数,不包括字节类型参数,如文件、字节流,剔除sign字段,剔除值为空的参数let paramsList = [...paramsMap].filter(([k1, v1]) => k1 !== 'sign' && v1);//2.按照字符的键值ASCII码递增排序paramsList.sort();//3.组合成“参数=参数值”的格式,并且把这些参数用&字符连接起来let paramsString = paramsList.map(([k, v]) => `${k}=${v}`).join('&');let privateKey = fs.readFileSync(this.accountSettings.APP_PRIVATE_KEY_PATH, 'utf8');let signType = paramsMap.get('sign_type');return this._signWithPrivateKey(signType, paramsString, privateKey);
}/*** 通过私钥给字符串签名* @param signType 返回参数的签名类型:RSA2或RSA* @param content 需要加密的字符串* @param privateKey 私钥* @returns {number | PromiseLike<ArrayBuffer>}* @private*/
_signWithPrivateKey(signType, content, privateKey) {let sign;if (signType.toUpperCase() === 'RSA2') {sign = crypto.createSign("RSA-SHA256");} else if (signType.toUpperCase() === 'RSA') {sign = crypto.createSign("RSA-SHA1");} else {throw new Error('请传入正确的签名方式,signType:' + signType);}sign.update(content);return sign.sign(privateKey, 'base64');
}
当我们调用_buildSign()方法的时候,需要传入一个参数,就是我们第一步构建出来的params,函数返回的就是我们需要的sign参数,下面来看看它具体做了什么。
1.筛选字段:获取所有请求参数,不包括字节类型参数,如文件、字节流,剔除sign字段,剔除值为空的参数。
//1.获取所有请求参数,不包括字节类型参数,如文件、字节流,剔除sign字段,剔除值为空的参数
let paramsList = [...paramsMap].filter(([k1, v1]) => k1 !== 'sign' && v1);
2.根据key的ascii排序:按照第一个字符的键值ASCII码递增排序(字母升序排序),如果遇到相同字符则按照第二个字符的键值ASCII码递增排序,以此类推。
//2.按照字符的键值ASCII码递增排序
paramsList.sort();
3.拼接字符串:将排序后的参数与其对应值,组合成“参数=参数值”的格式,并且把这些参数用&字符连接起来,此时生成的字符串为待签名字符串。
//3.组合成“参数=参数值”的格式,并且把这些参数用&字符连接起来
let paramsString = paramsList.map(([k, v]) => `${k}=${v}`).join('&');
4.需要把上一步获取到的待签名字符串进行签名,签名分为两种,根据传递给支付宝的参数sign_type来判断(商户生成签名字符串所使用的签名算法类型,目前支持RSA2和RSA,推荐使用RSA2),此时还需要把我们的应用私钥给取出来,用来签名。应用的私钥就是我们在一开始配置应用的时候,在生成应用公钥的时候与之对应的私钥。
需要注意的是我们将私钥存储在文件中的时候,需要在第一行和最后一行分别加上一行,否则会报错
5.接下来调用_signWithPrivateKey方法即可获取到我们的sign参数的内容了
第三步:对所有的参数的value进行编码,并获得最终字符串
params.set('sign', this._buildSign(params));
return [...params].map(([k, v]) => `${k}=${encodeURIComponent(v)}`).join('&');
将我们上一步获取到的签名也设置到sign中,然后将所有的value进行encode,最终用“=“和“&“拼接成字符串返回给前端,到这里我们就完成了所有的步骤:)
Node.js实现服务器对支付结果异步通知的验签
对于App支付产生的交易,支付宝会根据原始支付API中传入的异步通知地址notify_url,通过POST请求的形式将支付结果作为参数通知到商户系统。异步通知的详细参数列表请参考:https://docs.open.alipay.com/204/105301/。
接受异步通知这一步非常的重要,用户是否真正的支付成功绝大部分是依赖于这个请求,我们不可能根据客户端返回的支付结果来判断,也不可能每一笔账都去蚂蚁金服的后台去对账。所以一定要处理好支付宝给我们发的请求,一定要对收到参数进行验证签名,保证这个请求确实是支付宝给我们发送的,而不是某人捏造的请求,处理不好会造成很大的损失。
小提示:但我们收到请求并且处理完成后必须打印输出“success”(不包含引号)。如果商户反馈给支付宝的字符不是success这7个字符,支付宝服务器会不断重发通知,直到超过24小时22分钟。一般情况下,25小时以内完成8次通知(通知的间隔频率一般是:4m,10m,10m,1h,2h,6h,15h)。
下面是接受请求的路由的处理代码:
handler.aliGateway = function (req, res, next) {let notifyTime = req.body.notify_time;//通知时间:通知的发送时间。格式为yyyy-MM-dd HH:mm:sslet notifyType = req.body.notify_type;//通知类型:通知的类型let notifyId = req.body.notify_id;//通知校验ID:通知校验IDlet appId = req.body.app_id;//支付宝分配给开发者的应用Id:支付宝分配给开发者的应用Idlet charset = req.body.charset;//编码格式:编码格式,如utf-8、gbk、gb2312等let version = req.body.version;//接口版本:调用的接口版本,固定为:1.0let signType = req.body.sign_type;//签名类型:商户生成签名字符串所使用的签名算法类型,目前支持RSA2和RSA,推荐使用RSA2let sign = req.body.sign;//签名:请参考<a href="#yanqian" class="bi-link">异步返回结果的验签</a>let tradeNo = req.body.trade_no;//支付宝交易号:支付宝交易凭证号let outTradeNo = req.body.out_trade_no;//商户订单号:原支付请求的商户订单号let outBizNo = req.body.out_biz_no;//商户业务号:商户业务ID,主要是退款通知中返回退款申请的流水号let buyerId = req.body.buyer_id;//买家支付宝用户号:买家支付宝账号对应的支付宝唯一用户号。以2088开头的纯16位数字let buyerLogonId = req.body.buyer_logon_id;//买家支付宝账号:买家支付宝账号let sellerId = req.body.seller_id;//卖家支付宝用户号:卖家支付宝用户号let sellerEmail = req.body.seller_email;//卖家支付宝账号:卖家支付宝账号let tradeStatus = req.body.trade_status;//交易状态:交易目前所处的状态,见<a href="#jiaoyi" class="bi-link">交易状态说明</a>let totalAmount = req.body.total_amount;//订单金额:本次交易支付的订单金额,单位为人民币(元)let receiptAmount = req.body.receipt_amount;//实收金额:商家在交易中实际收到的款项,单位为元let invoiceAmount = req.body.invoice_amount;//开票金额:用户在交易中支付的可开发票的金额let buyerPayAmount = req.body.buyer_pay_amount;//付款金额:用户在交易中支付的金额let pointAmount = req.body.point_amount;//集分宝金额:使用集分宝支付的金额let refundFee = req.body.refund_fee;//总退款金额:退款通知中,返回总退款金额,单位为元,支持两位小数let subject = req.body.subject;//订单标题:商品的标题/交易标题/订单标题/订单关键字等,是请求时对应的参数,原样通知回来let body = req.body.body;//商品描述:该订单的备注、描述、明细等。对应请求时的body参数,原样通知回来let gmtCreate = req.body.gmt_create;//交易创建时间:该笔交易创建的时间。格式为yyyy-MM-dd HH:mm:sslet gmtPayment = req.body.gmt_payment;//交易付款时间:该笔交易的买家付款时间。格式为yyyy-MM-dd HH:mm:sslet gmtRefund = req.body.gmt_refund;//交易退款时间:该笔交易的退款时间。格式为yyyy-MM-dd HH:mm:ss.Slet gmtClose = req.body.gmt_close;//交易结束时间:该笔交易结束时间。格式为yyyy-MM-dd HH:mm:sslet fundBillList = req.body.fund_bill_list;//支付金额信息:支付成功的各个渠道金额信息,详见<a href="#zijin" class="bi-link">资金明细信息说明</a>let passbackParams = req.body.passback_params;//回传参数:公共回传参数,如果请求时传递了该参数,则返回给商户时会在异步通知时将该参数原样返回。本参数必须进行UrlEncode之后才可以发送给支付宝let voucherDetailList = req.body.voucher_detail_list;//优惠券信息:本交易支付时所使用的所有优惠券信息,详见<a href="#youhui" class="bi-link">优惠券信息说明</a>let payHelper = new AliPayHelper(DefineProto.AliAccountType.AAT_REMIND);let isSuccess = payHelper.verifySign(req.body);if (isSuccess) {if (tradeStatus === 'TRADE_FINISHED') {//交易状态TRADE_FINISHED的通知触发条件是商户签约的产品不支持退款功能的前提下,买家付款成功;或者,商户签约的产品支持退款功能的前提下,交易已经成功并且已经超过可退款期限。} else if (tradeStatus === 'TRADE_SUCCESS') {//状态TRADE_SUCCESS的通知触发条件是商户签约的产品支持退款功能的前提下,买家付款成功} else if (tradeStatus === 'WAIT_BUYER_PAY') {} else if (tradeStatus === 'TRADE_CLOSED') {}res.send('success');} else {res.send('fail');}
};
可以看到上面验签的核心代码就是payHelper.verifySign(req.body),我们来具体看看支付是要求我们如何验签的,参考文档:https://docs.open.alipay.com/204/105301/
很多操作都和签名的时候类似,唯一需要注意的是:验签的时候用的是支付宝的公钥而不是应用的公钥
需要注意的是我们将公钥存储在文件中的时候,需要在第一行和最后一行分别加上一行,否则会报错
贴上具体的验签代码:
/*** 验证支付宝异步通知的合法性* @param params 支付宝异步通知结果的参数* @returns {*}*/
verifySign(params) {try {let sign = params['sign'];//签名let signType = params['sign_type'];//签名类型let paramsMap = new Map();for (let key in params) {paramsMap.set(key, params[key]);}let paramsList = [...paramsMap].filter(([k1, v1]) => k1 !== 'sign' && k1 !== 'sign_type' && v1);//2.按照字符的键值ASCII码递增排序paramsList.sort();//3.组合成“参数=参数值”的格式,并且把这些参数用&字符连接起来let paramsString = paramsList.map(([k, v]) => `${k}=${decodeURIComponent(v)}`).join('&');let publicKey = fs.readFileSync(this.accountSettings.ALI_PUBLIC_KEY_PATH, 'utf8');return this._verifyWithPublicKey(signType, sign, paramsString, publicKey);} catch (e) {console.error(e);return false;}
}/*** 验证签名* @param signType 返回参数的签名类型:RSA2或RSA* @param sign 返回参数的签名* @param content 参数组成的待验签串* @param publicKey 支付宝公钥* @returns {*} 是否验证成功* @private*/
_verifyWithPublicKey(signType, sign, content, publicKey) {try {let verify;if (signType.toUpperCase() === 'RSA2') {verify = crypto.createVerify('RSA-SHA256');} else if (signType.toUpperCase() === 'RSA') {verify = crypto.createVerify('RSA-SHA1');} else {throw new Error('未知signType:' + signType);}verify.update(content);return verify.verify(publicKey, sign, 'base64')} catch (err) {console.error(err);return false;}
}
到这里我们的三大块已经介绍完成啦,贴上完整的代码:
const path = require('path');
const fs = require('fs');
const moment = require('moment');
const crypto = require('crypto');let ALI_PAY_SETTINGS = {APP_ID: '2016091100487933',APP_GATEWAY_URL: 'xxxxxxx',//用于接收支付宝异步通知AUTH_REDIRECT_URL: 'xxxxxxx',//第三方授权或用户信息授权后回调地址。授权链接中配置的redirect_uri的值必须与此值保持一致。APP_PRIVATE_KEY_PATH: path.join(__dirname, 'pem', 'remind', 'sandbox', 'app-private.pem'),//应用私钥APP_PUBLIC_KEY_PATH: path.join(__dirname, 'pem', 'remind', 'sandbox', 'app-public.pem'),//应用公钥ALI_PUBLIC_KEY_PATH: path.join(__dirname, 'pem', 'remind', 'sandbox', 'ali-public.pem'),//阿里公钥AES_PATH: path.join(__dirname, 'pem', 'remind', 'sandbox', 'aes.txt'),//aes加密(暂未使用)
};class AliPayHelper {/*** 构造方法* @param accountType 用于以后区分多支付账号*/constructor(accountType) {this.accountType = accountType;this.accountSettings = ALI_PAY_SETTINGS;}/*** 构建app支付需要的参数* @param subject 商品名称* @param outTradeNo 自己公司的订单号* @param totalAmount 金额* @returns {string}*/buildParams(subject, outTradeNo, totalAmount) {let params = new Map();params.set('app_id', this.accountSettings.APP_ID);params.set('method', 'alipay.trade.app.pay');params.set('charset', 'utf-8');params.set('sign_type', 'RSA2');params.set('timestamp', moment().format('YYYY-MM-DD HH:mm:ss'));params.set('version', '1.0');params.set('notify_url', this.accountSettings.APP_GATEWAY_URL);params.set('biz_content', this._buildBizContent(subject, outTradeNo, totalAmount));params.set('sign', this._buildSign(params));return [...params].map(([k, v]) => `${k}=${encodeURIComponent(v)}`).join('&');}/*** 根据参数构建签名* @param paramsMap Map对象* @returns {number|PromiseLike<ArrayBuffer>}* @private*/_buildSign(paramsMap) {//1.获取所有请求参数,不包括字节类型参数,如文件、字节流,剔除sign字段,剔除值为空的参数let paramsList = [...paramsMap].filter(([k1, v1]) => k1 !== 'sign' && v1);//2.按照字符的键值ASCII码递增排序paramsList.sort();//3.组合成“参数=参数值”的格式,并且把这些参数用&字符连接起来let paramsString = paramsList.map(([k, v]) => `${k}=${v}`).join('&');let privateKey = fs.readFileSync(this.accountSettings.APP_PRIVATE_KEY_PATH, 'utf8');let signType = paramsMap.get('sign_type');return this._signWithPrivateKey(signType, paramsString, privateKey);}/*** 通过私钥给字符串签名* @param signType 返回参数的签名类型:RSA2或RSA* @param content 需要加密的字符串* @param privateKey 私钥* @returns {number | PromiseLike<ArrayBuffer>}* @private*/_signWithPrivateKey(signType, content, privateKey) {let sign;if (signType.toUpperCase() === 'RSA2') {sign = crypto.createSign("RSA-SHA256");} else if (signType.toUpperCase() === 'RSA') {sign = crypto.createSign("RSA-SHA1");} else {throw new Error('请传入正确的签名方式,signType:' + signType);}sign.update(content);return sign.sign(privateKey, 'base64');}/*** 生成业务请求参数的集合* @param subject 商品的标题/交易标题/订单标题/订单关键字等。* @param outTradeNo 商户网站唯一订单号* @param totalAmount 订单总金额,单位为元,精确到小数点后两位,取值范围[0.01,100000000]* @returns {string} json字符串* @private*/_buildBizContent(subject, outTradeNo, totalAmount) {let bizContent = {subject: subject,out_trade_no: outTradeNo,total_amount: totalAmount,product_code: 'QUICK_MSECURITY_PAY',};return JSON.stringify(bizContent);}/*** 验证支付宝异步通知的合法性* @param params 支付宝异步通知结果的参数* @returns {*}*/verifySign(params) {try {let sign = params['sign'];//签名let signType = params['sign_type'];//签名类型let paramsMap = new Map();for (let key in params) {paramsMap.set(key, params[key]);}let paramsList = [...paramsMap].filter(([k1, v1]) => k1 !== 'sign' && k1 !== 'sign_type' && v1);//2.按照字符的键值ASCII码递增排序paramsList.sort();//3.组合成“参数=参数值”的格式,并且把这些参数用&字符连接起来let paramsString = paramsList.map(([k, v]) => `${k}=${decodeURIComponent(v)}`).join('&');let publicKey = fs.readFileSync(this.accountSettings.ALI_PUBLIC_KEY_PATH, 'utf8');return this._verifyWithPublicKey(signType, sign, paramsString, publicKey);} catch (e) {console.error(e);return false;}}/*** 验证签名* @param signType 返回参数的签名类型:RSA2或RSA* @param sign 返回参数的签名* @param content 参数组成的待验签串* @param publicKey 支付宝公钥* @returns {*} 是否验证成功* @private*/_verifyWithPublicKey(signType, sign, content, publicKey) {try {let verify;if (signType.toUpperCase() === 'RSA2') {verify = crypto.createVerify('RSA-SHA256');} else if (signType.toUpperCase() === 'RSA') {verify = crypto.createVerify('RSA-SHA1');} else {throw new Error('未知signType:' + signType);}verify.update(content);return verify.verify(publicKey, sign, 'base64')} catch (err) {console.error(err);return false;}}}module.exports = AliPayHelper;
如果有写的不对的地方麻烦在评论中指出,如果有疑问也欢迎提问哦~
Node.js接入支付宝(蚂蚁金服)支付相关推荐
- 支付宝花呗接口接入php,蚂蚁金服支付宝花呗分期支付接入实战分享|分分钟钟快速接入demo 企业开通接入花呗支付就是这么easy 想分就分 花呗技术走一波...
" Yuema约吗?一起学技术,一起成长!他山之石,可以攻玉系列" 程序的世界,就是有坑的地方!分享踩坑的心得与体验!每天分享一点点! 关注公众号,可获取每日分享!小手点点,即可关 ...
- 蚂蚁金服-支付风险识别亚军方案!
比赛名称:蚂蚁金服-支付风险识别 比赛链接: https://dc.cloud.alipay.com/index#/topic/intro?id=4 比赛背景 基于移动互联网的线下支付.保险.理财等新 ...
- 支付宝蚂蚁金服是怎么把前端性能监控做到极致的?
本文来自蚂蚁金服前端技术专家杨森在 ArchSummit 北京 2018 的分享,他将分享如何通过 Performance 相关的 API 准确的采集用户性能数据,并如何通过大数据计算加工最终产出用户 ...
- node.js接入支付宝小程序的实名认证接口
本文档可以对照官方文档来看:支付宝身份验证 一.配置支付宝开放平台 1.1 开启身份认证功能 1.2 使用支付宝开放平台开发助手生成密钥 二.开始写代码 2.1 添加node.js的sdk impo ...
- 支付宝(蚂蚁金服开放平台)-支付结果异步通知-验签
问题描述: 按文档https://doc.open.alipay.com/docs/doc.htm?spm=a219a.7629140.0.0.rIlDQO&treeId=204&ar ...
- 蚂蚁金服支付平台代码配置
1.添加依赖 <dependency> <groupId>com.alipay.sdk</groupId> <artifactId>alipay-sdk ...
- 支付宝-蚂蚁金服一面
2019.5.20中午 一通电话,小哥哥电话打来 记录下问题把 1自我介绍 2介绍下你的项目的闪光点把,我说了中心账户系统,他问我做这个的目的是为了什么? 其实做这个没什么目的,因为要对接ssp,ss ...
- 蚂蚁金服 CTO 程立登台新加坡 Money 20/20 Asia,传递技术让世界更平等的愿景
2018 年 3 月 13 日,全球顶级支付金融类行业峰会 Money 20/20 Asia 正式在新加坡召开,蚂蚁金服 CTO 兼国际事业群 COO 程立登台做主题演讲,"技术让世界更平等 ...
- 蚂蚁金服开源服务注册中心 SOFARegistry
SOFAStack(Scalable Open Financial Architecture Stack )是蚂蚁金服自主研发的金融级分布式架构,包含了构建金融级云原生架构所需的各个组件,是在金融场景 ...
最新文章
- 看懂SQL Server的查询计划(绝对好文!)
- leetcode 839 Similar String Groups
- 图解Linux命令实例
- HD 2602 Bone Collector (0-1背包)
- clcikhouse Code: 1000. DB::Exception: File not found
- Chrome浏览器必装的扩展工具
- c语言右上左下遍历数组,如何将一个数组的元素循环左移?
- Python 网络爬虫与信息获取(一)—— requests 库的网络爬虫
- 七本前端(javascript、css)最著名工具书免费下载
- 误删数据?如何快速恢复Oracle数据库
- 计算机的内存大小有何作用,电脑内存用处有多大?你可能想不到!
- webp图片文件无需转换直接下载成JPG或者gif等其他格式的方法
- ERROR : Error appeared during Puppet run: x.x.x.x _keystone.pp
- 苹果手机热点总断怎么解决?
- 基于51单片机的频率计
- [附源码]计算机毕业设计JAVA音乐网站
- opencart之购物车立即购买
- ESP32 ADF windows开发环境搭建 适配ADF到ESP32A1S
- Arduino点亮LED灯带
- mysql数据库中查询第几条到第几条数据_在 mysql 数据库中,从查询结果的第四条记录开始选取5条记录,下面 sql 语句正确的是( )...