更新
This commit is contained in:
parent
4e211ed1fe
commit
7eb8c3a5ad
@ -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
6
SMS/.idea/vcs.xml
Normal 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>
|
||||||
@ -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.
Binary file not shown.
Binary file not shown.
@ -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" />
|
||||||
|
|||||||
18
SMS/app/src/main/java/com/yunzer/sms/SmsDeliverReceiver.kt
Normal file
18
SMS/app/src/main/java/com/yunzer/sms/SmsDeliverReceiver.kt
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -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
BIN
backend/backend.zip
Normal file
Binary file not shown.
@ -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);
|
||||||
|
|||||||
@ -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` 中声明:
|
||||||
|
|||||||
@ -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. 后续扩展建议
|
||||||
|
|
||||||
- 增加任务重试与失败原因字段
|
- 增加任务重试与失败原因字段
|
||||||
- 增加短信去重与解析规则配置(正则规则下发)
|
- 增加短信去重与解析规则配置(正则规则下发)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user