API文档

码支付订单接口 & 免签短信接口说明

安全说明:所有订单接口都需要 api_token 验证,token 通过会员登录获取,不可伪造。

1. 接口地址

https://wndog.qianxi.xin/api/order/

2. 鉴权方式

所有订单接口必须携带 api_token 参数,支持以下方式传递:

方式示例
URL参数?api_token=xxxx
POST表单api_token=xxxx
POST JSON{"api_token": "xxxx"}

3. 创建订单

POST /api/order/create?api_token=xxxx

智能金额调整:系统会自动确保订单金额唯一性。5分钟内有相同金额的待支付订单时,自动加0.01元,避免支付匹配冲突。
参数必填类型说明
api_tokenstring会员API Token(鉴权必填)
titlestring商品名称
amountfloat金额(元),系统会自动调整确保唯一性
descriptionstring商品描述
pay_typestring支付方式: wechat/alipay/any(默认)
expire_minutesint过期分钟数(默认5,最长1440)
notify_urlstring支付成功回调地址
extrastring扩展数据(JSON)
金额自动调整规则
场景处理方式
5分钟内无相同金额的待支付订单使用原始金额创建订单
5分钟内有相同金额的待支付订单自动加0.01元,直到找到可用金额
原订单支付成功/取消/过期该金额可再次使用
最多调整次数100次(最多加1元)

示例场景:

用户A请求创建 ¥1.00 订单 → 创建成功,金额 ¥1.00
用户A再次请求创建 ¥1.00 订单 → 自动调整为 ¥1.01
用户A第三次请求 ¥1.00 → 自动调整为 ¥1.02
第一个订单 ¥1.00 支付成功 → 下次可再次创建 ¥1.00

请求示例:

POST /api/order/create?api_token=你的API_TOKEN
Content-Type: application/json

{
    "title": "VIP会员月卡",
    "description": "一个月VIP会员服务",
    "amount": 29.90,
    "pay_type": "any",
    "expire_minutes": 5,
    "notify_url": "https://your-site.com/callback"
}

返回字段说明:

字段类型说明
order_idint订单ID
order_nostring系统订单号(ORD开头)
titlestring商品名称
descriptionstring商品描述
amountfloat实际支付金额(可能已调整)
original_amountfloat原始请求金额
amount_adjustedbool金额是否被自动调整
pay_typestring支付方式: wechat/alipay/any
statusstring订单状态: pending
expire_atstring过期时间
expire_minutesint过期分钟数
created_atstring创建时间
qrcodesarray收款二维码列表

qrcodes 字段说明:

字段类型说明
idint收款码ID
typestring类型: wechat/alipay
namestring收款码名称
image_pathstring二维码图片路径
qr_contentstring二维码原始内容
alipay_uidstring支付宝UID(微信码为空)
free_pay_enabledint是否开启免输入金额模式(支付宝专用): 1开启 0关闭。开启后配合 alipay_uid 生成免输入金额的支付链接

返回示例:

{
    "code": 0,
    "msg": "订单创建成功",
    "data": {
        "order_id": 200,
        "order_no": "ORD20260703022150516692",
        "title": "测试支付订单 - 万能狗",
        "description": "",
        "amount": 0.01,
        "original_amount": 0.01,
        "amount_adjusted": false,
        "pay_type": "any",
        "status": "pending",
        "expire_at": "2026-07-03 02:26:50",
        "expire_minutes": 5,
        "created_at": "2026-07-03 02:21:50",
        "fee_amount": 0.01,
        "is_free_fee": false,
        "is_platform_subsidy": false,
        "platform_member_id": null,
        "qr_member_id": 1,
        "qrcodes": [
            {
                "id": 2,
                "type": "wechat",
                "name": "微信",
                "image_path": "uploads/member/qr_1_1779603622_2890.png",
                "qr_content": "wxp://f2f0h-xrGByLyteXzvdnIk8AmwLeF5r6PMIkRBC9bTwDsaM",
                "alipay_uid": "",
                "free_pay_enabled": 1
            },
            {
                "id": 13,
                "type": "alipay",
                "name": "支付宝",
                "image_path": "uploads/member/qr_1_1782270911_8382.png",
                "qr_content": "https://qr.alipay.com/fkx11764k1cdynkuev8mc17",
                "alipay_uid": "2088642730792055",
                "free_pay_enabled": 0
            }
        ]
    }
}
手续费说明:
  • is_free_fee=true: 会员有有效套餐或等级免手续费,fee_amount=0
  • is_platform_subsidy=true: 余额不足时由平台会员代收,订单金额进入平台会员收款码
  • qr_member_id: 实际收款码所属会员ID。代收时为平台会员ID,否则为当前会员ID

