更新
This commit is contained in:
parent
2a6b14f698
commit
e4eab9ad89
@ -19,6 +19,9 @@ use app\model\System\OperationLog;
|
|||||||
|
|
||||||
class LoginController extends BaseController
|
class LoginController extends BaseController
|
||||||
{
|
{
|
||||||
|
private const SMS_CODE_TTL_SECONDS = 300; // 5分钟
|
||||||
|
private const SMS_CODE_RESEND_SECONDS = 60; // 60秒可重发
|
||||||
|
|
||||||
private function generateToken($userInfo): string
|
private function generateToken($userInfo): string
|
||||||
{
|
{
|
||||||
return JwtService::generateToken($userInfo);
|
return JwtService::generateToken($userInfo);
|
||||||
@ -29,6 +32,186 @@ class LoginController extends BaseController
|
|||||||
return JwtService::verifyToken($token);
|
return JwtService::verifyToken($token);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function getSiteSettingValue(string $label): string
|
||||||
|
{
|
||||||
|
$row = SystemSiteSettings::where('label', $label)
|
||||||
|
->where('delete_time', null)
|
||||||
|
->order('id', 'asc')
|
||||||
|
->find();
|
||||||
|
return $row ? (string)$row['value'] : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function smsCodeCacheKey(string $scene, string $tenantName, string $account, string $phone): string
|
||||||
|
{
|
||||||
|
return 'sms_code:' . md5($scene . '|' . $tenantName . '|' . $account . '|' . $phone);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function smsCodeThrottleKey(string $scene, string $tenantName, string $account, string $phone): string
|
||||||
|
{
|
||||||
|
return 'sms_code_throttle:' . md5($scene . '|' . $tenantName . '|' . $account . '|' . $phone);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function postJson(string $url, array $payload, array $headers): array
|
||||||
|
{
|
||||||
|
$body = json_encode($payload, JSON_UNESCAPED_UNICODE);
|
||||||
|
if ($body === false) {
|
||||||
|
return ['ok' => false, 'error' => 'json_encode failed'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$context = stream_context_create([
|
||||||
|
'http' => [
|
||||||
|
'method' => 'POST',
|
||||||
|
'header' => implode("\r\n", $headers),
|
||||||
|
'content' => $body,
|
||||||
|
'timeout' => 10,
|
||||||
|
'ignore_errors' => true,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$respBody = @file_get_contents($url, false, $context);
|
||||||
|
$status = 0;
|
||||||
|
if (!empty($http_response_header) && is_array($http_response_header)) {
|
||||||
|
foreach ($http_response_header as $line) {
|
||||||
|
if (preg_match('#^HTTP/\\S+\\s+(\\d+)#', $line, $m)) {
|
||||||
|
$status = (int)$m[1];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($respBody === false) {
|
||||||
|
$last = error_get_last();
|
||||||
|
return ['ok' => false, 'status' => $status, 'error' => (string)($last['message'] ?? 'http request failed')];
|
||||||
|
}
|
||||||
|
|
||||||
|
$decoded = json_decode((string)$respBody, true);
|
||||||
|
if (!is_array($decoded)) {
|
||||||
|
$decoded = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'ok' => $status >= 200 && $status < 300,
|
||||||
|
'status' => $status,
|
||||||
|
'body' => (string)$respBody,
|
||||||
|
'json' => $decoded,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function sendSmsCodeInternal(string $scene, string $tenantName, string $account, string $phone): Json
|
||||||
|
{
|
||||||
|
$throttleKey = $this->smsCodeThrottleKey($scene, $tenantName, $account, $phone);
|
||||||
|
if (Cache::has($throttleKey)) {
|
||||||
|
return json(['code' => 429, 'msg' => '验证码发送过于频繁,请稍后再试']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$backendUrl = $this->getSiteSettingValue('backendUrl');
|
||||||
|
$apiKey = $this->getSiteSettingValue('apiKey');
|
||||||
|
if ($backendUrl === '' || $apiKey === '') {
|
||||||
|
return json(['code' => 500, 'msg' => '短信网关配置缺失,请联系管理员']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$code = (string)random_int(100000, 999999);
|
||||||
|
$sceneTextMap = [
|
||||||
|
'register' => '注册',
|
||||||
|
'reset' => '找回密码',
|
||||||
|
'login' => '登录',
|
||||||
|
];
|
||||||
|
$sceneText = $sceneTextMap[$scene] ?? '验证';
|
||||||
|
$content = "【云泽网】{$sceneText}验证码:{$code},5分钟内有效。";
|
||||||
|
|
||||||
|
$enqueueUrl = rtrim($backendUrl, '/') . '/api/v1/business/outbound-tasks';
|
||||||
|
$resp = $this->postJson($enqueueUrl, [
|
||||||
|
'phone' => $phone,
|
||||||
|
'content' => $content,
|
||||||
|
], [
|
||||||
|
'X-Api-Key: ' . $apiKey,
|
||||||
|
'Content-Type: application/json; charset=utf-8',
|
||||||
|
'Accept: application/json',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (empty($resp['ok'])) {
|
||||||
|
return json([
|
||||||
|
'code' => 500,
|
||||||
|
'msg' => '验证码短信发送失败',
|
||||||
|
'detail' => [
|
||||||
|
'http_status' => $resp['status'] ?? null,
|
||||||
|
'body_preview' => isset($resp['body']) ? substr((string)$resp['body'], 0, 200) : '',
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$codeKey = $this->smsCodeCacheKey($scene, $tenantName, $account, $phone);
|
||||||
|
Cache::set($codeKey, $code, self::SMS_CODE_TTL_SECONDS);
|
||||||
|
Cache::set($throttleKey, 1, self::SMS_CODE_RESEND_SECONDS);
|
||||||
|
|
||||||
|
return json(['code' => 200, 'msg' => '验证码已发送']);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildLoginSuccessResponse($user, $tenant): Json
|
||||||
|
{
|
||||||
|
$tid = (int)$tenant->id;
|
||||||
|
|
||||||
|
try {
|
||||||
|
$loginCount = isset($user['login_count']) && $user['login_count'] !== null ? (int)$user['login_count'] : 0;
|
||||||
|
AdminUser::where('id', $user['id'])->update([
|
||||||
|
'login_count' => $loginCount + 1,
|
||||||
|
'last_login_ip' => $this->request->ip(),
|
||||||
|
'last_login_time' => date('Y-m-d H:i:s')
|
||||||
|
]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
error_log('更新登录信息失败: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
$userInfo = [
|
||||||
|
'id' => $user['id'],
|
||||||
|
'account' => $user['account'],
|
||||||
|
'name' => $user['name'],
|
||||||
|
'group_id' => $user['group_id'],
|
||||||
|
'tid' => $tid,
|
||||||
|
'tenant' => $tenant
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($user['group_id']) {
|
||||||
|
$userGroup = AdminUserGroup::where('id', $user['group_id'])->find();
|
||||||
|
if ($userGroup && $userGroup->rights) {
|
||||||
|
$userInfo['rights'] = json_decode($userGroup->rights, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$token = $this->generateToken($userInfo);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->logFail('登录管理', '登录', 'Token生成失败: ' . $e->getMessage());
|
||||||
|
return json(['code' => 500, 'msg' => '登录失败,请稍后重试']);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$cacheKey = 'admin_user_' . $user['id'] . '_' . $tid;
|
||||||
|
\think\facade\Cache::set($cacheKey, $userInfo, 86400 * 7);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
error_log('用户缓存写入失败: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$this->logSuccess('登录管理', '登录', [
|
||||||
|
'id' => $user['id'],
|
||||||
|
'tid' => $tid,
|
||||||
|
'tenant' => $tenant
|
||||||
|
], $userInfo);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
error_log('登录日志记录失败: ' . $e->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
return json([
|
||||||
|
'code' => 200,
|
||||||
|
'msg' => '登录成功',
|
||||||
|
'data' => [
|
||||||
|
'token' => $token,
|
||||||
|
'user' => $userInfo
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 登录接口
|
* 登录接口
|
||||||
* @return Json
|
* @return Json
|
||||||
@ -191,6 +374,87 @@ class LoginController extends BaseController
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送手机号登录验证码
|
||||||
|
*/
|
||||||
|
public function sendLoginCode(): Json
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$data = $this->request->post();
|
||||||
|
$this->validate($data, [
|
||||||
|
'tenant_name|租户名称' => 'require|length:1,128',
|
||||||
|
'phone|手机号' => 'require|mobile',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$tenant = Tenant::where('tenant_name', (string)$data['tenant_name'])
|
||||||
|
->where('status', 1)
|
||||||
|
->find();
|
||||||
|
if (!$tenant) {
|
||||||
|
return json(['code' => 400, 'msg' => '租户不存在或已禁用']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = AdminUser::where('tid', (int)$tenant['id'])
|
||||||
|
->where('phone', (string)$data['phone'])
|
||||||
|
->where('status', 1)
|
||||||
|
->where('delete_time', null)
|
||||||
|
->find();
|
||||||
|
if (!$user) {
|
||||||
|
return json(['code' => 404, 'msg' => '手机号未绑定可用账号']);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->sendSmsCodeInternal('login', (string)$data['tenant_name'], (string)$data['phone'], (string)$data['phone']);
|
||||||
|
} catch (ValidateException $e) {
|
||||||
|
return json(['code' => 400, 'msg' => $e->getError()]);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
return json(['code' => 500, 'msg' => '发送失败:' . $e->getMessage()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 手机号验证码登录
|
||||||
|
*/
|
||||||
|
public function loginBySms(): Json
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$data = $this->request->post();
|
||||||
|
$this->validate($data, [
|
||||||
|
'tenant_name|租户名称' => 'require|length:1,128',
|
||||||
|
'phone|手机号' => 'require|mobile',
|
||||||
|
'sms_code|短信验证码' => 'require|length:4,8',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$codeKey = $this->smsCodeCacheKey('login', (string)$data['tenant_name'], (string)$data['phone'], (string)$data['phone']);
|
||||||
|
$cachedCode = (string)Cache::get($codeKey, '');
|
||||||
|
if ($cachedCode === '' || $cachedCode !== (string)$data['sms_code']) {
|
||||||
|
return json(['code' => 400, 'msg' => '短信验证码错误或已过期']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$tenant = Tenant::where('tenant_name', (string)$data['tenant_name'])
|
||||||
|
->where('status', 1)
|
||||||
|
->field(['id', 'tenant_name'])
|
||||||
|
->find();
|
||||||
|
if (!$tenant) {
|
||||||
|
return json(['code' => 401, 'msg' => '租户不存在或已禁用']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = AdminUser::where('tid', (int)$tenant['id'])
|
||||||
|
->where('phone', (string)$data['phone'])
|
||||||
|
->where('status', 1)
|
||||||
|
->where('delete_time', null)
|
||||||
|
->find();
|
||||||
|
if (!$user) {
|
||||||
|
return json(['code' => 401, 'msg' => '手机号未绑定可用账号']);
|
||||||
|
}
|
||||||
|
|
||||||
|
Cache::delete($codeKey);
|
||||||
|
return $this->buildLoginSuccessResponse($user, $tenant);
|
||||||
|
} catch (ValidateException $e) {
|
||||||
|
return json(['code' => 400, 'msg' => $e->getError()]);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
return json(['code' => 500, 'msg' => '登录失败:' . $e->getMessage()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 退出登录
|
* 退出登录
|
||||||
* @return Json
|
* @return Json
|
||||||
@ -298,6 +562,182 @@ class LoginController extends BaseController
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册账号(按租户维度)
|
||||||
|
*/
|
||||||
|
public function sendRegisterCode(): Json
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$data = $this->request->post();
|
||||||
|
$this->validate($data, [
|
||||||
|
'tenant_name|租户名称' => 'require|length:1,128',
|
||||||
|
'account|账号' => 'require|length:3,32',
|
||||||
|
'phone|手机号' => 'require|mobile',
|
||||||
|
]);
|
||||||
|
return $this->sendSmsCodeInternal('register', (string)$data['tenant_name'], (string)$data['account'], (string)$data['phone']);
|
||||||
|
} catch (ValidateException $e) {
|
||||||
|
return json(['code' => 400, 'msg' => $e->getError()]);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
return json(['code' => 500, 'msg' => '发送失败:' . $e->getMessage()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function register(): Json
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$data = $this->request->post();
|
||||||
|
$this->validate($data, [
|
||||||
|
'tenant_name|租户名称' => 'require|length:1,128',
|
||||||
|
'account|账号' => 'require|length:3,32',
|
||||||
|
'name|姓名' => 'require|length:2,32',
|
||||||
|
'password|密码' => 'require|length:6,32',
|
||||||
|
'confirm_password|确认密码' => 'require',
|
||||||
|
'phone|手机号' => 'require|mobile',
|
||||||
|
'sms_code|短信验证码' => 'require|length:4,8',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ((string)$data['password'] !== (string)$data['confirm_password']) {
|
||||||
|
return json(['code' => 400, 'msg' => '两次输入的密码不一致']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$codeKey = $this->smsCodeCacheKey('register', (string)$data['tenant_name'], (string)$data['account'], (string)$data['phone']);
|
||||||
|
$cachedCode = (string)Cache::get($codeKey, '');
|
||||||
|
if ($cachedCode === '' || $cachedCode !== (string)$data['sms_code']) {
|
||||||
|
return json(['code' => 400, 'msg' => '短信验证码错误或已过期']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$tenant = Tenant::where('tenant_name', (string)$data['tenant_name'])
|
||||||
|
->where('status', 1)
|
||||||
|
->find();
|
||||||
|
if (!$tenant) {
|
||||||
|
return json(['code' => 400, 'msg' => '租户不存在或已禁用']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$exists = AdminUser::where('tid', (int)$tenant['id'])
|
||||||
|
->where('account', (string)$data['account'])
|
||||||
|
->where('delete_time', null)
|
||||||
|
->find();
|
||||||
|
if ($exists) {
|
||||||
|
return json(['code' => 400, 'msg' => '账号已存在']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$now = date('Y-m-d H:i:s');
|
||||||
|
$user = new AdminUser();
|
||||||
|
$user->save([
|
||||||
|
'tid' => (int)$tenant['id'],
|
||||||
|
'account' => (string)$data['account'],
|
||||||
|
'name' => (string)$data['name'],
|
||||||
|
'phone' => (string)$data['phone'],
|
||||||
|
'email' => (string)($data['email'] ?? ''),
|
||||||
|
'password' => md5((string)$data['password']),
|
||||||
|
'status' => 1,
|
||||||
|
'group_id' => 0,
|
||||||
|
'login_count' => 0,
|
||||||
|
'create_time' => $now,
|
||||||
|
'update_time' => $now,
|
||||||
|
]);
|
||||||
|
|
||||||
|
Cache::delete($codeKey);
|
||||||
|
|
||||||
|
return json(['code' => 200, 'msg' => '注册成功']);
|
||||||
|
} catch (ValidateException $e) {
|
||||||
|
return json(['code' => 400, 'msg' => $e->getError()]);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
return json(['code' => 500, 'msg' => '注册失败:' . $e->getMessage()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 忘记密码:通过租户 + 账号 + 手机号重置密码
|
||||||
|
*/
|
||||||
|
public function sendResetCode(): Json
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$data = $this->request->post();
|
||||||
|
$this->validate($data, [
|
||||||
|
'tenant_name|租户名称' => 'require|length:1,128',
|
||||||
|
'account|账号' => 'require|length:3,32',
|
||||||
|
'phone|手机号' => 'require|mobile',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$tenant = Tenant::where('tenant_name', (string)$data['tenant_name'])
|
||||||
|
->where('status', 1)
|
||||||
|
->find();
|
||||||
|
if (!$tenant) {
|
||||||
|
return json(['code' => 400, 'msg' => '租户不存在或已禁用']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = AdminUser::where('tid', (int)$tenant['id'])
|
||||||
|
->where('account', (string)$data['account'])
|
||||||
|
->where('phone', (string)$data['phone'])
|
||||||
|
->where('delete_time', null)
|
||||||
|
->find();
|
||||||
|
if (!$user) {
|
||||||
|
return json(['code' => 404, 'msg' => '账号不存在或手机号不匹配']);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->sendSmsCodeInternal('reset', (string)$data['tenant_name'], (string)$data['account'], (string)$data['phone']);
|
||||||
|
} catch (ValidateException $e) {
|
||||||
|
return json(['code' => 400, 'msg' => $e->getError()]);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
return json(['code' => 500, 'msg' => '发送失败:' . $e->getMessage()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function resetPassword(): Json
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$data = $this->request->post();
|
||||||
|
$this->validate($data, [
|
||||||
|
'tenant_name|租户名称' => 'require|length:1,128',
|
||||||
|
'account|账号' => 'require|length:3,32',
|
||||||
|
'phone|手机号' => 'require|mobile',
|
||||||
|
'new_password|新密码' => 'require|length:6,32',
|
||||||
|
'confirm_password|确认密码' => 'require',
|
||||||
|
'sms_code|短信验证码' => 'require|length:4,8',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ((string)$data['new_password'] !== (string)$data['confirm_password']) {
|
||||||
|
return json(['code' => 400, 'msg' => '两次输入的密码不一致']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$codeKey = $this->smsCodeCacheKey('reset', (string)$data['tenant_name'], (string)$data['account'], (string)$data['phone']);
|
||||||
|
$cachedCode = (string)Cache::get($codeKey, '');
|
||||||
|
if ($cachedCode === '' || $cachedCode !== (string)$data['sms_code']) {
|
||||||
|
return json(['code' => 400, 'msg' => '短信验证码错误或已过期']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$tenant = Tenant::where('tenant_name', (string)$data['tenant_name'])
|
||||||
|
->where('status', 1)
|
||||||
|
->find();
|
||||||
|
if (!$tenant) {
|
||||||
|
return json(['code' => 400, 'msg' => '租户不存在或已禁用']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = AdminUser::where('tid', (int)$tenant['id'])
|
||||||
|
->where('account', (string)$data['account'])
|
||||||
|
->where('phone', (string)$data['phone'])
|
||||||
|
->where('delete_time', null)
|
||||||
|
->find();
|
||||||
|
if (!$user) {
|
||||||
|
return json(['code' => 404, 'msg' => '账号不存在或手机号不匹配']);
|
||||||
|
}
|
||||||
|
|
||||||
|
AdminUser::where('id', (int)$user['id'])->update([
|
||||||
|
'password' => md5((string)$data['new_password']),
|
||||||
|
'update_time' => date('Y-m-d H:i:s'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
Cache::delete($codeKey);
|
||||||
|
|
||||||
|
return json(['code' => 200, 'msg' => '密码重置成功']);
|
||||||
|
} catch (ValidateException $e) {
|
||||||
|
return json(['code' => 400, 'msg' => $e->getError()]);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
return json(['code' => 500, 'msg' => '重置失败:' . $e->getMessage()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取极验3.0的id和key
|
* 获取极验3.0的id和key
|
||||||
* @return Json
|
* @return Json
|
||||||
|
|||||||
676
app/admin/controller/System/SmsController.php
Normal file
676
app/admin/controller/System/SmsController.php
Normal file
@ -0,0 +1,676 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace app\admin\controller\System;
|
||||||
|
|
||||||
|
use app\admin\BaseController;
|
||||||
|
use app\model\System\SystemSiteSettings;
|
||||||
|
use think\facade\Db;
|
||||||
|
use think\facade\Log;
|
||||||
|
use think\db\exception\DbException;
|
||||||
|
use think\exception\ValidateException;
|
||||||
|
use think\facade\Request;
|
||||||
|
|
||||||
|
class SmsController extends BaseController
|
||||||
|
{
|
||||||
|
private function getSiteSettingValue(string $label): string
|
||||||
|
{
|
||||||
|
$row = SystemSiteSettings::where('label', $label)->order('id', 'asc')->find();
|
||||||
|
if (!$row) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return (string)$row['value'];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function upsertSiteSetting(string $label, string $value): void
|
||||||
|
{
|
||||||
|
$row = SystemSiteSettings::where('label', $label)->order('id', 'asc')->find();
|
||||||
|
if ($row) {
|
||||||
|
SystemSiteSettings::update(['value' => $value], ['id' => $row['id']]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$model = new SystemSiteSettings();
|
||||||
|
$model->label = $label;
|
||||||
|
$model->value = $value;
|
||||||
|
$model->sort = 0;
|
||||||
|
$model->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取短信网关配置
|
||||||
|
*/
|
||||||
|
public function getSmsInfo()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$backendUrl = $this->getSiteSettingValue('backendUrl');
|
||||||
|
$apiKey = $this->getSiteSettingValue('apiKey');
|
||||||
|
|
||||||
|
// 前端约定返回 data[0],字段名兼容 backend_url/api_key 与 backendUrl/apiKey
|
||||||
|
$data = [[
|
||||||
|
'backend_url' => $backendUrl,
|
||||||
|
'api_key' => $apiKey,
|
||||||
|
'backendUrl' => $backendUrl,
|
||||||
|
'apiKey' => $apiKey,
|
||||||
|
]];
|
||||||
|
|
||||||
|
$this->logSuccess('短信管理', '获取短信网关配置', ['data' => $data]);
|
||||||
|
|
||||||
|
return json([
|
||||||
|
'code' => 200,
|
||||||
|
'msg' => '获取成功',
|
||||||
|
'data' => $data,
|
||||||
|
]);
|
||||||
|
} catch (DbException $e) {
|
||||||
|
$this->logFail('短信管理', '获取短信网关配置', $e->getMessage());
|
||||||
|
return json([
|
||||||
|
'code' => 500,
|
||||||
|
'msg' => '获取失败:' . $e->getMessage(),
|
||||||
|
'data' => [],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编辑短信网关配置(本表设计为单条配置)
|
||||||
|
*/
|
||||||
|
public function editSmsInfo()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$raw = $this->request->post();
|
||||||
|
$data = [
|
||||||
|
'backend_url' => $raw['backendUrl'] ?? $raw['backend_url'] ?? '',
|
||||||
|
'api_key' => $raw['apiKey'] ?? $raw['api_key'] ?? '',
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->validate($data, [
|
||||||
|
'backend_url|短信网关地址' => 'require|max:255',
|
||||||
|
'api_key|短信网关 API KEY' => 'require|max:255',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->upsertSiteSetting('backendUrl', (string)$data['backend_url']);
|
||||||
|
$this->upsertSiteSetting('apiKey', (string)$data['api_key']);
|
||||||
|
|
||||||
|
$updated = [[
|
||||||
|
'backend_url' => $this->getSiteSettingValue('backendUrl'),
|
||||||
|
'api_key' => $this->getSiteSettingValue('apiKey'),
|
||||||
|
]];
|
||||||
|
|
||||||
|
$this->logSuccess('短信管理', '更新短信网关配置', ['data' => $updated]);
|
||||||
|
|
||||||
|
return json([
|
||||||
|
'code' => 200,
|
||||||
|
'msg' => '更新成功',
|
||||||
|
'data' => $updated,
|
||||||
|
]);
|
||||||
|
} catch (ValidateException $e) {
|
||||||
|
$this->logFail('短信管理', '更新短信网关配置', $e->getMessage());
|
||||||
|
return json([
|
||||||
|
'code' => 400,
|
||||||
|
'msg' => $e->getError(),
|
||||||
|
]);
|
||||||
|
} catch (DbException $e) {
|
||||||
|
$this->logFail('短信管理', '更新短信网关配置', $e->getMessage());
|
||||||
|
return json([
|
||||||
|
'code' => 500,
|
||||||
|
'msg' => '更新失败:' . $e->getMessage(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 后台发起“短信测试”:入队一条任务,等待 Android 网关轮询取走并发送
|
||||||
|
*/
|
||||||
|
public function sendTestSms()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$raw = $this->request->post();
|
||||||
|
$phone = trim((string)($raw['phone'] ?? $raw['testPhone'] ?? ''));
|
||||||
|
$content = trim((string)($raw['content'] ?? ''));
|
||||||
|
|
||||||
|
if (empty($phone)) {
|
||||||
|
return json(['code' => 400, 'msg' => '缺少测试手机号']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Android 网关文档:国际格式 +8613712345678
|
||||||
|
if (!preg_match('/^\+\d{6,15}$/', $phone)) {
|
||||||
|
return json(['code' => 400, 'msg' => '请使用国际格式手机号(以 + 开头,后为数字)']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取当前网关配置(来自 mete_system_site_settings label/value)
|
||||||
|
$apiKey = $this->getSiteSettingValue('apiKey');
|
||||||
|
$backendUrl = $this->getSiteSettingValue('backendUrl');
|
||||||
|
if (empty($apiKey)) {
|
||||||
|
return json(['code' => 400, 'msg' => '请先配置短信网关 API KEY']);
|
||||||
|
}
|
||||||
|
if (empty($backendUrl)) {
|
||||||
|
return json(['code' => 400, 'msg' => '请先配置短信网关地址 backendUrl']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$code = (string)random_int(100000, 999999);
|
||||||
|
if ($content === '') {
|
||||||
|
$content = '短信测试验证码:' . $code;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 兼容:调用 Uniapp/sms 后端入队发送任务
|
||||||
|
$enqueueUrl = rtrim($backendUrl, '/') . '/api/v1/business/outbound-tasks';
|
||||||
|
$enqueuePayload = [
|
||||||
|
'phone' => $phone,
|
||||||
|
'content' => $content,
|
||||||
|
];
|
||||||
|
$enqueueHeaders = [
|
||||||
|
'X-Api-Key: ' . (string)$apiKey,
|
||||||
|
'Content-Type: application/json; charset=utf-8',
|
||||||
|
'Accept: application/json',
|
||||||
|
];
|
||||||
|
|
||||||
|
Log::write('[sms_enqueue] url=' . $enqueueUrl . ' phone=' . $phone);
|
||||||
|
$enqueueResp = $this->postJson($enqueueUrl, $enqueuePayload, $enqueueHeaders);
|
||||||
|
if (empty($enqueueResp['ok'])) {
|
||||||
|
Log::write('[sms_enqueue] failed http_status=' . ($enqueueResp['status'] ?? '') . ' body=' . ($enqueueResp['body'] ?? ''));
|
||||||
|
return json([
|
||||||
|
'code' => 500,
|
||||||
|
'msg' => '短信网关入队失败:' . ($enqueueResp['error'] ?? 'unknown'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$smsGatewayTaskId = (string)($enqueueResp['json']['taskId'] ?? '');
|
||||||
|
Log::write('[sms_enqueue] success taskId=' . $smsGatewayTaskId);
|
||||||
|
|
||||||
|
$enqueueDebug = [
|
||||||
|
'enqueueOk' => (bool)($enqueueResp['ok'] ?? false),
|
||||||
|
'enqueueStatus' => $enqueueResp['status'] ?? null,
|
||||||
|
'enqueueJsonKeys' => array_keys((array)($enqueueResp['json'] ?? [])),
|
||||||
|
'gatewayTaskId' => $smsGatewayTaskId,
|
||||||
|
];
|
||||||
|
|
||||||
|
// 同步一条本地记录(用于前端列表展示/对账)
|
||||||
|
$now = date('Y-m-d H:i:s');
|
||||||
|
$taskId = Db::name('mete_system_sms_tasks')->insertGetId([
|
||||||
|
'api_key' => (string)$apiKey,
|
||||||
|
'tid' => $this->getTenantId(),
|
||||||
|
'phone' => $phone,
|
||||||
|
'content' => $content,
|
||||||
|
'status' => 0, // pending(本地不自动同步网关状态)
|
||||||
|
'code' => $code, // 预期验证码(用于对账/展示)
|
||||||
|
'create_time' => $now,
|
||||||
|
'update_time' => $now,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->logSuccess('短信管理', '发送测试短信(调用网关入队)', [
|
||||||
|
'to' => $phone,
|
||||||
|
'local_task_id' => $taskId ?? null,
|
||||||
|
'gateway_task_id' => $smsGatewayTaskId,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return json([
|
||||||
|
'code' => 200,
|
||||||
|
'msg' => '测试短信任务已入队,等待网关发送',
|
||||||
|
'data' => [
|
||||||
|
'taskId' => $smsGatewayTaskId ?: ($taskId ?? null),
|
||||||
|
'code' => $code,
|
||||||
|
],
|
||||||
|
'debug' => empty($smsGatewayTaskId) ? $enqueueDebug : null,
|
||||||
|
]);
|
||||||
|
} catch (ValidateException $e) {
|
||||||
|
$this->logFail('短信管理', '发送测试短信', $e->getMessage());
|
||||||
|
return json(['code' => 400, 'msg' => $e->getError()]);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$this->logFail('短信管理', '发送测试短信', $e->getMessage());
|
||||||
|
return json(['code' => 500, 'msg' => '发送失败:' . $e->getMessage()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 简单 JSON POST(无 curl 扩展时使用)
|
||||||
|
* @return array{ok:bool,status?:int,body?:string,error?:string,json?:array}
|
||||||
|
*/
|
||||||
|
private function postJson(string $url, array $payload, array $headers): array
|
||||||
|
{
|
||||||
|
$body = json_encode($payload, JSON_UNESCAPED_UNICODE);
|
||||||
|
if ($body === false) {
|
||||||
|
return ['ok' => false, 'error' => 'json_encode failed'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$headerStr = implode("\r\n", $headers);
|
||||||
|
$context = stream_context_create([
|
||||||
|
'http' => [
|
||||||
|
'method' => 'POST',
|
||||||
|
'header' => $headerStr,
|
||||||
|
'content' => $body,
|
||||||
|
'timeout' => 10,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$respBody = @file_get_contents($url, false, $context);
|
||||||
|
$status = 0;
|
||||||
|
if (!empty($http_response_header) && is_array($http_response_header)) {
|
||||||
|
foreach ($http_response_header as $line) {
|
||||||
|
if (preg_match('#^HTTP/\\S+\\s+(\\d+)#', $line, $m)) {
|
||||||
|
$status = (int)$m[1];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($respBody === false) {
|
||||||
|
$last = error_get_last();
|
||||||
|
$errMsg = $last['message'] ?? 'http request failed';
|
||||||
|
return ['ok' => false, 'status' => $status, 'error' => (string)$errMsg];
|
||||||
|
}
|
||||||
|
|
||||||
|
$decoded = null;
|
||||||
|
$decoded = json_decode((string)$respBody, true);
|
||||||
|
if (!is_array($decoded)) {
|
||||||
|
$decoded = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$ok = $status >= 200 && $status < 300;
|
||||||
|
return [
|
||||||
|
'ok' => $ok,
|
||||||
|
'status' => $status,
|
||||||
|
'body' => (string)$respBody,
|
||||||
|
'json' => $decoded,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 简单 JSON GET(无 curl 扩展时使用)
|
||||||
|
* @return array{ok:bool,status?:int,body?:string,error?:string,json?:array}
|
||||||
|
*/
|
||||||
|
private function httpGetJson(string $url, array $headers): array
|
||||||
|
{
|
||||||
|
$headerStr = implode("\r\n", $headers);
|
||||||
|
$context = stream_context_create([
|
||||||
|
'http' => [
|
||||||
|
'method' => 'GET',
|
||||||
|
'header' => $headerStr,
|
||||||
|
'timeout' => 10,
|
||||||
|
// 对 4xx/5xx 也返回响应体,便于排障
|
||||||
|
'ignore_errors' => true,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$respBody = @file_get_contents($url, false, $context);
|
||||||
|
$status = 0;
|
||||||
|
if (!empty($http_response_header) && is_array($http_response_header)) {
|
||||||
|
foreach ($http_response_header as $line) {
|
||||||
|
if (preg_match('#^HTTP/\\S+\\s+(\\d+)#', $line, $m)) {
|
||||||
|
$status = (int)$m[1];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($respBody === false) {
|
||||||
|
$last = error_get_last();
|
||||||
|
$errMsg = $last['message'] ?? 'http request failed';
|
||||||
|
return ['ok' => false, 'status' => $status, 'error' => (string)$errMsg];
|
||||||
|
}
|
||||||
|
|
||||||
|
$decoded = json_decode((string)$respBody, true);
|
||||||
|
if (!is_array($decoded)) {
|
||||||
|
$decoded = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$ok = $status >= 200 && $status < 300;
|
||||||
|
return [
|
||||||
|
'ok' => $ok,
|
||||||
|
'status' => $status,
|
||||||
|
'body' => (string)$respBody,
|
||||||
|
'json' => $decoded,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Android 网关轮询:获取待发送任务
|
||||||
|
*
|
||||||
|
* 约定:
|
||||||
|
* - apiKey 通过 header `X-API-KEY` / `X-SMS-GATEWAY-API-KEY` 或 body/query `apiKey`
|
||||||
|
*/
|
||||||
|
public function gatewayPoll()
|
||||||
|
{
|
||||||
|
$apiKey = $this->getApiKeyFromRequest();
|
||||||
|
if (empty($apiKey)) {
|
||||||
|
Log::write('[sms_gateway_poll] missing apiKey');
|
||||||
|
return json(['code' => 401, 'msg' => '缺少 apiKey']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 不强制要求 mete_system_site_settings(apiKey) 与网关入参完全一致;
|
||||||
|
// 直接按任务表的 api_key 匹配,提高兼容性与排障速度。
|
||||||
|
$apiKeySetting = $this->getSiteSettingValue('apiKey');
|
||||||
|
if (empty($apiKeySetting) || (string)$apiKeySetting !== (string)$apiKey) {
|
||||||
|
Log::write('[sms_gateway_poll] apiKey differs from setting; setting=' . $this->maskKey($apiKeySetting) . ' req=' . $this->maskKey($apiKey));
|
||||||
|
}
|
||||||
|
|
||||||
|
$task = Db::name('mete_system_sms_tasks')
|
||||||
|
->where('api_key', (string)$apiKey)
|
||||||
|
->where('status', 0)
|
||||||
|
->where('delete_time', null)
|
||||||
|
->order('id', 'asc')
|
||||||
|
->find();
|
||||||
|
|
||||||
|
// 没有任务
|
||||||
|
if (!$task) {
|
||||||
|
Log::write('[sms_gateway_poll] no task for apiKey=' . $this->maskKey($apiKey));
|
||||||
|
return json([
|
||||||
|
'code' => 200,
|
||||||
|
'msg' => 'no task',
|
||||||
|
'data' => null,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 标记为处理中,避免被重复领取
|
||||||
|
Db::name('mete_system_sms_tasks')->where('id', (int)$task['id'])->update([
|
||||||
|
'status' => 1,
|
||||||
|
'update_time' => date('Y-m-d H:i:s'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
Log::write('[sms_gateway_poll] send task id=' . (int)$task['id'] . ' phone=' . (string)$task['phone']);
|
||||||
|
|
||||||
|
return json([
|
||||||
|
'code' => 200,
|
||||||
|
'msg' => 'success',
|
||||||
|
'data' => [
|
||||||
|
'taskId' => $task['id'],
|
||||||
|
'task_id' => $task['id'],
|
||||||
|
'phone' => $task['phone'],
|
||||||
|
'content' => $task['content'],
|
||||||
|
'smsContent' => $task['content'],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Android 网关上报:上传实际收到的短信内容并解析验证码
|
||||||
|
*/
|
||||||
|
public function gatewayReport()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$raw = $this->request->post();
|
||||||
|
$apiKey = $this->getApiKeyFromRequest();
|
||||||
|
$taskId = $raw['taskId'] ?? $raw['task_id'] ?? $raw['id'] ?? null;
|
||||||
|
$smsContent = trim((string)($raw['smsContent'] ?? $raw['sms_content'] ?? $raw['content'] ?? $raw['message'] ?? $raw['sms'] ?? ''));
|
||||||
|
$status = $raw['status'] ?? $raw['result'] ?? $raw['state'] ?? null; // success/failed/int
|
||||||
|
|
||||||
|
if (empty($apiKey)) {
|
||||||
|
return json(['code' => 401, 'msg' => '缺少 apiKey']);
|
||||||
|
}
|
||||||
|
if (empty($taskId)) {
|
||||||
|
return json(['code' => 400, 'msg' => '缺少 taskId']);
|
||||||
|
}
|
||||||
|
if ($smsContent === '') {
|
||||||
|
return json(['code' => 400, 'msg' => '缺少 smsContent']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$task = Db::name('mete_system_sms_tasks')
|
||||||
|
->where('id', (int)$taskId)
|
||||||
|
->where('api_key', (string)$apiKey)
|
||||||
|
->where('delete_time', null)
|
||||||
|
->find();
|
||||||
|
|
||||||
|
if (!$task) {
|
||||||
|
Log::write('[sms_gateway_report] task not found; taskId=' . (int)$taskId . ' apiKey=' . $this->maskKey($apiKey));
|
||||||
|
return json(['code' => 404, 'msg' => '任务不存在或 apiKey 不匹配']);
|
||||||
|
}
|
||||||
|
|
||||||
|
Log::write('[sms_gateway_report] received taskId=' . (int)$taskId . ' status=' . (string)($status ?? ''));
|
||||||
|
|
||||||
|
// 从短信内容里抓取验证码(通用策略:4-6 位数字)
|
||||||
|
$parsedCode = '';
|
||||||
|
if (preg_match('/(\d{4,6})/', $smsContent, $m)) {
|
||||||
|
$parsedCode = (string)$m[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
$update = [
|
||||||
|
'report_raw' => $smsContent,
|
||||||
|
'code' => $parsedCode !== '' ? $parsedCode : (string)$task['code'],
|
||||||
|
'update_time' => date('Y-m-d H:i:s'),
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($status === 'failed' || $status === 'error' || $status === 2) {
|
||||||
|
$update['status'] = 2;
|
||||||
|
} elseif ($status === 'success' || $status === 'reported' || $status === 3 || $status === 1) {
|
||||||
|
// success => reported/done
|
||||||
|
$update['status'] = 3;
|
||||||
|
} else {
|
||||||
|
// 默认当做已上报
|
||||||
|
$update['status'] = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
Db::name('mete_system_sms_tasks')->where('id', (int)$task['id'])->update($update);
|
||||||
|
|
||||||
|
$this->logSuccess('短信管理', '网关上报短信(解析验证码)', [
|
||||||
|
'task_id' => $task['id'],
|
||||||
|
'parsed_code' => $update['code'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
return json(['code' => 200, 'msg' => '上报成功']);
|
||||||
|
} catch (DbException $e) {
|
||||||
|
$this->logFail('短信管理', '网关上报短信', $e->getMessage());
|
||||||
|
return json(['code' => 500, 'msg' => '服务器错误:' . $e->getMessage()]);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$this->logFail('短信管理', '网关上报短信', $e->getMessage());
|
||||||
|
return json(['code' => 500, 'msg' => '服务器错误:' . $e->getMessage()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理员:获取短信任务列表(租户隔离)
|
||||||
|
*
|
||||||
|
* 支持参数(可选):
|
||||||
|
* - status: 任务状态
|
||||||
|
* - phone: 手机号关键词
|
||||||
|
*/
|
||||||
|
public function getSmsTaskList()
|
||||||
|
{
|
||||||
|
$backendUrl = $this->getSiteSettingValue('backendUrl');
|
||||||
|
$apiKey = $this->getSiteSettingValue('apiKey');
|
||||||
|
|
||||||
|
if (empty($backendUrl) || empty($apiKey)) {
|
||||||
|
return json([
|
||||||
|
'code' => 400,
|
||||||
|
'msg' => '短信网关配置未完成(backendUrl/apiKey 缺失)',
|
||||||
|
'list' => [],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$status = (string)Request::param('status', '');
|
||||||
|
$phoneKeyword = trim((string)Request::param('phone', ''));
|
||||||
|
$limit = (int)Request::param('limit', 50, 'int');
|
||||||
|
$limit = $limit > 0 ? min($limit, 200) : 50;
|
||||||
|
|
||||||
|
// 前端 status: 0/1/2/3 -> uniapp backend status: pending/sending/failed/success
|
||||||
|
$statusMap = [
|
||||||
|
'0' => 'pending',
|
||||||
|
'1' => 'sending',
|
||||||
|
'2' => 'failed',
|
||||||
|
'3' => 'success',
|
||||||
|
];
|
||||||
|
$uniStatus = $status !== '' && isset($statusMap[$status]) ? $statusMap[$status] : '';
|
||||||
|
|
||||||
|
$url = rtrim($backendUrl, '/') . '/api/v1/business/outbound-tasks?limit=' . $limit;
|
||||||
|
if ($uniStatus !== '') {
|
||||||
|
$url .= '&status=' . urlencode($uniStatus);
|
||||||
|
}
|
||||||
|
if ($phoneKeyword !== '') {
|
||||||
|
$url .= '&phone=' . urlencode($phoneKeyword);
|
||||||
|
}
|
||||||
|
|
||||||
|
$headers = [
|
||||||
|
'X-Api-Key: ' . (string)$apiKey,
|
||||||
|
'Accept: application/json',
|
||||||
|
];
|
||||||
|
|
||||||
|
$resp = $this->httpGetJson($url, $headers);
|
||||||
|
if (empty($resp['ok'])) {
|
||||||
|
// 兼容:如果 uniapp 后端还未部署新增查询接口,则 fallback 到本地队列表(只能看到“已入队”,无法看到发送成功/失败)
|
||||||
|
$fallback = [];
|
||||||
|
try {
|
||||||
|
$fWhere = [
|
||||||
|
['api_key', '=', (string)$apiKey],
|
||||||
|
['delete_time', '=', null],
|
||||||
|
];
|
||||||
|
if ($phoneKeyword !== '') {
|
||||||
|
$fWhere[] = ['phone', 'like', '%' . $phoneKeyword . '%'];
|
||||||
|
}
|
||||||
|
// 前端 status=0/1/2/3,局部队列表字段同样使用数字状态
|
||||||
|
if ($status !== '') {
|
||||||
|
$fWhere[] = ['status', '=', (int)$status];
|
||||||
|
}
|
||||||
|
|
||||||
|
$fallback = Db::name('mete_system_sms_tasks')
|
||||||
|
->where($fWhere)
|
||||||
|
->order('id', 'desc')
|
||||||
|
->limit($limit)
|
||||||
|
->select()
|
||||||
|
->toArray();
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$fallback = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return json([
|
||||||
|
'code' => 200,
|
||||||
|
'msg' => 'uniapp 查询任务接口不可用,已使用本地队列表(仅展示入队状态)',
|
||||||
|
'detail' => [
|
||||||
|
'http_status' => $resp['status'] ?? null,
|
||||||
|
'body_preview' => isset($resp['body']) ? substr((string)$resp['body'], 0, 300) : '',
|
||||||
|
],
|
||||||
|
'list' => $fallback,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$tasks = (array)($resp['json']['tasks'] ?? []);
|
||||||
|
|
||||||
|
$statusNumMap = [
|
||||||
|
'pending' => 0,
|
||||||
|
'sending' => 1,
|
||||||
|
'failed' => 2,
|
||||||
|
'success' => 3,
|
||||||
|
];
|
||||||
|
|
||||||
|
$list = array_map(function ($t) use ($statusNumMap) {
|
||||||
|
$content = (string)($t['content'] ?? '');
|
||||||
|
$parsedCode = '';
|
||||||
|
if (preg_match('/(\d{4,6})/', $content, $m)) {
|
||||||
|
$parsedCode = (string)$m[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
$statusStr = (string)($t['status'] ?? '');
|
||||||
|
|
||||||
|
return [
|
||||||
|
'id' => $t['taskId'] ?? '',
|
||||||
|
'phone' => $t['phone'] ?? '',
|
||||||
|
'content' => $content,
|
||||||
|
'code' => $parsedCode,
|
||||||
|
'status' => $statusNumMap[$statusStr] ?? 0,
|
||||||
|
'create_time' => isset($t['createdAt']) ? date('Y-m-d H:i:s', (int)($t['createdAt'] / 1000)) : '',
|
||||||
|
'update_time' => isset($t['updatedAt']) ? date('Y-m-d H:i:s', (int)($t['updatedAt'] / 1000)) : '',
|
||||||
|
];
|
||||||
|
}, $tasks);
|
||||||
|
|
||||||
|
return json([
|
||||||
|
'code' => 200,
|
||||||
|
'msg' => 'success',
|
||||||
|
'list' => $list,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理员:编辑短信任务(租户隔离)
|
||||||
|
*
|
||||||
|
* body:
|
||||||
|
* - status
|
||||||
|
* - code
|
||||||
|
* - report_raw
|
||||||
|
*/
|
||||||
|
public function editSmsTask($id = null)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$tid = $this->getTenantId();
|
||||||
|
$id = $this->request->param('id', $id);
|
||||||
|
|
||||||
|
$data = Request::only(['status', 'code', 'report_raw']);
|
||||||
|
|
||||||
|
if (empty($id)) {
|
||||||
|
return json(['code' => 400, 'msg' => '任务ID不能为空']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($data['status'])) {
|
||||||
|
return json(['code' => 400, 'msg' => '状态不能为空']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->validate($data, [
|
||||||
|
'status|状态' => 'integer',
|
||||||
|
'code|验证码' => 'max:20',
|
||||||
|
'report_raw|上报内容' => 'max:100000',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$task = Db::name('mete_system_sms_tasks')
|
||||||
|
->where('id', (int)$id)
|
||||||
|
->where('tid', (int)$tid)
|
||||||
|
->find();
|
||||||
|
|
||||||
|
if (!$task) {
|
||||||
|
return json(['code' => 404, 'msg' => '任务不存在或无权限']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$update = [
|
||||||
|
'status' => (int)$data['status'],
|
||||||
|
'code' => (string)($data['code'] ?? ''),
|
||||||
|
'report_raw' => (string)($data['report_raw'] ?? ''),
|
||||||
|
'update_time' => date('Y-m-d H:i:s'),
|
||||||
|
];
|
||||||
|
|
||||||
|
Db::name('mete_system_sms_tasks')
|
||||||
|
->where('id', (int)$id)
|
||||||
|
->where('tid', (int)$tid)
|
||||||
|
->update($update);
|
||||||
|
|
||||||
|
return json(['code' => 200, 'msg' => '编辑成功']);
|
||||||
|
} catch (ValidateException $e) {
|
||||||
|
return json(['code' => 400, 'msg' => $e->getError()]);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
return json(['code' => 500, 'msg' => '编辑失败:' . $e->getMessage()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getApiKeyFromRequest(): string
|
||||||
|
{
|
||||||
|
$req = Request::instance();
|
||||||
|
|
||||||
|
$apiKey = (string)$req->header('X-SMS-GATEWAY-API-KEY', '');
|
||||||
|
if ($apiKey !== '') {
|
||||||
|
return $apiKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
$apiKey = (string)$req->header('X-API-KEY', '');
|
||||||
|
if ($apiKey !== '') {
|
||||||
|
return $apiKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
$apiKey = (string)$req->header('X-SMS-API-KEY', '');
|
||||||
|
if ($apiKey !== '') {
|
||||||
|
return $apiKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
$apiKey = (string)$req->header('Authorization', '');
|
||||||
|
if ($apiKey !== '' && preg_match('/Bearer\\s+(.+)/i', $apiKey, $m)) {
|
||||||
|
return (string)$m[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
// body/query
|
||||||
|
$apiKey = (string)($req->post('apiKey') ?? $req->post('api_key') ?? $req->param('apiKey', '') ?? $req->param('api_key', ''));
|
||||||
|
return $apiKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function maskKey(string $key): string
|
||||||
|
{
|
||||||
|
$key = (string)$key;
|
||||||
|
if ($key === '') return '';
|
||||||
|
if (strlen($key) <= 6) return '***';
|
||||||
|
return substr($key, 0, 3) . '***' . substr($key, -3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -14,21 +14,46 @@ use app\model\System\AdminUser;
|
|||||||
|
|
||||||
class UserController extends BaseController
|
class UserController extends BaseController
|
||||||
{
|
{
|
||||||
|
private function getCurrentTenantIdOrFail(): int
|
||||||
|
{
|
||||||
|
$tid = $this->getTenantId();
|
||||||
|
if ($tid <= 0) {
|
||||||
|
throw new \RuntimeException('未获取到有效租户信息');
|
||||||
|
}
|
||||||
|
return $tid;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取所有用户信息
|
* 获取所有用户信息
|
||||||
* @return Json
|
* @return Json
|
||||||
*/
|
*/
|
||||||
public function getAllUsers()
|
public function getAllUsers()
|
||||||
{
|
{
|
||||||
$users = AdminUser::where('delete_time', null)->field('id, account, name, phone, birth, email, qq, sex, group_id, status, last_login_ip, login_count, create_time, update_time')->select()->toArray();
|
try {
|
||||||
return json([
|
$tid = $this->getCurrentTenantIdOrFail();
|
||||||
'code' => 200,
|
$users = AdminUser::where('delete_time', null)
|
||||||
'msg' => '获取成功',
|
->where('tid', $tid)
|
||||||
'data' => [
|
->field('id, tid, account, name, phone, birth, email, qq, sex, group_id, status, last_login_ip, login_count, create_time, update_time')
|
||||||
'list' => $users,
|
->select()
|
||||||
'total' => count($users)
|
->toArray();
|
||||||
]
|
return json([
|
||||||
]);
|
'code' => 200,
|
||||||
|
'msg' => '获取成功',
|
||||||
|
'data' => [
|
||||||
|
'list' => $users,
|
||||||
|
'total' => count($users)
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
return json([
|
||||||
|
'code' => 401,
|
||||||
|
'msg' => '获取用户失败:' . $e->getMessage(),
|
||||||
|
'data' => [
|
||||||
|
'list' => [],
|
||||||
|
'total' => 0
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -37,15 +62,33 @@ class UserController extends BaseController
|
|||||||
*/
|
*/
|
||||||
public function getTenantUsers(int $tenantId)
|
public function getTenantUsers(int $tenantId)
|
||||||
{
|
{
|
||||||
$users = AdminUser::where('delete_time', null)->where('tid', $tenantId)->field('id, account, name, phone, birth, email, qq, sex, group_id, status, last_login_ip, login_count, create_time, update_time')->select()->toArray();
|
try {
|
||||||
return json([
|
$tid = $this->getCurrentTenantIdOrFail();
|
||||||
'code' => 200,
|
if ($tenantId !== $tid) {
|
||||||
'msg' => '获取成功',
|
return json([
|
||||||
'data' => [
|
'code' => 403,
|
||||||
'list' => $users,
|
'msg' => '禁止跨租户查看用户'
|
||||||
'total' => count($users)
|
]);
|
||||||
]
|
}
|
||||||
]);
|
$users = AdminUser::where('delete_time', null)
|
||||||
|
->where('tid', $tid)
|
||||||
|
->field('id, tid, account, name, phone, birth, email, qq, sex, group_id, status, last_login_ip, login_count, create_time, update_time')
|
||||||
|
->select()
|
||||||
|
->toArray();
|
||||||
|
return json([
|
||||||
|
'code' => 200,
|
||||||
|
'msg' => '获取成功',
|
||||||
|
'data' => [
|
||||||
|
'list' => $users,
|
||||||
|
'total' => count($users)
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
return json([
|
||||||
|
'code' => 401,
|
||||||
|
'msg' => '获取用户失败:' . $e->getMessage()
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -54,9 +97,18 @@ class UserController extends BaseController
|
|||||||
*/
|
*/
|
||||||
public function getUserInfo(int $id)
|
public function getUserInfo(int $id)
|
||||||
{
|
{
|
||||||
|
$tid = $this->getTenantId();
|
||||||
$user = AdminUser::where('id', $id)
|
$user = AdminUser::where('id', $id)
|
||||||
|
->where('tid', $tid)
|
||||||
|
->where('delete_time', null)
|
||||||
->field('id, account, name, phone, email, birth, qq, sex, group_id, status, create_time, last_login_ip')
|
->field('id, account, name, phone, email, birth, qq, sex, group_id, status, create_time, last_login_ip')
|
||||||
->find();
|
->find();
|
||||||
|
if (!$user) {
|
||||||
|
return json([
|
||||||
|
'code' => 404,
|
||||||
|
'msg' => '用户不存在或无权限访问'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
// 记录操作日志
|
// 记录操作日志
|
||||||
$this->logSuccess('用户管理', '获取用户信息', ['id' => $id]);
|
$this->logSuccess('用户管理', '获取用户信息', ['id' => $id]);
|
||||||
@ -75,10 +127,20 @@ class UserController extends BaseController
|
|||||||
public function changePassword(int $id, string $password)
|
public function changePassword(int $id, string $password)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
AdminUser::where('id', $id)->update([
|
$tid = $this->getCurrentTenantIdOrFail();
|
||||||
|
$affected = AdminUser::where('id', $id)
|
||||||
|
->where('tid', $tid)
|
||||||
|
->where('delete_time', null)
|
||||||
|
->update([
|
||||||
'password' => md5($password),
|
'password' => md5($password),
|
||||||
'update_time' => date('Y-m-d H:i:s')
|
'update_time' => date('Y-m-d H:i:s')
|
||||||
]);
|
]);
|
||||||
|
if (!$affected) {
|
||||||
|
return json([
|
||||||
|
'code' => 404,
|
||||||
|
'msg' => '用户不存在或无权限修改'
|
||||||
|
]);
|
||||||
|
}
|
||||||
// 记录操作日志
|
// 记录操作日志
|
||||||
$this->logSuccess('用户管理', '修改密码', ['id' => $id]);
|
$this->logSuccess('用户管理', '修改密码', ['id' => $id]);
|
||||||
return json([
|
return json([
|
||||||
@ -102,18 +164,20 @@ class UserController extends BaseController
|
|||||||
public function addUser()
|
public function addUser()
|
||||||
{
|
{
|
||||||
$data = request()->param();
|
$data = request()->param();
|
||||||
|
$tid = $this->getTenantId();
|
||||||
|
if ($tid <= 0) {
|
||||||
|
return json([
|
||||||
|
'code' => 401,
|
||||||
|
'msg' => '未获取到有效租户信息'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
$data['password'] = md5($data['password']);
|
$data['password'] = md5($data['password']);
|
||||||
$data['create_time'] = date('Y-m-d H:i:s');
|
$data['create_time'] = date('Y-m-d H:i:s');
|
||||||
$data['update_time'] = $data['create_time'];
|
$data['update_time'] = $data['create_time'];
|
||||||
$data['group_id'] = 2;
|
$data['group_id'] = 2;
|
||||||
|
$data['tid'] = $tid;
|
||||||
if (!isset($data['tid']) || empty($data['tid'])) {
|
|
||||||
return json([
|
|
||||||
'code' => 400,
|
|
||||||
'msg' => '租户ID不能为空'
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$id = AdminUser::insertGetId($data);
|
$id = AdminUser::insertGetId($data);
|
||||||
$this->logSuccess('用户管理', '添加用户', ['data' => $data]);
|
$this->logSuccess('用户管理', '添加用户', ['data' => $data]);
|
||||||
return json([
|
return json([
|
||||||
@ -131,8 +195,25 @@ class UserController extends BaseController
|
|||||||
{
|
{
|
||||||
$data = request()->param();
|
$data = request()->param();
|
||||||
unset($data['_t'], $data['id']);
|
unset($data['_t'], $data['id']);
|
||||||
|
unset($data['tid']);
|
||||||
|
$tid = $this->getTenantId();
|
||||||
|
if ($tid <= 0) {
|
||||||
|
return json([
|
||||||
|
'code' => 401,
|
||||||
|
'msg' => '未获取到有效租户信息'
|
||||||
|
]);
|
||||||
|
}
|
||||||
$data['update_time'] = date('Y-m-d H:i:s');
|
$data['update_time'] = date('Y-m-d H:i:s');
|
||||||
AdminUser::where('id', $id)->update($data);
|
$affected = AdminUser::where('id', $id)
|
||||||
|
->where('tid', $tid)
|
||||||
|
->where('delete_time', null)
|
||||||
|
->update($data);
|
||||||
|
if (!$affected) {
|
||||||
|
return json([
|
||||||
|
'code' => 404,
|
||||||
|
'msg' => '用户不存在或无权限编辑'
|
||||||
|
]);
|
||||||
|
}
|
||||||
$this->logSuccess('用户管理', '编辑用户', ['id' => $id]);
|
$this->logSuccess('用户管理', '编辑用户', ['id' => $id]);
|
||||||
return json([
|
return json([
|
||||||
'code' => 200,
|
'code' => 200,
|
||||||
@ -146,16 +227,29 @@ class UserController extends BaseController
|
|||||||
*/
|
*/
|
||||||
public function deleteUser(int $id)
|
public function deleteUser(int $id)
|
||||||
{
|
{
|
||||||
$user = AdminUser::where('id', $id)->where('delete_time', null)->find();
|
$tid = $this->getTenantId();
|
||||||
|
if ($tid <= 0) {
|
||||||
|
return json([
|
||||||
|
'code' => 401,
|
||||||
|
'msg' => '未获取到有效租户信息'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = AdminUser::where('id', $id)
|
||||||
|
->where('tid', $tid)
|
||||||
|
->where('delete_time', null)
|
||||||
|
->find();
|
||||||
|
|
||||||
if (!$user) {
|
if (!$user) {
|
||||||
return json([
|
return json([
|
||||||
'code' => 404,
|
'code' => 404,
|
||||||
'msg' => '用户不存在或已删除'
|
'msg' => '用户不存在、已删除或无权限操作'
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
AdminUser::where('id', $id)->update([
|
AdminUser::where('id', $id)
|
||||||
|
->where('tid', $tid)
|
||||||
|
->update([
|
||||||
'delete_time' => date('Y-m-d H:i:s')
|
'delete_time' => date('Y-m-d H:i:s')
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@ -11,6 +11,12 @@ Route::get('storage/:path', 'app\\admin\\controller\\StorageController@index')->
|
|||||||
// 登录路由
|
// 登录路由
|
||||||
Route::post('loginInfo', 'app\\admin\\controller\\LoginController@loginInfo');
|
Route::post('loginInfo', 'app\\admin\\controller\\LoginController@loginInfo');
|
||||||
Route::post('login', 'app\\admin\\controller\\LoginController@login');
|
Route::post('login', 'app\\admin\\controller\\LoginController@login');
|
||||||
|
Route::post('sendLoginCode', 'app\\admin\\controller\\LoginController@sendLoginCode');
|
||||||
|
Route::post('loginBySms', 'app\\admin\\controller\\LoginController@loginBySms');
|
||||||
|
Route::post('sendRegisterCode', 'app\\admin\\controller\\LoginController@sendRegisterCode');
|
||||||
|
Route::post('sendResetCode', 'app\\admin\\controller\\LoginController@sendResetCode');
|
||||||
|
Route::post('register', 'app\\admin\\controller\\LoginController@register');
|
||||||
|
Route::post('resetPassword', 'app\\admin\\controller\\LoginController@resetPassword');
|
||||||
Route::post('logout', 'app\\admin\\controller\\LoginController@logout');
|
Route::post('logout', 'app\\admin\\controller\\LoginController@logout');
|
||||||
Route::get('user/info', 'app\\admin\\controller\\LoginController@userInfo');
|
Route::get('user/info', 'app\\admin\\controller\\LoginController@userInfo');
|
||||||
|
|
||||||
|
|||||||
33
app/admin/route/routes/sms.php
Normal file
33
app/admin/route/routes/sms.php
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use think\facade\Route;
|
||||||
|
|
||||||
|
// 短信管理路由
|
||||||
|
Route::group('sms', function () {
|
||||||
|
// 获取短信网关配置
|
||||||
|
Route::get('info', 'app\\admin\\controller\\System\\SmsController@getSmsInfo');
|
||||||
|
|
||||||
|
// 编辑短信网关配置
|
||||||
|
Route::post('editinfo', 'app\\admin\\controller\\System\\SmsController@editSmsInfo');
|
||||||
|
|
||||||
|
// 发送测试短信(入队任务)
|
||||||
|
Route::post('sendtest', 'app\\admin\\controller\\System\\SmsController@sendTestSms');
|
||||||
|
|
||||||
|
// Android 网关轮询待发送任务
|
||||||
|
Route::post('gateway/poll', 'app\\admin\\controller\\System\\SmsController@gatewayPoll');
|
||||||
|
// 兼容:如果网关端只请求 poll(不带 gateway)
|
||||||
|
Route::post('poll', 'app\\admin\\controller\\System\\SmsController@gatewayPoll');
|
||||||
|
Route::get('poll', 'app\\admin\\controller\\System\\SmsController@gatewayPoll');
|
||||||
|
|
||||||
|
// Android 网关上报短信内容(解析验证码)
|
||||||
|
Route::post('gateway/report', 'app\\admin\\controller\\System\\SmsController@gatewayReport');
|
||||||
|
// 兼容:如果网关端只请求 report(不带 gateway)
|
||||||
|
Route::post('report', 'app\\admin\\controller\\System\\SmsController@gatewayReport');
|
||||||
|
|
||||||
|
// 管理员:短信任务列表(租户隔离)
|
||||||
|
Route::get('taskList', 'app\\admin\\controller\\System\\SmsController@getSmsTaskList');
|
||||||
|
|
||||||
|
// 管理员:编辑短信任务(租户隔离)
|
||||||
|
Route::post('taskEdit/:id', 'app\\admin\\controller\\System\\SmsController@editSmsTask');
|
||||||
|
});
|
||||||
|
|
||||||
29
app/model/System/SmsTask.php
Normal file
29
app/model/System/SmsTask.php
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace app\model\System;
|
||||||
|
|
||||||
|
use think\Model;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 短信发送任务表(Android 网关轮询获取)
|
||||||
|
*/
|
||||||
|
class SmsTask extends Model
|
||||||
|
{
|
||||||
|
// 数据库表名
|
||||||
|
protected $name = 'mete_system_sms_tasks';
|
||||||
|
|
||||||
|
protected $type = [
|
||||||
|
'id' => 'integer',
|
||||||
|
'tid' => 'integer',
|
||||||
|
'api_key' => 'string',
|
||||||
|
'phone' => 'string',
|
||||||
|
'content' => 'string',
|
||||||
|
'status' => 'integer',
|
||||||
|
'code' => 'string',
|
||||||
|
'report_raw' => 'string',
|
||||||
|
'create_time' => 'datetime',
|
||||||
|
'update_time' => 'datetime',
|
||||||
|
'delete_time' => 'datetime',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
@ -9,24 +9,36 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.post-img {
|
.post-img {
|
||||||
width: 330px;
|
width: 100%; /* 强制宽度撑满父容器 */
|
||||||
height: 160px;
|
height: 160px; /* 固定高度 */
|
||||||
object-fit: cover;
|
object-fit: cover; /* 核心:保持比例裁剪 */
|
||||||
|
object-position: center; /* 确保裁剪时以中心为准(默认通常是中心,但显式声明更稳妥) */
|
||||||
|
display: block; /* 消除行内元素的底部间隙 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-img img {
|
||||||
|
width: 100%; /* 宽度撑满父容器 */
|
||||||
|
height: 160px; /* 设定固定高度 */
|
||||||
|
display: block; /* 移除图片底部的幽灵间隙 */
|
||||||
|
|
||||||
|
/* 核心属性:居中缩放的关键 */
|
||||||
|
object-fit: cover; /* 保持原比例,裁切多余部分以填充容器 */
|
||||||
|
object-position: center; /* 确保缩放裁切时,视觉焦点在图片正中心 */
|
||||||
}
|
}
|
||||||
|
|
||||||
.recent-posts {
|
.recent-posts {
|
||||||
background-color: #f9f9f9 !important;
|
background-color: #f9f9f9 !important;
|
||||||
}
|
}
|
||||||
.feature-box {
|
.feature-box {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
.feature-box img {
|
.feature-box img {
|
||||||
width: 150px;
|
width: 150px;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
.feature-box img:hover {
|
.feature-box img:hover {
|
||||||
transform: scale(1.1);
|
transform: scale(1.1);
|
||||||
transition: all 0.3s ease-in-out;
|
transition: all 0.3s ease-in-out;
|
||||||
}
|
}
|
||||||
|
|||||||
40
read_sms_site_settings.php
Normal file
40
read_sms_site_settings.php
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
$envPath = __DIR__ . DIRECTORY_SEPARATOR . '.env';
|
||||||
|
$lines = @file($envPath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||||
|
if (!$lines) {
|
||||||
|
echo "failed to read env: {$envPath}\n";
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$map = [];
|
||||||
|
foreach ($lines as $line) {
|
||||||
|
$line = trim((string)$line);
|
||||||
|
if ($line === '' || strpos($line, '=') === false) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$parts = explode('=', $line, 2);
|
||||||
|
$map[trim((string)$parts[0])] = trim((string)$parts[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$dsn = 'mysql:host=' . $map['DB_HOST'] . ';port=' . $map['DB_PORT'] . ';dbname=' . $map['DB_NAME'] . ';charset=' . $map['DB_CHARSET'];
|
||||||
|
$pdo = new PDO($dsn, $map['DB_USER'], $map['DB_PASS'], [
|
||||||
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare('SELECT label,value FROM ' . $map['DB_PREFIX'] . 'system_site_settings WHERE label IN (?, ?) ORDER BY id ASC');
|
||||||
|
$stmt->execute(['backendUrl', 'apiKey']);
|
||||||
|
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
foreach ($rows as $r) {
|
||||||
|
$label = (string)$r['label'];
|
||||||
|
$value = (string)$r['value'];
|
||||||
|
if ($label === 'apiKey') {
|
||||||
|
$mask = substr($value, 0, 3) . '***' . substr($value, -3);
|
||||||
|
echo $label . '=' . $mask . PHP_EOL;
|
||||||
|
} else {
|
||||||
|
echo $label . '=' . $value . PHP_EOL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
75
test_sms_gateway_tasklist.php
Normal file
75
test_sms_gateway_tasklist.php
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
$envPath = __DIR__ . DIRECTORY_SEPARATOR . '.env';
|
||||||
|
$lines = @file($envPath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||||
|
if (!$lines) {
|
||||||
|
echo "failed to read env\n";
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$map = [];
|
||||||
|
foreach ($lines as $line) {
|
||||||
|
$line = trim((string)$line);
|
||||||
|
if ($line === '' || strpos($line, '=') === false) continue;
|
||||||
|
[$k, $v] = explode('=', $line, 2);
|
||||||
|
$map[trim($k)] = trim($v);
|
||||||
|
}
|
||||||
|
|
||||||
|
$dsn = 'mysql:host=' . $map['DB_HOST'] . ';port=' . $map['DB_PORT'] . ';dbname=' . $map['DB_NAME'] . ';charset=' . $map['DB_CHARSET'];
|
||||||
|
$pdo = new PDO($dsn, $map['DB_USER'], $map['DB_PASS'], [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare('SELECT label,value FROM ' . $map['DB_PREFIX'] . 'system_site_settings WHERE label IN (?, ?) ORDER BY id ASC');
|
||||||
|
$stmt->execute(['backendUrl', 'apiKey']);
|
||||||
|
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
$backendUrl = '';
|
||||||
|
$apiKey = '';
|
||||||
|
foreach ($rows as $r) {
|
||||||
|
if ($r['label'] === 'backendUrl') $backendUrl = (string)$r['value'];
|
||||||
|
if ($r['label'] === 'apiKey') $apiKey = (string)$r['value'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($backendUrl === '' || $apiKey === '') {
|
||||||
|
echo "missing backendUrl/apiKey\n";
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$url = rtrim($backendUrl, '/') . '/api/v1/business/outbound-tasks?limit=3';
|
||||||
|
$headers = [
|
||||||
|
'X-Api-Key: ' . $apiKey,
|
||||||
|
'Accept: application/json',
|
||||||
|
];
|
||||||
|
|
||||||
|
$headerStr = implode("\r\n", $headers);
|
||||||
|
$context = stream_context_create([
|
||||||
|
'http' => [
|
||||||
|
'method' => 'GET',
|
||||||
|
'header' => $headerStr,
|
||||||
|
'timeout' => 10,
|
||||||
|
// 关键:即便 HTTP code 非 2xx,也让 file_get_contents 返回响应体
|
||||||
|
'ignore_errors' => true,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$respBody = @file_get_contents($url, false, $context);
|
||||||
|
|
||||||
|
$status = 0;
|
||||||
|
if (!empty($http_response_header) && is_array($http_response_header)) {
|
||||||
|
foreach ($http_response_header as $line) {
|
||||||
|
if (preg_match('#^HTTP/\\S+\\s+(\\d+)#', $line, $m)) {
|
||||||
|
$status = (int)$m[1];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "GET $url\n";
|
||||||
|
echo "HTTP status=$status\n";
|
||||||
|
if ($respBody === false) {
|
||||||
|
echo "body=false\n";
|
||||||
|
} else {
|
||||||
|
$preview = substr((string)$respBody, 0, 500);
|
||||||
|
echo "body_preview=" . str_replace(["\r", "\n"], ['\\r', '\\n'], $preview) . "\n";
|
||||||
|
}
|
||||||
|
|
||||||
Loading…
Reference in New Issue
Block a user