feat(publish-plan): add publish plan query and API routes

- Introduced `buildPublishPlanQuery` method in `DyVideoCron` model to create a base query for the publish plan module, filtering records based on user ID and project status.
- Added new API routes under `v1/publish-plan` for listing, starting, and stopping publish plans, requiring user authentication.
This commit is contained in:
nepiedg
2026-04-02 09:00:48 +00:00
parent c909ebdf88
commit 044586d60a
5 changed files with 896 additions and 0 deletions
+115
View File
@@ -0,0 +1,115 @@
<?php
declare(strict_types=1);
namespace app\api\controller\v1;
use app\api\common\Response;
use app\api\controller\BaseController;
use app\api\service\PublishPlanService;
use think\App;
/**
* 发布计划控制器。
*
* 本控制器用于给小程序“发布计划”页面提供基础能力:
* 1. 计划列表查询
* 2. 按状态筛选
* 3. 按 acgpmw `cron.php` 对齐的 start / stop 最小动作
*/
class PublishPlan extends BaseController
{
protected PublishPlanService $publishPlanService;
public function __construct(App $app)
{
parent::__construct($app);
$this->publishPlanService = new PublishPlanService();
}
/**
* 发布计划列表。
*
* GET /api/v1/publish-plan/list
*
* 请求参数:
* - `status`:筛选值,支持 `all` / `running` / `stopped` / `finished`
* - `page`:页码,可选
* - `page_size`:每页数量,可选
*
* 返回结构:
* - `filters`:状态筛选项
* - `summary`:顶部统计卡片
* - `pagination`:分页信息
* - `list`:计划卡片数据
*/
public function index()
{
try {
$payload = $this->request->payload ?? null;
if (!$payload || empty($payload['userid'])) {
return Response::error('未登录', 401);
}
$result = $this->publishPlanService->getPlanList((int) $payload['userid'], [
'status' => (string) $this->request->get('status', 'all'),
'page' => (int) $this->request->get('page', 1),
'page_size' => (int) $this->request->get('page_size', 20),
]);
return Response::success($result);
} catch (\Exception $exception) {
return Response::error($exception->getMessage(), $exception->getCode() ?: 500);
}
}
/**
* 开启发布计划。
*
* 这里按 acgpmw `cron::fabu()` 逻辑对齐,只更新 `jrstop=0`。
* 已完成计划不提供恢复操作,避免小程序在未完整对齐复杂编辑流程时误操作。
*
* @param int $id 计划ID
* @return \think\response\Json
*/
public function start(int $id)
{
try {
$payload = $this->request->payload ?? null;
if (!$payload || empty($payload['userid'])) {
return Response::error('未登录', 401);
}
$result = $this->publishPlanService->startPlan((int) $payload['userid'], $id);
return Response::success($result, '设置成功');
} catch (\Exception $exception) {
return Response::error($exception->getMessage(), $exception->getCode() ?: 500);
}
}
/**
* 暂停发布计划。
*
* 这里按 acgpmw `cron::stop()` 逻辑对齐:
* 1. 更新 `jrstop=1`
* 2. 尝试把 `dy_cron_account` 中待执行状态改为 5
*
* @param int $id 计划ID
* @return \think\response\Json
*/
public function stop(int $id)
{
try {
$payload = $this->request->payload ?? null;
if (!$payload || empty($payload['userid'])) {
return Response::error('未登录', 401);
}
$result = $this->publishPlanService->stopPlan((int) $payload['userid'], $id);
return Response::success($result, '设置成功');
} catch (\Exception $exception) {
return Response::error($exception->getMessage(), $exception->getCode() ?: 500);
}
}
}
+27
View File
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace app\api\model; namespace app\api\model;
use think\Model; use think\Model;
use think\db\Query;
/** /**
* 发布任务模型(对应 dy_video_cron 表) * 发布任务模型(对应 dy_video_cron 表)
@@ -32,4 +33,30 @@ class DyVideoCron extends Model
->where('status', '<>', 3) ->where('status', '<>', 3)
->count(); ->count();
} }
/**
* 创建“发布计划模块”基础查询。
*
* 这里直接对齐 acgpmw 以下两个列表控制器的取数范围:
* 1. `cron::cron_list()``project_id = 0 and status not in (9,10)`
* 2. `ai_project_cron::cron_list()``project_id > 0`
*
* 小程序发布计划主页面只聚合这两类数据,避免把 `dy_video_cron`
* 中其他业务模块(如 AI 文案等)的记录误当成发布计划展示出来。
*
* @param int $userid 当前登录用户ID
* @return Query
*/
public static function buildPublishPlanQuery(int $userid): Query
{
return self::where('userid', $userid)
->where(function (Query $query) {
$query->where(function (Query $innerQuery) {
$innerQuery->where('project_id', 0)
->whereNotIn('status', [9, 10]);
})->whereOr(function (Query $innerQuery) {
$innerQuery->where('project_id', '>', 0);
});
});
}
} }
+8
View File
@@ -4,6 +4,7 @@ declare(strict_types=1);
use think\facade\Route; use think\facade\Route;
use app\api\controller\v1\Auth; use app\api\controller\v1\Auth;
use app\api\controller\v1\Platform; use app\api\controller\v1\Platform;
use app\api\controller\v1\PublishPlan;
/** /**
* API 应用路由 * API 应用路由
@@ -25,3 +26,10 @@ Route::group('v1/auth', function () {
Route::group('v1/platform', function () { Route::group('v1/platform', function () {
Route::get('accounts', [Platform::class, 'accounts']); Route::get('accounts', [Platform::class, 'accounts']);
})->middleware(\app\api\middleware\Auth::class); })->middleware(\app\api\middleware\Auth::class);
// v1 发布计划接口(需登录)
Route::group('v1/publish-plan', function () {
Route::get('list', [PublishPlan::class, 'index']);
Route::post('start/:id', [PublishPlan::class, 'start']);
Route::post('stop/:id', [PublishPlan::class, 'stop']);
})->middleware(\app\api\middleware\Auth::class);
+713
View File
@@ -0,0 +1,713 @@
<?php
declare(strict_types=1);
namespace app\api\service;
use app\api\model\DyVideoCron;
use app\api\model\DyVideoUser;
use app\api\model\Member;
use think\Collection;
use think\db\Query;
use think\facade\Db;
/**
* 发布计划服务。
*
* 本服务按 acgpmw 发布计划模块最小可用范围实现:
* 1. 普通发布计划:`controller/cron.php::cron_list()`
* 2. AI 项目计划:`controller/ai_project_cron.php::cron_list()`
* 3. 启停动作:`controller/cron.php::fabu()` / `controller/cron.php::stop()`
*
* 当前不擅自补齐删除、编辑等复杂动作,避免偏离原始程序。
*/
class PublishPlanService
{
/**
* 平台中文名直接按基线常用映射返回给前端,避免小程序自行猜测。
*/
private const PLATFORM_NAME_MAP = [
0 => '抖音',
1 => '快手',
2 => '百家号',
3 => '小红书',
4 => '视频号',
5 => 'B站',
6 => '公众号',
10 => 'TikTok',
];
/**
* 获取发布计划列表。
*
* @param int $userid 当前登录用户ID
* @param array<string, mixed> $params 前端筛选参数
* @return array<string, mixed>
* @throws \Exception
*/
public function getPlanList(int $userid, array $params = []): array
{
$member = Member::findByUserid($userid);
if (!$member) {
throw new \Exception('用户不存在', 4004);
}
$statusFilter = $this->normalizeStatusFilter((string) ($params['status'] ?? 'all'));
$page = max(1, (int) ($params['page'] ?? 1));
$pageSize = min(50, max(1, (int) ($params['page_size'] ?? 20)));
// 基础查询范围严格按 acgpmw 发布计划模块两个列表控制器对齐。
$baseQuery = DyVideoCron::buildPublishPlanQuery($userid);
$total = (int) (clone $this->applyStatusFilter(clone $baseQuery, $statusFilter))->count();
$records = $this->applyStatusFilter(clone $baseQuery, $statusFilter)
->field([
'id',
'userid',
'name',
'status',
'jrstop',
'project_id',
'project_type',
'platform',
'fbvuids',
'atvuids',
'fbvuids_a',
'time_range',
'starttime',
'endtime',
'meirinum',
'plnum',
'yifanum',
'suc_fbnum',
'err_fbnum',
'fbdatetime',
'addtime',
'tplids',
'videotemplates',
])
->order(['id' => 'desc'])
->page($page, $pageSize)
->select();
$projectNameMap = $this->loadProjectNames($userid, $records);
$accountInfoMap = $this->loadAccountInfos($records);
$items = [];
foreach ($records as $record) {
$items[] = $this->buildPlanItem($record->toArray(), $projectNameMap, $accountInfoMap);
}
return [
'filters' => $this->buildFilters($baseQuery, $statusFilter),
'summary' => $this->buildSummary($baseQuery),
'pagination' => [
'page' => $page,
'page_size' => $pageSize,
'total' => $total,
'has_more' => $page * $pageSize < $total,
],
'list' => $items,
];
}
/**
* 开启发布计划。
*
* 此处按 acgpmw `cron::fabu()` 对齐,只恢复 `jrstop=0`。
*
* @param int $userid 当前登录用户ID
* @param int $id 计划ID
* @return array<string, int>
* @throws \Exception
*/
public function startPlan(int $userid, int $id): array
{
$plan = $this->getOwnedPlan($userid, $id);
if ((int) $plan->status === 3) {
throw new \Exception('已完成计划不支持继续开启', 4004);
}
$affected = DyVideoCron::where('id', $id)
->where('userid', $userid)
->update(['jrstop' => 0]);
if (!$affected) {
throw new \Exception('设置失败', 500);
}
return ['id' => $id, 'jrstop' => 0];
}
/**
* 暂停发布计划。
*
* 此处按 acgpmw `cron::stop()` 对齐,除设置 `jrstop=1` 外,
* 还尝试把 `dy_cron_account` 中当前待执行记录改成状态 5。
*
* @param int $userid 当前登录用户ID
* @param int $id 计划ID
* @return array<string, int>
* @throws \Exception
*/
public function stopPlan(int $userid, int $id): array
{
$plan = $this->getOwnedPlan($userid, $id);
if ((int) $plan->status === 3) {
throw new \Exception('已完成计划不支持暂停', 4004);
}
$affected = DyVideoCron::where('id', $id)
->where('userid', $userid)
->update(['jrstop' => 1]);
if (!$affected) {
throw new \Exception('设置失败', 500);
}
// 按 acgpmw `cron::stop()` 对齐处理新模式账号执行队列。
Db::connect('dbmember')
->name('dy_cron_account')
->where('userid', $userid)
->where('cron_id', $id)
->where('status', 0)
->update(['status' => 5]);
return ['id' => $id, 'jrstop' => 1];
}
/**
* 加载当前页涉及到的项目名称。
*
* AI 项目计划列表在 acgpmw 中会把 `project_id` 映射成项目名,
* 这里同样在服务端完成,减少小程序字段推断。
*
* @param int $userid 当前登录用户ID
* @param Collection<int, DyVideoCron> $records 当前页记录
* @return array<int, string>
*/
private function loadProjectNames(int $userid, Collection $records): array
{
$projectIds = [];
foreach ($records as $record) {
$projectId = (int) $record->project_id;
if ($projectId > 0) {
$projectIds[] = $projectId;
}
}
$projectIds = array_values(array_unique($projectIds));
if (empty($projectIds)) {
return [];
}
/** @var array<int, string> $projectMap */
$projectMap = Db::connect('dbmember')
->name('dy_ai_project')
->where('userid', $userid)
->whereIn('id', $projectIds)
->column('name', 'id');
return $projectMap;
}
/**
* 加载当前页计划中用到的账号信息。
*
* @param Collection<int, DyVideoCron> $records 当前页记录
* @return array<int, array<string, mixed>>
*/
private function loadAccountInfos(Collection $records): array
{
$accountIds = [];
foreach ($records as $record) {
$accountIds = array_merge(
$accountIds,
$this->decodeIdList($record->fbvuids),
$this->decodeIdList($record->atvuids),
$this->decodeIdList($record->fbvuids_a)
);
}
$accountIds = array_values(array_unique(array_filter(array_map('intval', $accountIds))));
if (empty($accountIds)) {
return [];
}
$accounts = DyVideoUser::whereIn('id', $accountIds)
->field(['id', 'platform', 'dy_nickname'])
->select()
->toArray();
$accountMap = [];
foreach ($accounts as $account) {
$accountMap[(int) $account['id']] = $account;
}
return $accountMap;
}
/**
* 把单条计划记录转换成小程序卡片数据。
*
* 字段映射依据:
* - 发布进度:`plnum / yifanum / suc_fbnum / err_fbnum`
* - 账号集合:`fbvuids / atvuids / fbvuids_a`
* - 时间展示:`time_range / starttime / endtime / fbdatetime`
* - 视频信息:`tplids / videotemplates`
*
* @param array<string, mixed> $record 原始记录
* @param array<int, string> $projectNameMap 项目名称映射
* @param array<int, array<string, mixed>> $accountInfoMap 账号信息映射
* @return array<string, mixed>
*/
private function buildPlanItem(array $record, array $projectNameMap, array $accountInfoMap): array
{
$publishAccountIds = $this->decodeIdList($record['fbvuids'] ?? null);
$atAccountIds = $this->decodeIdList($record['atvuids'] ?? null);
$syncAccountIds = $this->decodeIdList($record['fbvuids_a'] ?? null);
$materialIds = $this->decodeIdList($record['tplids'] ?? null);
$videoTemplateIds = $this->decodeIdList($record['videotemplates'] ?? null);
$publishAccounts = $this->resolveAccountNames($publishAccountIds, $accountInfoMap);
$atAccounts = $this->resolveAccountNames($atAccountIds, $accountInfoMap);
$syncAccounts = $this->resolveAccountNames($syncAccountIds, $accountInfoMap);
$statusInfo = $this->buildStatusBlock(
(int) ($record['status'] ?? 0),
(int) ($record['jrstop'] ?? 0)
);
$actionInfo = $this->buildActionBlock($statusInfo);
$projectId = (int) ($record['project_id'] ?? 0);
$projectType = (int) ($record['project_type'] ?? 0);
$platform = (int) ($record['platform'] ?? 0);
return [
'id' => (int) ($record['id'] ?? 0),
'name' => (string) ($record['name'] ?? ''),
'plan_type' => $projectId > 0 ? 'ai_project' : 'normal',
'plan_type_text' => $projectId > 0 ? 'AI 项目计划' : '发布任务',
'project_id' => $projectId,
'project_type' => $projectType,
'project_name' => $projectId > 0
? ($projectNameMap[$projectId] ?? '无项目(异常)')
: '',
'platform' => $platform,
'platform_name' => $projectType === 1
? '作品下载'
: (self::PLATFORM_NAME_MAP[$platform] ?? ('平台' . $platform)),
'status' => $statusInfo,
'action' => $actionInfo,
'publish_time_text' => $this->buildPublishTimeText($record),
'schedule_text' => $this->buildScheduleText($record),
'created_at_text' => $this->formatDateTime((int) ($record['addtime'] ?? 0)),
'metrics' => [
[
'key' => 'progress',
'label' => '已发 / 总量',
'value' => (int) ($record['yifanum'] ?? 0) . ' / ' . (int) ($record['plnum'] ?? 0),
'tone' => 'blue',
],
[
'key' => 'result',
'label' => '成功 / 失败',
'value' => (int) ($record['suc_fbnum'] ?? 0) . ' / ' . (int) ($record['err_fbnum'] ?? 0),
'tone' => 'green',
],
[
'key' => 'accounts',
'label' => '关联账号',
'value' => (string) (count($publishAccountIds) + count($atAccountIds) + count($syncAccountIds)),
'tone' => 'amber',
],
],
'account_summary_text' => $this->buildAccountSummaryText($publishAccounts, $atAccounts, $syncAccounts),
'video_info_text' => $this->buildVideoInfoText($materialIds, $videoTemplateIds),
];
}
/**
* 构建状态筛选项。
*
* 这里把小程序筛选态映射为更稳定的业务语义:
* - `running``status != 3 and jrstop = 0`
* - `stopped``status != 3 and jrstop = 1`
* - `finished``status = 3`
*
* @param Query $baseQuery 发布计划基础查询
* @param string $currentStatus 当前筛选值
* @return array<string, mixed>
*/
private function buildFilters(Query $baseQuery, string $currentStatus): array
{
$items = [];
foreach ($this->getStatusFilterMap() as $key => $label) {
$count = (int) (clone $this->applyStatusFilter(clone $baseQuery, $key))->count();
$items[] = [
'key' => $key,
'name' => $label,
'count' => $count,
];
}
return [
'current_status' => $currentStatus,
'items' => $items,
];
}
/**
* 构建顶部统计卡片。
*
* @param Query $baseQuery 发布计划基础查询
* @return array<string, int>
*/
private function buildSummary(Query $baseQuery): array
{
return [
'total' => (int) (clone $baseQuery)->count(),
'running_count' => (int) (clone $this->applyStatusFilter(clone $baseQuery, 'running'))->count(),
'stopped_count' => (int) (clone $this->applyStatusFilter(clone $baseQuery, 'stopped'))->count(),
'finished_count' => (int) (clone $this->applyStatusFilter(clone $baseQuery, 'finished'))->count(),
];
}
/**
* 应用状态筛选。
*
* @param Query $query 查询对象
* @param string $statusFilter 筛选值
* @return Query
*/
private function applyStatusFilter(Query $query, string $statusFilter): Query
{
switch ($statusFilter) {
case 'running':
$query->where('status', '<>', 3)->where('jrstop', 0);
break;
case 'stopped':
$query->where('status', '<>', 3)->where('jrstop', 1);
break;
case 'finished':
$query->where('status', 3);
break;
case 'all':
default:
break;
}
return $query;
}
/**
* 规范化状态筛选参数。
*
* @param string $statusFilter 前端传入的筛选值
* @return string
* @throws \Exception
*/
private function normalizeStatusFilter(string $statusFilter): string
{
$statusFilter = $statusFilter !== '' ? $statusFilter : 'all';
if (!array_key_exists($statusFilter, $this->getStatusFilterMap())) {
throw new \Exception('状态筛选参数错误', 400);
}
return $statusFilter;
}
/**
* 获取筛选映射。
*
* @return array<string, string>
*/
private function getStatusFilterMap(): array
{
return [
'all' => '全部',
'running' => '进行中',
'stopped' => '已停止',
'finished' => '已完成',
];
}
/**
* 构建计划状态块。
*
* @param int $status 原始状态码
* @param int $jrstop 今日暂停标记
* @return array<string, mixed>
*/
private function buildStatusBlock(int $status, int $jrstop): array
{
if ($status === 3) {
return [
'key' => 'finished',
'text' => '已完成',
'tone' => 'success',
'raw_status' => $status,
'jrstop' => $jrstop,
];
}
if ($jrstop === 1) {
return [
'key' => 'stopped',
'text' => '已停止',
'tone' => 'danger',
'raw_status' => $status,
'jrstop' => $jrstop,
];
}
$statusTextMap = [
1 => '发布中',
5 => '定时执行',
9 => '定时执行',
10 => '计划执行中',
20 => '作品生成中',
];
return [
'key' => 'running',
'text' => $statusTextMap[$status] ?? ('状态 ' . $status),
'tone' => 'primary',
'raw_status' => $status,
'jrstop' => $jrstop,
];
}
/**
* 根据状态块构建可执行动作。
*
* @param array<string, mixed> $statusInfo 状态块
* @return array<string, string>|null
*/
private function buildActionBlock(array $statusInfo): ?array
{
if ($statusInfo['key'] === 'finished') {
return null;
}
if ($statusInfo['key'] === 'stopped') {
return [
'type' => 'start',
'text' => '继续计划',
'tone' => 'primary',
];
}
return [
'type' => 'stop',
'text' => '暂停计划',
'tone' => 'danger',
];
}
/**
* 生成发布时间文案。
*
* @param array<string, mixed> $record 原始计划记录
* @return string
*/
private function buildPublishTimeText(array $record): string
{
$fbdatetime = (int) ($record['fbdatetime'] ?? 0);
if ($fbdatetime > 0) {
return $this->formatDate($fbdatetime);
}
return $this->formatDate((int) ($record['addtime'] ?? 0));
}
/**
* 生成执行时间文案。
*
* 普通计划按 `time_range` / `starttime` / `endtime` 对齐展示,
* 下载型项目计划没有明确发布时间段时,返回固定文案。
*
* @param array<string, mixed> $record 原始计划记录
* @return string
*/
private function buildScheduleText(array $record): string
{
$projectType = (int) ($record['project_type'] ?? 0);
if ($projectType === 1) {
return '0点至24点';
}
$timeRange = trim((string) ($record['time_range'] ?? ''));
if ($timeRange !== '') {
$timePoints = array_values(array_filter(array_map('trim', explode(',', $timeRange)), static function ($item) {
return $item !== '';
}));
if (!empty($timePoints)) {
return implode('、', array_map(static function ($item) {
return $item . '点';
}, $timePoints));
}
}
$startTime = (int) ($record['starttime'] ?? 0);
$endTime = (int) ($record['endtime'] ?? 0);
if ($startTime > 0 || $endTime > 0) {
return $startTime . '点至' . $endTime . '点';
}
return '未设置';
}
/**
* 生成账号摘要文案。
*
* @param array<int, string> $publishAccounts 发布账号名
* @param array<int, string> $atAccounts @账号名
* @param array<int, string> $syncAccounts 同步账号名
* @return string
*/
private function buildAccountSummaryText(array $publishAccounts, array $atAccounts, array $syncAccounts): string
{
$parts = [];
if (!empty($publishAccounts)) {
$parts[] = '发布 ' . count($publishAccounts) . ' 个';
}
if (!empty($atAccounts)) {
$parts[] = '@账号 ' . count($atAccounts) . ' 个';
}
if (!empty($syncAccounts)) {
$parts[] = '同步 ' . count($syncAccounts) . ' 个';
}
if (empty($parts)) {
return '暂无关联账号';
}
return implode(' / ', $parts);
}
/**
* 生成视频信息摘要。
*
* @param array<int> $materialIds 素材ID集合
* @param array<int> $videoTemplateIds 视频模板ID集合
* @return string
*/
private function buildVideoInfoText(array $materialIds, array $videoTemplateIds): string
{
$parts = [];
if (!empty($materialIds)) {
$parts[] = '素材 ' . count($materialIds) . ' 个';
}
if (!empty($videoTemplateIds)) {
$parts[] = '视频模板 ' . count($videoTemplateIds) . ' 个';
}
if (empty($parts)) {
return '暂无视频信息';
}
return implode(' / ', $parts);
}
/**
* 解析序列化 ID 列表。
*
* `dy_video_cron` 中多个账号和素材字段都按 PHP serialize 存储,
* 这里统一做兼容解析,避免前端处理历史格式。
*
* @param mixed $serializedValue 原始字段值
* @return array<int>
*/
private function decodeIdList($serializedValue): array
{
if (empty($serializedValue) || !is_string($serializedValue)) {
return [];
}
$decoded = @unserialize($serializedValue);
if (!is_array($decoded)) {
return [];
}
return array_values(array_unique(array_filter(array_map('intval', $decoded))));
}
/**
* 把账号 ID 集合映射为账号名称集合。
*
* @param array<int> $accountIds 账号ID集合
* @param array<int, array<string, mixed>> $accountInfoMap 账号信息映射
* @return array<int, string>
*/
private function resolveAccountNames(array $accountIds, array $accountInfoMap): array
{
$names = [];
foreach ($accountIds as $accountId) {
if (!isset($accountInfoMap[$accountId])) {
continue;
}
$account = $accountInfoMap[$accountId];
$platformName = self::PLATFORM_NAME_MAP[(int) ($account['platform'] ?? 0)] ?? '平台';
$nickname = trim((string) ($account['dy_nickname'] ?? ''));
$names[] = $nickname !== '' ? ($platformName . '·' . $nickname) : ($platformName . '·账号' . $accountId);
}
return $names;
}
/**
* 获取当前用户拥有的发布计划。
*
* @param int $userid 当前登录用户ID
* @param int $id 计划ID
* @return DyVideoCron
* @throws \Exception
*/
private function getOwnedPlan(int $userid, int $id): DyVideoCron
{
$plan = DyVideoCron::buildPublishPlanQuery($userid)
->where('id', $id)
->find();
if (!$plan) {
throw new \Exception('发布计划不存在', 404);
}
return $plan;
}
/**
* 格式化日期。
*
* @param int $timestamp Unix 时间戳
* @return string
*/
private function formatDate(int $timestamp): string
{
if ($timestamp <= 0) {
return '未设置';
}
return date('Y-m-d', $timestamp);
}
/**
* 格式化日期时间。
*
* @param int $timestamp Unix 时间戳
* @return string
*/
private function formatDateTime(int $timestamp): string
{
if ($timestamp <= 0) {
return '未设置';
}
return date('Y-m-d H:i', $timestamp);
}
}
+33
View File
@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
use think\facade\Route;
use app\api\controller\v1\Auth;
use app\api\controller\v1\Platform;
/**
* 全局路由入口。
*
* 当前项目使用 ThinkPHP 8 的根级 `route/app.php` 自动加载机制。
* 之前把 API 路由写在 `app/api/route/app.php`,框架不会自动扫描该位置,
* 因此会导致 `api/v1/platform/accounts` 等接口返回 404。
*
* 这里统一把 API 路由注册到真正生效的位置。
*/
// v1 认证接口(公开)
Route::post('api/v1/auth/login', [Auth::class, 'login']);
Route::post('api/v1/auth/register', [Auth::class, 'register']);
Route::post('api/v1/auth/refresh', [Auth::class, 'refresh']);
// v1 认证接口(需登录)
Route::group('api/v1/auth', function () {
Route::get('me', [Auth::class, 'me']);
Route::post('logout', [Auth::class, 'logout']);
Route::post('password', [Auth::class, 'password']);
})->middleware(\app\api\middleware\Auth::class);
// v1 平台账号管理接口(需登录)
Route::group('api/v1/platform', function () {
Route::get('accounts', [Platform::class, 'accounts']);
})->middleware(\app\api\middleware\Auth::class);