4. 订单列表

GET /api/order/list?api_token=xxxx&page=1&size=20

参数必填说明
api_token会员API Token
page页码(默认1)
size每页条数(默认20,最大100)
status筛选状态: pending/paid/expired/cancelled/refunded
keyword搜索关键词(商品名/订单号/备注)
start_date开始日期(2026-01-01)
end_date结束日期(2026-12-31)

5. 订单详情

GET /api/order/detail?api_token=xxxx&order_no=ORD20260530143022123456

返回订单完整信息及操作日志。

6. 取消订单

POST /api/order/cancel?api_token=xxxx

{
    "order_no": "ORD20260530143022123456"
}
只能取消待支付(pending)状态的订单

7. 退款订单

POST /api/order/refund?api_token=xxxx

{
    "order_no": "ORD20260530143022123456",
    "reason": "用户申请退款"
}
只能退款已支付(paid)状态的订单

8. 手动重试回调

POST /api/order/notify?api_token=xxxx

{
    "order_no": "ORD20260530143022123456"
}

9. 订单统计

GET /api/order/stats?api_token=xxxx&period=today

period值说明
today今天
week最近7天
month最近30天
all全部

10. 订单状态说明

状态说明
pending待支付
paid已支付
expired已过期
cancelled已取消
refunded已退款

11. 回调通知格式

订单支付成功后,系统会向 notify_url 发送 POST 请求:

{
    "order_no": "ORD20260530143022123456",
    "title": "VIP会员月卡",
    "amount": 29.90,
    "pay_type": "alipay",
    "status": "paid",
    "payer": "付款人昵称",
    "remark": "付款备注",
    "paid_at": "2026-05-30 14:31:05",
    "created_at": "2026-05-30 14:30:22",
    "timestamp": 1748615465
}
收到回调后请返回 HTTP 200,否则系统会重试通知。

常见问题

Q: 创建订单返回金额与请求不一致?

A: 系统会自动调整金额避免冲突。返回的 amount 是实际创建的金额,original_amount 是你请求的原始金额,amount_adjusted 为 true 表示已调整。

Q: 订单一直显示待支付?

A: 订单默认5分钟过期。请确认:1) 付款金额与订单金额完全一致(精确到分);2) 付款在过期时间内完成;3) App已正常上报收款到服务器。

Q: 收到回调但缺少 status 字段?

A: 回调数据包含 status: "paid" 字段。如果第三方系统需要兼容,建议同时检查 statuspaid_at 字段。

Q: 如何获取 api_token?

A: 通过会员登录接口 POST /api/member/login 获取,或在会员中心查看。

Q: 订单过期后会自动处理吗?

A: 会。系统有定时任务每分钟检查,将过期订单标记为 expired。也可以通过 Cron 脚本 cron_order_expire.php 手动执行。

Q: 同一时间能创建多少订单?

A: 无限制。但系统会自动调整金额确保每个订单金额唯一,最多调整100次(加1元)。

安全说明:所有短信接口都需要 api_token 验证,短信发送前需在会员中心开启短信开关,并通过事务确保扣费与发送的一致性。

1. 接口地址

https://wndog.qianxi.xin/api/member/

2. 鉴权方式

所有短信接口必须携带 api_token 参数,支持以下方式传递:

方式示例
URL参数?api_token=xxxx
POST表单api_token=xxxx
POST JSON{"api_token": "xxxx"}

3. 发送短信验证码

POST /api/member/sms-send?api_token=xxxx

