This commit is contained in:
李志强 2026-03-26 20:51:36 +08:00
parent 4e211ed1fe
commit 7eb8c3a5ad
13 changed files with 122 additions and 4 deletions

View File

@ -4,10 +4,10 @@
<selectionStates> <selectionStates>
<SelectionState runConfigName="app"> <SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" /> <option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2026-03-25T08:24:18.087919900Z"> <DropdownSelection timestamp="2026-03-26T12:09:09.946169900Z">
<Target type="DEFAULT_BOOT"> <Target type="DEFAULT_BOOT">
<handle> <handle>
<DeviceId pluginId="Default" identifier="serial=emulator-5554;connection=1f9ceb70" /> <DeviceId pluginId="PhysicalDevice" identifier="serial=lvzdxotknne6u4nv" />
</handle> </handle>
</Target> </Target>
</DropdownSelection> </DropdownSelection>

6
SMS/.idea/vcs.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
</component>
</project>

View File

@ -15,7 +15,7 @@
首次进入 App 会让你填写: 首次进入 App 会让你填写:
- `backendUrl`:例如 `http://192.168.1.10:7788` - `backendUrl`:例如 `https://yzsms.yunzer.cn`
- `apiKey`:后端 `.env` 里的 `SMS_GATEWAY_API_KEY` - `apiKey`:后端 `.env` 里的 `SMS_GATEWAY_API_KEY`
- (不再需要 `deviceId`:由后端根据 `apiKey` 自动归属任务/短信) - (不再需要 `deviceId`:由后端根据 `apiKey` 自动归属任务/短信)

Binary file not shown.

View File

@ -2,6 +2,7 @@
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.RECEIVE_SMS" /> <uses-permission android:name="android.permission.RECEIVE_SMS" />
<uses-permission android:name="android.permission.READ_SMS" />
<uses-permission android:name="android.permission.SEND_SMS" /> <uses-permission android:name="android.permission.SEND_SMS" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />

View File

@ -0,0 +1,18 @@
package com.yunzer.sms
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
/**
* 用于接收 SMS_DELIVER默认短信应用角色相关
* 这里复用现有 SmsReceiver 的处理逻辑
*/
class SmsDeliverReceiver : BroadcastReceiver() {
private val delegate = SmsReceiver()
override fun onReceive(context: Context, intent: Intent) {
delegate.onReceive(context, intent)
}
}

View File

@ -68,6 +68,13 @@ npm start
- `onlyMatched=true`:只返回 `parseStatus=matched` 的验证码 - `onlyMatched=true`:只返回 `parseStatus=matched` 的验证码
- 返回:`{ inbounds: [...] }` - 返回:`{ inbounds: [...] }`
6. 业务系统查询出站发送任务状态(新增)
- `GET /api/v1/business/outbound-tasks?limit=50&status=&phone=`
- 请求头:`X-Api-Key: <SMS_GATEWAY_API_KEY>`
- status 取值:
- `pending` / `sending` / `failed` / `success`
- 返回:`{ tasks: [{ taskId, phone, content, status, ...}] }`
## 数据表 ## 数据表
- `inbound_sms` - `inbound_sms`

BIN
backend/backend.zip Normal file

Binary file not shown.

View File

