chore: 重写初始提交(清空历史,整理后全量提交)

This commit is contained in:
ywxapp
2026-08-16 16:54:14 +08:00
commit 6c1a106bc1
1808 changed files with 238144 additions and 0 deletions
+430
View File
@@ -0,0 +1,430 @@
<?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()=trueywxapp.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'] ?: '获取成功');
}
}