验证码一致性说明(重要):本接口基于实际下发到用户手机的验证码由接口生成,接口会同时返回该验证码(verify_code 字段)。
业务系统进行验证码校验时,必须使用接口返回的 verify_code 字段,不要自己生成验证码,否则与用户收到的会不一致。
扣费逻辑:发送短信时优先扣除短信包条数,若无条数则从余额扣除单条短信价格(后台可配置)。
⚠️ 必传参数说明:API发信必须传 signature_id(签名ID)和 template_id(模板ID),请先调用"查询可用签名和模板资源"接口获取可用的ID列表。
参数必填类型说明
api_tokenstring会员API Token(鉴权必填)
phonestring接收短信的手机号
scenestring发送场景: api(默认)/login/verify/reset_pwd
code_lengthint验证码长度(默认4位)
valid_timeint验证码有效时间(秒,默认300)
signature_id是(必传)int签名ID(通过查询可用签名接口获取)
template_id是(必传)int模板ID(通过查询可用模板接口获取)
from_domainstring 注:会员中心设置了白名单域名 这个值就必须传域名否则发送不了

请求示例(推荐,带 from_domain 以适配白名单):

POST /api/member/sms-send?api_token=你的API_TOKEN
Content-Type: application/json

{
    "phone": "13800138000",
    "signature_id": 1,
    "template_id": 1,
    "scene": "api",
    "code_length": 4,
    "from_domain": "你的域名"
}

返回字段说明:

字段类型说明
verify_codestring实际下发的验证码(由接口生成,与用户手机收到的一致)
cost_typestring扣费类型: sms_count=短信条数 / balance=余额
cost_amountfloat/int扣费金额(余额扣费)或条数(短信包扣费)
sms_count_remainingint短信包剩余条数
balance_remainingfloat余额剩余金额

返回示例(成功):

{
    "code": 0,
    "msg": "短信发送成功",
    "data": {
        "verify_code": "3829",
        "cost_type": "sms_count",
        "cost_amount": 1,
        "sms_count_remaining": 99,
        "balance_remaining": 100.00
    }
}

返回示例(余额扣费):

{
    "code": 0,
    "msg": "短信发送成功",
    "data": {
        "verify_code": "3829",
        "cost_type": "balance",
        "cost_amount": 0.10,
        "sms_count_remaining": 0,
        "balance_remaining": 99.90
    }
}

4. 短信发送记录

GET /api/member/sms-logs?api_token=xxxx&page=1&page_size=20

参数必填说明
api_token会员API Token
page页码(默认1)
page_size每页条数(默认20,最大50)

返回字段说明:

字段类型说明
idint日志ID
phonestring接收手机号
scenestring发送场景
template_codestring短信模板CODE
sign_namestring短信签名
cost_typestring扣费类型: sms_count/balance
cost_amountfloat/int扣费金额或条数
sms_count_beforeint扣费前短信条数
sms_count_afterint扣费后短信条数
balance_beforefloat扣费前余额
balance_afterfloat扣费后余额
statusint状态: 0=发送中 1=成功 2=失败
created_atstring发送时间

返回示例:

{
    "code": 0,
    "msg": "ok",
    "data": {
        "list": [
            {
                "id": 1,
                "phone": "13800138000",
                "scene": "api",
                "template_code": "SMS_123456",
                "sign_name": "万能狗",
                "cost_type": "sms_count",
                "cost_amount": 1,
                "sms_count_before": 100,
                "sms_count_after": 99,
                "balance_before": 100.00,
                "balance_after": 100.00,
                "status": 1,
                "created_at": "2026-07-12 10:30:00"
            }
        ],
        "total": 1,
        "page": 1,
        "page_size": 20
    }
}

5. 扣费逻辑说明

条件扣费方式说明
短信包条数 > 0扣除1条短信包cost_type=sms_count
短信包为0,余额 ≥ 单条价格从余额扣除cost_type=balance
短信包为0,余额不足拒绝发送返回 403 错误
发送失败自动退还扣费事务回滚,条数/余额恢复
事务保障:扣费和短信发送采用数据库事务处理,使用行锁(FOR UPDATE)防止并发问题。若短信发送失败,已扣除的费用会自动退还。

5.5 查询可用签名和模板资源

在发送短信前,可先调用此接口获取当前可用的签名ID和模板ID列表。

GET /api/member/sms-resources?api_token=xxxx

请求参数:

返回示例:

