初始化本地仓库

This commit is contained in:
2026-08-31 11:33:59 +08:00
commit 663f74b633
2390 changed files with 402918 additions and 0 deletions
@@ -0,0 +1,113 @@
<?php
declare(strict_types=1);
namespace app\service\api\system;
use app\model\system\Config;
use core\base\BaseService;
/**
* 协议服务
*/
class AgreementService extends BaseService
{
/**
* 获取协议内容
*/
public function getAgreementContent(string $key): array
{
$agreements = [
'privacy' => [
'title' => '隐私政策',
'content' => $this->getPrivacyPolicyContent()
],
'user' => [
'title' => '用户协议',
'content' => $this->getUserAgreementContent()
],
'service' => [
'title' => '服务条款',
'content' => $this->getServiceTermsContent()
]
];
if (!isset($agreements[$key])) {
throw new \Exception('协议不存在', 404);
}
return [
'code' => 200,
'msg' => '获取成功',
'data' => $agreements[$key]
];
}
/**
* 获取隐私政策内容
*/
private function getPrivacyPolicyContent(): string
{
return "# 隐私政策
## 1. 信息收集
我们收集您在使用服务时主动提供的信息,包括但不限于姓名、联系方式等。
## 2. 信息使用
我们使用收集的信息来提供和改进服务,保护用户安全,并遵守法律法规。
## 3. 信息共享
除非获得您的明确同意,否则我们不会与第三方分享您的个人信息。
## 4. 信息安全
我们采取合理的安全措施保护您的个人信息安全。
## 5. 政策变更
我们可能会适时更新本隐私政策,请定期查看最新版本。";
}
/**
* 获取用户协议内容
*/
private function getUserAgreementContent(): string
{
return "# 用户协议
## 1. 接受条款
通过使用我们的服务,您同意遵守本协议的所有条款和条件。
## 2. 用户注册
您需要提供真实、准确、完整的注册信息,并保持信息的及时更新。
## 3. 使用规则
您承诺遵守法律法规,不得利用服务从事任何违法或不正当的活动。
## 4. 服务变更
我们保留随时修改、暂停或终止服务的权利,恕不另行通知。
## 5. 责任限制
在法律允许的范围内,我们对服务的适用性、可靠性不作任何保证。";
}
/**
* 获取服务条款内容
*/
private function getServiceTermsContent(): string
{
return "# 服务条款
## 1. 服务内容
我们提供在线服务,包括但不限于信息展示、用户交互等功能。
## 2. 服务费用
基础服务免费提供,部分增值服务可能需要支付相应费用。
## 3. 服务中断
因系统维护、升级等原因,服务可能会暂时中断,我们将尽力减少影响。
## 4. 用户责任
用户应妥善保管账户信息,对其账户下的所有活动承担全部责任。
## 5. 终止服务
如用户违反协议条款,我们有权终止提供服务。";
}
}
@@ -0,0 +1,125 @@
<?php
declare(strict_types=1);
namespace app\service\api\system;
use app\model\system\Config;
use core\base\BaseService;
use support\Redis;
use Webman\Http\Request;
/**
* 验证码服务
*/
class CaptchaService extends BaseService
{
/**
* 获取验证码图片
*/
public function getCaptchaImage(): array
{
// 生成验证码
$captcha = $this->generateCaptcha();
// 存储验证码到Redis,5分钟过期
Redis::setex('captcha:' . $captcha['key'], 300, $captcha['code']);
return [
'code' => 200,
'msg' => '获取成功',
'data' => [
'captcha_key' => $captcha['key'],
'captcha_image' => $captcha['image'],
'expire_time' => 300
]
];
}
/**
* 发送短信验证码
*/
public function sendSmsCode(string $type): array
{
$request = \request();
$mobile = $request->post('mobile');
if (empty($mobile)) {
throw new \Exception('手机号不能为空', 400);
}
// 验证手机号格式
if (!preg_match('/^1[3-9]\\d{9}$/', $mobile)) {
throw new \Exception('手机号格式不正确', 400);
}
// 检查发送频率
$lastSendTime = Redis::get('sms_last_send:' . $mobile);
if ($lastSendTime && time() - $lastSendTime < 60) {
throw new \Exception('发送过于频繁,请稍后再试', 429);
}
// 生成验证码
$code = sprintf('%06d', mt_rand(0, 999999));
// 存储验证码到Redis,10分钟过期
$cacheKey = 'sms_code:' . $mobile;
Redis::setex($cacheKey, 600, $code);
// 记录发送时间
Redis::setex('sms_last_send:' . $mobile, 60, time());
// 发送短信(这里应该调用短信服务商API)
$this->sendSms($mobile, $code, $type);
return [
'code' => 200,
'msg' => '验证码发送成功'
];
}
/**
* 生成验证码
*/
private function generateCaptcha(): array
{
// 生成随机验证码
$chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
$code = '';
for ($i = 0; $i < 4; $i++) {
$code .= $chars[mt_rand(0, strlen($chars) - 1)];
}
// 生成唯一key
$key = md5(uniqid() . microtime(true));
// 这里应该生成验证码图片,暂时返回模拟数据
$image = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==';
return [
'key' => $key,
'code' => $code,
'image' => $image
];
}
/**
* 发送短信
*/
private function sendSms(string $mobile, string $code, string $type): void
{
// 这里应该调用短信服务商API
// 暂时模拟发送成功
$templates = [
'login' => '您的登录验证码是:{code},有效期10分钟',
'register' => '您的注册验证码是:{code},有效期10分钟',
'bind' => '您的绑定验证码是:{code},有效期10分钟',
'reset' => '您的密码重置验证码是:{code},有效期10分钟'
];
$template = $templates[$type] ?? '您的验证码是:{code},有效期10分钟';
$content = str_replace('{code}', $code, $template);
// 记录发送日志
\Log::info("短信发送: {$mobile} - {$content}");
}
}
@@ -0,0 +1,364 @@
<?php
declare(strict_types=1);
/**
*+------------------
* madong
*+------------------
* Copyright (c) https://gitee.com/motion-code All rights reserved.
*+------------------
* Author: Mr. April (405784684@qq.com)
*+------------------
* Official Website: http://www.madong.tech
*/
namespace app\service\api\system;
use app\dao\system\ConfigDao;
use app\scope\global\AccessPermissionScope;
use core\base\BaseService;
class ConfigService extends BaseService
{
public function __construct(ConfigDao $dao)
{
$this->dao = $dao;
}
/**
* 获取配置项
*
* @param string $code 配置code
* @param mixed $default 默认值
* @param array $options 选项,如 ['group_code' => '...']
*
* @return mixed
* @throws \Exception
*/
public function config(string $code, mixed $default = [], array $options = []): mixed
{
$map = ['code' => $code];
// 如果选项中有分组,则添加到查询条件
if (!empty($options['group_code'])) {
$map['group_code'] = $options['group_code'];
}
// 查询配置
$configModel = $this->dao->get($map, ['*'], [], '', [AccessPermissionScope::class]);
// 如果配置不存在,自动创建
if (!$configModel) {
$configData = [
'code' => $code,
'name' => $options['name'] ?? $code,
'group_code' => $options['group_code'] ?? 'default',
'content' => is_array($default) ? json_encode($default, JSON_UNESCAPED_UNICODE) : $default,
'type' => $options['type'] ?? (is_array($default) ? 'json' : 'string'),
'enabled' => 1,
'sort' => 0,
'remark' => $options['remark'] ?? '',
];
try {
$this->dao->save($configData);
} catch (\Exception $e) {
}
return $default;
}
$content = $configModel->getOriginal('content',null);
// 如果content是数组/JSON格式,返回整个数组;否则返回原值
if (is_string($content) && !empty($content)) {
$decoded = json_decode($content, true);
return json_last_error() === JSON_ERROR_NONE ? $decoded : $content;
}
return $content ?? $default;
}
/**
* 获取配置分组详情
*
* @param string $groupCode 配置分组
* @param array $defaults 默认配置项
* @param array $options 选项参数
* - enabled_only: bool 是否只获取启用的配置项,默认true
* - with_metadata: bool 是否包含配置元数据,默认false
*
* @return array
* @throws \ReflectionException
*/
public function getByGroup(string $groupCode, array $defaults = [], array $options = []): array
{
// 验证groupCode参数,防止SQL注入
if (empty($groupCode)) {
return $defaults;
}
$enabledOnly = $options['enabled_only'] ?? true;
$withMetadata = $options['with_metadata'] ?? false;
$map = ['group_code' => $groupCode];
if ($enabledOnly) {
$map['enabled'] = 1;
}
if ($withMetadata) {
// 获取完整配置信息
$configs = $this->dao->getList($map);
$processedData = [];
foreach ($configs as $config) {
$content = $config['content'] ?? null;
if (is_string($content) && !empty($content)) {
$decoded = json_decode($content, true);
$config['content'] = json_last_error() === JSON_ERROR_NONE ? $decoded : $content;
}
if (isset($options['key_by']) && $options['key_by'] === 'code') {
$processedData[$config['code']] = $config;
} else {
$processedData[] = $config;
}
}
return $processedData;
} else {
// 只获取配置内容
$data = $this->dao->getColumn($map, 'content', 'code');
// 处理每个配置项的内容,如果是JSON格式则解码
$processedData = [];
foreach ($data as $code => $content) {
if (is_string($content) && !empty($content)) {
$decoded = json_decode($content, true);
$processedData[$code] = json_last_error() === JSON_ERROR_NONE ? $decoded : $content;
} else {
$processedData[$code] = $content;
}
}
// 合并默认配置
return array_merge($defaults, $processedData);
}
}
/**
* 获取所有配置并按分组整理
*
* @param array $options 选项参数
* - enabled_only: bool 是否只获取启用的配置项,默认true
* - group_filter: array 分组过滤,只返回指定分组的配置
* - with_metadata: bool 是否包含配置项的完整元数据,默认false
* - key_by: string 按指定字段作为键名('code' 或 'id'),默认null
* - sort_groups: bool 是否对分组进行排序,默认false
* - sort_configs: bool 是否对配置项进行排序,默认false
*
* @return array
* @throws \Exception
*/
public function getAllGrouped(array $options = []): array
{
$enabledOnly = $options['enabled_only'] ?? true;
$groupFilter = $options['group_filter'] ?? [];
$withMetadata = $options['with_metadata'] ?? false;
$keyBy = $options['key_by'] ?? null;
$sortGroups = $options['sort_groups'] ?? false;
$sortConfigs = $options['sort_configs'] ?? false;
$map = [];
if ($enabledOnly) {
$map['enabled'] = 1;
}
if (!empty($groupFilter)) {
$map['group_code'] = $groupFilter;
}
$configs = $this->dao->getList($map);
$grouped = [];
foreach ($configs as $config) {
$groupCode = $config['group_code'] ?? 'default';
// 处理内容
$content = $config['content'];
if (is_string($content) && !empty($content)) {
$decoded = json_decode($content, true);
$config['content'] = json_last_error() === JSON_ERROR_NONE ? $decoded : $content;
}
// 初始化分组
if (!isset($grouped[$groupCode])) {
$grouped[$groupCode] = [];
}
// 根据选项决定返回的数据结构
if ($withMetadata) {
// 返回完整配置信息
if ($keyBy && isset($config[$keyBy])) {
$grouped[$groupCode][$config[$keyBy]] = $config;
} else {
$grouped[$groupCode][] = $config;
}
} else {
// 只返回配置内容
if ($keyBy && isset($config[$keyBy])) {
$grouped[$groupCode][$config[$keyBy]] = $config['content'];
} else {
$grouped[$groupCode][$config['code']] = $config['content'];
}
}
}
// 排序处理
if ($sortGroups) {
ksort($grouped);
}
if ($sortConfigs) {
foreach ($grouped as &$configs) {
if ($withMetadata) {
// 对元数据模式按code排序
uasort($configs, function ($a, $b) {
return strcmp($a['code'] ?? '', $b['code'] ?? '');
});
} else {
// 对内容模式按键名排序
ksort($configs);
}
}
}
return $grouped;
}
/**
* 获取配置项中的某个键值
*
* @param string $code 配置项
* @param string $key 要获取的键,支持多级键,如 key1.key2.key3
* @param mixed $default 默认值,默认null
* @param array $options 选项参数
* - group_code: string 配置分组,默认null(在所有分组中查找)
* - require_group: bool 是否必须指定分组,默认false
* - fallback_to_all: bool 当未找到分组配置时,是否在所有分组中查找,默认true
* - search_groups: array 搜索的分组列表,按顺序查找
*
* @return mixed
* @throws \Exception
*/
public function getValue(string $code, string $key, mixed $default = null, array $options = []): mixed
{
$groupCode = $options['group_code'] ?? null;
$requireGroup = $options['require_group'] ?? false;
$fallbackToAll = $options['fallback_to_all'] ?? true;
$searchGroups = $options['search_groups'] ?? [];
// 如果指定了搜索分组列表,按顺序查找
if (!empty($searchGroups) && is_array($searchGroups)) {
foreach ($searchGroups as $searchGroupCode) {
$config = $this->config($code, null, ['group_code' => $searchGroupCode]);
if (is_array($config)) {
$value = $this->getNestedValue($config, $key);
if ($value !== null) {
return $value;
}
}
}
return $default;
}
// 如果指定了分组,优先在该分组中查找
if ($groupCode !== null) {
$config = $this->config($code, null, ['group_code' => $groupCode]);
if (is_array($config)) {
$value = $this->getNestedValue($config, $key);
if ($value !== null) {
return $value;
}
}
// 如果未找到且允许回退到所有分组,则尝试在所有分组中查找
if ($fallbackToAll && !$requireGroup) {
$config = $this->config($code, null);
if (is_array($config)) {
$value = $this->getNestedValue($config, $key);
if ($value !== null) {
return $value;
}
}
}
return $default;
}
// 默认在所有分组中查找
$config = $this->config($code, null);
if (is_array($config)) {
$value = $this->getNestedValue($config, $key);
if ($value !== null) {
return $value;
}
}
return $default;
}
/**
* 根据多级键获取嵌套数组中的值
*
* @param array $array 要查找的数组
* @param string $key 多级键,如 key1.key2.key3
*
* @return mixed|null 找到的值或null
*/
private function getNestedValue(array $array, string $key): mixed
{
// 如果key不包含点号,直接返回对应值
if (!str_contains($key, '.')) {
return $array[$key] ?? null;
}
// 拆分多级键
$keys = explode('.', $key);
$value = $array;
// 逐级查找
foreach ($keys as $k) {
if (!is_array($value) || !isset($value[$k])) {
return null;
}
$value = $value[$k];
}
return $value;
}
/**
* 检查配置项是否存在
*
* @param string $code 配置code
* @param array $options 扩展选项参数
* - group_code: string 配置分组
*
* @return bool
* @throws \Exception
*/
public function exists(string $code, array $options = []): bool
{
$map = ['code' => $code];
if (isset($options['group_code'])) {
// 验证group_code格式
if (!preg_match('/^[a-zA-Z0-9_-]+$/', $options['group_code'])) {
throw new \Exception('Invalid group_code format');
}
$map['group_code'] = $options['group_code'];
}
$config = $this->dao->get($map);
return !empty($config);
}
}
@@ -0,0 +1,249 @@
<?php
declare(strict_types=1);
namespace app\service\api\system;
use app\model\system\Config;
use core\base\BaseService;
use support\Redis;
/**
* 微信服务
*/
class WechatService extends BaseService
{
/**
* 获取微信授权码
*/
public function getWechatAuthCode(array $params): array
{
$redirectUrl = $params['redirect_url'] ?? '';
if (empty($redirectUrl)) {
throw new \Exception('回调URL不能为空', 400);
}
$config = config('wechat');
$appId = $config['app_id'] ?? '';
if (empty($appId)) {
throw new \Exception('微信配置未完成', 500);
}
// 生成授权URL
$scope = 'snsapi_userinfo'; // 需要获取用户信息
$state = md5(uniqid() . microtime(true));
$authUrl = "https://open.weixin.qq.com/connect/oauth2/authorize?" . http_build_query([
'appid' => $appId,
'redirect_uri' => urlencode($redirectUrl),
'response_type' => 'code',
'scope' => $scope,
'state' => $state
]) . "#wechat_redirect";
return [
'code' => 200,
'msg' => '获取成功',
'data' => [
'auth_url' => $authUrl,
'state' => $state
]
];
}
/**
* 同步微信用户信息
*/
public function syncWechatUserInfo(array $params): array
{
$code = $params['code'] ?? '';
if (empty($code)) {
throw new \Exception('授权码不能为空', 400);
}
$config = config('wechat');
if (empty($config['app_id']) || empty($config['app_secret'])) {
throw new \Exception('微信配置未完成', 500);
}
// 获取access_token
$accessToken = $this->getAccessTokenByCode($code, $config['app_id'], $config['app_secret']);
// 获取用户信息
$userInfo = $this->getUserInfoByToken($accessToken['access_token'], $accessToken['openid']);
return [
'code' => 200,
'msg' => '同步成功',
'data' => $userInfo
];
}
/**
* 获取微信 JSSDK 配置
*/
public function getWechatJssdkConfig(array $params): array
{
$url = $params['url'] ?? '';
if (empty($url)) {
throw new \Exception('URL不能为空', 400);
}
$config = config('wechat');
$appId = $config['app_id'] ?? '';
if (empty($appId)) {
throw new \Exception('微信配置未完成', 500);
}
// 获取jsapi_ticket
$ticket = $this->getJsapiTicket();
// 生成签名
$nonceStr = $this->generateNonceStr();
$timestamp = time();
$signature = $this->generateSignature($ticket, $nonceStr, $timestamp, $url);
return [
'code' => 200,
'msg' => '获取成功',
'data' => [
'appId' => $appId,
'timestamp' => $timestamp,
'nonceStr' => $nonceStr,
'signature' => $signature,
'url' => $url
]
];
}
/**
* 通过code获取access_token
*/
private function getAccessTokenByCode(string $code, string $appId, string $appSecret): array
{
$url = "https://api.weixin.qq.com/sns/oauth2/access_token?" . http_build_query([
'appid' => $appId,
'secret' => $appSecret,
'code' => $code,
'grant_type' => 'authorization_code'
]);
// 这里应该调用微信API,暂时返回模拟数据
return [
'access_token' => 'mock_access_token',
'expires_in' => 7200,
'refresh_token' => 'mock_refresh_token',
'openid' => 'mock_openid_' . uniqid(),
'scope' => 'snsapi_userinfo'
];
}
/**
* 通过token获取用户信息
*/
private function getUserInfoByToken(string $accessToken, string $openid): array
{
$url = "https://api.weixin.qq.com/sns/userinfo?" . http_build_query([
'access_token' => $accessToken,
'openid' => $openid,
'lang' => 'zh_CN'
]);
// 这里应该调用微信API,暂时返回模拟数据
return [
'openid' => $openid,
'nickname' => '微信用户',
'sex' => 1,
'province' => '广东省',
'city' => '深圳市',
'country' => '中国',
'headimgurl' => '',
'privilege' => [],
'unionid' => ''
];
}
/**
* 获取jsapi_ticket
*/
private function getJsapiTicket(): string
{
// 检查缓存
$cacheKey = 'wechat_jsapi_ticket';
$ticket = Redis::get($cacheKey);
if ($ticket) {
return $ticket;
}
// 获取access_token
$accessToken = $this->getAccessToken();
// 获取ticket
$url = "https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token={$accessToken}&type=jsapi";
// 这里应该调用微信API,暂时返回模拟数据
$mockTicket = 'mock_jsapi_ticket_' . uniqid();
// 缓存2小时
Redis::setex($cacheKey, 7200, $mockTicket);
return $mockTicket;
}
/**
* 获取access_token
*/
private function getAccessToken(): string
{
// 检查缓存
$cacheKey = 'wechat_access_token';
$accessToken = Redis::get($cacheKey);
if ($accessToken) {
return $accessToken;
}
$config = config('wechat');
$url = "https://api.weixin.qq.com/cgi-bin/token?" . http_build_query([
'grant_type' => 'client_credential',
'appid' => $config['app_id'],
'secret' => $config['app_secret']
]);
// 这里应该调用微信API,暂时返回模拟数据
$mockToken = 'mock_access_token_' . uniqid();
// 缓存2小时
Redis::setex($cacheKey, 7200, $mockToken);
return $mockToken;
}
/**
* 生成随机字符串
*/
private function generateNonceStr(int $length = 16): string
{
$chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
$str = "";
for ($i = 0; $i < $length; $i++) {
$str .= substr($chars, mt_rand(0, strlen($chars) - 1), 1);
}
return $str;
}
/**
* 生成签名
*/
private function generateSignature(string $ticket, string $nonceStr, int $timestamp, string $url): string
{
$string = "jsapi_ticket={$ticket}&noncestr={$nonceStr}&timestamp={$timestamp}&url={$url}";
return sha1($string);
}
}