@ -183,6 +183,65 @@ router.post("/api/v1/business/outbound-tasks", (req, res) => {
} }
}); });
// 6) 业务侧查询发送任务状态(用于业务系统展示)
router.get("/api/v1/business/outbound-tasks", (req, res) => {
const deviceId = getDeviceIdFromHeader(req);
if (!deviceId) return res.status(401).json({ error: "missing api key for device identification" });
const limit = Math.min(Number(req.query.limit || 50), 200);
const status = req.query.status ? String(req.query.status) : "";
const phone = req.query.phone ? safeString(req.query.phone) : "";
try {
const where = ["device_id = ?"];
const params = [deviceId];
if (status !== "") {
where.push("status = ?");
params.push(status);
}
if (phone !== "") {
where.push("phone LIKE ?");
params.push(`%${phone}%`);
}
const rows = db
.prepare(
`
SELECT
task_id,
phone,
content,
status,
retry_count,
last_error,
created_at,
updated_at
FROM outbound_tasks
WHERE ${where.join(" AND ")}
ORDER BY created_at DESC
LIMIT ?
`
)
.all(...params, limit);
res.json({
tasks: rows.map((r) => ({
taskId: r.task_id,
phone: r.phone,
content: r.content,
status: r.status,
retryCount: r.retry_count,
lastError: r.last_error,
createdAt: r.created_at,
updatedAt: r.updated_at,
})),
});
} catch (e) {
res.status(500).json({ error: "failed to query outbound tasks", detail: String(e?.message || e) });
}
});
// 5) 业务侧读取入站验证码MVP 用) // 5) 业务侧读取入站验证码MVP 用)
router.get("/api/v1/business/inbound-sms", (req, res) => { router.get("/api/v1/business/inbound-sms", (req, res) => {
const deviceId = getDeviceIdFromHeader(req); const deviceId = getDeviceIdFromHeader(req);

View File

@ -60,6 +60,15 @@
## 5. Android 权限与系统能力 ## 5. Android 权限与系统能力
### 5.3 默认短信应用角色ROLE_SMS
部分 Android 系统/ROM 在后台/静默发送短信时,会要求应用先成为“默认短信应用”(`ROLE_SMS`)。
如果你发现需要手动确认发送(或发送会被系统拦截),请按以下方式处理:
1. 首次启动 App点击保存配置后等待系统弹出“默认短信应用”角色授权系统界面可能叫“短信应用/默认短信应用”)
2. 授权完成后App 的前台服务会继续轮询并自动发送,无需每次手动确认
> 说明:`ROLE_SMS` 授权至少需要一次用户确认,之后应自动生效。
## 5.1 必要权限 ## 5.1 必要权限
`AndroidManifest.xml` 中声明: `AndroidManifest.xml` 中声明:

View File

@ -46,7 +46,25 @@ npm start
3. 安卓端把入站验证码短信通过 `POST /api/v1/sms/inbound` 上报后端 3. 安卓端把入站验证码短信通过 `POST /api/v1/sms/inbound` 上报后端
4. 你业务系统再从后端读取/查询验证码结果(你可在现有业务端对接这些接口) 4. 你业务系统再从后端读取/查询验证码结果(你可在现有业务端对接这些接口)
## 4. 后续扩展建议 ## 4. 业务系统接入对接示例TP 管理后台)
如果你的业务系统是 ThinkPHP例如你现在的 TP 后台管理页),建议不要直接写本仓库的数据库表,而是调用本短信网关后端提供的入队接口:
1) 确保安卓端填写的 `backendUrl` 指向本短信网关后端(例如 `https://yzsms.yunzer.cn`
2) 确保安卓端填写的 `apiKey` 与后端 `.env` 里的 `SMS_GATEWAY_API_KEY` 完全一致
3) 业务系统发送短信时,调用:
- `POST {backendUrl}/api/v1/business/outbound-tasks`
- 请求头:`X-Api-Key: SMS_GATEWAY_API_KEY`
- body`{ "phone": "+8613712345678", "content": "短信测试验证码123456" }`
4) 安卓端会通过轮询:
- `GET {backendUrl}/api/v1/device/tasks`
自动拉取任务并发送,然后回传到:
- `POST {backendUrl}/api/v1/sms/outbound/result`
5) 查询发送任务状态(可选,用于业务侧展示)
- `GET {backendUrl}/api/v1/business/outbound-tasks?limit=50`
- 请求头:`X-Api-Key: SMS_GATEWAY_API_KEY`
## 5. 后续扩展建议
- 增加任务重试与失败原因字段 - 增加任务重试与失败原因字段
- 增加短信去重与解析规则配置(正则规则下发) - 增加短信去重与解析规则配置(正则规则下发)