{
  "code": 0,
  "msg": "获取成功",
  "data": {
    "signatures": [
      {
        "id": 1,
        "name": "万能狗",
        "remark": "系统备案签名",
        "status": 1
      }
    ],
    "templates": [
      {
        "id": 1,
        "name": "登录验证码",
        "template_code": "SMS_123456",
        "scene": "login",
        "scene_label": "登录",
        "template_content": "【万能狗】您的验证码为${code},${min}分钟内有效。",
        "template_params": {
          "code": "验证码",
          "min": "有效分钟"
        },
        "status": 1
      }
    ]
  }
}

使用说明:

  • 调用此接口需要 api_token 鉴权
  • 返回的 id 字段就是发信接口需要的 signature_idtemplate_id
  • 建议对接方在对接时先调用此接口缓存可用资源,避免每次都查询

5.6 PHP调用案例

以下是直接可用的 PHP 调用代码示例:

对接技巧(白名单适配):请求里带上 from_domain=$_SERVER['HTTP_HOST']

案例0.5:带 from_domain 发送短信(推荐服务器端对接)

<?php
/**
 * 带 from_domain 的短信发送示例(适配白名单)
 * 适合:服务器端 curl / SDK / 跨域后端对接
 * from_domain 直接用 $_SERVER['HTTP_HOST'] 即可
 */
function sendSmsWithDomain($apiBase, $apiToken, $phone, $fromDomain, $signatureId, $templateId, $options = []) {
    $url = $apiBase . '/api/member/sms-send?api_token=' . urlencode($apiToken);
    $data = array_merge([
        'phone'        => $phone,
        'scene'        => 'api',
        'code_length'  => 4,
        'valid_time'   => 300,
        'signature_id' => $signatureId,   // ★ 必传:签名ID
        'template_id'  => $templateId,    // ★ 必传:模板ID
        'from_domain'  => $fromDomain,    // ★ 关键:加上这一行,白名单设置了这个值也必须传
    ], $options);

    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $url);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data, JSON_UNESCAPED_UNICODE));
    curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_TIMEOUT, 30);
    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
    $response = curl_exec($ch);
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    // 所有业务错误都是 HTTP 200,通过 body.code 字段判断(避免被 SDK 误判成"网络异常")
    if ($httpCode !== 200) {
        return ['success' => false, 'msg' => 'HTTP网络错误: ' . $httpCode];
    }
    $result = json_decode($response, true);
    if (!is_array($result)) {
        return ['success' => false, 'msg' => '响应非JSON: ' . $response];
    }
    $result['success'] = ($result['code'] === 0);
    return $result;
}

// 调用示例:白名单配了 就直接用 $_SERVER['HTTP_HOST'] 传值
$apiBase      = $_SERVER['HTTP_HOST'];       // API域名
$apiToken     = '你的会员API_TOKEN';
$fromDomain   = '$_SERVER['HTTP_HOST']';                // ★ 填当前调用方的域名推荐直接使用变量 $_SERVER['HTTP_HOST']
$signatureId  = 1;                              // ★ 必传:签名ID(通过接口查询可用签名获取)
$templateId   = 1;                              // ★ 必传:模板ID(通过接口查询可用模板获取)
$phone        = '13800138000';

$result = sendSmsWithDomain($apiBase, $apiToken, $phone, $fromDomain, $signatureId, $templateId);
if ($result['success']) {
    echo "发送成功,验证码:" . $result['data']['verify_code'] . "\n";
    echo "扣费方式:" . $result['data']['cost_type'] . "\n";
} else {
    // msg 字段就是明确的错误信息(域名非白名单 / 条数不足 / SDK报错 等)
    echo "发送失败:[" . $result['code'] . "] " . $result['msg'] . "\n";
}
?>

案例0:先查询可用的签名和模板

<?php
/**
 * 先调用此接口获取可用的签名ID和模板ID
 * 建议在系统初始化时调用一次,结果可缓存到本地
 */
function getSmsResources($apiBase, $apiToken) {
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $apiBase . '/api/member/sms-resources?api_token=' . urlencode($apiToken));
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_TIMEOUT, 10);
    $response = curl_exec($ch);
    curl_close($ch);

    $result = json_decode($response, true);
    if ($result['code'] !== 0) {
        throw new Exception('获取资源失败: ' . $result['msg']);
    }
    return $result['data'];  // ['signatures' => [...], 'templates' => [...]]
}

