431 lines
16 KiB
PHP
431 lines
16 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>
|
||
// +----------------------------------------------------------------------
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace app\Api\Controller\V1;
|
||
|
||
use ywxapp\controller\ApiController;
|
||
use ywxapp\service\RemoteService;
|
||
use think\Request;
|
||
|
||
/**
|
||
* 插件商店接口(瘦分发器,不持有市场领域逻辑):
|
||
*
|
||
* - 客户机模式(is_market_client()=true,ywxapp.api_url 指向中心站):
|
||
* 经 RemoteService 代理中心站 /appmall/api/addon/*。
|
||
* - 中心站模式(api_url 为空/指向本机):
|
||
* 进程内委托 appmall 插件的 MarketService(订单/授权/收益/支付回调全部在插件侧)。
|
||
* - 未安装 appmall 插件且非客户机:市场功能不可用,返回明确提示。
|
||
*
|
||
* 中心站领域数据(appmarket_* 表)已全部收敛到 addon/appmall,核心不再直接读写。
|
||
*/
|
||
class Addon extends ApiController
|
||
{
|
||
// notify/payResult 由支付平台回调/跳转,无需登录;info 公开
|
||
protected $noNeedLogin = ['info', 'notify', 'payResult'];
|
||
protected $needRight = ['*'];
|
||
|
||
/**
|
||
* 中心站模式下委托的插件市场服务
|
||
* @var \addon\appmall\service\MarketService|null
|
||
*/
|
||
protected $market = null;
|
||
|
||
public function initialize()
|
||
{
|
||
if (!is_market_client() && class_exists(\addon\appmall\service\MarketService::class)) {
|
||
$this->market = \addon\appmall\service\MarketService::instance();
|
||
$this->market->ensureTables();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 本机为中心站但未安装市场插件时的统一出口
|
||
*/
|
||
protected function marketUnavailable(): \think\response\Json
|
||
{
|
||
return $this->result->error('市场服务不可用:本机未配置中心站地址(ywxapp.api_url),也未安装 appmall 插件', 503);
|
||
}
|
||
|
||
/**
|
||
* 将插件领域异常转换为 Result 错误响应
|
||
*/
|
||
protected function marketError(\Throwable $e): \think\response\Json
|
||
{
|
||
$code = (int) $e->getCode();
|
||
$code = $code >= 400 && $code < 600 ? $code : 500;
|
||
if ($code === 404) {
|
||
return $this->result->setStatusCode(404)->error($e->getMessage(), 404);
|
||
}
|
||
return $this->result->error($e->getMessage(), $code);
|
||
}
|
||
|
||
/**
|
||
* 获取插件信息(支持列表和详情)
|
||
*/
|
||
public function info(Request $request): \think\response\Json
|
||
{
|
||
if (is_market_client()) {
|
||
return $this->remoteInfo($request);
|
||
}
|
||
if (!$this->market) {
|
||
return $this->marketUnavailable();
|
||
}
|
||
$id = (int) $request->get('id', 0);
|
||
if ($id > 0) {
|
||
try {
|
||
$data = $this->market->info($id, $this->getLoginUid($request) ?? 0);
|
||
} catch (\Throwable $e) {
|
||
return $this->marketError($e);
|
||
}
|
||
$data['installed'] = is_dir(ADDON_PATH . ($data['addon']['name'] ?? '') . DIRECTORY_SEPARATOR);
|
||
return $this->result->success($data, '获取成功');
|
||
}
|
||
$r = $this->market->lists(
|
||
max((int) $request->get('page', 1), 1),
|
||
(int) $request->get('per_page', 15)
|
||
);
|
||
$this->result->setCount($r['total']);
|
||
return $this->result->success(['data' => $r['items']], '获取成功');
|
||
}
|
||
|
||
/**
|
||
* 轻量解析请求中的 JWT,返回用户ID(不触发完整登录副作用)
|
||
*/
|
||
protected function getLoginUid(Request $request): ?int
|
||
{
|
||
$auth = $request->header('authorization');
|
||
if (!$auth || !str_starts_with($auth, 'Bearer ')) {
|
||
return null;
|
||
}
|
||
try {
|
||
$token = \ywxapp\service\JwtService::instance()->parseAndValidate(substr($auth, 7));
|
||
$uid = $token->claims()->get('uid');
|
||
return $uid ? (int) $uid : null;
|
||
} catch (\Throwable) {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 发起购买:创建/复用待支付订单并拉起支付(或模拟支付直接签发授权)
|
||
*/
|
||
public function buy(Request $request)
|
||
{
|
||
if (is_market_client()) {
|
||
return $this->remoteBuy($request);
|
||
}
|
||
if (!$this->market) {
|
||
return $this->marketUnavailable();
|
||
}
|
||
$userId = $this->auth->model->uid ?? null;
|
||
if (empty($userId)) {
|
||
return $this->result->setStatusCode(401)->error('请先登录后再购买');
|
||
}
|
||
try {
|
||
$r = $this->market->buy(
|
||
(int) $userId,
|
||
(int) $request->post('addon_id'),
|
||
(string) $request->post('method', 'alipay'),
|
||
(string) $request->root(true)
|
||
);
|
||
} catch (\Throwable $e) {
|
||
return $this->marketError($e);
|
||
}
|
||
return $this->result->success($r['data'], $r['message']);
|
||
}
|
||
|
||
/**
|
||
* 支付结果查询(前端轮询用)
|
||
*/
|
||
public function orderStatus(Request $request): \think\response\Json
|
||
{
|
||
if (is_market_client()) {
|
||
return $this->remoteOrderStatus($request);
|
||
}
|
||
if (!$this->market) {
|
||
return $this->marketUnavailable();
|
||
}
|
||
$userId = $this->auth->model->uid ?? null;
|
||
if (empty($userId)) {
|
||
return $this->result->setStatusCode(401)->error('请先登录');
|
||
}
|
||
try {
|
||
$data = $this->market->orderStatus((int) $userId, (string) $request->get('trade_no'));
|
||
} catch (\Throwable $e) {
|
||
return $this->marketError($e);
|
||
}
|
||
return $this->result->success($data, '获取成功');
|
||
}
|
||
|
||
/**
|
||
* 支付完成落地页(支付宝 return_url 跳转,无需登录,仅展示状态)
|
||
*/
|
||
public function payResult(Request $request): \think\response\Json
|
||
{
|
||
if (is_market_client()) {
|
||
$r = RemoteService::instance()->payResult($request->get());
|
||
return $this->result->success($r['data'] ?? ['status' => 0], $r['message'] ?: '获取成功');
|
||
}
|
||
if (!$this->market) {
|
||
return $this->marketUnavailable();
|
||
}
|
||
$r = $this->market->payResult((string) $request->get('out_trade_no', ''));
|
||
return $this->result->success($r['data'], $r['message']);
|
||
}
|
||
|
||
/**
|
||
* 退款 / 吊销授权(买家或运营)
|
||
* POST /api/v1/addon/refund {trade_no 或 order_id}
|
||
*/
|
||
public function refund(Request $request): \think\response\Json
|
||
{
|
||
if (is_market_client()) {
|
||
return $this->remoteRefund($request);
|
||
}
|
||
if (!$this->market) {
|
||
return $this->marketUnavailable();
|
||
}
|
||
$userId = $this->auth->model->uid ?? null;
|
||
if (empty($userId)) {
|
||
return $this->result->setStatusCode(401)->error('请先登录');
|
||
}
|
||
try {
|
||
$r = $this->market->refund(
|
||
(int) $userId,
|
||
(string) $request->post('trade_no', ''),
|
||
(int) $request->post('order_id', 0)
|
||
);
|
||
} catch (\Throwable $e) {
|
||
return $this->marketError($e);
|
||
}
|
||
return $this->result->success($r['data'], $r['message']);
|
||
}
|
||
|
||
/**
|
||
* 我的已购插件(买家视角):列出已购付费插件 + 授权状态 + 是否可退款
|
||
*/
|
||
public function mine(Request $request): \think\response\Json
|
||
{
|
||
if (is_market_client()) {
|
||
return $this->remoteMine($request);
|
||
}
|
||
if (!$this->market) {
|
||
return $this->marketUnavailable();
|
||
}
|
||
$userId = $this->auth->model->uid ?? null;
|
||
if (empty($userId)) {
|
||
return $this->result->setStatusCode(401)->error('请先登录');
|
||
}
|
||
return $this->result->success($this->market->mine((int) $userId), '获取成功');
|
||
}
|
||
|
||
/**
|
||
* 支付宝/微信异步回调:验签 -> 校验金额 -> 订单置已付 -> 生成授权
|
||
*/
|
||
public function notify()
|
||
{
|
||
if (is_market_client()) {
|
||
return response(RemoteService::instance()->notify(input()));
|
||
}
|
||
if (!$this->market) {
|
||
return response('fail');
|
||
}
|
||
return $this->market->notify((string) input('method', 'alipay'));
|
||
}
|
||
|
||
/**
|
||
* 用户下载已购插件(需登录 + 有效授权 + 次数限制)
|
||
*/
|
||
public function download(Request $request)
|
||
{
|
||
if (is_market_client()) {
|
||
return $this->remoteDownload($request);
|
||
}
|
||
if (!$this->market) {
|
||
return $this->marketUnavailable();
|
||
}
|
||
$userId = $this->auth->model->uid ?? null;
|
||
if (empty($userId)) {
|
||
return $this->result->setStatusCode(401)->error('请先登录');
|
||
}
|
||
try {
|
||
$r = $this->market->download((int) $userId, (int) $request->get('addon_id'), $request);
|
||
} catch (\Throwable $e) {
|
||
return $this->marketError($e);
|
||
}
|
||
return download($r['file'], $r['filename']);
|
||
}
|
||
|
||
// ============ 客户机模式(ywxapp.api_url 指向其他服务器,见 is_market_client())下的代理实现 ============
|
||
|
||
/**
|
||
* 远程:插件详情 / 列表(代理中心站)
|
||
*/
|
||
protected function remoteInfo(Request $request): \think\response\Json
|
||
{
|
||
$id = (int) $request->get('id', 0);
|
||
$uid = $this->getLoginUid($request) ?? 0;
|
||
if ($id > 0) {
|
||
$r = RemoteService::instance()->info($id, $uid);
|
||
if (!$r['success']) {
|
||
return $this->result->setStatusCode(404)->error($r['message'] ?: '获取失败', 404);
|
||
}
|
||
$addon = $r['data']['addon'] ?? [];
|
||
$installed = is_dir(ADDON_PATH . ($addon['name'] ?? '') . DIRECTORY_SEPARATOR);
|
||
return $this->result->success([
|
||
'addon' => $addon,
|
||
'purchased' => $r['data']['purchased'] ?? false,
|
||
'installed' => $installed,
|
||
], '获取成功');
|
||
}
|
||
// 透传分页/筛选参数给中心站,由服务端完成分页(避免全量拉回后本地切片)
|
||
$params = [
|
||
'keyword' => $request->get('keyword', ''),
|
||
'category' => $request->get('category', ''),
|
||
'order' => $request->get('order', ''),
|
||
'page' => max((int) $request->get('page', 1), 1),
|
||
'page_size' => min(max((int) $request->get('per_page', 15), 1), 50),
|
||
];
|
||
$r = RemoteService::instance()->lists($params);
|
||
if (!$r['success']) {
|
||
return $this->result->error($r['message'] ?: '获取失败');
|
||
}
|
||
// 优先消费中心站服务端分页(Market::lists 在 page>0 时返回 data.list/data.total);
|
||
// 兼容历史 data.count 键;老中心站未分页时降级为本地切片。
|
||
if (isset($r['data']['list']) && (isset($r['data']['total']) || isset($r['data']['count']))) {
|
||
$items = $r['data']['list'];
|
||
$total = (int) ($r['data']['total'] ?? $r['data']['count']);
|
||
} else {
|
||
$rows = $r['data']['list'] ?? [];
|
||
$perPage = min(max((int) $request->get('per_page', 15), 1), 50);
|
||
$page = max((int) $request->get('page', 1), 1);
|
||
$total = count($rows);
|
||
$items = array_slice($rows, ($page - 1) * $perPage, $perPage);
|
||
}
|
||
$this->result->setCount($total);
|
||
return $this->result->success(['data' => $items], '获取成功');
|
||
}
|
||
|
||
/**
|
||
* 远程:发起购买(代理中心站)
|
||
*/
|
||
protected function remoteBuy(Request $request): \think\response\Json
|
||
{
|
||
$userId = $this->auth->model->uid ?? null;
|
||
if (empty($userId)) {
|
||
return $this->result->setStatusCode(401)->error('请先登录后再购买');
|
||
}
|
||
$addonId = (int) $request->post('addon_id');
|
||
$method = $request->post('method', 'alipay');
|
||
$r = RemoteService::instance()->buy($addonId, $userId, $method);
|
||
if (!$r['success']) {
|
||
return $this->result->error($r['message'] ?: '购买失败');
|
||
}
|
||
return $this->result->success($r['data'] ?? [], $r['message'] ?: '操作成功');
|
||
}
|
||
|
||
/**
|
||
* 远程:订单状态(代理中心站)
|
||
*/
|
||
protected function remoteOrderStatus(Request $request): \think\response\Json
|
||
{
|
||
$tradeNo = $request->get('trade_no');
|
||
$userId = $this->auth->model->uid ?? null;
|
||
if (empty($userId)) {
|
||
return $this->result->setStatusCode(401)->error('请先登录');
|
||
}
|
||
$r = RemoteService::instance()->orderStatus($tradeNo, $userId);
|
||
if (!$r['success']) {
|
||
return $this->result->error($r['message'] ?: '查询失败');
|
||
}
|
||
return $this->result->success($r['data'] ?? [], '获取成功');
|
||
}
|
||
|
||
/**
|
||
* 远程:下载已购插件(代理中心站 /appmall/api/index,付费插件需已购买)
|
||
*/
|
||
protected function remoteDownload(Request $request)
|
||
{
|
||
$userId = $this->auth->model->uid ?? null;
|
||
if (empty($userId)) {
|
||
return $this->result->setStatusCode(401)->error('请先登录');
|
||
}
|
||
$addonId = (int) $request->get('addon_id');
|
||
$r = RemoteService::instance()->info($addonId, $userId);
|
||
if (!$r['success']) {
|
||
return $this->result->error($r['message'] ?: '插件信息获取失败');
|
||
}
|
||
$addon = $r['data']['addon'] ?? [];
|
||
$name = $addon['name'] ?? '';
|
||
$version = $addon['version'] ?? '';
|
||
if ($name === '' || $version === '') {
|
||
return $this->result->error('插件信息不完整', 404);
|
||
}
|
||
if (!empty($addon['price']) && (float) $addon['price'] > 0 && empty($r['data']['purchased'])) {
|
||
return $this->result->error('您尚未购买该插件', 403);
|
||
}
|
||
try {
|
||
$domain = (string) $request->domain();
|
||
$srcFile = RemoteService::instance()->downloadBinary($name, $version, $userId, $domain);
|
||
} catch (\Exception $e) {
|
||
return $this->result->error($e->getMessage(), 404);
|
||
}
|
||
$tmpDir = root_path() . 'runtime' . DIRECTORY_SEPARATOR . 'addon_download' . DIRECTORY_SEPARATOR;
|
||
if (!is_dir($tmpDir)) {
|
||
@mkdir($tmpDir, 0755, true);
|
||
}
|
||
$tmpFile = $tmpDir . $name . '-' . $version . '.zip';
|
||
if (is_file($srcFile)) {
|
||
copy($srcFile, $tmpFile);
|
||
@unlink($srcFile);
|
||
} else {
|
||
return $this->result->error('下载失败:未获取到文件', 404);
|
||
}
|
||
return new \ywxapp\library\StreamZipResponse($tmpFile, $name . '-' . $version . '.zip');
|
||
}
|
||
|
||
/**
|
||
* 远程:退款 / 吊销授权(代理中心站)
|
||
*/
|
||
protected function remoteRefund(Request $request): \think\response\Json
|
||
{
|
||
$userId = $this->auth->model->uid ?? null;
|
||
if (empty($userId)) {
|
||
return $this->result->setStatusCode(401)->error('请先登录');
|
||
}
|
||
$r = RemoteService::instance()->refund(
|
||
(string) $request->post('trade_no', ''),
|
||
(int) $request->post('order_id', 0),
|
||
$userId
|
||
);
|
||
if (!$r['success']) {
|
||
return $this->result->error($r['message'] ?: '退款失败');
|
||
}
|
||
return $this->result->success($r['data'] ?? [], $r['message'] ?: '退款成功');
|
||
}
|
||
|
||
/**
|
||
* 远程:我的已购插件(代理中心站)
|
||
*/
|
||
protected function remoteMine(Request $request): \think\response\Json
|
||
{
|
||
$userId = $this->auth->model->uid ?? null;
|
||
if (empty($userId)) {
|
||
return $this->result->setStatusCode(401)->error('请先登录');
|
||
}
|
||
$r = RemoteService::instance()->my($userId);
|
||
if (!$r['success']) {
|
||
return $this->result->error($r['message'] ?: '获取失败');
|
||
}
|
||
return $this->result->success($r['data'] ?? ['list' => []], $r['message'] ?: '获取成功');
|
||
}
|
||
}
|