- 18 张公共表更名(addon/attachment/configure/links/spider_log/spider_stat/sms/notice/ad/task/prop/medal/help/card -> common_*,member_wallets->member_wallet,score_rule/score_log -> member_*,addon_config->common_addonconf),模型全部对齐新表名,Db 直引用清零 - Attachment 模型补 表名绑定,修复富文本上传查 wxapp_attachment 1146 隐患 - install.sql + 迁移 SQL:backend_admin/backend_role delete_at 默认 0,修复软删除(NULL != 0)误过滤导致后台菜单为空 - AppService::boot() 支持插件 info.php 声明 commands 自动注册插件命令(psr-4 自动加载,坏类名自动跳过) - 各插件(haonav/mqttbroker/wxchat/articles/blog/forum 等)字段与配置同步调整 - 框架版本 1.1.0 -> 1.1.1
359 lines
12 KiB
PHP
359 lines
12 KiB
PHP
<?php
|
||
// +----------------------------------------------------------------------
|
||
// | YwxApp [ WE CAN DO IT JUST THINK ]
|
||
// +----------------------------------------------------------------------
|
||
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
|
||
// +----------------------------------------------------------------------
|
||
// | Author: ywxapp<admin@ywxapp.cn>
|
||
// +----------------------------------------------------------------------
|
||
|
||
namespace ywxapp\library;
|
||
|
||
use BcMath\Number;
|
||
use think\exception\HttpResponseException;
|
||
use think\facade\Request;
|
||
use think\Response;
|
||
|
||
|
||
class Result
|
||
{
|
||
/**
|
||
* Summary of StatusCode
|
||
* @var int
|
||
*/
|
||
protected $StatusCode = 200;
|
||
public $Message = null;
|
||
|
||
/**
|
||
* Summary of Headers
|
||
* @var array
|
||
*/
|
||
protected $Headers = [];
|
||
|
||
protected $AccessToken;
|
||
|
||
protected $AccessExpired;
|
||
|
||
protected $RefreshToken;
|
||
|
||
protected $RefreshExpired;
|
||
|
||
protected $Total = 0;
|
||
|
||
/**
|
||
* Summary of Type
|
||
* @var string
|
||
*/
|
||
protected $Type = 'json';
|
||
|
||
|
||
protected $isCout = false;
|
||
|
||
|
||
public function __construct()
|
||
{
|
||
if (! Request::isAjax()) {
|
||
$this->Type = 'html';
|
||
}
|
||
}
|
||
|
||
|
||
public function success($data = null, $message = 'success', int $code = 0)
|
||
{
|
||
$result = ['code' => 0, 'message' => $message, 'timestamp' => time()];
|
||
if ($data) {
|
||
$result['data'] = $data;
|
||
}
|
||
if ($this->Total > 0) {
|
||
$result['count'] = $this->Total;
|
||
}
|
||
if ($this->AccessToken != null) {
|
||
$result['access_token'] = $this->AccessToken;
|
||
$result['access_expired'] = $this->AccessExpired;
|
||
}
|
||
if ($this->RefreshToken != null) {
|
||
$result['refresh_token'] = $this->RefreshToken;
|
||
$result['refresh_expired'] = $this->RefreshExpired;
|
||
}
|
||
$response = '';
|
||
if ('html' == strtolower($this->Type)) {
|
||
\think\facade\View::config(['view_path' => root_path() . 'ywxapp/view/']);
|
||
\think\facade\View::layout(false);
|
||
$result = \think\facade\View::fetch('success.html', $result);
|
||
}
|
||
$response = Response::create($result, strtolower($this->Type), $this->StatusCode)->header($this->Headers);
|
||
$this->applyTokenCookies($response);
|
||
$response->send();
|
||
app()->http->end($response);
|
||
exit;
|
||
// throw new HttpResponseException($response);
|
||
}
|
||
|
||
|
||
public function error($message = 'Error', int $code = 1, $data = null)
|
||
{
|
||
$result = ['code' => $code, 'message' => $message, 'timestamp' => time()];
|
||
if ($data) {
|
||
$result['data'] = $data;
|
||
}
|
||
if ($this->AccessToken != null) {
|
||
$result['access_token'] = $this->AccessToken;
|
||
$result['access_expired'] = $this->AccessExpired;
|
||
}
|
||
if ($this->RefreshToken != null) {
|
||
$result['refresh_token'] = $this->RefreshToken;
|
||
$result['refresh_expired'] = $this->RefreshExpired;
|
||
}
|
||
$response = '';
|
||
// 若已被强制为 JSON(如插件 API 在 MultiApp 中间件里 setType('json')),
|
||
// 或请求本身是 ajax,则统一返回 JSON;否则按 HTML 渲染。
|
||
$type = ($this->Type === 'json') ? 'json' : (Request::isAjax() ? 'json' : 'html');
|
||
|
||
if ($this->StatusCode == 401) {
|
||
$result['url'] = url('login/index');
|
||
$result['code'] = $this->StatusCode;
|
||
if ('html' == strtolower($type)) {
|
||
\think\facade\View::config(['view_path' => root_path() . 'ywxapp' . DIRECTORY_SEPARATOR . 'view' . DIRECTORY_SEPARATOR]);
|
||
$result = \think\facade\View::fetch('401.html', $result);
|
||
}
|
||
$response = Response::create($result, strtolower($type), 401);
|
||
$this->applyTokenCookies($response);
|
||
} else {
|
||
if ('html' == strtolower($type)) {
|
||
\think\facade\View::config(['view_path' => root_path() . 'ywxapp' . DIRECTORY_SEPARATOR . 'view' . DIRECTORY_SEPARATOR]);
|
||
\think\facade\View::layout(false);
|
||
$result = \think\facade\View::fetch('error.html', $result);
|
||
}
|
||
$response = Response::create($result, strtolower($type), $this->StatusCode)->header($this->Headers);
|
||
$this->applyTokenCookies($response);
|
||
}
|
||
$response->send();
|
||
app()->http->end($response);
|
||
exit;
|
||
throw new HttpResponseException($response);
|
||
}
|
||
|
||
/**
|
||
* 返回封装后的 API 数据到客户端(已弃用,字段契约与 success/error 不一致).
|
||
*
|
||
* 请改用 apiSuccess() / apiError():统一 JSON、统一字段(code/msg/data/timestamp),
|
||
* 业务 code 与 HTTP 状态码解耦,不再受后台 HTML/401 跳转逻辑影响。
|
||
*
|
||
* @param mixed $data 要返回的数据
|
||
* @param int $code 返回的 code
|
||
* @param mixed $msg 提示信息
|
||
* @param string $type 返回数据格式
|
||
* @param array $header 发送的 Header 信息
|
||
*
|
||
* @deprecated 使用 apiSuccess() / apiError() 替代
|
||
*/
|
||
protected function result($data, $code = 0, $msg = '', $type = '', array $header = [])
|
||
{
|
||
$result = [
|
||
'code' => $code,
|
||
'msg' => $msg,
|
||
'time' => Request::server('REQUEST_TIME'),
|
||
'data' => $data,
|
||
];
|
||
$type = $type ?: $this->getResponseType();
|
||
$response = Response::create($result, $type)->header($header);
|
||
|
||
throw new HttpResponseException($response);
|
||
}
|
||
|
||
/**
|
||
* API 成功响应(强制 JSON,契约稳定).
|
||
*
|
||
* 专供 App / 小程序 / 第三方客户端调用,与后台 HTML 响应完全解耦:
|
||
* - 无论请求是否 ajax,固定返回 JSON;
|
||
* - 统一字段:code(0) / msg / data / timestamp;
|
||
* - 若本实例持有 token,自动附加 access_token / refresh_token 等字段;
|
||
* - 若设置了 count,自动附加 count(分页场景)。
|
||
*
|
||
* @param mixed $data 业务数据,可为数组 / 模型 / null
|
||
* @param string $message 成功提示语
|
||
* @param int $count 分页总数(可选,>0 时附加 count 字段)
|
||
*
|
||
* @return void 通过 Response 直接输出并终止
|
||
*/
|
||
public function apiSuccess($data = null, string $message = 'success', int $count = 0): void
|
||
{
|
||
$result = ['code' => 0, 'msg' => $message, 'timestamp' => time()];
|
||
if ($data !== null) {
|
||
$result['data'] = $data;
|
||
}
|
||
if ($count > 0) {
|
||
$result['count'] = $count;
|
||
}
|
||
if ($this->AccessToken !== null) {
|
||
$result['access_token'] = $this->AccessToken;
|
||
$result['access_expired'] = $this->AccessExpired;
|
||
}
|
||
if ($this->RefreshToken !== null) {
|
||
$result['refresh_token'] = $this->RefreshToken;
|
||
$result['refresh_expired'] = $this->RefreshExpired;
|
||
}
|
||
$response = Response::create($result, 'json', $this->StatusCode)->header($this->Headers);
|
||
$this->applyTokenCookies($response);
|
||
$response->send();
|
||
app()->http->end($response);
|
||
exit;
|
||
}
|
||
|
||
/**
|
||
* API 失败响应(强制 JSON,业务 code 与 HTTP 状态解耦).
|
||
*
|
||
* 与 apiSuccess 配套,专供客户端调用:
|
||
* - 固定返回 JSON;
|
||
* - 统一字段:code / msg / data(可选) / timestamp;
|
||
* - 业务 code 原样透传(如 401 失败返回 code=401,而非被覆盖);
|
||
* - HTTP 状态码由 $httpStatus 控制(默认 200,鉴权失败传 401)。
|
||
*
|
||
* @param string $message 失败提示语
|
||
* @param int $code 业务状态码(默认 1,鉴权失败建议 401)
|
||
* @param mixed $data 附加数据(可选)
|
||
* @param int $httpStatus HTTP 状态码(默认 200)
|
||
*
|
||
* @return void 通过 Response 直接输出并终止
|
||
*/
|
||
public function apiError(string $message = 'Error', int $code = 1, $data = null, int $httpStatus = 200): void
|
||
{
|
||
$result = ['code' => $code, 'msg' => $message, 'timestamp' => time()];
|
||
if ($data !== null) {
|
||
$result['data'] = $data;
|
||
}
|
||
if ($this->AccessToken !== null) {
|
||
$result['access_token'] = $this->AccessToken;
|
||
$result['access_expired'] = $this->AccessExpired;
|
||
}
|
||
if ($this->RefreshToken !== null) {
|
||
$result['refresh_token'] = $this->RefreshToken;
|
||
$result['refresh_expired'] = $this->RefreshExpired;
|
||
}
|
||
$response = Response::create($result, 'json', $httpStatus)->header($this->Headers);
|
||
$this->applyTokenCookies($response);
|
||
$response->send();
|
||
app()->http->end($response);
|
||
exit;
|
||
}
|
||
|
||
/**
|
||
* URL 重定向.
|
||
*
|
||
* @param string $url 跳转的 URL 表达式
|
||
* @param array|int $params 其它 URL 参数
|
||
* @param int $code http code
|
||
* @param array $with 隐式传参
|
||
*/
|
||
public function redirect($url, $code = 302, $params = [], $with = []): self
|
||
{
|
||
if (is_int($params)) {
|
||
$code = $params;
|
||
}
|
||
$response = \redirect($url);
|
||
$response->code($code)->with($with);
|
||
throw new HttpResponseException($response);
|
||
}
|
||
|
||
/**
|
||
* 设置Token
|
||
* @param string $token
|
||
* @return void
|
||
*/
|
||
public function setAccessToken($token, $expired = null): self
|
||
{
|
||
$this->AccessToken = $token;
|
||
$this->AccessExpired = $expired;
|
||
return app()->result;
|
||
}
|
||
|
||
/**
|
||
* 设置Token
|
||
* @param string $token
|
||
* @return void
|
||
*/
|
||
public function setRefreshToken($token, $expired = null): self
|
||
{
|
||
$this->RefreshToken = $token;
|
||
$this->RefreshExpired = $expired;
|
||
return $this;
|
||
}
|
||
|
||
public function setStatusCode(int $code = 200): self
|
||
{
|
||
$this->StatusCode = $code;
|
||
return $this;
|
||
}
|
||
|
||
|
||
public function setMessage(string $message = ''): self
|
||
{
|
||
$this->Message = $message;
|
||
return $this;
|
||
}
|
||
|
||
|
||
public function setHeaders(array $header = []): self
|
||
{
|
||
$this->Headers = $header;
|
||
return $this;
|
||
}
|
||
|
||
/**
|
||
* 强制设置响应类型(json/html)
|
||
* 插件 API 由小程序/APP 等客户端调用,请求不带 X-Requested-With,
|
||
* 在 MultiApp 中间件里统一 setType('json') 以确保返回 JSON。
|
||
* @param string $type
|
||
* @return self
|
||
*/
|
||
public function setType(string $type = 'json'): self
|
||
{
|
||
$this->Type = strtolower($type);
|
||
return app()->result;
|
||
}
|
||
|
||
/**
|
||
* 把当前持有的 access/refresh token 写入响应 Cookie(path=/,非 httponly)。
|
||
*
|
||
* 背景:登录/注册的 token 经 JwtService 存入本 Result 单例(setAccessToken/setRefreshToken),
|
||
* 但 persistTokens() 用的全局 Cookie 门面与 Response 持有的 Cookie 实例并非同一个,
|
||
* 仅靠门面 set 的 token 不会被 Response::send() 输出,导致整页跳转(菜单点击、iframe)
|
||
* 不带 token → 后端读不到 → 跳登录页。此处直接写到 Response 实例上,确保 100% 随响应输出。
|
||
*
|
||
* @param Response $response
|
||
* @return void
|
||
*/
|
||
protected function applyTokenCookies(Response $response): void
|
||
{
|
||
if ($this->AccessToken !== null && $this->AccessToken !== '') {
|
||
$expire = $this->AccessExpired ? ((int) $this->AccessExpired - time()) : 0;
|
||
$response->cookie('access_token', (string) $this->AccessToken, [
|
||
'expire' => $expire,
|
||
'path' => '/',
|
||
'httponly' => false,
|
||
]);
|
||
}
|
||
if ($this->RefreshToken !== null && $this->RefreshToken !== '') {
|
||
$expire = $this->RefreshExpired ? ((int) $this->RefreshExpired - time()) : 0;
|
||
$response->cookie('refresh_token', (string) $this->RefreshToken, [
|
||
'expire' => $expire,
|
||
'path' => '/',
|
||
'httponly' => false,
|
||
]);
|
||
}
|
||
}
|
||
|
||
|
||
public function setCount(int $total): self
|
||
{
|
||
$this->Total = $total;
|
||
return app()->result;
|
||
}
|
||
|
||
|
||
public static function instance($options = []): Result
|
||
{
|
||
return app()->result;
|
||
}
|
||
}
|