// 调用并打印
$resources = getSmsResources('https://www.wndog.com', '你的API_TOKEN');
echo "可用的签名:\n";
foreach ($resources['signatures'] as $sig) {
    echo "  ID={$sig['id']}  名称={$sig['name']}\n";
}
echo "可用的模板:\n";
foreach ($resources['templates'] as $tpl) {
    echo "  ID={$tpl['id']}  场景={$tpl['scene_label']}  CODE={$tpl['template_code']}\n";
}
?>

案例1:使用cURL直接调用(推荐)

<?php
/**
 * 万能狗短信API调用示例
 */

// ====== 配置 ======
$apiBase     = 'https://www.wndog.com';     // 万能狗域名
$apiToken    = '你的会员API_TOKEN';         // 会员中心 → API Token
$phone       = '13800138000';              // 接收短信的手机号
$signatureId = 1;                           // ★ 必传:签名ID(通过查询可用签名接口获取)
$templateId  = 1;                           // ★ 必传:模板ID(通过查询可用模板接口获取)

// ====== 方式一:JSON传参(推荐) ======
function sendSms($apiBase, $apiToken, $phone, $signatureId, $templateId, $options = []) {
    $url = $apiBase . '/api/member/sms-send?api_token=' . urlencode($apiToken);

    $data = array_merge([
        'phone'        => $phone,
        'scene'        => 'api',
        'code_length'  => 4,
        'valid_time'   => 300,
        'signature_id' => $signatureId,   // ★ 必传:签名ID
        'template_id'  => $templateId,    // ★ 必传:模板ID
    ], $options);

    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $url);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
    curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_TIMEOUT, 30);
    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
    $response = curl_exec($ch);
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    if ($httpCode !== 200) {
        return ['success' => false, 'msg' => 'HTTP错误: ' . $httpCode];
    }
    return json_decode($response, true);
}

// ====== 方式二:表单传参 ======
function sendSmsByForm($apiBase, $apiToken, $phone, $signatureId, $templateId) {
    $url = $apiBase . '/api/member/sms-send?api_token=' . urlencode($apiToken);

    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $url);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query(['phone' => $phone, 'scene' => 'api', 'signature_id' => $signatureId, 'template_id' => $templateId]));
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_TIMEOUT, 30);
    $response = curl_exec($ch);
    curl_close($ch);
    return json_decode($response, true);
}

// ====== 方式三:使用GuzzleHttp(需composer安装guzzlehttp/guzzle) ======
function sendSmsByGuzzle($apiBase, $apiToken, $phone, $signatureId, $templateId) {
    $client = new \GuzzleHttp\Client();
    $response = $client->post($apiBase . '/api/member/sms-send', [
        'query'   => ['api_token' => $apiToken],
        'json'    => ['phone' => $phone, 'scene' => 'api', 'signature_id' => $signatureId, 'template_id' => $templateId],
        'timeout' => 30,
    ]);
    return json_decode($response->getBody(), true);
}

// ====== 调用示例 ======
$signatureId = 1;   // ★ 必传:签名ID(通过查询可用签名接口获取)
$templateId  = 1;   // ★ 必传:模板ID(通过查询可用模板接口获取)

$result = sendSms($apiBase, $apiToken, $phone, $signatureId, $templateId);

if ($result && $result['code'] === 0) {
    echo "发送成功!\n";
    echo "验证码: " . $result['data']['verify_code'] . "\n";
    echo "扣费方式: " . $result['data']['cost_type'] . "\n";
    echo "短信条数剩余: " . $result['data']['sms_count_remaining'] . "\n";
    echo "余额剩余: " . $result['data']['balance_remaining'] . "\n";
} else {
    echo "发送失败: " . ($result['msg'] ?? '未知错误') . "\n";
    echo "错误码: " . ($result['code'] ?? 'N/A') . "\n";
}
?>

案例2:登录验证码场景(使用login模板)

<?php
// 发送登录验证码
$phone = $_POST['phone'] ?? '';
$signatureId = 1;   // ★ 必传:签名ID
$templateId  = 1;   // ★ 必传:模板ID(对应login场景的模板)

$result = sendSms('https://www.wndog.com', '你的API_TOKEN', $phone, $signatureId, $templateId, [
    'scene'        => 'login',
    'code_length'  => 6,
    'valid_time'   => 600,   // 10分钟有效
]);

if ($result['code'] === 0) {
    // ★★★ 关键点 ★★★
    // 必须使用接口返回的 verify_code(这是接口实际下发的验证码,与用户手机收到的一致)
    // 不要自己用 mt_rand() 等函数生成,否则会与用户收到的对不上
    $_SESSION['sms_code']    = $result['data']['verify_code'];  // 用接口返回的!
    $_SESSION['sms_expire']  = time() + 600;
    $_SESSION['sms_phone']   = $phone;
    echo json_encode(['success' => true, 'msg' => '验证码已发送']);
} else {
    echo json_encode(['success' => false, 'msg' => $result['msg']]);
}
?>

案例3:批量发送(循环调用)

<?php
// 批量发送短信(注意:每个手机号都单独调用一次API)
$phones = ['13800138000', '13900139000', '13700137000'];
$success = 0;
$failed  = [];

foreach ($phones as $phone) {
    $result = sendSms('https://www.wndog.com', '你的API_TOKEN', $phone);
    if ($result['code'] === 0) {
        $success++;
    } else {
        $failed[$phone] = $result['msg'];
    }
    sleep(1);  // 间隔1秒,避免频率限制
}

echo "成功: $success, 失败: " . count($failed) . "\n";
print_r($failed);
?>

案例4:查询发送记录

<?php
// 查询最近的发送记录
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://www.wndog.com/api/member/sms-logs?api_token=你的API_TOKEN&page=1&page_size=20&status=1');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
curl_close($ch);

$result = json_decode($response, true);
if ($result['code'] === 0) {
    echo "总共发送: " . $result['data']['total'] . " 条\n";
    foreach ($result['data']['list'] as $log) {
        echo "[" . $log['created_at'] . "] " . $log['phone'] . " - " .
             ($log['cost_type'] === 'sms_count' ? "扣短信条数" : "扣余额¥" . $log['cost_amount']) . "\n";
    }
}
?>

6. 错误码定义

错误码说明
0成功
400参数错误(手机号为空、格式错误、签名算法错、签名过期等)
401未认证 / api_token无效或缺失
403短信功能未开启 / 签名模板配置无效 / 域名非白名单 / 短信条数不足且余额不足
404接口不存在
405请求方法不允许(如sms-send必须用POST)
429发送频率限制(60秒内重复发送同一手机号)
500短信发送失败(接口返回错误,会附带SDK返回的真实错误原因)

7. 安全措施

措施说明
身份验证通过 api_token 验证会员身份,token 不可伪造
事务处理扣费使用数据库事务 + FOR UPDATE 行锁,防止并发问题
失败回退短信发送失败时自动退还已扣除的条数或余额
完整日志每次发送记录到 pm_sms_log,含IP、扣费前后状态、API响应
频率限制同一手机号60秒内只能发送一次,防止滥用
HTTPS传输建议使用HTTPS加密传输API请求

常见问题

Q: 如何开启短信功能?

A: 登录会员中心 → 设置 → 短信服务,打开短信开关。

Q: 为什么业务错误(403/429/500)返回的 HTTP 状态码是 200?

A: 因为 fetch / axios / 大部分自研 SDK 会把 HTTP 非 200 的响应直接抛进 catch,统一显示"网络异常",把真实的 msg(域名非白名单、条数不足、阿里云签名报错等)完全吞掉,导致用户根本不知道错在哪。
所以本系统所有业务错误都用 HTTP 200 返回,真实错误码放在 JSON code 字段里,错误原因在 msg 字段里,客户端只要判断 body.code === 0 就是成功,否则直接显示 body.msg,不会再出现"网络异常"三个字。

Q: 短信包用完了怎么办?

A: 可以在会员中心 → 增值服务中购买短信包,也可以不购买直接从余额扣费(按单条短信价格扣除)。

Q: 发送失败会扣费吗?

A: 不会。系统使用事务处理,发送失败时已扣除的条数或余额会自动退还。如果是阿里云SDK错误,msg 字段会直接附带 SDK 真实错误信息(如 RAM权限不足、签名未过审、模板变量不对),方便排查。