初始化本地仓库

This commit is contained in:
2026-08-31 11:33:59 +08:00
commit 663f74b633
2390 changed files with 402918 additions and 0 deletions
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+7
View File
@@ -0,0 +1,7 @@
# 项目规则
## 本地项目与版本控制
- 本项目为本地项目,不使用 Git 进行状态检查、差异比较、提交或任何其他操作。
- 后续回答、验证命令和开发说明中不出现 Git 或 Git 命令。
- 如需确认文件改动,以实际文件内容、语法检查、自动化测试或运行时验证为准。
@@ -0,0 +1,35 @@
---
name: backend-bootstrap
description: 启动引导规范,应用初始化流程和事件钩子
globs:
- "app/bootstrap/**/*.php"
---
## 引导流程
```
启动顺序:
1. config/ 配置加载
2. app/bootstrap/ 应用引导
3. 中间件注册
4. 路由注册
5. 服务提供者注册
```
## 代码模板
```php
<?php
// app/bootstrap/{name}.php
return function () {
// 初始化逻辑
};
```
## 检查清单
- [ ] 引导代码执行顺序是否正确
- [ ] 是否有异常处理
- [ ] 是否遵循框架启动流程
+38
View File
@@ -0,0 +1,38 @@
---
name: backend-cache
description: 缓存策略规范,支持 Redis/File 适配器、分布式锁
globs:
- "core/cache/**/*.php"
---
## 缓存驱动
| 驱动 | 说明 |
|------|------|
| Redis | 生产环境推荐,支持分布式锁 |
| File | 开发环境,文件缓存 |
## 使用方式
```php
use core\cache\CacheService;
// 设置缓存
CacheService::set('key', $value, 3600);
// 获取缓存
$value = CacheService::get('key');
// 分布式锁
$lock = CacheService::lock('lock_name', 10);
if ($lock->acquire()) {
// 业务逻辑
$lock->release();
}
```
## 检查清单
- [ ] 缓存键名是否唯一且有前缀
- [ ] 缓存的 TTL 是否合理
- [ ] 锁是否在 finally 中释放
+54
View File
@@ -0,0 +1,54 @@
---
name: backend-command
description: 命令行命令规范(Webman Command),支持 make/config/install/metadata/plugin
globs:
- "app/command/**/*.php"
---
## 文件位置
```
app/command/{Name}Command.php
```
## 代码模板
```php
<?php
namespace app\command;
use app\command\BaseCommand;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
#[AsCommand(
name: '{name}',
description: '{description}',
aliases: ['{name}'],
hidden: false
)]
class {Name}Command extends BaseCommand
{
public function __invoke(InputInterface $input, OutputInterface $output): int
{
$output->writeln('Done');
return self::SUCCESS;
}
}
```
## 检查清单
- [ ] 命令名称是否 kebab-case
- [ ] 是否使用 `#[AsCommand]` 注册命令
- [ ] 是否有描述信息
- [ ] 是否返回正确的 exit code
## 项目真实约定
- 当前项目命令通过 `Symfony\Component\Console\Attribute\AsCommand` 自动注册
- 当前项目命令通常继承 `app\command\BaseCommand`
- 现有命令普遍使用 `public function __invoke(InputInterface $input, OutputInterface $output): int` 作为执行入口
+47
View File
@@ -0,0 +1,47 @@
---
name: backend-config
description: 配置体系规范,覆盖 28 个配置文件的结构、加载顺序、环境变量覆盖规则
globs:
- "config/**/*.php"
- "app/**/config/**/*.php"
---
## 配置体系
### 全局配置 (config/)
| 文件 | 说明 |
|------|------|
| `app.php` | 应用基础配置 |
| `database.php` | 数据库连接配置 |
| `cache.php` | 缓存配置 (Redis/File) |
| `log.php` | 日志配置 |
| `route.php` | 路由配置 |
| `middleware.php` | 全局中间件 |
| `exception.php` | 异常处理配置 |
| `thinkorm.php` | Laravel Eloquent ORM 配置 |
### 环境变量覆盖
所有配置项可通过 `.env` 文件覆盖,使用 `env()` 函数读取:
```php
return [
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', 3306),
];
```
## 关键约定
- 配置返回 PHP 数组,键名使用 snake_case
- 敏感信息必须通过 `env()` 从环境变量读取
- 动态配置存储在数据库中,通过 Config Service 读取
- 插件配置放在 `plugin/{name}/config/` 下
## 检查清单
- [ ] 配置键名是否 snake_case
- [ ] 敏感信息是否使用 env() 读取
- [ ] 是否有合理的默认值
- [ ] 插件配置是否放在正确路径
+39
View File
@@ -0,0 +1,39 @@
---
name: backend-crontab
description: 定时任务规范(Crontab),基于 Webman Crontab 组件
globs:
- "app/crontab/**/*.php"
---
## 文件位置
```
app/crontab/{Name}Task.php
```
## 代码模板
```php
<?php
namespace app\crontab;
use Webman\Crontab\Task;
class {Name}Task extends Task
{
protected string $crontab = '*/5 * * * *'; // 每5分钟
public function run(): void
{
// 任务逻辑
}
}
```
## 检查清单
- [ ] crontab 表达式是否正确
- [ ] 任务是否有错误处理
- [ ] 是否注册到配置中
- [ ] 长时间任务是否使用了队列
+52
View File
@@ -0,0 +1,52 @@
---
name: backend-enum
description: 枚举规范,继承 IEnum 接口,支持 label()/tryFrom() 模式
globs:
- "app/enum/**/*.php"
- "app/**/enum/**/*.php"
---
## 文件位置
```
app/enum/{module}/{Name}Enum.php
```
## 代码模板
```php
<?php
namespace app\enum\{module};
use core\interface\IEnum;
enum {Name}Enum: int implements IEnum
{
case ACTIVE = 1;
case INACTIVE = 0;
public function label(): string
{
return match ($this) {
self::ACTIVE => '启用',
self::INACTIVE => '禁用',
};
}
}
```
## 关键约定
- 所有枚举实现 `core\interface\IEnum` 接口
- 枚举使用 PHP 8.1+ native enum,backed by int 或 string
- 必须实现 `label()` 方法返回中文描述
- 枚举值一旦发布不可修改(只能追加)
- 模型字段转换使用 `$casts` 中的 `EnumsCast`
## 检查清单
- [ ] 是否实现 IEnum 接口
- [ ] 是否包含 label() 方法
- [ ] 枚举值是否稳定(发布后不改)
- [ ] 命名空间是否正确
+51
View File
@@ -0,0 +1,51 @@
---
name: backend-excel
description: Excel 导入导出规范(基于 PhpSpreadsheet)
globs:
- "core/excel/**/*.php"
---
## 导出
```php
use core\excel\ExcelExportService;
$params = [
'filename' => '数据导出',
'sheetName' => '数据导出',
'format' => 'xlsx',
'isHeader' => true,
'fields' => [
['field' => 'name', 'title' => '名称'],
['field' => 'status', 'title' => '状态'],
],
];
ExcelExportService::export($params, static function (callable $chunkHandler): void {
$chunkHandler([
['name' => '名称', 'status' => '启用'],
]);
});
```
### 明细数组按行导出
`app\adminapi\controller\Crud::export()` 在写入 Excel 前会调用可覆写的 `expandExportRows(iterable $items)`。当一条主记录包含多个明细(如维修记录的 `parts`)时,在对应控制器覆盖该方法并为每项明细 `yield` 一条数组:主记录字段重复,明细字段取当前项。不要将多项明细拼接成同一个单元格,以保证 Excel 可筛选、复制和统计。
## 导入
```php
use core\excel\ExcelImportService;
$rows = ExcelImportService::import($filePath, [
'name' => 'required',
'status' => 'integer',
]);
```
## 检查清单
- [ ] 导出列名是否与数据键名一致
- [ ] 大数据量是否分页导出
- [ ] 导入是否有数据校验
- [ ] 导入是否有事务保护
@@ -0,0 +1,34 @@
---
name: backend-exception
description: 异常体系规范,15 种异常类 + 全局异常处理器
globs:
- "core/exception/**/*.php"
- "app/**/exception/**/*.php"
---
## 异常体系
| 异常类 | 用途 |
|--------|------|
| `BusinessException` | 业务异常(通用) |
| `NotFoundException` | 资源不存在 |
| `UnauthorizedException` | 未认证/令牌过期 |
| `ForbiddenException` | 无权限 |
| `ValidationException` | 参数校验失败 |
| `TooManyRequestsException` | 请求频率限制 |
## 全局异常处理器
位置:`core/exception/Handler.php`
自动处理:
- 404 → NotFoundException
- 401 → UnauthorizedException
- 403 → ForbiddenException
- 500 → 服务器内部错误
## 检查清单
- [ ] 异常是否继承自正确的基类
- [ ] 全局处理器是否覆盖了相关异常
- [ ] 异常消息是否用户友好(中文)
@@ -0,0 +1,445 @@
---
name: backend-gen-api-controller
description: Generate API controller for madong module前台API控制器. Supports both app (main project) and plugin targets. Creates controller extending Base class with OpenAPI attributes and standard REST endpoints for public/member access.
globs:
- "app/api/controller/**/*.php"
- "plugin/**/app/api/controller/**/*.php"
---
# Generate API Controller (前台 API 控制器)
Create API controller for module前台用户访问).
## File Location
**App (主项目):**
```
app/api/controller/{module}/{Model}Controller.php
```
**Plugin (插件):**
```
plugin/{plugin}/app/api/controller/{module}/{Model}Controller.php
```
## Target Variables
| 变量 | App | Plugin |
|------|-----|--------|
| `{prefix}` | `` (空) | `plugin\{Plugin}` |
| `{ns}` | `app` | `plugin\{Plugin}\app` |
| `{service_ns}` | `app\service\api` | `plugin\{Plugin}\app\service\api` |
## API Controller Features
- **继承 Base**: 继承 `app\api\controller\Base` 基类
- **无需权限验证**: 不使用 PermissionMiddleware
- **支持会员认证**: 认证属性和中间件必须先以当前项目 `app/api` 的实际实现为准。当前项目已核对的 `app/api` 控制器中未发现 `MemberAuthRequired` 的实际使用,不应直接套用该属性。
- **轻量级**: 只返回必要的数据
- **雪花ID**: 主键使用雪花ID
## API Controller Template
```php
<?php
declare(strict_types=1);
/**
*+------------------
* madong
*+------------------
* Copyright (c) https://gitee.com/motion-code All rights reserved.
*+------------------
* Author: Mr. April (405784684@qq.com)
*+------------------
*/
namespace {ns}\api\controller\{module};
use app\api\controller\Base;
use app\api\attribute\MemberAuthRequired;
use {service_ns}\{module}\{Model}Service;
use core\tool\Json;
use madong\swagger\annotation\response\SimpleResponse;
use madong\swagger\attribute\AllowAnonymous;
use OpenApi\Attributes as OA;
use Webman\Http\Request;
use Webman\Http\Response;
/**
* {Model}前台API控制器
*/
#[OA\Tag(name: '{Module}模块')]
final class {Model}Controller extends Base
{
public function __construct(private {Model}Service $service)
{
}
/**
* 获取列表
*/
#[OA\Get(
path: '/{api_prefix}',
summary: '获取{Model}列表',
tags: ['{Module}模块'],
responses: [
new OA\Response(response: 200, description: '获取成功'),
]
)]
#[AllowAnonymous(requireToken: false, requirePermission: false)]
#[SimpleResponse(schema: [], example: [])]
public function index(Request $request): Response
{
try {
$page = (int) $request->input('page', 1);
$limit = (int) $request->input('limit', 20);
$where = $this->buildWhere($request);
$order = $request->input('order', 'id desc');
$total = $this->service->count($where);
$list = $this->service->selectList($where, '*', $page, $limit, $order);
return Json::success('ok', compact('list', 'total'));
} catch (\Exception $e) {
return Json::fail($e->getMessage());
}
}
/**
* 获取详情
*/
#[OA\Get(
path: '/{api_prefix}/{id}',
summary: '获取{Model}详情',
tags: ['{Module}模块'],
parameters: [
new OA\Parameter(name: 'id', description: '{Model}ID', in: 'path', required: true, schema: new OA\Schema(type: 'integer')),
],
responses: [
new OA\Response(response: 200, description: '获取成功'),
new OA\Response(response: 404, description: '不存在'),
]
)]
#[AllowAnonymous(requireToken: false, requirePermission: false)]
#[SimpleResponse(schema: [], example: [])]
public function show(Request $request, int $id): Response
{
try {
$data = $this->service->get($id);
if (empty($data)) {
return Json::fail('{Model}不存在', [], 404);
}
return Json::success('ok', $data);
} catch (\Exception $e) {
return Json::fail($e->getMessage());
}
}
/**
* 创建{Model}
*/
#[OA\Post(
path: '/{api_prefix}',
summary: '创建{Model}',
tags: ['{Module}模块'],
requestBody: new OA\RequestBody(
required: true,
content: new OA\JsonContent(
required: [{required_fields}],
properties: [
{form_fields}
]
)
),
responses: [
new OA\Response(response: 200, description: '创建成功'),
new OA\Response(response: 401, description: '未登录'),
]
)]
#[MemberAuthRequired]
#[SimpleResponse(schema: [], example: [])]
public function store(Request $request): Response
{
try {
$data = $request->post();
$model = $this->service->save($data);
$pk = $model->getPk();
return Json::success('创建成功', [$pk => $model->getAttribute($pk)]);
} catch (\Exception $e) {
return Json::fail($e->getMessage());
}
}
/**
* 更新{Model}
*/
#[OA\Put(
path: '/{api_prefix}/{id}',
summary: '更新{Model}',
tags: ['{Module}模块'],
parameters: [
new OA\Parameter(name: 'id', description: '{Model}ID', in: 'path', required: true, schema: new OA\Schema(type: 'integer')),
],
responses: [
new OA\Response(response: 200, description: '更新成功'),
new OA\Response(response: 401, description: '未登录'),
]
)]
#[MemberAuthRequired]
#[SimpleResponse(schema: [], example: [])]
public function update(Request $request, int $id): Response
{
try {
$data = $request->post();
$this->service->update($id, $data);
return Json::success('更新成功');
} catch (\Exception $e) {
return Json::fail($e->getMessage());
}
}
/**
* 删除{Model}
*/
#[OA\Delete(
path: '/{api_prefix}/{id}',
summary: '删除{Model}',
tags: ['{Module}模块'],
parameters: [
new OA\Parameter(name: 'id', description: '{Model}ID', in: 'path', required: true, schema: new OA\Schema(type: 'integer')),
],
responses: [
new OA\Response(response: 200, description: '删除成功'),
new OA\Response(response: 401, description: '未登录'),
]
)]
#[MemberAuthRequired]
#[SimpleResponse(schema: [], example: [])]
public function destroy(Request $request, int $id): Response
{
try {
$this->service->delete($id);
return Json::success('删除成功');
} catch (\Exception $e) {
return Json::fail($e->getMessage());
}
}
/**
* 构建查询条件
*/
protected function buildWhere(Request $request): array
{
$where = [];
// 添加常用的查询条件
$searchFields = [{search_fields}];
foreach ($searchFields as $field) {
$value = $request->input($field);
if ($value !== null && $value !== '') {
$where[$field] = $value;
}
}
// 状态条件
if ($request->input('status') !== null) {
$where['status'] = (int) $request->input('status');
}
return $where;
}
}
```
## Complete Example (App)
```php
<?php
declare(strict_types=1);
/**
*+------------------
* madong
*+------------------
* Copyright (c) https://gitee.com/motion-code All rights reserved.
*+------------------
* Author: Mr. April (405784684@qq.com)
*+------------------
*/
namespace app\api\controller\question;
use app\api\controller\Base;
use app\api\attribute\MemberAuthRequired;
use app\service\api\question\QuestionService;
use core\tool\Json;
use madong\swagger\annotation\response\SimpleResponse;
use madong\swagger\attribute\AllowAnonymous;
use OpenApi\Attributes as OA;
use Webman\Http\Request;
use Webman\Http\Response;
/**
* 问题前台API控制器
*/
#[OA\Tag(name: '问答模块')]
final class QuestionController extends Base
{
public function __construct(private QuestionService $service)
{
}
#[OA\Get(path: '/ask/question', summary: '获取问题列表', tags: ['问答模块'])]
#[AllowAnonymous(requireToken: false, requirePermission: false)]
public function index(Request $request): Response
{
try {
$page = (int) $request->input('page', 1);
$limit = (int) $request->input('limit', 20);
$where = $this->buildWhere($request);
$total = $this->service->count($where);
$list = $this->service->selectList($where, '*', $page, $limit, 'id desc');
return Json::success('ok', compact('list', 'total'));
} catch (\Exception $e) {
return Json::fail($e->getMessage());
}
}
#[OA\Get(path: '/ask/question/{id}', summary: '获取问题详情', tags: ['问答模块'])]
#[AllowAnonymous(requireToken: false, requirePermission: false)]
public function show(Request $request, int $id): Response
{
try {
$data = $this->service->get($id);
if (empty($data)) {
return Json::fail('问题不存在', [], 404);
}
return Json::success('ok', $data);
} catch (\Exception $e) {
return Json::fail($e->getMessage());
}
}
#[OA\Post(path: '/ask/question', summary: '发布问题', tags: ['问答模块'])]
#[MemberAuthRequired]
public function store(Request $request): Response
{
try {
$data = $request->post();
$model = $this->service->save($data);
return Json::success('发布成功', ['id' => $model->id]);
} catch (\Exception $e) {
return Json::fail($e->getMessage());
}
}
protected function buildWhere(Request $request): array
{
$where = [];
if ($request->input('category_id')) {
$where['category_id'] = (int) $request->input('category_id');
}
$where['status'] = 1;
return $where;
}
}
```
## Complete Example (Plugin)
```php
<?php
declare(strict_types=1);
/**
*+------------------
* madong
*+------------------
* Copyright (c) https://gitee.com/motion-code All rights reserved.
*+------------------
* Author: Mr. April (405784684@qq.com)
*+------------------
*/
namespace plugin\official\app\api\controller\question;
use app\api\controller\Base;
use app\api\attribute\MemberAuthRequired;
use plugin\official\app\service\api\question\QuestionService;
use core\tool\Json;
use madong\swagger\annotation\response\SimpleResponse;
use madong\swagger\attribute\AllowAnonymous;
use OpenApi\Attributes as OA;
use Webman\Http\Request;
use Webman\Http\Response;
/**
* 问题前台API控制器
*/
#[OA\Tag(name: '问答模块')]
final class QuestionController extends Base
{
public function __construct(private QuestionService $service)
{
}
#[OA\Get(path: '/ask/question', summary: '获取问题列表', tags: ['问答模块'])]
#[AllowAnonymous(requireToken: false, requirePermission: false)]
public function index(Request $request): Response
{
try {
$page = (int) $request->input('page', 1);
$limit = (int) $request->input('limit', 20);
$where = $this->buildWhere($request);
$total = $this->service->count($where);
$list = $this->service->selectList($where, '*', $page, $limit, 'id desc');
return Json::success('ok', compact('list', 'total'));
} catch (\Exception $e) {
return Json::fail($e->getMessage());
}
}
// ... 其他方法同上
}
```
## API Path Convention
| Method | Path | Auth | Description |
|--------|------|------|-------------|
| GET | `/{api_prefix}` | 公开 | 列表(分页) |
| GET | `/{api_prefix}/{id}` | 公开 | 详情 |
| POST | `/{api_prefix}` | 会员 | 创建 |
| PUT | `/{api_prefix}/{id}` | 会员 | 更新 |
| DELETE | `/{api_prefix}/{id}` | 会员 | 删除 |
## Authentication Options
### 公开接口
```php
#[AllowAnonymous(requireToken: false, requirePermission: false)]
```
### 会员专用接口
请先检查当前项目 `app/api` 的真实认证中间件、控制器属性和 token 解析方式,再配置会员认证;不要假设项目一定存在 `MemberAuthRequired`。
## Auto-generation Checklist
When generating API controller:
- [ ] Set correct namespace based on target
- [ ] Import required classes according to the actual project implementation (Base, Json, and verified authentication attributes/middleware)
- [ ] Extend Base controller
- [ ] Inject Service in constructor
- [ ] Add OpenAPI attributes for each action
- [ ] Use appropriate auth attribute (AllowAnonymous or MemberAuthRequired)
- [ ] Implement standard CRUD methods
- [ ] Use proper API path convention
- [ ] Add buildWhere() for query building
@@ -0,0 +1,384 @@
---
name: backend-gen-api-service
description: Generate API service class for madong module前台API服务层. Supports both app (main project) and plugin targets. Creates service extending BaseService with DAO injection and standard CRUD operations for public/member access.
globs:
- "app/service/api/**/*.php"
- "plugin/**/app/service/api/**/*.php"
---
# Generate API Service (前台 API 服务层)
Create service class for module (前台 API 访问).
## File Location
**App (主项目):**
```
app/service/api/{module}/{Model}Service.php
```
**Plugin (插件):**
```
plugin/{plugin}/app/service/api/{module}/{Model}Service.php
```
## Target Variables
| 变量 | App | Plugin |
|------|-----|--------|
| `{prefix}` | `` (空) | `plugin\{Plugin}` |
| `{ns}` | `app` | `plugin\{Plugin}\app` |
| `{dao_ns}` | `app\dao` | `plugin\{Plugin}\app\dao` |
| `{model_ns}` | `app\model` | `plugin\{Plugin}\app\model` |
## API Service Features
- **继承 BaseService**: 继承 `core\base\BaseService`
- **轻量级操作**: 不触发事件,适合公开访问
- **雪花ID**: 支持雪花ID作为主键
- **会员关联**: 支持与会员关联的数据操作
## API Service Template
```php
<?php
declare(strict_types=1);
/**
*+------------------
* madong
*+------------------
* Copyright (c) https://gitee.com/motion-code All rights reserved.
*+------------------
* Author: Mr. April (405784684@qq.com)
*+------------------
*/
namespace {ns}\service\api\{module};
use {dao_ns}\{module}\{Model}Dao;
use {model_ns}\{module}\{Model};
use core\base\BaseService;
/**
* {Model}前台API服务类
*
* Class {Model}Service
*/
class {Model}Service extends BaseService
{
/**
* 注入{Model}Dao
*
* @param {Model}Dao $dao
*/
public function __construct({Model}Dao $dao)
{
$this->dao = $dao;
}
/**
* 获取{Model}Dao
*
* @return {Model}Dao
*/
public function getDao(): {Model}Dao
{
return $this->dao;
}
/**
* 获取列表
*
* @param array $where 查询条件
* @param string $field 查询字段
* @param int $page 页码
* @param int $limit 每页数量
* @param string $order 排序
* @return array
*/
public function selectList(array $where = [], string $field = '*', int $page = 1, int $limit = 20, string $order = 'id desc'): array
{
$list = $this->dao->selectList($where, $field, $page, $limit, $order);
return {Model}::formatList($list);
}
/**
* 获取总数
*
* @param array $where 查询条件
* @return int
*/
public function count(array $where = []): int
{
return $this->dao->count($where);
}
/**
* 获取详情
*
* @param int $id
* @param array $field
* @return array
*/
public function get(int $id, array $field = ['*']): array
{
$data = $this->dao->findOrFail($id, $field);
return {Model}::formatDetail($data);
}
/**
* 创建
*
* @param array $data
* @return {Model}
*/
public function save(array $data): {Model}
{
// 如果需要关联会员,自动设置会员ID
if ($this->hasMemberField() && isset($this->memberId)) {
$memberField = $this->getMemberField();
if ($memberField && !isset($data[$memberField])) {
$data[$memberField] = $this->memberId;
}
}
return $this->dao->create($data);
}
/**
* 更新
*
* @param int $id
* @param array $data
* @return bool
*/
public function update(int $id, array $data): bool
{
// 检查是否为会员自己的数据
if ($this->hasMemberField()) {
$memberField = $this->getMemberField();
$record = $this->dao->findWhere([$memberField => $this->memberId, 'id' => $id]);
if (!$record) {
throw new \Exception('无权修改此数据');
}
}
return $this->dao->update($id, $data);
}
/**
* 删除
*
* @param int $id
* @return bool
*/
public function delete(int $id): bool
{
// 检查是否为会员自己的数据
if ($this->hasMemberField()) {
$memberField = $this->getMemberField();
$record = $this->dao->findWhere([$memberField => $this->memberId, 'id' => $id]);
if (!$record) {
throw new \Exception('无权删除此数据');
}
}
return $this->dao->delete($id);
}
/**
* 获取会员ID
*
* @return int|null
*/
protected function getMemberId(): ?int
{
try {
$currentMember = \app\api\CurrentMember::create();
return $currentMember->id();
} catch (\Exception $e) {
return null;
}
}
/**
* 检查是否有会员关联字段
*
* @return bool
*/
protected function hasMemberField(): bool
{
return in_array('member_id', (new {Model}())->getFillable());
}
/**
* 获取会员关联字段名
*
* @return string|null
*/
protected function getMemberField(): ?string
{
return $this->hasMemberField() ? 'member_id' : null;
}
}
```
## Complete Example (App)
```php
<?php
declare(strict_types=1);
/**
*+------------------
* madong
*+------------------
* Copyright (c) https://gitee.com/motion-code All rights reserved.
*+------------------
* Author: Mr. April (405784684@qq.com)
*+------------------
*/
namespace app\service\api\question;
use app\dao\question\QuestionDao;
use app\model\question\Question;
use core\base\BaseService;
/**
* 问题前台API服务类
*
* Class QuestionService
*/
class QuestionService extends BaseService
{
public function __construct(QuestionDao $dao)
{
$this->dao = $dao;
}
public function selectList(array $where = [], string $field = '*', int $page = 1, int $limit = 20, string $order = 'id desc'): array
{
$list = $this->dao->selectList($where, $field, $page, $limit, $order);
return Question::formatList($list);
}
public function count(array $where = []): int
{
return $this->dao->count($where);
}
public function get(int $id, array $field = ['*']): array
{
$data = $this->dao->findOrFail($id, $field);
return Question::formatDetail($data);
}
public function save(array $data): Question
{
if (isset($this->memberId)) {
$data['member_id'] = $this->memberId;
}
return $this->dao->create($data);
}
public function update(int $id, array $data): bool
{
$record = $this->dao->findWhere(['member_id' => $this->memberId, 'id' => $id]);
if (!$record) {
throw new \Exception('无权修改此问题');
}
return $this->dao->update($id, $data);
}
public function delete(int $id): bool
{
$record = $this->dao->findWhere(['member_id' => $this->memberId, 'id' => $id]);
if (!$record) {
throw new \Exception('无权删除此问题');
}
return $this->dao->delete($id);
}
protected function getMemberId(): ?int
{
try {
return \app\api\CurrentMember::create()->id();
} catch (\Exception $e) {
return null;
}
}
}
```
## Complete Example (Plugin)
```php
<?php
declare(strict_types=1);
/**
*+------------------
* madong
*+------------------
* Copyright (c) https://gitee.com/motion-code All rights reserved.
*+------------------
* Author: Mr. April (405784684@qq.com)
*+------------------
*/
namespace plugin\official\app\service\api\question;
use plugin\official\app\dao\question\QuestionDao;
use plugin\official\app\model\question\Question;
use core\base\BaseService;
/**
* 问题前台API服务类
*
* Class QuestionService
*/
class QuestionService extends BaseService
{
public function __construct(QuestionDao $dao)
{
$this->dao = $dao;
}
// ... 方法同上
}
```
## Service Methods
### CRUD Operations
| Method | Description |
|--------|-------------|
| `selectList()` | Get paginated list |
| `count()` | Get total count |
| `get()` | Get single record |
| `save()` | Create new record (with member_id if applicable) |
| `update()` | Update existing record (with ownership check) |
| `delete()` | Delete record (with ownership check) |
### Member Support Methods
| Method | Description |
|--------|-------------|
| `getMemberId()` | Get current member ID |
| `hasMemberField()` | Check if model has member_id field |
| `getMemberField()` | Get member field name |
## Auto-generation Checklist
When generating API service:
- [ ] Set correct namespace based on target
- [ ] Import BaseService
- [ ] Import DAO
- [ ] Import Model class
- [ ] Inject DAO in constructor
- [ ] Implement standard CRUD methods
- [ ] Add member ownership check for update/delete
- [ ] Add getMemberId() helper method
@@ -0,0 +1,418 @@
---
name: backend-gen-controller
description: Generate CRUD controller for madong module. Supports both app (main project) and plugin targets. Creates controller extending Crud base class with OpenAPI attributes and standard REST endpoints.
globs:
- "app/**/controller/**/*.php"
- "plugin/**/app/**/controller/**/*.php"
---
# Step 4: Generate Admin Controller
Create CRUD controller for module.
## File Location
**App (主项目):**
```
app/adminapi/controller/{module}/{Model}Controller.php
```
**Plugin (插件):**
```
plugin/{plugin}/app/adminapi/controller/{module}/{Model}Controller.php
```
## Target Variables
| 变量 | App | Plugin |
|------|-----|--------|
| `{prefix}` | `` (空) | `plugin\{Plugin}` |
| `{ns}` | `app` | `plugin\{Plugin}\app` |
| `{service_ns}` | `app\service\admin` | `plugin\{Plugin}\app\service\admin` |
## Controller Template
```php
<?php
declare(strict_types=1);
/**
*+------------------
* madong
*+------------------
* Copyright (c) https://gitee.com/motion-code All rights reserved.
*+------------------
* Author: Mr. April (405784684@qq.com)
*+------------------
*/
namespace {ns}\adminapi\controller\{module};
use app\adminapi\controller\Crud;
use app\adminapi\middleware\AccessTokenMiddleware;
use app\adminapi\middleware\OperationMiddleware;
use app\adminapi\middleware\PermissionMiddleware;
use {service_ns}\{module}\{Model}Service;
use core\tool\Json;
use madong\swagger\attribute\Permission;
use madong\swagger\annotation\response\SimpleResponse;
use OpenApi\Attributes as OA;
use support\annotation\Middleware;
use support\Request;
use WebmanTech\Swagger\DTO\SchemaConstants;
#[Middleware(AccessTokenMiddleware::class, PermissionMiddleware::class, OperationMiddleware::class)]
final class {Model}Controller extends Crud
{
public function __construct({Model}Service $service)
{
$this->service = $service;
}
#[OA\Get(
path: '/{module}/{model}',
summary: '列表',
tags: ['{Module}-{Model}'],
parameters: [
new OA\Parameter(name: "page", description: "页码", in: "query", schema: new OA\Schema(type: "integer")),
new OA\Parameter(name: "limit", description: "每页数量", in: "query", schema: new OA\Schema(type: "integer")),
],
responses: [
new OA\Response(
response: 200,
description: "成功",
content: new OA\JsonContent(properties: [
new OA\Property(property: "code", type: "integer", example: 0),
new OA\Property(property: "msg", type: "string", example: "获取成功"),
new OA\Property(property: "data", properties: [
new OA\Property(property: "total", type: "integer", example: 10),
new OA\Property(property: "list", type: "array", items: new OA\Items()),
], type: "object"),
])
),
]
)]
#[Permission("{module}:{model}:list")]
public function index(Request $request): \support\Response
{
try {
[$where, $format, $limit, $field, $order, $page] = $this->selectInput($request);
$methods = [
'select' => 'formatSelect',
'tree' => 'formatTree',
'table_tree' => 'formatTableTree',
'normal' => 'formatNormal',
];
$format_function = $methods[$format] ?? 'formatNormal';
$total = $this->service->getCount($where);
$list = $this->service->selectList($where, $field, $page, $limit, $order, [], false);
return call_user_func([$this, $format_function], $list, $total);
} catch (\Throwable $e) {
return Json::fail($e->getMessage());
}
}
#[OA\Get(
path: '/{module}/{model}/{id}',
summary: '详情',
tags: ['{Module}-{Model}'],
responses: [
new OA\Response(
response: 200,
description: "成功",
content: new OA\JsonContent(properties: [
new OA\Property(property: "code", type: "integer", example: 0),
new OA\Property(property: "msg", type: "string", example: "获取成功"),
])
),
],
x: [
SchemaConstants::X_PROPERTY_IN => 'id',
]
)]
#[Permission("{module}:{model}:read")]
public function show(Request $request): \support\Response
{
return parent::show($request);
}
#[OA\Post(
path: '/{module}/{model}',
summary: '创建',
tags: ['{Module}-{Model}'],
responses: [
new OA\Response(
response: 200,
description: "成功",
content: new OA\JsonContent(properties: [
new OA\Property(property: "code", type: "integer", example: 0),
new OA\Property(property: "msg", type: "string", example: "创建成功"),
])
),
],
)]
#[Permission("{module}:{model}:create")]
#[SimpleResponse(example: '{"code":0,"msg":"created","id":1}')]
public function store(Request $request): \support\Response
{
return parent::store($request);
}
#[OA\Put(
path: '/{module}/{model}/{id}',
summary: '更新',
tags: ['{Module}-{Model}'],
parameters: [
new OA\Parameter(name: "id", description: "ID", in: "path", required: true, schema: new OA\Schema(type: "integer")),
],
responses: [
new OA\Response(
response: 200,
description: "成功",
content: new OA\JsonContent(properties: [
new OA\Property(property: "code", type: "integer", example: 0),
new OA\Property(property: "msg", type: "string", example: "更新成功"),
])
),
]
)]
#[Permission("{module}:{model}:update")]
public function update(Request $request): \support\Response
{
return parent::update($request);
}
#[OA\Post(
path: '/{module}/{model}/export',
summary: '导出',
tags: ['{Module}-{Model}']
)]
#[Permission("{module}:{model}:list")]
#[SimpleResponse(example: ['code' => 0, 'message' => 'success', 'data' => []])]
public function export(Request $request): \support\Response
{
return parent::export($request);
}
#[OA\Delete(
path: '/{module}/{model}/{id}',
summary: '删除',
tags: ['{Module}-{Model}'],
parameters: [
new OA\Parameter(name: "id", description: "ID", in: "path", required: true, schema: new OA\Schema(type: "integer")),
],
responses: [
new OA\Response(
response: 200,
description: "成功",
content: new OA\JsonContent(properties: [
new OA\Property(property: "code", type: "integer", example: 0),
new OA\Property(property: "msg", type: "string", example: "删除成功"),
])
),
],
x: [
SchemaConstants::X_PROPERTY_IN => 'id',
]
)]
#[Permission("{module}:{model}:delete")]
public function destroy(Request $request): \support\Response
{
return parent::destroy($request);
}
#[OA\Delete(
path: '/{module}/{model}',
summary: '批量删除',
tags: ['{Module}-{Model}'],
x: [SchemaConstants::X_SCHEMA_REQUEST => \app\schema\request\BatchDeleteRequest::class],
)]
#[Permission("{module}:{model}:delete")]
public function batchDelete(Request $request): \support\Response
{
return parent::destroy($request);
}
}
```
## Complete Example (App)
```php
<?php
declare(strict_types=1);
/**
*+------------------
* madong
*+------------------
* Copyright (c) https://gitee.com/motion-code All rights reserved.
*+------------------
* Author: Mr. April (405784684@qq.com)
*+------------------
*/
namespace app\adminapi\controller\member;
use app\adminapi\controller\Crud;
use app\adminapi\middleware\AccessTokenMiddleware;
use app\adminapi\middleware\OperationMiddleware;
use app\adminapi\middleware\PermissionMiddleware;
use app\service\admin\member\MemberService;
use core\tool\Json;
use madong\swagger\attribute\Permission;
use madong\swagger\annotation\response\SimpleResponse;
use OpenApi\Attributes as OA;
use support\annotation\Middleware;
use support\Request;
use WebmanTech\Swagger\DTO\SchemaConstants;
#[Middleware(AccessTokenMiddleware::class, PermissionMiddleware::class, OperationMiddleware::class)]
final class MemberController extends Crud
{
public function __construct(MemberService $service)
{
$this->service = $service;
}
#[OA\Get(
path: '/member/user',
summary: '会员用户列表',
tags: ['会员用户'],
parameters: [
new OA\Parameter(name: "username", description: "用户名", in: "query", schema: new OA\Schema(type: "string")),
new OA\Parameter(name: "nickname", description: "昵称", in: "query", schema: new OA\Schema(type: "string")),
new OA\Parameter(name: "page", description: "页码", in: "query", schema: new OA\Schema(type: "integer")),
new OA\Parameter(name: "limit", description: "每页数量", in: "query", schema: new OA\Schema(type: "integer")),
],
responses: [
new OA\Response(
response: 200,
description: "成功",
content: new OA\JsonContent(properties: [
new OA\Property(property: "code", type: "integer", example: 0),
new OA\Property(property: "msg", type: "string", example: "获取成功"),
])
),
]
)]
#[Permission("member:user:list")]
public function index(Request $request): \support\Response
{
try {
[$where, $format, $limit, $field, $order, $page] = $this->selectInput($request);
$methods = [
'select' => 'formatSelect',
'tree' => 'formatTree',
'table_tree' => 'formatTableTree',
'normal' => 'formatNormal',
];
$format_function = $methods[$format] ?? 'formatNormal';
$total = $this->service->getCount($where);
$list = $this->service->selectList($where, $field, $page, $limit, $order, ['level', 'tags'], false);
return call_user_func([$this, $format_function], $list, $total);
} catch (\Throwable $e) {
return Json::fail($e->getMessage());
}
}
#[OA\Get(
path: '/member/user/{id}',
summary: '会员用户详情',
tags: ['会员用户'],
responses: [
new OA\Response(response: 200, description: "成功"),
],
x: [SchemaConstants::X_PROPERTY_IN => 'id']
)]
#[Permission("member:user:read")]
public function show(Request $request): \support\Response
{
return parent::show($request);
}
#[OA\Post(path: '/member/user', summary: '创建', tags: ['会员用户'])]
#[Permission("member:user:create")]
#[SimpleResponse(example: '{"code":0,"msg":"created","id":1}')]
public function store(Request $request): \support\Response
{
return parent::store($request);
}
#[OA\Put(path: '/member/user/{id}', summary: '更新', tags: ['会员用户'])]
#[Permission("member:user:update")]
public function update(Request $request): \support\Response
{
return parent::update($request);
}
#[OA\Delete(path: '/member/user/{id}', summary: '删除', tags: ['会员用户'])]
#[Permission("member:user:delete")]
public function destroy(Request $request): \support\Response
{
return parent::destroy($request);
}
}
```
## API Path Convention
| Method | Path | Description |
|--------|------|-------------|
| GET | `/{module}/{model}` | List with pagination |
| GET | `/{module}/{model}/{id}` | Get single record |
| POST | `/{module}/{model}` | Create new record |
| POST | `/{module}/{model}/export` | Export records |
| PUT | `/{module}/{model}/{id}` | Update record |
| DELETE | `/{module}/{model}/{id}` | Delete single record |
| DELETE | `/{module}/{model}` | Batch delete records |
## Permission Codes
| Action | Permission | Description |
|--------|------------|-------------|
| List | `{module}:{model}:list` | View list |
| Export | `{module}:{model}:list` | Export records |
| View | `{module}:{model}:read` | View detail |
| Create | `{module}:{model}:create` | Create new |
| Update | `{module}:{model}:update` | Update existing |
| Delete | `{module}:{model}:delete` | Delete records |
## Middleware Stack
The controller uses three middleware:
1. **AccessTokenMiddleware**: Authentication
2. **PermissionMiddleware**: Permission check
3. **OperationMiddleware**: Operation logging
## Inherited Crud Methods
The Crud base controller provides:
| Method | Description |
|--------|-------------|
| `index()` | List with pagination |
| `export()` | Export records |
| `show()` | Get single record |
| `store()` | Create record |
| `update()` | Update record |
| `destroy()` | Delete record (single and batch) |
| `selectInput()` | Parse request parameters |
| `formatSelect()` | Format for select dropdown |
| `formatTree()` | Format for tree structure |
| `formatTableTree()` | Format for table tree |
| `formatNormal()` | Format for normal list |
## Auto-generation Checklist
When generating controller:
- [ ] Set correct namespace based on target
- [ ] Import required classes (Controller, Service, Middleware, Json, etc.)
- [ ] Add Middleware attribute with three middlewares
- [ ] Extend Crud base controller
- [ ] Inject Service in constructor
- [ ] Add OpenAPI attributes for each action
- [ ] Add Permission attribute for each action
- [ ] Use proper API path convention
+321
View File
@@ -0,0 +1,321 @@
---
name: backend-gen-crud
description: Generates complete CRUD module for both main project (app) and plugins. Uses target parameter (app/plugin) to determine paths. This skill orchestrates sub-skills to generate migrations, models, controllers, services, DAOs, validates, schemas, Vue frontend pages, and route configuration.
globs:
- "app/**/*.php"
- "app/**/resource/**/*.vue"
- "frontend/admin/src/apps/**/*.ts"
- "plugin/**/*.php"
- "plugin/**/resource/**/*.vue"
---
# madong CRUD Generator
Generate complete CRUD module with one command - from table definition to full backend and frontend implementation.
## Target: App vs Plugin
| 特性 | App (主项目) | Plugin (插件) |
|------|-------------|---------------|
| **命名空间前缀** | `app\{module}\...` | `plugin\{Plugin}\app\...` |
| **Model 基类** | `core\base\BaseModel` | `plugin\{Plugin}\app\model\BaseModel` |
| **Controller 基类** | `app\adminapi\controller\Crud` | `app\adminapi\controller\Crud` |
| **API Controller 基类** | `app\api\controller\Base` | `app\api\controller\Base` |
| **DAO 基类** | `core\base\BaseDao` | `core\base\BaseDao` |
| **Service 基类** | `core\base\BaseService` | `core\base\BaseService` |
| **Validate 基类** | `core\base\BaseValidate` | `core\base\BaseValidate` |
| **Migration 路径** | `resource/database/migrations/` | `resource/database/migrations/` |
| **前端路径** | `frontend/admin/src/views/{section}/{feature}/` 与 `frontend/admin/src/api/{section}/{feature}/` | `resource/template/admin/views/{module}/` |
| **多语言路径** | `resource/translations/` | `plugin/{plugin}/resource/translations/` |
## Input: Table Definition
```markdown
Module: Ask
Target: app # 或 plugin
Table: questions
Fields:
- id (primary key, snowflake)
- name (string, 100, required)
- category_id (foreign key -> categories)
- description (text, nullable)
- price (decimal 10,2, required)
- stock (integer, default 0)
- status (tinyint, default 1)
```
### 特殊标记
| 标记 | 说明 |
|------|------|
| `snowflake` | 使用雪花ID作为主键 (默认) |
| `no-api` | 不生成前台API |
| `public-create` | 允许公开创建(无需登录) |
---
## Output: Complete Module Structure
### App (主项目)
```
app/
├── adminapi/
│ ├── controller/{module}/{Model}Controller.php
│ └── validate/{module}/{Model}Validate.php
├── api/
│ ├── controller/{module}/{Model}Controller.php
│ └── validate/{module}/{Model}Validate.php
├── service/
│ ├── admin/{module}/{Model}Service.php
│ └── api/{module}/{Model}Service.php
├── dao/{module}/{Model}Dao.php
├── model/{module}/{Model}.php
├── schema/
│ ├── request/{module}/{Model}Request.php
│ └── response/{module}/{Model}Response.php
└── config/
└── route.php
resource/
├── database/
│ └── migrations/{timestamp}_create_{table_name}_table.php
└── translations/
├── zh_CN/{module}.php
└── en/{module}.php
frontend/admin/src/apps/{module}/
├── views/{model}/
│ ├── index.vue
│ ├── form.vue
│ └── schemas/index.tsx
├── api/{module}/index.ts
└── locales/lang/
├── zh-cn/{module}.json
└── en/{module}.json
```
### Plugin (插件)
```
plugin/{plugin}/
├── app/
│ ├── adminapi/
│ │ ├── controller/{module}/{Model}Controller.php
│ │ └── validate/{module}/{Model}Validate.php
│ ├── api/
│ │ ├── controller/{module}/{Model}Controller.php
│ │ └── validate/{module}/{Model}Validate.php
│ ├── service/
│ │ ├── admin/{module}/{Model}Service.php
│ │ └── api/{module}/{Model}Service.php
│ ├── dao/{module}/{Model}Dao.php
│ ├── model/{module}/{Model}.php
│ └── schema/
│ ├── request/{module}/{Model}Request.php
│ └── response/{module}/{Model}Response.php
├── config/
│ └── route.php
└── resource/
├── database/
│ └── migrations/{timestamp}_create_{table_name}_table.php
└── translations/
├── zh_CN/{module}.php
└── en/{module}.php
```
---
## Path Variables
根据 `target` 参数,路径变量替换如下:
| 变量 | App | Plugin |
|------|-----|--------|
| `{prefix}` | `` (空) | `plugin\{Plugin}` |
| `{base}` | `app` | `plugin/{plugin}/app` |
| `{ns}` | `app` | `plugin\{Plugin}\app` |
| `{model_ns}` | `app\model` | `plugin\{Plugin}\app\model` |
| `{dao_ns}` | `app\dao` | `plugin\{Plugin}\app\dao` |
| `{service_ns}` | `app\service\admin` | `plugin\{Plugin}\app\service\admin` |
| `{validate_ns}` | `app\adminapi\validate` | `plugin\{Plugin}\app\adminapi\validate` |
### 完整路径映射
| 文件类型 | App 路径 | Plugin 路径 |
|----------|----------|-------------|
| Model | `app/model/{module}/{Model}.php` | `plugin/{plugin}/app/model/{module}/{Model}.php` |
| Controller | `app/adminapi/controller/{module}/{Model}Controller.php` | `plugin/{plugin}/app/adminapi/controller/{module}/{Model}Controller.php` |
| API Controller | `app/api/controller/{module}/{Model}Controller.php` | `plugin/{plugin}/app/api/controller/{module}/{Model}Controller.php` |
| Validate | `app/adminapi/validate/{module}/{Model}Validate.php` | `plugin/{plugin}/app/adminapi/validate/{module}/{Model}Validate.php` |
| Service | `app/service/admin/{module}/{Model}Service.php` | `plugin/{plugin}/app/service/admin/{module}/{Model}Service.php` |
| API Service | `app/service/api/{module}/{Model}Service.php` | `plugin/{plugin}/app/service/api/{module}/{Model}Service.php` |
| DAO | `app/dao/{module}/{Model}Dao.php` | `plugin/{plugin}/app/dao/{module}/{Model}Dao.php` |
| Schema Request | `app/schema/request/{module}/{Model}Request.php` | `plugin/{plugin}/app/schema/request/{module}/{Model}Request.php` |
| Schema Response | `app/schema/response/{module}/{Model}Response.php` | `plugin/{plugin}/app/schema/response/{module}/{Model}Response.php` |
| Migration | `resource/database/migrations/*.php` | `resource/database/migrations/*.php` |
---
## Pre-checks (执行前)
- **Target 确认**: 用户未指定时,询问是主项目(app)还是插件(plugin)
- **Module 存在性**: 若目录不存在,需要创建目录结构
- **文件冲突**: 目标文件已存在时,先询问覆盖或增量修改
- **依赖检查**: 检查外键关联的模型是否存在
- **API 支持**: 询问是否需要生成前台 API
---
## Output Contract (输出契约)
- 必须生成所有步骤的完整产物(除非使用 `no-api` 标记)
- 所有路径、类名、命名空间与 `{Module}/{Model}/{table}` 一致
- 前端 API 路径与后端路由前缀保持一致
- 使用雪花ID作为主键。当前项目迁移优先采用 `bigInteger('id')->primary()`,除非相邻现有迁移明确使用 `unsigned()`。
- Validate 使用 Laravel Validation 规则
- Schema 使用 OpenAPI 属性定义
- DAO 继承 `core\base\BaseDao`,实现 `setModel()` 方法
- Service 继承 `core\base\BaseService`,注入 DAO
---
## Consistency Validation (生成后校验)
- Migration 主键使用 `bigInteger('id')->primary()` 支持雪花ID,并以当前项目相邻迁移写法为准
- Model `$fillable` 与 Migration 字段一致,使用 `SoftDeletes` trait
- Model `$casts` 正确设置字段类型转换
- Controller 的 `$this->service` 正确注入
- Controller 使用 `Crud` 基类,继承标准 CRUD 方法
- Validate 使用 `BaseValidate` 基类,定义 rules、message、scene
- DAO 的 `setModel()` 返回正确的模型类
- Service 注入对应 DAO,实现业务逻辑
- Schema 使用 `BaseRequestDTO`/`BaseResponseDTO`,添加 OpenAPI 属性
- Frontend API 路径与后端路由一致
- Route 配置正确注册 Swagger 扫描路径
---
## Fallback Rules (失败兜底)
- 输入缺失关键字段(如 table/fields)时必须先澄清
- 复杂关系或非标准业务逻辑需用户确认后再生成
- 外键关联的表不存在时应询问是否创建
---
## Workflow
### Step 1: Parse Table Definition → `01-parse-table`
Extract from user input:
- Target (app 或 plugin)
- Plugin name (snake_case) - 仅 plugin 模式
- Module name (PascalCase)
- Model name (PascalCase singular)
- Table name (snake_case)
- Fields with types, constraints, and relationships
- Special flags: `snowflake`, `no-api`, `public-create`
### Step 2: Generate Migration → `02-migration`
> **重要**: 主键必须使用 `bigInteger` 支持雪花ID。当前项目真实迁移通常使用 `bigInteger('id')->primary()`。
```php
$table->bigInteger('id')->primary();
```
### Step 3: Generate Model → `03-model`
> 使用 `core\base\BaseModel` 基类,`SoftDeletes` trait
### Step 4: Generate Validate → `04-validate`
> 使用 `core\base\BaseValidate` 基类
### Step 5: Generate DAO → `06-dao`
> 使用 `core\base\BaseDao` 基类,实现 `setModel()` 方法
### Step 6: Generate Service → `05-service`
> 使用 `core\base\BaseService` 基类,注入 DAO
### Step 7: Generate Controller → `04-controller`
> 继承 `app\adminapi\controller\Crud`,添加 OpenAPI 属性和权限注解
### Step 8: Generate Schema → `08-schema`
> 使用 `BaseRequestDTO`/`BaseResponseDTO`,添加 OpenAPI 属性
### Step 9: Generate Route → `09-route`
> 配置 Swagger 扫描路径
### Step 10: Generate Frontend → `07-frontend`
> 生成 Vue 页面和 API 调用代码
---
## API Controller Support (前台 API)
### 前台 API 特点
| 特点 | 说明 |
|------|------|
| 继承 Base | 继承 `app\api\controller\Base` |
| 公开访问 | 列表和详情使用 `#[AllowAnonymous]` |
| 会员操作 | 创建、更新、删除使用 `#[MemberAuthRequired]` |
| 数据隔离 | 支持会员数据隔离和权限检查 |
| 雪花ID | 使用雪花ID作为主键 |
### API 路由示例
```
GET /{api_prefix} # 公开 - 获取列表
GET /{api_prefix}/{id} # 公开 - 获取详情
POST /{api_prefix} # 会员 - 创建
PUT /{api_prefix}/{id} # 会员 - 更新
DELETE /{api_prefix}/{id} # 会员 - 删除
```
---
## Boundaries
- ✅ **Always**: Generate all files, follow madong conventions
- ✅ **Always**: Use bigInteger for primary key (snowflake ID support)
- ✅ **Always**: Use SoftDeletes for all models
- ✅ **Always**: Use unsignedInteger for timestamps
- ✅ **Always**: Use Laravel Validation rules in Validate
- ✅ **Always**: Use OpenAPI attributes in Controller and Schema
- ✅ **Always**: Use BaseDao with setModel() method
- ✅ **Always**: Inject DAO in Service constructor
- ✅ **Always**: Generate Schema for API documentation
- ✅ **Always**: Generate Route with Swagger configuration
- ✅ **Always**: Respect target parameter (app/plugin)
- ⚠️ **Ask first**: Complex relationships, pivot tables, custom business logic
- ⚠️ **Ask first**: Whether API support is needed
- 🚫 **Never**: Skip validation, ignore foreign key constraints
- 🚫 **Never**: Use Laravel default id() method for primary key
---
## Quick Commands
| Command | Description |
|---------|------------|
| `create CRUD for questions` | Create complete question CRUD (prompt for target) |
| `create CRUD for questions app` | Create CRUD for main project |
| `create CRUD for questions plugin` | Create CRUD for plugin |
| `create CRUD for questions no-api` | Create CRUD without API frontend |
| `add fields to questions` | Add fields to existing question table |
| `generate model for questions` | Generate model only |
| `generate dao for questions` | Generate DAO only |
| `generate service for questions` | Generate service only |
| `generate controller for questions` | Generate controller only |
| `generate validate for questions` | Generate validate only |
| `generate schema for questions` | Generate schema only |
| `generate route for questions` | Generate/update route only |
+133
View File
@@ -0,0 +1,133 @@
---
name: backend-gen-dao
description: Generate DAO class. Only needs setModel() and optional getKeywordFields(). BaseDao provides all CRUD methods.
globs:
- "app/dao/**/*.php"
- "plugin/**/app/dao/**/*.php"
---
# DAO 生成规范
## 原则
**DAO 只需定义 `setModel()`,其他方法全部使用 BaseDao 基类!**
当前项目真实约定补充:
- 必须显式导入 `use core\base\BaseDao;`,不能只写 `extends BaseDao`。
- `getKeywordFields()` 优先定义为 `protected`。
- 如无必要,不要重写 `selectList()`。
- 如确需重写 `selectList()` 以补 `with` 或默认排序,方法签名必须与当前项目 `BaseDao` 保持一致。
BaseDao 提供的方法:
- `selectList()` - 分页查询
- `get()` / `getOne()` - 获取单条
- `save()` / `saveAll()` - 创建/批量创建
- `update()` / `batchUpdate()` - 更新/批量更新
- `delete()` / `destroy()` - 删除
- `count()` / `getCount()` - 计数
- `value()` / `getColumn()` - 获取字段值
- `search()` - 搜索
- `sum()` / `getMax()` / `getMin()` - 统计
- `transaction()` - 事务
## 文件位置
**App:**
```
app/dao/{module}/{Model}Dao.php
```
**Plugin:**
```
plugin/{Plugin}/app/dao/{module}/{Model}Dao.php
```
## DAO 模板
```php
<?php
declare(strict_types=1);
/**
*+------------------
* madong
*+------------------
* Copyright (c) https://gitee.com/motion-code All rights reserved.
*+------------------
* Author: Mr. April (405784684@qq.com)
*+------------------
*/
namespace {ns}\dao\{module};
use {model_ns}\{module}\{Model};
use core\base\BaseDao;
/**
* {Model}数据访问层
*/
class {Model}Dao extends BaseDao
{
protected function setModel(): string
{
return {Model}::class;
}
}
```
### 重写 `selectList()` 的正确方式
仅在需要追加固定关联加载或默认排序时重写,例如:
```php
public function selectList(
array $where,
string|array $field = '*',
int $page = 0,
int $limit = 0,
string $order = '',
array $with = [],
bool $search = false,
?array $withoutScopes = null
): ?Collection {
$with = array_unique(array_merge($with, ['relationA', 'relationB']));
$order = $order ?: 'sort asc, id desc';
return parent::selectList($where, $field, $page, $limit, $order, $with, $search, $withoutScopes);
}
```
不要写成旧签名,例如缺少 `string|array $field`、`$search`、`$withoutScopes` 等参数,否则容易在运行时与项目当前基类不兼容。
## 可选配置
### 关键词搜索字段
```php
protected function getKeywordFields(): array
{
return ['name', 'title'];
}
```
## 调用示例
```php
// 注入 Service 后,Service 通过 __call 自动代理到 DAO
$list = $this->dao->selectList($where, '*', $page, $limit);
$item = $this->dao->get($id);
$count = $this->dao->count($where);
$this->dao->save($data);
$this->dao->update($id, $data);
$this->dao->delete($id);
```
## 生成检查清单
- [ ] 命名空间正确(App/Plugin)
- [ ] 引入 BaseDao
- [ ] 引入 Model 类
- [ ] 实现 `setModel()` 返回模型类名
- [ ] 可选:定义 `getKeywordFields()` 搜索字段
- [ ] 若重写 `selectList()`,签名与当前项目 `BaseDao` 完全一致
+223
View File
@@ -0,0 +1,223 @@
---
name: backend-gen-event
description: 生成事件类(Event),支持事件触发、可修改事件扩展
globs:
- "app/**/event/**/*.php"
- "plugin/**/app/**/event/**/*.php"
---
# 事件 (Event) 生成规范
## 目录结构
```
{module}/event/
└── {Name}Event.php # 事件类文件
# {module}: adminapi / api / plugin\{Plugin}\app\adminapi
# {Name}: 事件名称,如 LoginLog, PointsChanged
```
## 事件命名规范
| 场景 | 命名示例 | 触发事件名 |
|------|----------|------------|
| 登录日志 | `LoginLogEvent` | `adminapi.login.log` |
| 操作日志 | `OperationLogEvent` | `adminapi.operation.log` |
| 积分变动 | `PointsChangedEvent` | `adminapi.points.changed` |
| 会员等级更新 | `MemberLevelUpdatedEvent` | `adminapi.member.level.updated` |
| 审核状态 | `ReviewApprovedEvent` | `adminapi.review.approved` |
## 事件触发事件名规范
- 格式:`{app}.{module}.{action}`
- AdminAPI: `adminapi.{module}.{action}`
- API: `member.{module}.{action}`
## 事件类模板
```php
<?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 {namespace};
use Webman\Event\Event;
/**
* {事件描述}
*/
class {Name}Event
{
/**
* 会员ID
*/
public int|string $memberId;
/**
* 变动前积分
*/
public int $oldPoints;
/**
* 变动后积分
*/
public int $newPoints;
/**
* 变动的积分数量
*/
public int $points;
/**
* 变动类型(增加/减少)
*/
public {EnumClass} $type;
/**
* 变动原因/备注
*/
public string $remark;
/**
* 关联ID(如订单ID、活动ID等,可选)
*/
public int|string|null $relatedId;
/**
* 额外数据(可选)
*/
public ?array $extra;
/**
* 构造函数
*/
public function __construct(
int|string $memberId,
int $points,
{EnumClass} $type,
int $oldPoints,
int $newPoints,
string $remark = '',
int|string|null $relatedId = null,
?array $extra = null
) {
$this->memberId = $memberId;
$this->points = $points;
$this->type = $type;
$this->oldPoints = $oldPoints;
$this->newPoints = $newPoints;
$this->remark = $remark;
$this->relatedId = $relatedId;
$this->extra = $extra;
}
/**
* 触发事件
*/
public function dispatch(): void
{
Event::emit('{app}.{module}.{action}', $this);
}
}
```
## 属性类型说明
| 类型 | 说明 |
|------|------|
| `int\|string` | ID 类字段 |
| `int` | 数量、状态码 |
| `string` | 名称、备注 |
| `array` | 扩展数据 |
| `{EnumClass}` | 枚举类型(推荐使用) |
| `?array` | 可选数组 |
## 事件触发方式
### 方式一:事件内部触发(推荐)
```php
// 在事件类中
public function dispatch(): void
{
Event::emit('adminapi.points.changed', $this);
}
// 调用时
$event = new PointsChangedEvent(...);
$event->dispatch();
```
### 方式二:直接触发
```php
Event::emit('adminapi.points.changed', new PointsChangedEvent(...));
```
## 可修改的事件(用于扩展)
如果事件需要在监听器中修改数据,使用引用属性:
```php
class MemberInfoFetchedEvent
{
public int $memberId;
public array $userData = []; // 可修改
public array $permissions = []; // 可追加
public array $extra = []; // 可追加
// 添加权限方法
public function addPermission(string $permission): void
{
if (!in_array($permission, $this->permissions)) {
$this->permissions[] = $permission;
}
}
// 添加额外字段
public function addExtra(string $key, mixed $value): void
{
$this->extra[$key] = $value;
}
}
```
## 路径变量
| 变量 | App | Plugin |
|------|-----|--------|
| `{namespace}` | `app\adminapi\event` | `plugin\{Plugin}\app\adminapi\event` |
| `{app}` | `adminapi` / `api` | `adminapi` / `api` |
| `{module}` | 业务模块 | 业务模块 |
| `{action}` | 操作名称 | 操作名称 |
## 生成示例
### 积分变动事件
```
模块: adminapi
事件名: PointsChangedEvent
```
生成文件: `app/adminapi/event/PointsChangedEvent.php`
### 登录日志事件
```
模块: adminapi
事件名: LoginLogEvent
```
生成文件: `app/adminapi/event/LoginLogEvent.php`
@@ -0,0 +1,259 @@
---
name: backend-gen-listener
description: 生成监听器类(Listener),处理事件并注册到 config/event.php
globs:
- "app/**/listener/**/*.php"
- "plugin/**/app/**/listener/**/*.php"
---
# 监听器 (Listener) 生成规范
## 目录结构
```
{module}/listener/
└── {Name}Listener.php # 监听器类文件
# {module}: adminapi / api
# {Name}: 监听名称,如 LoginLog, PointsChanged
```
## 监听器命名规范
| 事件 | 监听器 | 触发事件名 |
|------|--------|------------|
| `LoginLogEvent` | `LoginLogListener` | `adminapi.login.log` |
| `OperationLogEvent` | `OperationLogListener` | `adminapi.operation.log` |
| `PointsChangedEvent` | `PointsChangedListener` | `adminapi.points.changed` |
| `MemberLevelUpdatedEvent` | `MemberLevelUpdatedListener` | `adminapi.member.level.updated` |
| `ReviewApprovedEvent` | `ReviewApprovedListener` | `adminapi.review.approved` |
## 监听器模板
```php
<?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 {namespace};
use app\adminapi\event\{Name}Event;
use app\dao\{domain}\{Name}Dao;
use support\Container;
/**
* {监听描述}
*/
class {Name}Listener
{
/**
* 处理{事件名}事件
*
* @param {Name}Event $event
* @return void
* @throws \Exception
*/
public function handle({Name}Event $event): void
{
// 业务逻辑处理
$this->process($event);
}
/**
* 处理业务逻辑
*
* @param {Name}Event $event
* @return void
* @throws \Exception
*/
private function process({Name}Event $event): void
{
/** @var {Name}Dao $dao */
$dao = Container::make({Name}Dao::class);
$dao->save([
'field1' => $event->field1,
'field2' => $event->field2,
// ...
]);
}
}
```
## DAO 调用方式
使用 `Container::make()` 获取 DAO 实例:
```php
/** @var LoginLogDao $dao */
$dao = Container::make(LoginLogDao::class);
$dao->save([...]);
```
## 监听器注册
监听器需要在 `config/event.php` 中注册:
```php
return [
// 事件名 => [[监听器类, 方法名], ...]
'adminapi.login.log' => [
[\app\adminapi\listener\LoginLogListener::class, 'handle'],
],
// 支持多个监听器
'adminapi.points.changed' => [
[\app\adminapi\listener\PointsChangedListener::class, 'handle'],
// 可以添加更多监听器
],
];
```
## 复杂监听器示例
处理积分变动并触发其他事件:
```php
<?php
declare(strict_types=1);
namespace app\adminapi\listener;
use app\adminapi\event\PointsChangedEvent;
use app\adminapi\event\MemberLevelUpdatedEvent;
use app\dao\member\MemberDao;
use app\dao\member\MemberLevelDao;
use app\dao\member\MemberPointsDao;
use app\enum\member\PointType;
use support\Container;
/**
* 积分变动监听器
*
* 处理积分变动的事件:
* 1. 更新会员积分
* 2. 记录积分流水
* 3. 触发会员等级更新事件
*/
class PointsChangedListener
{
/**
* 处理积分变动事件
*/
public function handle(PointsChangedEvent $event): void
{
// 更新会员积分
$this->updateMemberPoints($event);
// 记录积分流水
$this->recordPointsLog($event);
// 触发会员等级更新事件
$this->updateMemberLevel($event);
}
/**
* 更新会员积分
*/
private function updateMemberPoints(PointsChangedEvent $event): void
{
/** @var MemberDao $dao */
$dao = Container::make(MemberDao::class);
$member = $dao->get($event->memberId);
if (!$member) {
return;
}
$member->points = $event->newPoints;
$member->save();
}
/**
* 记录积分流水
*/
private function recordPointsLog(PointsChangedEvent $event): void
{
/** @var MemberPointsDao $dao */
$dao = Container::make(MemberPointsDao::class);
$dao->save([
'member_id' => $event->memberId,
'type' => $event->type->value,
'points' => $event->points,
'balance' => $event->newPoints,
'remark' => $event->remark,
'related_id' => $event->relatedId
]);
}
/**
* 更新会员等级
*/
private function updateMemberLevel(PointsChangedEvent $event): void
{
// ... 等级更新逻辑
// 触发新事件
$levelEvent = new MemberLevelUpdatedEvent(...);
$levelEvent->dispatch();
}
}
```
## 异常处理
监听器中的异常应该被捕获,不影响主流程:
```php
public function handle(XXXEvent $event): void
{
try {
// 业务逻辑
} catch (\Exception $e) {
// 记录日志但不影响主流程
error_log('XXXListener error: ' . $e->getMessage());
}
}
```
## 路径变量
| 变量 | App | Plugin |
|------|-----|--------|
| `{namespace}` | `app\adminapi\listener` | `plugin\{Plugin}\app\adminapi\listener` |
## 生成示例
### 登录日志监听器
```
事件: LoginLogEvent
```
生成文件: `app/adminapi/listener/LoginLogListener.php`
### 积分变动监听器
```
事件: PointsChangedEvent
```
生成文件: `app/adminapi/listener/PointsChangedListener.php`
## 注意事项
1. **命名对应**:监听器名称与事件名称对应
2. **handle 方法**:监听器必须包含 `handle` 方法
3. **事件类型**:handle 方法参数必须指定具体的事件类
4. **异常处理**:建议捕获异常,避免影响主流程
5. **事件注册**:监听器需要在 `config/event.php` 中注册才能生效
@@ -0,0 +1,417 @@
---
name: backend-gen-migrate
description: Generate database migration file for madong module. Supports both app (main project) and plugin targets. Creates migration with proper field types, soft deletes, and indexes following madong conventions. Primary key uses bigInteger for snowflake ID support.
---
# Step 2: Generate Migration
Create database migration file for module.
## File Location
**App (主项目):**
```
resource/database/migrations/{timestamp}_create_{table_name}_table.php
```
**Plugin (插件):**
```
plugin/{plugin}/resource/database/migrations/{timestamp}_create_{table_name}_table.php
```
## 单独执行指定迁移
当前项目命令位于 `backend/` 目录,支持通过第二个位置参数传入迁移文件名,只执行指定的待迁移文件:
```powershell
# 先预览指定迁移,不实际执行
php webman madong-migrate up {migration_file_name} --dry-run
# 单独执行指定迁移(可带不带 .php 后缀)
php webman madong-migrate up {migration_file_name}
# 查看执行状态
php webman madong-migrate status
```
示例:
```powershell
php webman madong-migrate up 2026_08_24_000001_create_system_feedback_table --dry-run
php webman madong-migrate up 2026_08_24_000001_create_system_feedback_table
```
说明:
- `{migration_file_name}` 是 `resource/database/migrations/` 下的迁移文件名,不需要填写完整路径。
- 指定迁移已经执行时,命令会提示 `Migration already executed`,不会重复执行。
- 指定文件不存在时,命令会提示 `Migration file not found`。
- 不传迁移名时,`up` 仍会按文件名顺序执行全部待迁移文件。
- 当前迁移命令已避免对 MySQL DDL 包裹外层事务,防止建表隐式提交后出现 `There is no active transaction`。
### 插件迁移的单文件执行
`madong-migrate` 仅搜索主项目的 `resource/database/migrations/`,不能传入 `plugin/{plugin}/resource/database/migrations/` 的路径或文件名。因此,插件新增迁移需要通过插件安装/升级流程执行;紧急单独执行某一个已确认安全的插件迁移时,可在 `backend/` 目录使用以下命令:
```powershell
php -r "require 'vendor/autoload.php'; require 'support/bootstrap.php'; `$schema = support\Db::connection()->getSchemaBuilder(); `$migration = require 'plugin/produce/resource/database/migrations/{migration_file_name}.php'; `$migration->up(`$schema);"
```
执行后应验证目标列或表,例如:
```powershell
php -r "require 'vendor/autoload.php'; require 'support/bootstrap.php'; echo support\Db::connection()->getSchemaBuilder()->hasColumn('produce_repair_record', 'images') ? 'images 字段已存在' . PHP_EOL : 'images 字段不存在' . PHP_EOL;"
```
注意:此紧急方式不会写入插件迁移日志;常规发布仍应通过插件安装或升级流程,以维护完整迁移记录。
## Table Naming Convention
| Target | Table Name | Example |
|--------|------------|---------|
| App | `{prefix}_{module}_{models}` | `ask_questions` |
| Plugin | `{prefix}_{plugin}_{module}_{models}` | `official_ask_questions` |
## Key Features
- **Snowflake ID**: Primary key uses `bigInteger` instead of `id()` for snowflake ID support
- **Soft Deletes**: Always include `deleted_at` field
- **Timestamps**: Use `unsignedInteger` for created_at and updated_at
- **Table Prefix**: Respect system table prefix configuration
## Migration Template (Snowflake ID)
```php
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use support\Db;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
$tablePrefix = config('admin.database.table_prefix');
$table = $tablePrefix . '{table_name}';
// 创建表
if (!Db::schema()->hasTable($table)) {
Db::schema()->create($table, function (Blueprint $table) use ($table) {
$table->comment = '{table_comment}';
// 主键 - 使用 bigInteger 支持雪花ID
$table->bigInteger('id')->unsigned()->primary();
{fields}
// 软删除
$table->unsignedInteger('deleted_at')->nullable()->comment('删除时间');
// 时间戳
$table->unsignedInteger('created_at')->comment('创建时间');
$table->unsignedInteger('updated_at')->comment('更新时间');
// 索引
{indexes}
});
}
}
/**
* Reverse the migrations.
*/
public function down(): void
{
$tablePrefix = config('admin.database.table_prefix');
$table = $tablePrefix . '{table_name}';
if (Db::schema()->hasTable($table)) {
Db::schema()->dropIfExists($table);
}
}
};
```
## Field Types
### String Field
```php
$table->string('name', 100)->comment('名称');
$table->string('email', 255)->unique()->comment('邮箱');
$table->string('cover', 255)->nullable()->comment('封面图');
```
### Text Field
```php
$table->text('description')->nullable()->comment('描述');
$table->text('content')->comment('内容');
```
### Integer Fields
```php
$table->unsignedInteger('sort')->default(0)->comment('排序');
$table->unsignedInteger('views')->default(0)->comment('浏览量');
$table->unsignedBigInteger('category_id')->comment('分类ID');
```
### Decimal Field
```php
$table->decimal('price', 10, 2)->default(0)->comment('价格');
$table->decimal('balance', 10, 2)->default(0)->comment('余额');
```
### Boolean Field
```php
$table->boolean('is_recommend')->default(false)->comment('是否推荐');
$table->boolean('is_hot')->default(false)->comment('是否热门');
```
### JSON Field
```php
$table->json('images')->nullable()->comment('图片列表');
$table->json('extra')->nullable()->comment('扩展数据');
```
### Enum Field (using tinyInteger)
```php
$table->tinyInteger('status')->default(1)->comment('状态: 1正常 2禁用');
$table->tinyInteger('type')->default(1)->comment('类型: 1文本 2图片 3视频');
```
### Foreign Key
```php
$table->unsignedBigInteger('category_id')->comment('分类ID');
$table->unsignedBigInteger('user_id')->comment('用户ID');
$table->unsignedBigInteger('parent_id')->nullable()->comment('父级ID');
// 外键约束(可选)
$table->foreign('category_id')->references('id')->on($tablePrefix . 'categories')->onDelete('cascade');
```
### Member ID (for API tables)
```php
$table->unsignedBigInteger('member_id')->comment('会员ID');
$table->index('member_id', '{table}_member_id_index');
```
## Index Types
### Single Index
```php
$table->index('status', '{table}_status_index');
$table->index('category_id', '{table}_category_id_index');
$table->index(['status', 'created_at'], '{table}_status_created_at_index');
```
### Unique Index
```php
$table->unique('name', '{table}_name_unique');
$table->unique(['user_id', 'type'], '{table}_user_type_unique');
```
## Complete Migration Example (App)
```php
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use support\Db;
return new class extends Migration
{
public function up(): void
{
$tablePrefix = config('admin.database.table_prefix');
$table = $tablePrefix . 'ask_questions';
if (!Db::schema()->hasTable($table)) {
Db::schema()->create($table, function (Blueprint $table) use ($table) {
$table->comment = '问答-问题表';
// 主键 - 使用 bigInteger 支持雪花ID
$table->bigInteger('id')->unsigned()->primary();
// 分类ID
$table->unsignedBigInteger('category_id')->comment('分类ID');
$table->index('category_id', 'ask_questions_category_id_index');
// 标题
$table->string('title', 200)->comment('标题');
// 内容
$table->text('content')->comment('内容');
// 价格
$table->decimal('price', 10, 2)->default(0)->comment('价格');
// 浏览量
$table->unsignedInteger('view_count')->default(0)->comment('浏览量');
// 状态
$table->tinyInteger('status')->default(1)->comment('状态: 1正常 2禁用');
$table->index('status', 'ask_questions_status_index');
// 软删除
$table->unsignedInteger('deleted_at')->nullable()->comment('删除时间');
// 时间戳
$table->unsignedInteger('created_at')->comment('创建时间');
$table->unsignedInteger('updated_at')->comment('更新时间');
});
}
}
public function down(): void
{
$tablePrefix = config('admin.database.table_prefix');
$table = $tablePrefix . 'ask_questions';
if (Db::schema()->hasTable($table)) {
Db::schema()->dropIfExists($table);
}
}
};
```
## Complete Migration Example (Plugin)
```php
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use support\Db;
return new class extends Migration
{
public function up(): void
{
$tablePrefix = config('admin.database.table_prefix');
$table = $tablePrefix . 'official_ask_questions';
if (!Db::schema()->hasTable($table)) {
Db::schema()->create($table, function (Blueprint $table) use ($table) {
$table->comment = '问答-问题表';
// 主键 - 使用 bigInteger 支持雪花ID
$table->bigInteger('id')->unsigned()->primary();
// 分类ID
$table->unsignedBigInteger('category_id')->comment('分类ID');
$table->index('category_id', 'official_ask_questions_category_id_index');
// 会员ID (可选,用于API数据隔离)
$table->unsignedBigInteger('member_id')->nullable()->comment('会员ID');
$table->index('member_id', 'official_ask_questions_member_id_index');
// 标题
$table->string('title', 200)->comment('标题');
// 内容
$table->text('content')->comment('内容');
// 价格
$table->decimal('price', 10, 2)->default(0)->comment('价格');
// 浏览量
$table->unsignedInteger('view_count')->default(0)->comment('浏览量');
// 状态
$table->tinyInteger('status')->default(1)->comment('状态: 1正常 2禁用');
$table->index('status', 'official_ask_questions_status_index');
// 软删除
$table->unsignedInteger('deleted_at')->nullable()->comment('删除时间');
// 时间戳
$table->unsignedInteger('created_at')->comment('创建时间');
$table->unsignedInteger('updated_at')->comment('更新时间');
});
}
}
public function down(): void
{
$tablePrefix = config('admin.database.table_prefix');
$table = $tablePrefix . 'official_ask_questions';
if (Db::schema()->hasTable($table)) {
Db::schema()->dropIfExists($table);
}
}
};
```
## Field Type Mapping
| User Input | Migration Method | Notes |
|------------|------------------|-------|
| string(n) | `string('name', n)` | VARCHAR |
| text | `text('name')` | TEXT |
| integer | `unsignedInteger('name')` | INT UNSIGNED |
| bigint | `unsignedBigInteger('name')` | BIGINT UNSIGNED |
| decimal(m,n) | `decimal('name', m, n)` | DECIMAL |
| tinyint | `tinyInteger('name')` | TINYINT |
| boolean | `boolean('name')` | TINYINT(1) |
| json | `json('name')` | JSON |
| date | `date('name')` | DATE |
| datetime | `dateTime('name')` | DATETIME |
| foreign_key | `unsignedBigInteger('name')` | BIGINT UNSIGNED |
## Naming Convention
| Item | App Convention | Plugin Convention |
|------|---------------|-------------------|
| Table | `{prefix}_{module}_{models}` | `{prefix}_{plugin}_{module}_{models}` |
| Index | `{table}_{column}_index` | `{table}_{column}_index` |
| Foreign Key | (auto) | (auto) |
## Auto-generation Checklist
When generating migration:
- [ ] Use `bigInteger('id')->unsigned()->primary()` instead of `id()`
- [ ] Check table prefix from config
- [ ] Generate timestamp prefix
- [ ] Use correct table name based on target (app/plugin)
- [ ] Add all fields with proper types
- [ ] Add index for foreign keys
- [ ] Add index for status field
- [ ] Add member_id field for API tables
- [ ] Add softDeletes field (deleted_at)
- [ ] Add created_at and updated_at fields
- [ ] Add table comment
## Common Issues
### 1. Duplicate Table
Always check with `hasTable()` before creating:
```php
if (!Db::schema()->hasTable($table)) {
// create table
}
```
### 2. Index Naming
Keep index names short but descriptive:
```php
// Good
$table->index('category_id', 'questions_category_id_index');
// Bad
$table->index('category_id', 'idx_questions_category_id');
```
### 3. Foreign Key Constraint
Only add foreign key constraints if needed:
```php
// Optional - adds CASCADE delete
$table->foreign('category_id')
->references('id')
->on($tablePrefix . 'categories')
->onDelete('cascade');
```
+474
View File
@@ -0,0 +1,474 @@
---
name: backend-gen-model
description: Generate Eloquent model for madong module. Supports both app (main project) and plugin targets. Creates model with SoftDeletes, relationships, and standard CRUD methods.
globs:
- "app/model/**/*.php"
- "plugin/**/app/model/**/*.php"
---
# Step 3: Generate Model
Create Eloquent model file for module.
## File Location
**App (主项目):**
```
app/model/{module}/{Model}.php
```
**Plugin (插件):**
```
plugin/{plugin}/app/model/{Model}.php
```
> 本项目插件模型采用扁平目录;仅在已有插件明确使用分模块目录时,才使用 `app/model/{module}/{Model}.php`。
## Target Variables
| 变量 | App | Plugin |
|------|-----|--------|
| `{prefix}` | `` (空) | `plugin\{Plugin}` |
| `{ns}` | `app` | `plugin\{Plugin}\app` |
| `{base_model}` | `core\base\BaseModel` | `plugin\{Plugin}\app\model\BaseModel` |
## Model Template
```php
<?php
declare(strict_types=1);
/**
*+------------------
* madong
*+------------------
* Copyright (c) https://gitee.com/motion-code All rights reserved.
*+------------------
* Author: Mr. April (405784684@qq.com)
*+------------------
*/
namespace {ns}\model\{module};
use {base_model};
use {ns}\model\{module}\{RelatedModel};
use Illuminate\Database\Eloquent\SoftDeletes;
/**
* {Model}模型类
*
* Class {Model}
*/
class {Model} extends BaseModel
{
/**
* 启用软删除
*/
use SoftDeletes;
/**
* 数据表名称
*/
protected $table = '{table_name}';
/**
* 数据表主键
*/
protected $primaryKey = 'id';
/**
* 是否自增
*/
public $incrementing = true;
/**
* 可批量赋值的字段
*/
protected $fillable = [
{fillable}
];
/**
* 隐藏字段
*/
protected $hidden = [
{hidden}
];
/**
* 类型转换
*/
protected $casts = [
{casts}
];
/**
* 追加字段
*/
protected $appends = [
{appends}
];
/**
* 获取{related}关联
*/
public function {related}(): \Illuminate\Database\Eloquent\Relations\{RelationType}
{
return $this->{relationMethod}({RelatedModel}::class, '{foreign_key}', '{owner_key}');
}
/**
* 设置密码(自动hash)
*/
public function setPasswordAttribute($value): void
{
$this->attributes['password'] = password_hash($value, PASSWORD_DEFAULT);
}
/**
* 验证密码
*/
public function verifyPassword($password): bool
{
return password_verify($password, $this->password);
}
}
```
## Complete Example (App)
```php
<?php
declare(strict_types=1);
/**
*+------------------
* madong
*+------------------
* Copyright (c) https://gitee.com/motion-code All rights reserved.
*+------------------
* Author: Mr. April (405784684@qq.com)
*+------------------
*/
namespace app\model\member;
use app\enum\common\EnabledStatus;
use app\enum\system\Sex;
use core\base\BaseModel;
use Illuminate\Database\Eloquent\SoftDeletes;
/**
* 会员模型
*
* Class Member
*/
class Member extends BaseModel
{
/**
* 启用软删除
*/
use SoftDeletes;
/**
* 数据表名称
*/
protected $table = 'member';
/**
* 数据表主键
*/
protected $primaryKey = 'id';
protected static function booted(): void
{
// Custom boot logic
}
/**
* 可批量赋值的字段
*/
protected $fillable = [
'id',
'username',
'email',
'phone',
'password',
'nickname',
'avatar',
'level_id',
'points',
'balance',
'gender',
'birthday',
'last_login_time',
'last_login_ip',
'login_count',
'enabled',
'created_at',
'updated_at',
'deleted_at',
'bio'
];
/**
* 隐藏字段
*/
protected $hidden = [
'password',
];
/**
* 追加字段
*/
protected $appends = [
'gender_text',
'enabled_text',
];
/**
* 获取性别文本
*/
public function getGenderTextAttribute(): string
{
return Sex::tryFrom($this->gender)?->label() ?? '未知';
}
/**
* 获取状态文本
*/
public function getEnabledTextAttribute(): string
{
return EnabledStatus::tryFrom($this->enabled)?->label() ?? '未知';
}
/**
* 关联会员等级
*/
public function level(): \Illuminate\Database\Eloquent\Relations\BelongsTo
{
return $this->belongsTo(MemberLevel::class, 'level_id', 'id');
}
/**
* 关联会员标签
*/
public function tags(): \Illuminate\Database\Eloquent\Relations\BelongsToMany
{
return $this->belongsToMany(MemberTag::class, MemberTagRelation::class, 'member_id', 'tag_id');
}
/**
* 关联签到记录
*/
public function signRecords(): \Illuminate\Database\Eloquent\Relations\HasMany
{
return $this->hasMany(MemberSign::class, 'member_id', 'id');
}
/**
* 设置密码
*/
public function setPasswordAttribute($value): void
{
$this->attributes['password'] = password_hash($value, PASSWORD_DEFAULT);
}
/**
* 验证密码
*/
public function verifyPassword($password): bool
{
return password_verify($password, $this->password);
}
}
```
## Complete Example (Plugin)
```php
<?php
declare(strict_types=1);
/**
*+------------------
* madong
*+------------------
* Copyright (c) https://gitee.com/motion-code All rights reserved.
*+------------------
* Author: Mr. April (405784684@qq.com)
*+------------------
*/
namespace plugin\official\app\model\question;
use plugin\official\app\model\BaseModel;
use Illuminate\Database\Eloquent\SoftDeletes;
/**
* 问题模型
*
* Class Question
*/
class Question extends BaseModel
{
use SoftDeletes;
protected $table = 'official_ask_questions';
protected $primaryKey = 'id';
public $incrementing = true;
protected $fillable = [
'category_id',
'title',
'content',
'price',
'view_count',
'status',
];
protected $casts = [
'category_id' => 'integer',
'price' => 'decimal:2',
'view_count' => 'integer',
'status' => 'integer',
'created_at' => 'integer',
'updated_at' => 'integer',
'deleted_at' => 'integer',
];
/**
* 关联分类
*/
public function category(): \Illuminate\Database\Eloquent\Relations\BelongsTo
{
return $this->belongsTo(Category::class, 'category_id', 'id');
}
}
```
## Relationship Types
### BelongsTo (belongs_to)
```php
/**
* 获取所属分类
*/
public function category(): \Illuminate\Database\Eloquent\Relations\BelongsTo
{
return $this->belongsTo(Category::class, 'category_id', 'id');
}
```
### HasMany (has_many)
```php
/**
* 获取评论列表
*/
public function comments(): \Illuminate\Database\Eloquent\Relations\HasMany
{
return $this->hasMany(Comment::class, 'question_id', 'id');
}
```
### BelongsToMany (many_to_many)
```php
/**
* 获取标签列表
*/
public function tags(): \Illuminate\Database\Eloquent\Relations\BelongsToMany
{
return $this->belongsToMany(
Tag::class,
MemberTagRelation::class, // 中间表模型
'member_id', // 当前模型在中间表的关联键
'tag_id' // 另一模型在中间表的关联键
);
}
```
### HasOne (has_one)
```php
/**
* 获取主图
*/
public function cover(): \Illuminate\Database\Eloquent\Relations\HasOne
{
return $this->hasOne(Image::class, 'question_id', 'id');
}
```
## Cast Types
| Type | Example |
|------|---------|
| integer | `'id' => 'integer'` |
| float | `'price' => 'float'` |
| decimal | `'price' => 'decimal:2'` |
| boolean | `'is_hot' => 'boolean'` |
| string | `'name' => 'string'` |
| array | `'data' => 'array'` |
| object | `'data' => 'object'` |
| datetime | `'created_at' => 'datetime'` |
| timestamp | `'created_at' => 'timestamp'` |
## Attribute Mutators
```php
/**
* 设置生日(自动转换时间戳)
*/
public function setBirthdayAttribute($value): void
{
if (empty($value)) {
$this->attributes['birthday'] = null;
return;
}
if (is_numeric($value)) {
$this->attributes['birthday'] = $value;
return;
}
try {
$carbon = \Carbon\Carbon::parse($value);
$this->attributes['birthday'] = $carbon->timestamp;
} catch (\Exception $e) {
$this->attributes['birthday'] = null;
}
}
/**
* 获取生日
*/
public function getBirthdayAttribute($value): ?string
{
if (empty($value)) {
return null;
}
try {
return \Carbon\Carbon::createFromTimestamp($value)->toIso8601String();
} catch (\Exception $e) {
return null;
}
}
```
## Auto-generation Checklist
When generating model:
- [ ] Set correct namespace based on target
- [ ] Import BaseModel (core\base or plugin variant)
- [ ] Import SoftDeletes trait
- [ ] Import related models
- [ ] Import Enum classes for status/text fields
- [ ] Set table name (plugin may have prefix)
- [ ] Set primary key
- [ ] Set fillable array
- [ ] Set hidden array (for sensitive fields)
- [ ] Set casts array
- [ ] Set appends array (for accessor attributes)
- [ ] Add accessor methods (getXxxTextAttribute)
- [ ] Add relationship methods
- [ ] Add mutator methods (setPasswordAttribute)
+186
View File
@@ -0,0 +1,186 @@
---
name: backend-gen-parse
description: Parse table definition to extract module name, model name, table name and field definitions. This is the first step in CRUD generation workflow.
---
# Step 1: Parse Table Definition
Extract key information from user-provided table definitions.
## Input Examples
### Example 1: Simple Table
```
Module: Ask
Table: tags
Fields:
- id (primary key)
- name (string, 50, required)
- sort (integer, default 0)
- status (tinyint, default 1)
- created_at (timestamp)
- updated_at (timestamp)
```
### Example 2: Table with Foreign Key
```
Module: Ask
Table: questions
Fields:
- id (primary key)
- category_id (foreign key -> ask.categories)
- title (string, 200, required)
- content (text, required)
- price (decimal 10,2, default 0)
- view_count (integer, default 0)
- status (tinyint, default 1)
- created_at (timestamp)
- updated_at (timestamp)
- deleted_at (timestamp, nullable)
```
### Example 3: Full Definition
```
Module: Market
Table: products
Fields:
- id (primary key)
- category_id (foreign key -> market.categories)
- name (string, 100, required)
- cover (string, 255, nullable) [image]
- price (decimal 10,2, required)
- stock (integer, default 0)
- sales (integer, default 0)
- description (text, nullable)
- content (text, nullable) [editor]
- sort (integer, default 0)
- status (tinyint, default 1)
- created_at (timestamp)
- updated_at (timestamp)
```
## Output Structure
```json
{
"plugin": "official",
"module": "ask",
"model": "Tag",
"table": "official_ask_tags",
"fields": [
{
"name": "id",
"type": "id",
"migration": "idIncrements()"
},
{
"name": "name",
"type": "string",
"length": 50,
"required": true,
"migration": "string('name', 50)"
},
{
"name": "sort",
"type": "integer",
"default": 0,
"migration": "unsignedInteger('sort')->default(0)"
},
{
"name": "status",
"type": "tinyint",
"default": 1,
"comment": "状态: 1正常 2禁用",
"migration": "tinyInteger('status')->default(1)"
},
{
"name": "created_at",
"type": "timestamp",
"migration": "unsignedInteger('created_at')"
},
{
"name": "updated_at",
"type": "timestamp",
"migration": "unsignedInteger('updated_at')"
},
{
"name": "deleted_at",
"type": "soft_delete",
"migration": "unsignedInteger('deleted_at')->nullable()"
}
],
"fillable": ["name", "sort", "status"],
"casts": {
"sort": "integer",
"status": "integer"
},
"foreign_keys": []
}
```
## Field Type Mapping
| Input Type | Migration Method | Vue Component |
|------------|------------------|----------------|
| string(n) | `string('x', n)` | `el-input` |
| text | `text('x')` | `el-input (textarea)` |
| integer | `unsignedInteger('x')` | `el-input-number` |
| bigint | `unsignedBigInteger('x')` | `el-input-number` |
| decimal(m,n) | `decimal('x', m, n)` | `el-input-number` |
| tinyint | `tinyInteger('x')` | `ApiDict` |
| foreign_key | `unsignedBigInteger('x')` | `ApiSelect` |
| date | `date('x')` | `DatePicker` |
| datetime | `dateTime('x')` | `DateTimePicker` |
| json | `json('x')` | `el-input` |
| image | `string('x', 255)` | `ImageUpload` |
| boolean | `boolean('x')` | `el-switch` |
## Naming Convention
| Item | Convention | Example |
|------|------------|---------|
| Plugin | snake_case | `official`, `market` |
| Module | PascalCase | `Ask`, `Market`, `Question` |
| Model | PascalCase singular | `Tag`, `Product`, `Question` |
| Table | snake_case with prefix | `official_ask_tags` |
| Field | snake_case | `user_name`, `created_at` |
## Field Attributes
Each field should have these attributes:
- `name`: Field name in snake_case
- `type`: Field type (string, integer, text, etc.)
- `length`: Optional length for string type
- `required`: Whether field is required
- `default`: Default value
- `nullable`: Whether field can be null
- `comment`: Field comment
- `migration`: Migration method call
- `fillable`: Whether in $fillable array
- `casts`: Type casting for model
## Validation Rules
Generate validation rules based on field attributes:
| Attribute | Validation |
|-----------|------------|
| required | `['required', 'max:255']` |
| string | `['string', 'max:255']` |
| integer | `['integer']` |
| decimal | `['numeric']` |
| email | `['email']` |
| unique | `['unique:table,column']` |
## Auto-generation Checklist
When parsing table definition:
- [ ] Extract plugin name
- [ ] Extract module name (PascalCase)
- [ ] Extract model name (PascalCase singular)
- [ ] Generate table name with prefix
- [ ] Parse each field with full attributes
- [ ] Generate fillable array
- [ ] Generate casts array
- [ ] Identify foreign key relationships
@@ -0,0 +1,330 @@
---
name: backend-gen-schema
description: Generate Schema classes for madong module. Supports both app (main project) and plugin targets. Creates request/response DTOs with OpenAPI attributes for API documentation.
globs:
- "app/schema/**/*.php"
- "plugin/**/app/schema/**/*.php"
---
# Generate Schema
Create Schema classes for API request/response DTOs.
## File Location
**App (主项目):**
```
app/schema/request/{module}/{Model}Request.php
app/schema/response/{module}/{Model}Response.php
```
**Plugin (插件):**
```
plugin/{plugin}/app/schema/request/{module}/{Model}Request.php
plugin/{plugin}/app/schema/response/{module}/{Model}Response.php
```
## Target Variables
| 变量 | App | Plugin |
|------|-----|--------|
| `{prefix}` | `` (空) | `plugin\{Plugin}` |
| `{ns}` | `app` | `plugin\{Plugin}\app` |
## Request Schema Template
```php
<?php
declare(strict_types=1);
/**
*+------------------
* madong
*+------------------
* Copyright (c) https://gitee.com/motion-code All rights reserved.
*+------------------
* Author: Mr. April (405784684@qq.com)
*+------------------
*/
namespace {ns}\schema\request\{module};
use OpenApi\Attributes as OA;
use madong\swagger\schema\BaseRequestDTO;
use WebmanTech\DTO\Attributes\ValidationRules;
/**
* {Model}请求Schema
*/
#[OA\Schema(
title: '{Model}请求',
description: '{Model}相关请求参数'
)]
class {Model}Request extends BaseRequestDTO
{
{properties}
}
```
## Complete Request Schema Example
```php
<?php
declare(strict_types=1);
/**
*+------------------
* madong
*+------------------
* Copyright (c) https://gitee.com/motion-code All rights reserved.
*+------------------
* Author: Mr. April (405784684@qq.com)
*+------------------
*/
namespace app\schema\request;
use OpenApi\Attributes as OA;
use madong\swagger\schema\BaseRequestDTO;
use WebmanTech\DTO\Attributes\ValidationRules;
#[OA\Schema(
title: 'ID查询请求',
description: '根据ID查询单条记录的请求参数'
)]
class IdRequest extends BaseRequestDTO
{
#[OA\Property(
property: 'id',
description: '记录ID',
type: 'string',
example: 1
)]
#[ValidationRules(rules: 'string|required|min:1')]
public int $id;
}
```
```php
<?php
declare(strict_types=1);
/**
*+------------------
* madong
*+------------------
* Copyright (c) https://gitee.com/motion-code All rights reserved.
*+------------------
* Author: Mr. April (405784684@qq.com)
*+------------------
*/
namespace app\schema\request;
use OpenApi\Attributes as OA;
use madong\swagger\schema\BaseRequestDTO;
use WebmanTech\DTO\Attributes\ValidationRules;
#[OA\Schema(
title: '批量删除请求',
description: '批量删除记录的请求参数'
)]
class BatchDeleteRequest extends BaseRequestDTO
{
#[OA\Property(
property: 'ids',
description: '要删除的ID列表',
type: 'array',
items: new OA\Items(type: 'integer'),
example: [1, 2, 3]
)]
#[ValidationRules(rules: 'required|array|min:1')]
public array $ids;
}
```
```php
<?php
declare(strict_types=1);
/**
*+------------------
* madong
*+------------------
* Copyright (c) https://gitee.com/motion-code All rights reserved.
*+------------------
* Author: Mr. April (405784684@qq.com)
*+------------------
*/
namespace app\schema\request;
use OpenApi\Attributes as OA;
use madong\swagger\schema\BaseRequestDTO;
use WebmanTech\DTO\Attributes\ValidationRules;
#[OA\Schema(
title: '状态更新请求',
description: '更新记录状态的请求参数'
)]
class ChangeStatusRequest extends BaseRequestDTO
{
#[OA\Property(
property: 'id',
description: '记录ID',
type: 'integer',
example: 1
)]
#[ValidationRules(rules: 'required|integer|min:1')]
public int $id;
#[OA\Property(
property: 'status',
description: '状态值',
type: 'integer',
example: 1
)]
#[ValidationRules(rules: 'required|in:0,1')]
public int $status;
}
```
## Common Request Schemas
### IdRequest - ID查询请求
```php
#[OA\Schema(title: 'ID查询请求')]
class IdRequest extends BaseRequestDTO
{
#[OA\Property(property: 'id', description: '记录ID', type: 'integer')]
#[ValidationRules(rules: 'integer|required|min:1')]
public int $id;
}
```
### BatchDeleteRequest - 批量删除请求
```php
#[OA\Schema(title: '批量删除请求')]
class BatchDeleteRequest extends BaseRequestDTO
{
#[OA\Property(property: 'ids', description: 'ID列表', type: 'array', items: new OA\Items(type: 'integer'))]
#[ValidationRules(rules: 'required|array|min:1')]
public array $ids;
}
```
### ChangeStatusRequest - 状态更新请求
```php
#[OA\Schema(title: '状态更新请求')]
class ChangeStatusRequest extends BaseRequestDTO
{
#[OA\Property(property: 'id', description: 'ID', type: 'integer')]
public int $id;
#[OA\Property(property: 'status', description: '状态', type: 'integer', example: 1)]
public int $status;
}
```
### PageRequest - 分页请求
```php
#[OA\Schema(title: '分页请求')]
class PageRequest extends BaseRequestDTO
{
#[OA\Property(property: 'page', description: '页码', type: 'integer', example: 1)]
#[ValidationRules(rules: 'integer|min:1')]
public int $page = 1;
#[OA\Property(property: 'limit', description: '每页数量', type: 'integer', example: 10)]
#[ValidationRules(rules: 'integer|min:1|max:100')]
public int $limit = 10;
}
```
## Response Schema Template
```php
<?php
declare(strict_types=1);
/**
*+------------------
* madong
*+------------------
* Copyright (c) https://gitee.com/motion-code All rights reserved.
*+------------------
* Author: Mr. April (405784684@qq.com)
*+------------------
*/
namespace {ns}\schema\response\{module};
use OpenApi\Attributes as OA;
/**
* {Model}响应Schema
*/
#[OA\Schema(
title: '{Model}响应',
description: '{Model}相关响应数据'
)]
class {Model}Response
{
#[OA\Property(property: 'id', description: 'ID', type: 'integer')]
public int $id;
{properties}
}
```
## Common Response Schemas
### ListResponse - 列表响应
```php
#[OA\Schema(
title: '列表响应',
description: '分页列表响应'
)]
class ListResponse
{
#[OA\Property(property: 'total', description: '总数', type: 'integer', example: 100)]
public int $total;
#[OA\Property(property: 'list', description: '列表数据', type: 'array', items: new OA\Items())]
public array $list;
}
```
### PageResponse - 分页响应
```php
#[OA\Schema(
title: '分页响应',
description: '分页响应数据'
)]
class PageResponse
{
#[OA\Property(property: 'total', description: '总数', type: 'integer')]
public int $total;
#[OA\Property(property: 'list', description: '列表数据', type: 'array', items: new OA\Items())]
public array $list;
#[OA\Property(property: 'page', description: '当前页', type: 'integer')]
public int $page;
#[OA\Property(property: 'limit', description: '每页数量', type: 'integer')]
public int $limit;
}
```
## Auto-generation Checklist
When generating Schema:
- [ ] Set correct namespace based on target
- [ ] Import BaseRequestDTO or BaseResponseDTO
- [ ] Import OpenApi\Attributes as OA
- [ ] Import ValidationRules attribute
- [ ] Add #[OA\Schema] class attribute
- [ ] Add #[OA\Property] for each property
- [ ] Add #[ValidationRules] for validation
- [ ] Define property type and default value
@@ -0,0 +1,141 @@
---
name: backend-gen-service
description: Generate Service class. Only needs DAO injection in constructor. BaseService __call method auto-delegates to DAO.
globs:
- "app/service/admin/**/*.php"
- "plugin/**/app/service/admin/**/*.php"
---
# Service 生成规范
## 原则
**Service 只需注入 DAO,其他方法全部由 `BaseService::__call` 自动代理到 DAO!**
当前项目真实约定补充:
- 必须显式导入 `use core\base\BaseService;`,不能只写 `extends BaseService`。
- 必须在构造函数中注入 DAO,例如 `__construct(FooDao $dao)`。
- 不要在 Service 内部手动 `new FooDao()`,避免与容器注入和项目真实风格不一致。
```php
class {Model}Service extends BaseService
{
public function __construct({Model}Dao $dao)
{
$this->dao = $dao;
}
}
```
`BaseService::__call()` 会自动将所有方法调用转发给 `$this->dao`。
## 文件位置
**App:**
```
app/service/admin/{module}/{Model}Service.php
```
**Plugin:**
```
plugin/{Plugin}/app/service/admin/{module}/{Model}Service.php
```
## Service 模板
```php
<?php
declare(strict_types=1);
/**
*+------------------
* madong
*+------------------
* Copyright (c) https://gitee.com/motion-code All rights reserved.
*+------------------
* Author: Mr. April (405784684@qq.com)
*+------------------
*/
namespace {ns}\service\admin\{module};
use {dao_ns}\{module}\{Model}Dao;
use core\base\BaseService;
/**
* {Model}服务层
*/
class {Model}Service extends BaseService
{
public function __construct({Model}Dao $dao)
{
$this->dao = $dao;
}
}
```
## 只有需要特殊业务逻辑时才加方法
```php
class {Model}Service extends BaseService
{
public function __construct({Model}Dao $dao)
{
$this->dao = $dao;
}
// 只有特殊业务逻辑才定义方法
// 普通 CRUD 直接通过 __call 代理到 DAO
}
```
### 业务逻辑示例
```php
/**
* 批量操作示例
*/
public function batchAssign(array $ids, int $categoryId): void
{
$this->transaction(function () use ($ids, $categoryId) {
foreach ($ids as $id) {
$item = $this->dao->get($id);
if ($item) {
$item->category_id = $categoryId;
$item->save();
}
}
});
}
```
## 调用示例
```php
// Service 实例化后直接调用,__call 自动代理到 DAO
$service = new {Model}Service(new {Model}Dao());
$list = $service->selectList($where, '*', $page, $limit); // 代理到 DAO
$item = $service->get($id); // 代理到 DAO
$count = $service->count($where); // 代理到 DAO
$service->save($data); // 代理到 DAO
```
## BaseService 提供的方法
| 方法 | 说明 |
|------|------|
| `transaction(callable)` | 事务执行 |
| `cacheDriver()` | 获取缓存服务 |
| `getPageValue()` | 获取分页配置 |
| `passwordHash()` | 密码加密 |
| `__call()` | 自动代理到 DAO |
## 生成检查清单
- [ ] 命名空间正确(App/Plugin)
- [ ] 引入 BaseService
- [ ] 引入 DAO 类
- [ ] 构造函数注入 DAO
- [ ] 只有特殊业务逻辑才加方法
- [ ] 未在 Service 内部手动 `new Dao()`
@@ -0,0 +1,311 @@
---
name: backend-gen-validate
description: Generate validation class for madong module. Supports both app (main project) and plugin targets. Creates validate extending BaseValidate with Laravel validation rules and scene definitions.
globs:
- "app/adminapi/validate/**/*.php"
- "app/api/validate/**/*.php"
- "plugin/**/app/adminapi/validate/**/*.php"
---
# Generate Validate
Create validation class for module.
## File Location
**App (主项目):**
```
app/adminapi/validate/{module}/{Model}Validate.php
```
**Plugin (插件):**
```
plugin/{plugin}/app/adminapi/validate/{module}/{Model}Validate.php
```
## Target Variables
| 变量 | App | Plugin |
|------|-----|--------|
| `{prefix}` | `` (空) | `plugin\{Plugin}` |
| `{ns}` | `app` | `plugin\{Plugin}\app` |
| `{model_ns}` | `app\model` | `plugin\{Plugin}\app\model` |
## Validate Template
```php
<?php
declare(strict_types=1);
/**
*+------------------
* madong
*+------------------
* Copyright (c) https://gitee.com/motion-code All rights reserved.
*+------------------
* Author: Mr. April (405784684@qq.com)
*+------------------
*/
namespace {ns}\adminapi\validate\{module};
use {model_ns}\{module}\{Model};
use core\base\BaseValidate;
use Illuminate\Validation\Rule;
/**
* {Model}验证器
*
* Class {Model}Validate
*/
class {Model}Validate extends BaseValidate
{
/**
* 验证规则
*/
public function rules(): array
{
// 更新请求优先读取路由 ID;无路由 ID 时兼容请求体中的 ID。
$id = request()->route?->param('id') ?? request()->input('id');
return [
{rules}
];
}
/**
* 验证消息
*/
protected array $message = [
{messages}
];
/**
* 验证场景
*/
protected array $scene = [
'store' => [
{store_fields}
],
'update' => [
{update_fields}
],
];
/**
* 创建验证场景
*/
public function sceneStore(): {Model}Validate
{
return $this->only($this->scene['store']);
}
/**
* 更新验证场景
*/
public function sceneUpdate(): {Model}Validate
{
return $this->only($this->scene['update']);
}
}
```
## Complete Example (App)
```php
<?php
declare(strict_types=1);
/**
*+------------------
* madong
*+------------------
* Copyright (c) https://gitee.com/motion-code All rights reserved.
*+------------------
* Author: Mr. April (405784684@qq.com)
*+------------------
*/
namespace app\adminapi\validate\member;
use app\model\member\Member;
use core\base\BaseValidate;
use Illuminate\Validation\Rule;
/**
* 会员验证器
*
* Class MemberValidate
*/
class MemberValidate extends BaseValidate
{
/**
* 验证规则
*/
public function rules(): array
{
// 更新请求优先读取路由 ID;无路由 ID 时兼容请求体中的 ID。
$id = request()->route?->param('id') ?? request()->input('id');
return [
'username' => ['required', 'max:50', Rule::unique(Member::class, 'username')->ignore($id, 'id')],
'email' => ['email', 'max:100'],
'phone' => ['max:20'],
'password' => 'required|min:6|max:60',
'nickname' => 'max:50',
'level_id' => 'integer|min:1',
'points' => 'integer|min:0',
'balance' => 'number|min:0',
'gender' => 'in:0,1,2',
'birthday' => 'date',
'enabled' => 'in:0,1',
];
}
/**
* 验证消息
*/
protected array $message = [
'username.required' => '用户名不能为空',
'username.max' => '用户名不能超过50个字符',
'username.unique' => '用户名已存在',
'email.email' => '邮箱格式不正确',
'email.max' => '邮箱不能超过100个字符',
'phone.max' => '手机号格式不正确',
'password.required' => '密码不能为空',
'password.min' => '密码不能少于6个字符',
'password.max' => '密码不能超过60个字符',
'nickname.max' => '昵称不能超过50个字符',
'level_id.integer' => '等级ID必须为整数',
'points.integer' => '积分必须为整数',
'balance.number' => '余额必须为数字',
'gender.in' => '性别值不正确',
'birthday.date' => '生日格式不正确',
'enabled.in' => '状态值不正确',
];
/**
* 验证场景
*/
protected array $scene = [
'store' => [
'username',
'email',
'phone',
'password',
'nickname',
'level_id',
'points',
'balance',
'gender',
'birthday',
'enabled',
],
'update' => [
'username',
'email',
'phone',
'nickname',
'level_id',
'points',
'balance',
'gender',
'birthday',
'enabled',
],
];
/**
* 创建验证场景
*/
public function sceneStore(): MemberValidate
{
return $this->only($this->scene['store']);
}
/**
* 更新验证场景
*/
public function sceneUpdate(): MemberValidate
{
return $this->only($this->scene['update']);
}
}
```
## Validation Rules Reference
| Rule | Example | Description |
|------|---------|-------------|
| required | `'field' => 'required'` | 必填 |
| nullable | `'field' => 'nullable'` | 可为空 |
| integer | `'field' => 'integer'` | 整数 |
| numeric | `'field' => 'numeric'` | 数字 |
| string | `'field' => 'string'` | 字符串 |
| email | `'field' => 'email'` | 邮箱格式 |
| max | `'field' => 'max:50'` | 最大长度 |
| min | `'field' => 'min:6'` | 最小长度 |
| in | `'field' => 'in:0,1,2'` | 枚举值 |
| unique | `'field' => Rule::unique(Model::class)` | 唯一性 |
| date | `'field' => 'date'` | 日期格式 |
| array | `'field' => 'array'` | 数组 |
| alpha | `'field' => 'alpha'` | 仅字母 |
| alpha_num | `'field' => 'alpha_num'` | 字母数字 |
## 更新唯一性校验
通用 `BaseService.update({ id, ...payload })` 会将 `id` 生成 REST URL:`PUT /resource/{id}`,并从请求体移除该字段。因此唯一性校验应从路由读取当前记录 ID:
```php
$id = request()->route?->param('id');
Rule::unique(Model::class, 'code')->ignore($id, 'id');
```
配置类编辑表单中的隐藏 `id` 字段不能配置 `ifDetail: true`;该属性会令字段不参与编辑表单,进而造成 `PUT /resource/undefined` 和自身重复误判。
## Custom Scene Example
```php
/**
* 重置密码验证
*/
public function sceneResetPassword(): MemberValidate
{
return $this->only([])
->append('id', 'require|integer|min:1')
->append('password', 'require|min:6|max:20');
}
/**
* 状态更新验证
*/
public function sceneStatus(): MemberValidate
{
return $this->only([])
->append('id', 'require|integer|min:1')
->append('status', 'require|in:0,1');
}
/**
* 批量删除验证
*/
public function sceneBatchDelete(): MemberValidate
{
return $this->only([])
->append('ids', 'require|array|min:1');
}
```
## Auto-generation Checklist
When generating validate:
- [ ] Set correct namespace based on target
- [ ] Import BaseValidate
- [ ] Import Model class
- [ ] Define rules() method with Laravel validation
- [ ] Use Rule::unique() for unique fields
- [ ] Define $message for custom error messages
- [ ] Define $scene for validation scenes
- [ ] Implement sceneStore() method
- [ ] Implement sceneUpdate() method
@@ -0,0 +1,38 @@
---
name: backend-generator
description: 代码生成器规范(generator),基于模板引擎的代码自动生成
globs:
- "core/generator/**/*.php"
---
## 生成器架构
| 组件 | 说明 |
|------|------|
| GeneratorEngine | 生成引擎核心 |
| factory/ | 各种生成器工厂(11种) |
| file/ | 文件生成器 |
| scene/ | 场景定义 |
| stubs/ | 代码模板文件 |
## 使用方式
```php
use core\generator\GeneratorEngine;
$engine = new GeneratorEngine([
'module' => 'article',
'model' => 'Article',
'table' => 'article',
'fields' => [...],
]);
$engine->generate();
```
## 检查清单
- [ ] 生成配置是否完整
- [ ] 模板文件是否存在
- [ ] 输出目录是否可写
- [ ] 生成的文件是否可覆盖
+291
View File
@@ -0,0 +1,291 @@
---
name: backend-lang
description: Generate backend lang translation files for madong plugin module. Creates PHP translation files with proper key patterns for both zh_CN and en languages.
---
# Step 8: Generate Backend Lang
Generate PHP translation files for plugin module backend.
## File Locations
```
plugin/{plugin}/resource/translations/zh_CN/{module}.php
plugin/{plugin}/resource/translations/en/{module}.php
```
## Translation Key Pattern
Backend i18n uses nested array structure: `{module}.{model}.*`
**Important**:
- PHP file returns nested array structure (NOT dot notation in keys)
- Key prefix `{module}.{model}` is represented as nested array `'{model}' => [...]`
- Do NOT include plugin name in keys
## Translation File Template (zh_CN)
```php
<?php
/**
* {Module} 多语言文件
* 插件: {plugin}
*/
return [
// 通用
'common' => [
'success' => '操作成功',
'fail' => '操作失败',
'delete_success' => '删除成功',
'delete_fail' => '删除失败',
'save_success' => '保存成功',
'save_fail' => '保存失败',
],
// {Model} 相关 - 注意:这里是嵌套数组,不是 'rich_text.template.title'
'{model}' => [
'title' => '{Model}管理',
'list' => '{Model}列表',
'add' => '添加{Model}',
'edit' => '编辑{Model}',
'delete' => '删除{Model}',
'detail' => '{Model}详情',
'export' => '导出{Model}',
'import' => '导入{Model}',
// 字段
'field' => [
'id' => 'ID',
'name' => '名称',
'sort' => '排序',
'status' => '状态',
'created_at' => '创建时间',
'updated_at' => '更新时间',
],
// 状态
'status' => [
'normal' => '正常',
'disable' => '禁用',
],
// 验证消息
'validate' => [
'name_required' => '请输入名称',
'name_max' => '名称最多50个字符',
'status_required' => '请选择状态',
],
// 搜索表单
'search' => [
'name' => '名称',
'name_placeholder' => '请输入名称',
'status' => '状态',
'status_placeholder' => '请选择状态',
'date_range' => '创建时间',
],
// 操作提示
'tips' => [
'add_success' => '添加{Model}成功',
'add_fail' => '添加{Model}失败',
'edit_success' => '编辑{Model}成功',
'edit_fail' => '编辑{Model}失败',
'delete_confirm' => '确认删除该{Model}吗?',
'delete_success' => '删除{Model}成功',
'delete_fail' => '删除{Model}失败',
],
],
];
```
## Translation File Template (en)
```php
<?php
/**
* {Module} Translation File
* Plugin: {plugin}
*/
return [
// Common
'common' => [
'success' => 'Operation successful',
'fail' => 'Operation failed',
'delete_success' => 'Deleted successfully',
'delete_fail' => 'Delete failed',
'save_success' => 'Saved successfully',
'save_fail' => 'Save failed',
],
// {Model} Related - Note: nested array structure
'{model}' => [
'title' => '{Model} Management',
'list' => '{Model} List',
'add' => 'Add {Model}',
'edit' => 'Edit {Model}',
'delete' => 'Delete {Model}',
'detail' => '{Model} Detail',
'export' => 'Export {Model}',
'import' => 'Import {Model}',
// Fields
'field' => [
'id' => 'ID',
'name' => 'Name',
'sort' => 'Sort',
'status' => 'Status',
'created_at' => 'Created At',
'updated_at' => 'Updated At',
],
// Status
'status' => [
'normal' => 'Normal',
'disable' => 'Disabled',
],
// Validation Messages
'validate' => [
'name_required' => 'Please enter name',
'name_max' => 'Name cannot exceed 50 characters',
'status_required' => 'Please select status',
],
// Search Form
'search' => [
'name' => 'Name',
'name_placeholder' => 'Please enter name',
'status' => 'Status',
'status_placeholder' => 'Please select status',
'date_range' => 'Created At',
],
// Operation Tips
'tips' => [
'add_success' => '{Model} added successfully',
'add_fail' => 'Failed to add {Model}',
'edit_success' => '{Model} edited successfully',
'edit_fail' => 'Failed to edit {Model}',
'delete_confirm' => 'Are you sure to delete this {Model}?',
'delete_success' => '{Model} deleted successfully',
'delete_fail' => 'Failed to delete {Model}',
],
],
];
```
## Usage in Backend Code
```php
// Using trans() helper - key format: {plugin}.{module}.{model}.key
// Plugin name is added by translation system based on file location
echo trans('{plugin}.{module}.{model}.title');
echo trans('{plugin}.{module}.{model}.field.name');
// Example for rich_text template in official plugin:
echo trans('official.rich_text.template.title');
echo trans('official.rich_text.template.field.name');
```
## Auto-generation Checklist
When generating backend i18n:
- [ ] Create `resource/translations/` directory if not exists
- [ ] Generate `zh_CN/{module}.php` with nested array structure
- [ ] Generate `en/{module}.php` with nested array structure
- [ ] Use `'{model}' => [...]` as top-level key (NOT dot notation)
- [ ] Do NOT include plugin name in PHP array keys
- [ ] Add all field labels, validation messages, and tips
## Key Naming Convention
| Type | PHP Array Structure | Full Key Used in Code | Example |
|------|---------------------|----------------------|---------|
| Title | `'{model}' => ['title' => '...']` | `trans('{plugin}.{module}.{model}.title')` | `official.rich_text.template.title` |
| Field | `'{model}' => ['field' => ['name' => '...']]` | `trans('{plugin}.{module}.{model}.field.name')` | `official.rich_text.template.field.name` |
| Status | `'{model}' => ['status' => ['normal' => '...']]` | `trans('{plugin}.{module}.{model}.status.normal')` | `official.rich_text.template.status.normal` |
| Search | `'{model}' => ['search' => ['name' => '...']]` | `trans('{plugin}.{module}.{model}.search.name')` | `official.rich_text.template.search.name` |
| Tips | `'{model}' => ['tips' => ['add_success' => '...']]` | `trans('{plugin}.{module}.{model}.tips.add_success')` | `official.rich_text.template.tips.add_success` |
## Common Mistakes to Avoid
1. ❌ **Wrong**: Using dot notation in PHP array keys
```php
return [
'rich_text.template.title' => '富文本模板管理',
];
```
2. ✅ **Correct**: Using nested array structure
```php
return [
'template' => [
'title' => '富文本模板管理',
],
];
```
3. ❌ **Wrong**: Including plugin name in keys
```php
return [
'official' => [
'rich_text' => [
'template' => [...]
]
]
];
```
4. ✅ **Correct**: Only module and model in keys
```php
return [
'template' => [...] // {model} as key
];
// Full key: official.rich_text.template.xxx
// Plugin and module added by translation system
```
## Example: rich_text/template for official plugin
File: `plugin/official/resource/translations/zh_CN/rich_text.php`
```php
<?php
return [
'common' => [
'success' => '操作成功',
'fail' => '操作失败',
],
// 'template' is the {model} name
'template' => [
'title' => '模板管理',
'field' => [
'id' => 'ID',
'title' => '标题',
'content' => '内容',
'sort' => '排序',
],
'search' => [
'title' => '标题',
],
'tips' => [
'add_success' => '添加模板成功',
],
],
];
```
Usage:
```php
trans('official.rich_text.template.title'); // 模板管理
trans('official.rich_text.template.field.title'); // 标题
trans('official.rich_text.template.tips.add_success'); // 添加模板成功
```
+400
View File
@@ -0,0 +1,400 @@
---
name: backend-logger
description: Help integrate backend debug logger component (core/logger) into code. Provides logger usage patterns, configuration guidance, and best practices for different log levels.
globs:
- "app/**/*.php"
- "plugin/**/*.php"
- "backend/core/logger/**/*.php"
---
# Madong Backend Logger Integration
快速集成后端调试日志组件到你的代码中。
## 组件介绍
`core/logger` 是一个基于 Monolog 的日志组件,提供静态方法调用接口,支持多种日志级别和自动文件分割。
### 核心特性
- ✅ 静态方法调用,使用简单
- ✅ 支持 8 种日志级别
- ✅ 按日期和级别自动分割日志文件
- ✅ 支持日志清理和保留策略
- ✅ 支持上下文数据
- ✅ 基于 Monolog,功能强大
---
## 快速开始
### 1. 基本使用
```php
use core\logger\Logger;
// 简单日志
Logger::info('用户登录成功');
Logger::error('数据库连接失败');
// 带上下文的日志
Logger::info('用户登录', ['user_id' => 123, 'ip' => '192.168.1.1']);
Logger::error('订单创建失败', ['order_id' => 456, 'error' => '库存不足']);
```
### 2. 日志级别
| 方法 | 级别值 | 使用场景 |
|------|--------|----------|
| `Logger::debug()` | 100 | 调试信息,开发环境使用 |
| `Logger::info()` | 200 | 一般信息,如用户操作 |
| `Logger::notice()` | 250 | 重要事件,但非错误 |
| `Logger::warning()` | 300 | 警告信息,潜在问题 |
| `Logger::error()` | 400 | 运行错误,需要关注 |
| `Logger::critical()` | 500 | 严重故障,需要立即处理 |
| `Logger::alert()` | 550 | 需要立即采取行动 |
| `Logger::emergency()` | 600 | 系统不可用,最高级别 |
---
## 配置说明
配置文件路径:`backend/core/logger/config/app.php`
```php
return [
'enable' => true,
'base' => [
'path' => runtime_path('logs'), // 日志存储路径
'channel' => 'core', // 日志通道名称
'retention_days' => 7, // 保留天数
'daily_rotation' => true, // 每日文件分割
],
'format' => [
'date' => 'Y-m-d H:i:s.u',
'output' => "[%datetime%] %channel%.%level_name%: %message% %context%\n",
],
'levels' => [
'debug' => config('app.debug', false), // 跟随应用调试模式
'min_level' => 'debug', // 最小记录级别
],
'handlers' => [
'stream' => true, // 启用文件处理器
'syslog' => false, // 禁用 syslog
],
];
```
---
## 使用场景
### 场景 1: 控制器中记录请求日志
```php
namespace app\adminapi\controller\user;
use core\base\Controller;
use core\logger\Logger;
use support\Request;
class UserController extends Controller
{
public function index(Request $request)
{
Logger::info('访问用户列表', [
'user_id' => $request->user->id ?? 0,
'page' => $request->get('page', 1),
'ip' => $request->getRealIp(),
]);
// 业务逻辑...
}
}
```
### 场景 2: 服务层记录业务日志
```php
namespace app\service\admin\order;
use core\base\BaseService;
use core\logger\Logger;
use app\dao\order\OrderDao;
class OrderService extends BaseService
{
public function createOrder(array $data)
{
Logger::info('开始创建订单', ['data' => $data]);
try {
$order = $this->dao->save($data);
Logger::info('订单创建成功', ['order_id' => $order->id]);
return $order;
} catch (\Throwable $e) {
Logger::error('订单创建失败', [
'data' => $data,
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
]);
throw $e;
}
}
}
```
### 场景 3: 异常处理
```php
namespace app\service\admin\payment;
use core\base\BaseService;
use core\logger\Logger;
class PaymentService extends BaseService
{
public function processPayment(int $orderId, float $amount)
{
try {
// 支付处理逻辑...
Logger::info('支付处理成功', [
'order_id' => $orderId,
'amount' => $amount,
]);
} catch (\Throwable $e) {
Logger::critical('支付处理失败', [
'order_id' => $orderId,
'amount' => $amount,
'error' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
]);
throw $e;
}
}
}
```
### 场景 4: 调试信息
```php
namespace app\service\admin\sync;
use core\base\BaseService;
use core\logger\Logger;
class SyncService extends BaseService
{
public function syncData(array $items)
{
Logger::debug('开始同步数据', ['count' => count($items)]);
foreach ($items as $index => $item) {
Logger::debug('处理数据项', [
'index' => $index,
'item' => $item,
]);
// 处理逻辑...
}
Logger::debug('数据同步完成');
}
}
```
### 场景 5: 性能监控
```php
namespace app\service\admin\report;
use core\base\BaseService;
use core\logger\Logger;
class ReportService extends BaseService
{
public function generateReport(string $type)
{
$startTime = microtime(true);
Logger::info('开始生成报表', ['type' => $type]);
// 报表生成逻辑...
$report = $this->buildReport($type);
$duration = round((microtime(true) - $startTime) * 1000, 2);
Logger::info('报表生成完成', [
'type' => $type,
'duration' => $duration . 'ms',
'size' => strlen($report),
]);
return $report;
}
}
```
---
## 日志文件结构
日志文件按日期和级别自动分割:
```
runtime/logs/
└── core/ # 通道名称
└── 2024-01/ # 年月目录
├── 01-DEBUG.log # 每日调试日志
├── 01-INFO.log # 每日信息日志
├── 01-WARNING.log # 每日警告日志
├── 01-ERROR.log # 每日错误日志
└── ...
```
---
## 日志清理
### 手动清理
```php
use core\logger\Logger;
// 清理过期日志(根据配置的 retention_days)
Logger::cleanup();
// 强制清理所有日志
Logger::cleanup(true);
// 清理指定通道的日志
Logger::cleanup(false, 'payment');
```
### 自动清理
建议在定时任务中配置自动清理:
```php
// config/crontab.php
return [
'0 2 * * *' => function () {
\core\logger\Logger::cleanup();
},
];
```
---
## 最佳实践
### 1. 选择合适的日志级别
```php
// ✅ 好的做法
Logger::debug('变量值', ['var' => $value]); // 调试信息
Logger::info('用户操作', ['action' => 'login']); // 一般信息
Logger::warning('库存不足', ['product_id' => 123]); // 警告
Logger::error('支付失败', ['order_id' => 456]); // 错误
Logger::critical('数据库连接失败'); // 严重错误
// ❌ 不好的做法
Logger::emergency('用户登录'); // 级别过高
Logger::debug('支付失败'); // 级别过低
```
### 2. 提供有意义的上下文
```php
// ✅ 好的做法
Logger::info('订单创建成功', [
'order_id' => $order->id,
'user_id' => $order->user_id,
'amount' => $order->amount,
'payment_type' => $order->payment_type,
]);
// ❌ 不好的做法
Logger::info('订单创建成功');
Logger::info('订单创建成功', ['data' => $order]); // 数据过多
```
### 3. 避免敏感信息
```php
// ✅ 好的做法
Logger::info('用户登录', [
'user_id' => $user->id,
'ip' => $request->getRealIp(),
]);
// ❌ 不好的做法
Logger::info('用户登录', [
'password' => $request->post('password'), // 敏感信息
'token' => $user->api_token, // 敏感信息
]);
```
### 4. 异常日志包含堆栈
```php
try {
// 业务逻辑...
} catch (\Throwable $e) {
Logger::error('操作失败', [
'error' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'trace' => $e->getTraceAsString(),
]);
throw $e;
}
```
---
## 集成步骤
### Step 1: 检查配置
确认 `backend/core/logger/config/app.php` 配置正确。
### Step 2: 引入 Logger
在需要记录日志的文件中引入:
```php
use core\logger\Logger;
```
### Step 3: 添加日志调用
根据业务场景选择合适的日志级别和位置。
### Step 4: 验证日志
检查 `runtime/logs/{channel}/{Y-m}/` 目录下的日志文件。
---
## Quick Commands
| Command | Description |
|---------|------------|
| `add logger to UserController` | 在 UserController 中添加日志 |
| `add logger to OrderService` | 在 OrderService 中添加日志 |
| `add debug logger to sync method` | 在同步方法中添加调试日志 |
| `add error logger to try-catch` | 在异常处理中添加错误日志 |
| `show logger usage examples` | 显示日志使用示例 |
| `configure logger for production` | 配置生产环境日志 |
---
## Boundaries
- ✅ **Always**: 使用静态方法调用 `Logger::xxx()`
- ✅ **Always**: 提供有意义的上下文数据
- ✅ **Always**: 选择合适的日志级别
- ✅ **Always**: 异常日志包含堆栈信息
- ⚠️ **Ask first**: 敏感信息是否需要脱敏
- 🚫 **Never**: 记录密码、token 等敏感信息
- 🚫 **Never**: 在循环中大量记录 debug 日志
- 🚫 **Never**: 使用过高的日志级别记录普通信息
@@ -0,0 +1,63 @@
---
name: backend-middleware
description: 中间件规范,支持全局中间件、路由中间件、控制器注解中间件
globs:
- "app/**/middleware/**/*.php"
---
## 文件位置
```
app/{app}/middleware/{Name}Middleware.php
```
## 中间件类型
| 类型 | 注册方式 | 示例 |
|------|---------|------|
| 全局 | `config/middleware.php` | AccessTokenMiddleware |
| 路由 | Route::group 第二个参数 | PermissionMiddleware |
| 注解 | `#[Middleware]` | OperationMiddleware |
## 代码模板
```php
<?php
namespace app\adminapi\middleware;
use Webman\Http\Request;
use Webman\Http\Response;
class {Name}Middleware
{
public function process(Request $request, callable $next): Response
{
// 前置处理
$response = $next($request);
// 后置处理
return $response;
}
}
```
## 插件复用规则
插件如需注册 `appapi` 或 `adminapi` 中间件,优先复用主项目中已经存在的实现:
```php
'appapi' => [
\app\api\middleware\ApiAccessTokenMiddleware::class,
],
'adminapi' => [
\app\adminapi\middleware\AccessTokenMiddleware::class,
],
```
除非插件目录中确实存在实现了 `process(Request $request, callable $handler): Response` 的中间件类,否则不要引用 `plugin\\{name}\\app\\middleware\\ApiAccessTokenMiddleware`。
## 检查清单
- [ ] 中间件注册方式是否正确
- [ ] 是否处理了异常情况
- [ ] 是否返回正确的 Response 对象
+31
View File
@@ -0,0 +1,31 @@
---
name: backend-monitor
description: 系统监控规范(ServerMonitor),监控磁盘/CPU/内存/Redis
globs:
- "core/monitor/**/*.php"
---
## 监控指标
| 指标 | 说明 |
|------|------|
| 磁盘 | 磁盘使用率 |
| CPU | CPU 负载 |
| 内存 | 内存使用率 |
| Redis | Redis 连接状态 |
| 数据库 | 数据库连接池 |
## 使用方式
```php
use core\monitor\ServerMonitor;
$status = ServerMonitor::check();
// 返回 {disk, cpu, memory, redis, db}
```
## 检查清单
- [ ] 监控项是否完整覆盖关键指标
- [ ] 告警阈值是否合理
- [ ] 监控数据是否有存储
+31
View File
@@ -0,0 +1,31 @@
---
name: backend-notify
description: 消息通知推送规范(基于 Webman Push),支持站内信/推送
globs:
- "core/notify/**/*.php"
---
## 通知类型
| 类型 | 说明 |
|------|------|
| 站内信 | 存储在数据库中的消息 |
| 实时推送 | Webman Push 即时推送 |
## 使用方式
```php
use core\notify\NotificationService;
// 发送通知
NotificationService::send($memberId, 'title', 'content');
// 推送
NotificationService::push($memberId, ['type' => 'order', 'data' => []]);
```
## 检查清单
- [ ] 通知目标是否正确
- [ ] 推送消息格式是否标准
- [ ] 通知是否有重试机制
+38
View File
@@ -0,0 +1,38 @@
---
name: backend-process
description: 自定义进程规范,基于 Workerman Worker 的自定义进程
globs:
- "app/process/**/*.php"
---
## 进程类型
| 进程 | 说明 |
|------|------|
| Http | HTTP 服务进程 |
| Monitor | 文件监控进程 |
| PushNotification | 推送通知进程 |
## 代码模板
```php
<?php
namespace app\process;
use Workerman\Worker;
class {Name}Process
{
public function onWorkerStart(Worker $worker): void
{
// 进程启动逻辑
}
}
```
## 检查清单
- [ ] 进程是否注册到 config/process.php
- [ ] 是否有内存泄漏风险
- [ ] 进程间通信是否正确
+48
View File
@@ -0,0 +1,48 @@
---
name: backend-queue
description: Redis 消息队列规范,基于 Webman Queue 组件
globs:
- "app/queue/**/*.php"
---
## 队列配置
```php
// config/redis.php
return [
'queue' => [
'host' => env('REDIS_HOST', '127.0.0.1'),
'port' => env('REDIS_PORT', 6379),
'password' => env('REDIS_PASSWORD', ''),
],
];
```
## 消费者模板
```php
<?php
namespace app\queue\redis;
use Webman\RedisQueue\Consumer;
class {Name}Consumer implements Consumer
{
public string $queue = '{queue_name}';
public string $connection = 'default';
public int $retrySeconds = 5;
public function consume($data): void
{
// 处理消息
}
}
```
## 检查清单
- [ ] 队列名称是否唯一
- [ ] 消费者是否有错误处理
- [ ] 重试机制是否合理
- [ ] 消费失败的消息是否记录日志
+295
View File
@@ -0,0 +1,295 @@
---
name: backend-route
description: Generate route configuration for madong module. Supports both app (main project) and plugin targets. Creates route.php files for adminapi and api with proper OpenAPI scan paths.
---
# Generate Route
Create route configuration files for modules.
## File Location
**App (主项目):**
```
app/adminapi/config/route.php
app/api/config/route.php
```
**Plugin (插件):**
```
plugin/{plugin}/config/route.php
```
## AdminAPI Route Template
```php
<?php
declare(strict_types=1);
/**
*+------------------
* madong
*+------------------
* Copyright (c) https://gitee.com/motion-code All rights reserved.
*+------------------
* Author: Mr. April (405784684@qq.com)
*+------------------
*/
use Webman\Route;
use WebmanTech\Swagger\Swagger;
use OpenApi\Annotations as OA;
/**
* 注册admin API路由
*/
Route::group('/adminapi', function () {
Swagger::create()->registerRoute([
'route_prefix' => '/openapi',
'register_route' => true,
'openapi_doc' => [
'scan_path' => [
base_path('app/schema'), // 基础schema
base_path('app/adminapi'),
base_path('app/install'),
],
'modify' => function (OA\OpenApi $openapi) {
$openapi->info->title = config('app.name') . ' API';
$openapi->info->version = '1.0.0';
$openapi->servers = [
new OA\Server([
'url' => '/adminapi',
'description' => request()->host(),
]),
];
if (!$openapi->components instanceof OA\Components) {
$openapi->components = new OA\Components([]);
}
$openapi->components->securitySchemes = [
new OA\SecurityScheme([
'securityScheme' => 'api_key',
'type' => 'apiKey',
'name' => config('madong.jwt.app.token_name', 'Authorization'),
'in' => 'header',
]),
];
},
],
]);
});
```
## API Route Template
```php
<?php
declare(strict_types=1);
/**
*+------------------
* madong
*+------------------
* Copyright (c) https://gitee.com/motion-code All rights reserved.
*+------------------
* Author: Mr. April (405784684@qq.com)
*+------------------
*/
use Webman\Route;
use WebmanTech\Swagger\Swagger;
use OpenApi\Annotations as OA;
/**
* 注册API路由
*/
Route::group('/api', function () {
Swagger::create()->registerRoute([
'route_prefix' => '/openapi',
'register_route' => true,
'openapi_doc' => [
'scan_path' => [
base_path('app/api'),
],
'modify' => function (OA\OpenApi $openapi) {
$openapi->info->title = config('app.name') . ' API';
$openapi->info->version = '1.0.0';
$openapi->servers = [
new OA\Server([
'url' => '/api',
'description' => request()->host(),
]),
];
if (!$openapi->components instanceof OA\Components) {
$openapi->components = new OA\Components([]);
}
$openapi->components->securitySchemes = [
new OA\SecurityScheme([
'securityScheme' => 'api_key',
'type' => 'apiKey',
'name' => config('core.jwt.app.token_name', 'Authorization'),
'in' => 'header',
]),
];
},
],
]);
});
```
## Plugin Route Template
```php
<?php
declare(strict_types=1);
/**
*+------------------
* madong
*+------------------
* Copyright (c) https://gitee.com/motion-code All rights reserved.
*+------------------
* Author: Mr. April (405784684@qq.com)
*+------------------
*/
use Webman\Route;
use WebmanTech\Swagger\Swagger;
use OpenApi\Annotations as OA;
/**
* 注册{Plugin}插件路由
*/
Route::group('/{plugin_path}', function () {
// AdminAPI 路由
Route::group('/adminapi', function () {
// 控制器自动加载,无需手动注册
});
// API 路由
Route::group('/api', function () {
// 控制器自动加载,无需手动注册
});
// Swagger 文档注册
Swagger::create()->registerRoute([
'route_prefix' => '/openapi',
'register_route' => true,
'openapi_doc' => [
'scan_path' => [
// 插件的 adminapi 目录
__DIR__ . '/app/adminapi',
// 插件的 api 目录
__DIR__ . '/app/api',
],
'modify' => function (OA\OpenApi $openapi) {
$openapi->info->title = '{Plugin} API';
$openapi->info->version = '1.0.0';
$openapi->servers = [
new OA\Server([
'url' => '/{plugin_path}',
'description' => request()->host(),
]),
];
if (!$openapi->components instanceof OA\Components) {
$openapi->components = new OA\Components([]);
}
$openapi->components->securitySchemes = [
new OA\SecurityScheme([
'securityScheme' => 'api_key',
'type' => 'apiKey',
'name' => config('madong.jwt.app.token_name', 'Authorization'),
'in' => 'header',
]),
];
},
],
]);
});
```
## Route Auto-Discovery
Controllers are auto-loaded by webman based on naming convention:
| Controller | Auto Route |
|------------|------------|
| `adminapi/controller/member/MemberController.php` | `/adminapi/member/member` |
| `adminapi/controller/member/UserController.php` | `/adminapi/member/user` |
| `api/controller/auth/AuthController.php` | `/api/auth/auth` |
### Route Naming Convention
```
app/adminapi/controller/{module}/{Model}Controller.php
↓
/{module}/{model}
```
## Swagger Scan Path Configuration
### AdminAPI OpenAPI Scan
```php
'scan_path' => [
base_path('app/schema'), // Schema DTOs
base_path('app/adminapi'), // AdminAPI controllers, validates
base_path('app/install'), // Install controllers
],
```
### API OpenAPI Scan
```php
'scan_path' => [
base_path('app/api'), // API controllers
],
```
## Custom OpenAPI JSON Endpoint
When the project requires the Swagger UI to load `/openapi.json` instead of the package default `/openapi/doc`, do not modify `vendor/webman-tech/swagger`.
Use `app\controller\OpenApiController` to render the Swagger UI and generate the JSON document with `ConfigOpenapiDocDTO` and `WebmanTech\Swagger\Overwrite\Generator`. In each API route group:
```php
$openApiController = new OpenApiController();
Route::get('/openapi', static fn () => $openApiController->index('/api/openapi.json'));
Route::get('/openapi.json', static fn () => $openApiController->document($openapiDocConfig));
$config = ConfigOpenapiDocDTO::fromConfig($openapiDocConfig);
RouteRegister::create()->register((new Reader())->getData($config->getScanSources()));
```
For Admin API, use `/adminapi/openapi.json` as the Swagger UI document URL. Keep the existing scan paths and OpenAPI `modify` callback in `$openapiDocConfig` so generated documentation and annotation route registration remain consistent.
### Plugin OpenAPI Scan
当前项目插件路由以 `plugin/{plugin}/config/route.php` 为入口,按访问端分成独立路由组。已验证的 `produce` 插件使用以下分组:
- `/api`:前台 Web 接口,扫描 `base_path('plugin/{plugin}/app/api')`。
- `/appapi`:移动端/业务端 APP 接口,扫描 `base_path('plugin/{plugin}/app/appapi')`。
- `/adminapi`:管理端接口,扫描 `base_path('plugin/{plugin}/app/adminapi')`。
为插件新增业务端接口时,不要把 APP 控制器放进 `app/api`;应放入 `plugin/{plugin}/app/appapi/controller`,并在插件路由中新增或确认 `/appapi` 分组扫描该目录。OpenAPI `servers.url` 对应设置为 `/appapi`。
```php
'scan_path' => [
base_path('plugin/{plugin}/app/appapi'),
],
```
## Auto-generation Checklist
When generating route:
- [ ] Create adminapi/config/route.php with Swagger
- [ ] Create api/config/route.php with Swagger
- [ ] Configure scan_path to include all relevant directories
- [ ] Set correct security scheme (Authorization header)
- [ ] Configure server URL and description
- [ ] For plugins, use __DIR__ for relative paths
+39
View File
@@ -0,0 +1,39 @@
---
name: backend-scope
description: 数据权限作用域规范(Scope),支持全局 Scope 和查询 Scope
globs:
- "app/scope/**/*.php"
---
## 数据权限模型
| 类型 | 说明 |
|------|------|
| 全局 Scope | 自动应用在所有查询上 |
| 查询 Scope | 通过 `->scopeName()` 调用 |
## 代码模板
```php
<?php
namespace app\scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Scope;
class {Name}Scope implements Scope
{
public function apply(Builder $builder, Model $model): void
{
$builder->where('{field}', '{value}');
}
}
```
## 检查清单
- [ ] Scope 是否注册到模型
- [ ] 全局 Scope 是否影响所有查询
- [ ] 查询 Scope 命名是否清晰
+61
View File
@@ -0,0 +1,61 @@
---
name: backend-swagger
description: Swagger/OpenAPI 注解规范,自动生成 API 文档
globs:
- "app/**/controller/**/*.php"
- "app/schema/**/*.php"
---
## 注解规范
### 控制器注解
```php
use OpenApi\Attributes as OA;
#[OA\Tag(name: '{module_name}管理')]
class {Model}Controller extends Crud
{
#[OA\Get(path: '/{module_route}', summary: '列表', tags: ['{module_name}管理'])]
#[Permission(code: '{module}:{model}:list')]
public function index(Request $request): \support\Response
```
### Schema 注解
```php
#[OA\Schema(title: '创建请求')]
class {Model}CreateRequest
{
#[OA\Property(property: 'name', description: '名称', type: 'string')]
public string $name;
}
```
## 扫描路径
| 模块 | 路径 |
|------|------|
| AdminAPI 路由分组 | `/adminapi` |
| AdminAPI 注解 path | `/{module_route}`,不要重复写 `/adminapi` |
| API 路由分组 | `/api` |
| API 注解 path | `/{module_route}`,不要重复写 `/api` |
## 文档生成机制
- AdminAPI 动态文档地址:`/adminapi/openapi.json`
- AdminAPI Swagger UI:`/adminapi/openapi`
- API 动态文档地址:`/api/openapi.json`
- API Swagger UI:`/api/openapi`
- 当前项目通过 `app/adminapi/config/route.php` 和 `app/api/config/route.php` 中的 `OpenApiController::document()` 请求时动态扫描生成文档。
- 当前项目尚未内置自动导出 `openapi.json` 到文件系统或 CI 的步骤。
- 路由注册也复用相同扫描源:`RouteRegister::create()->register((new Reader())->getData($config->getScanSources()))`。
## 检查清单
- [ ] 控制器类是否有 `#[OA\Tag]`
- [ ] 每个方法是否有 `#[OA\Get/Post/Put/Delete]`
- [ ] Schema 属性是否有 `#[OA\Property]`
- [ ] 路由已注册 Swagger 扫描路径
- [ ] 后端已重启,确保新控制器被注解路由扫描到
- [ ] 访问 `/adminapi/openapi.json` 或 `/api/openapi.json` 确认动态文档包含新路径
+334
View File
@@ -0,0 +1,334 @@
---
name: backend-tests
description: 通用的测试脚本管理技能,专注于测试脚本的归类、组织和管理,避免脚本散落在根目录
---
# Tester Skill
本技能专注于测试脚本的组织、归类和管理。当需要创建、管理或执行测试脚本时,应使用此技能来确保所有测试脚本都按标准目录结构归类存放。
## 何时使用此技能
当遇到以下情况时使用此技能:
1. 测试脚本散落在项目根目录,需要整理归类
2. 需要创建新的测试脚本,需要遵循标准目录结构
3. 需要管理和组织测试脚本文件
4. 需要执行批量测试或验证
5. 需要统一的测试脚本管理规范
## 如何使用此技能
### 1. 测试目录结构管理规范
所有测试脚本必须归类存放,禁止在项目根目录创建临时测试文件。使用以下简化目录结构(最多一层子目录):
```
backend/tests/
├── plugin/ # 插件相关测试(直接放文件)
├── unit/ # 单元测试(直接放文件)
├── integration/ # 集成测试(直接放文件)
├── e2e/ # 端到端测试(直接放文件)
├── scripts/ # 测试脚本工具(直接放文件)
└── fixtures/ # 测试数据文件(直接放文件)
```
### 2. 测试脚本创建和归类流程
创建测试脚本时应遵循以下流程:
1. **确定测试类型**:根据测试目的选择适当的目录(unit/integration/e2e/plugin等)
2. **创建标准目录**:如果目录不存在,先创建标准目录结构
3. **命名规范**:使用有意义的文件名,如 `user-model-test.php`、`api-auth-test.php`
4. **归类存放**:将脚本放在正确的子目录中,避免根目录
5. **清理临时文件**:测试完成后,清理并删除临时测试文件
6. **更新目录文档**:更新相应目录的 README 或说明文件
### 2. 测试脚本归类规则
所有测试脚本必须按以下规则归类存放(最多一层目录):
#### 2.1 按测试类型归类
- **单元测试**:`tests/unit/` - 测试单个类或方法(直接放文件)
- **集成测试**:`tests/integration/` - 测试多个组件的交互(直接放文件)
- **端到端测试**:`tests/e2e/` - 测试完整业务流程(直接放文件)
- **插件测试**:`tests/plugin/` - 专门测试插件功能(直接放文件)
- **性能测试**:`tests/performance/` - 测试性能和负载(直接放文件)
#### 2.2 命名规范
- 使用有意义的文件名,如 `user-model-test.php`、`api-auth-test.php`
- 文件名应反映测试内容,避免通用名称如 `test.php`、`check.php`
#### 2.3 禁止行为
- ❌ 禁止在项目根目录创建测试脚本
- ❌ 禁止在 `public/`、`vendor/` 等目录创建测试脚本
- ❌ 禁止使用临时文件名(如 `test1.php`、`temp.php`)
- ❌ 禁止测试脚本残留临时文件
- ❌ 禁止创建多层子目录结构(最多一层)
### 4. 测试脚本质量标准
所有测试脚本应满足以下质量标准:
- ✅ 按标准目录结构归类存放
- ✅ 使用有意义的文件名,描述测试内容
- ✅ 包含清晰的注释和文档说明
- ✅ 输出标准的测试结果格式(✓/✗/⚠)
- ✅ 自动清理临时文件和资源
- ✅ 支持命令行参数和配置选项
- ✅ 可独立运行,不依赖复杂环境
### 5. 常用测试脚本模板
#### 4.1 单元测试模板(简化版)
```php
<?php
// tests/unit/UserModelTest.php
namespace Tests\Unit;
use App\Models\User;
class UserModelTest
{
public function testUserCreation()
{
echo "=== 用户模型测试 ===\n";
// 测试用户创建逻辑
$user = new User(['name' => 'Test User']);
if ($user->name === 'Test User') {
echo "✓ 用户创建测试成功\n";
} else {
echo "✗ 用户创建测试失败\n";
}
}
}
```
#### 4.2 API集成测试模板(简化版)
```php
<?php
// tests/integration/AuthApiTest.php
namespace Tests\Integration;
class AuthApiTest
{
public function testLoginApi()
{
echo "=== API认证测试 ===\n";
// 测试登录API逻辑
$response = $this->simulateApiCall('/api/login', [
'username' => 'test',
'password' => 'password'
]);
if ($response['status'] === 200 && isset($response['token'])) {
echo "✓ API认证测试成功\n";
} else {
echo "✗ API认证测试失败\n";
}
}
private function simulateApiCall($endpoint, $data)
{
// 模拟API调用逻辑
return ['status' => 200, 'token' => 'mock_token'];
}
}
```
#### 4.3 插件安装测试模板(简化版)
```php
<?php
// tests/plugin/MigrationTest.php
namespace Tests\Plugin;
class MigrationTest
{
public function run()
{
echo "=== 迁移文件测试 ===\n";
// 测试迁移文件语法
$this->testMigrationSyntax();
// 测试索引名称长度
$this->testIndexNames();
// 测试引擎设置
$this->testEngineSettings();
}
private function testMigrationSyntax()
{
// 迁移文件语法测试逻辑
}
}
```
### 6. 批量测试执行和归类检查
#### 5.1 运行所有测试(简化版)
使用 `tests/scripts/run-all-tests.php` 脚本执行所有归类好的测试:
```php
<?php
// tests/scripts/run-all-tests.php
echo "=== 批量测试执行 ===\n";
// 扫描所有测试目录中的文件
$testDirectories = ['plugin', 'unit', 'integration', 'e2e'];
foreach ($testDirectories as $directory) {
$dirPath = __DIR__ . '/../' . $directory;
if (!is_dir($dirPath)) {
continue;
}
echo "=== 执行 {$directory} 测试 ===\n";
$files = scandir($dirPath);
foreach ($files as $file) {
if ($file !== '.' && $file !== '..' && pathinfo($file, PATHINFO_EXTENSION) === 'php') {
$testPath = $dirPath . '/' . $file;
include $testPath;
}
}
}
```
#### 5.2 检查测试脚本归类(简化版)
使用以下脚本检查是否有未归类的测试文件:
```php
<?php
// tests/scripts/check-classification.php
echo "=== 测试脚本归类检查 ===\n";
// 允许的目录
$allowedDirs = ['tests/', 'vendor/'];
// 查找项目根目录中的测试文件
function findTestFiles($dir)
{
$testFiles = [];
$patterns = ['/test.*\.php$/i', '/.*test\.php$/i', '/check.*\.php$/i'];
if ($handle = opendir($dir)) {
while (false !== ($entry = readdir($handle))) {
if ($entry === '.' || $entry === '..') continue;
$path = $dir . '/' . $entry;
if (is_dir($path)) {
// 递归查找子目录
$testFiles = array_merge($testFiles, findTestFiles($path));
} else {
// 检查是否是测试文件
foreach ($patterns as $pattern) {
if (preg_match($pattern, $entry)) {
$testFiles[] = $path;
break;
}
}
}
}
closedir($handle);
}
return $testFiles;
}
// 检查测试文件是否在允许的目录中
$projectRoot = realpath(__DIR__ . '/../../');
$allTestFiles = findTestFiles($projectRoot);
$unclassified = [];
foreach ($allTestFiles as $file) {
$isAllowed = false;
foreach ($allowedDirs as $allowed) {
if (strpos($file, $allowed) !== false) {
$isAllowed = true;
break;
}
}
if (!$isAllowed) {
$unclassified[] = $file;
}
}
// 输出结果
if (count($unclassified) > 0) {
echo "⚠ 发现 " . count($unclassified) . " 个未正确归类的测试脚本:\n\n";
foreach ($unclassified as $file) {
$relativePath = str_replace($projectRoot . '/', '', $file);
echo " • " . $relativePath . "\n";
}
echo "\n请将这些文件移动到 tests/ 目录下的相应子目录中。\n";
} else {
echo "✓ 所有测试脚本都已正确归类到 tests/ 目录中。\n";
}
```
### 6. 测试结果验证
每个测试脚本应输出明确的验证结果:
- ✓ 成功:绿色标记
- ✗ 失败:红色标记
- ⚠ 警告:黄色标记
- 详细说明:问题描述和修复建议
### 7. 临时文件管理
所有测试脚本应在完成后清理临时文件:
```php
// 清理临时文件
$tempFiles = ['test_*.php', 'check_*.php', 'fix_*.php'];
foreach ($tempFiles as $pattern) {
// 查找并删除临时文件
}
```
## 技能资源
本技能包含以下资源:
### 脚本资源 (`scripts/`)
- `create-test-directory.php`:创建测试目录结构的脚本
- `test-template-generator.php`:生成各种测试脚本模板
- `validate-migration-files.php`:验证迁移文件的完整脚本
- `check-index-names.php`:检查索引名称长度的脚本
### 参考资源 (`references/`)
- `mysql-limitations.md`:MySQL 限制参考(标识符长度、引擎等)
- `laravel-migration-guide.md`:Laravel 迁移最佳实践指南
- `plugin-install-flow.md`:插件安装流程和常见问题
- `testing-best-practices.md`:测试最佳实践和标准
### 资产资源 (`assets/`)
- `test-directory-structure.txt`:测试目录结构模板
- `test-script-templates/`:各种测试脚本模板文件
- `expected-output-examples/`:预期输出示例
## 执行流程示例
### 场景1:测试脚本归类检查
1. 使用 `tests/scripts/check-classification.php` 检查是否有未归类的测试脚本
2. 将发现的临时测试文件移动到正确的 `tests/` 子目录中
3. 使用有意义的文件名重命名测试文件
4. 执行 `tests/scripts/cleanup-temp.php` 清理残留临时文件
### 场景2:创建新的插件测试
1. 在 `tests/plugin/` 目录下创建新的测试文件,如 `official-plugin-test.php`
2. 编写测试逻辑,包含清晰的注释和输出格式
3. 执行测试验证功能是否正常
4. 确保测试完成后清理所有临时资源
### 场景3:批量执行测试
1. 使用 `tests/scripts/run-all-tests.php` 执行所有测试
2. 查看测试结果报告,识别失败的测试
3. 修复失败的测试或相关问题
4. 重新执行测试确保所有测试通过
通过本技能,可以系统化地管理和执行插件安装相关的测试工作,确保所有修复都经过充分验证,避免重复问题。
+35
View File
@@ -0,0 +1,35 @@
---
name: backend-upload
description: 文件上传规范,支持 Local/COS/OSS/Qiniu/S3 6种存储驱动
globs:
- "core/upload/**/*.php"
---
## 存储驱动
| 驱动 | 类 | 说明 |
|------|-----|------|
| Local | `storage/Local.php` | 本地存储 |
| COS | `storage/Cos.php` | 腾讯云 COS |
| OSS | `storage/Oss.php` | 阿里云 OSS |
| Qiniu | `storage/Qiniu.php` | 七牛云 |
| S3 | `storage/S3.php` | AWS S3 兼容 |
## 使用方式
```php
use core\upload\UploadFile;
// 上传文件
$url = UploadFile::upload($file, 'images');
// 删除文件
UploadFile::delete($url);
```
## 检查清单
- [ ] 上传配置是否包含 bucket/region/secret
- [ ] 文件类型是否在白名单内
- [ ] 文件大小是否限制
- [ ] 是否生成唯一文件名
@@ -0,0 +1,66 @@
---
name: cross-api-convention
description: 前后端 API 对接规范,响应格式/错误码/查询操作符/分页
globs:
- "**/*.php"
- "**/*.ts"
---
## 响应格式
### 成功响应
```json
{
"code": 0,
"msg": "ok",
"data": { "items": [...], "total": 100 }
}
```
### 失败响应
```json
{
"code": -1,
"msg": "操作失败",
"data": null
}
```
## 错误码
| code | 说明 |
|------|------|
| 0 | 成功 |
| -1 | 业务失败 |
| 400 | 参数错误 |
| 401 | 未认证 |
| 403 | 无权限 |
| 404 | 资源不存在 |
| 500 | 服务器错误 |
## 查询操作符
| 操作符 | 说明 | 示例 |
|--------|------|------|
| `LIKE_` | 模糊匹配 | `LIKE_name=菜单` |
| `EQ_` | 精确匹配 | `EQ_status=1` |
| `NE_` | 不等于 | `NE_setting_type=issue` |
| `GT_` | 大于 | `GT_sort=10` |
| `LT_` | 小于 | `LT_sort=100` |
| `BETWEEN_` | 范围 | `BETWEEN_created_at=1700000000,1800000000` |
## 分页参数
| 参数 | 默认值 | 说明 |
|------|--------|------|
| page | 1 | 页码 |
| limit | 15 | 每页条数 |
| field | '' | 排序字段 |
| order | 'desc' | 排序方向 |
## 检查清单
- [ ] 响应格式是否为 {code, msg, data}
- [ ] 成功时 code 是否为 0
- [ ] 列表是否返回 items 和 total
- [ ] 查询参数是否使用标准操作符
@@ -0,0 +1,47 @@
---
name: cross-app-skeleton
description: 站点脚手架规范,项目结构/目录组织/模块划分
globs:
- "**/*.php"
- "**/*.ts"
---
## 项目结构
```
madong/
├── backend/ # PHP Webman 后端
│ ├── app/ # 应用代码
│ │ ├── adminapi/ # 管理后台 API
│ │ ├── api/ # 前台 API
│ │ ├── model/ # 模型
│ │ ├── dao/ # 数据访问
│ │ ├── service/ # 服务层
│ │ ├── enum/ # 枚举
│ │ └── command/ # 命令
│ ├── core/ # 核心基础设施
│ ├── config/ # 配置
│ ├── plugin/ # 插件
│ └── packages/ # 本地包
├── frontend/
│ ├── admin/ # 管理后台 (Vue 3 + Element Plus)
│ └── web/ # 前台网站 (Nuxt 4)
└── doc/ # 文档
```
## 模块分层
| 层级 | 目录 | 依赖 |
|------|------|------|
| Controller | `{app}/controller/` | Service |
| Validate | `{app}/validate/` | - |
| Service | `{app}/service/` | DAO |
| DAO | `{app}/dao/` | Model |
| Model | `{app}/model/` | - |
| Schema | `{app}/schema/` | - |
## 检查清单
- [ ] 目录结构是否符合项目规范
- [ ] 模块分层是否正确
- [ ] 依赖方向是否单向(Controller→Service→DAO→Model)
+38
View File
@@ -0,0 +1,38 @@
---
name: cross-database
description: 数据库设计规范,命名约定/索引/迁移/ER 设计
globs:
- "database/**/*.php"
- "**/*.php"
---
## 命名约定
| 对象 | 规范 | 示例 |
|------|------|------|
| 数据库名 | snake_case | `madong_saas` |
| 表名 | `{prefix}_{module}_{table}` | `system_menu`, `member_user` |
| 字段名 | snake_case | `created_at`, `parent_id` |
| 主键 | `bigInteger` 雪花 ID | `id` |
| 索引 | 优先遵循所在模块既有命名;`produce` 插件当前使用 `idx_{column}` 等简短名称 | `idx_order_no`、`idx_step_id` |
| 外键 | `{table}_{foreign}_foreign` | `menu_parent_id_foreign` |
## 字段类型
| MySQL 类型 | 用途 |
|-----------|------|
| bigint | 主键(雪花 ID) |
| varchar | 字符串(长度 50-500) |
| text | 长文本 |
| json | JSON 数据 |
| tinyint | 状态/枚举(0/1) |
| timestamp | 时间戳 |
| decimal | 金额(10,2) |
## 检查清单
- [ ] 表名是否包含模块前缀
- [ ] 主键是否为 bigint 雪花 ID
- [ ] 是否包含 created_at/updated_at/deleted_at
- [ ] 索引是否覆盖查询条件
- [ ] 字段类型是否合理
@@ -0,0 +1,476 @@
---
name: cross-fullstack-admin-crud-feature
description: Full-stack development workflow for Madong Admin CRUD features. Covers requirement analysis, existing implementation verification, backend migration/model/DAO/service/validate/schema/controller, frontend API/page/schema, menu permissions, migration execution, validation, and skill auto-evolution.
globs:
- "backend/app/**/*.php"
- "backend/resource/database/migrations/**/*.php"
- "backend/resource/data/menu/**/*.php"
- "frontend/admin/src/api/**/*.ts"
- "frontend/admin/src/views/**/*.vue"
- "frontend/admin/src/views/**/*.tsx"
- ".windsurf/skills/**/*.md"
---
# Madong Admin 全栈 CRUD 功能开发流程
用于开发类似“应用授权”这类后台管理功能,覆盖数据库、后端、前端、菜单权限、迁移执行与校验。
## 适用场景
- 在 Admin 后台新增一个业务管理页面。
- 需要新增数据库表、后端 CRUD、前端列表/表单/API。
- 需要菜单入口和按钮权限。
- 需要参考现有模块目录结构,例如 `/tools/delegation`、`/setting/notice`。
- 需要自动生成字段或自定义操作,例如 Access Key / Secret Key 自动生成、重置密钥。
## 总原则
1. 先核对项目真实实现,再使用 skill 或模板。
2. 以现有项目代码为准,skill 与项目不一致时先更新 skill。
3. 优先复用官方现有组件和基类。
4. 不直接照搬 Laravel Artisan 命令,当前项目使用 Webman/Madong 命令体系。
5. 数据库迁移执行前必须先 dry-run,确认会执行哪些迁移。
6. 前后端路径、权限码、菜单码必须保持一致。
## 开发前检查
### 1. 确认参考模块
优先读取用户指定的参考模块。
常见参考:
- `frontend/admin/src/views/setting/notice`
- `frontend/admin/src/views/tools/delegation`
- `frontend/admin/src/api/tools/delegation`
- `backend/app/adminapi/controller/notice/NoticeController.php`
- `backend/app/adminapi/controller/plugin/PluginMadongAuthController.php`
### 2. 确认真实项目规范
至少核对以下内容:
- 后端控制器是否继承 `app\adminapi\controller\Crud`。
- 模型是否继承 `core\base\BaseModel`。
- DAO 是否继承 `core\base\BaseDao`。
- Service 是否继承 `core\base\BaseService`。
- Validate 是否继承 `core\base\BaseValidate`。
- 前端 CRUD 是否使用 `useCrud`、`CrudSchema`、`BasicCrud`。
- 前端 API 是否使用 `BaseService` 和 `@/utils/http`。
- 表单组件名是否存在于 `frontend/admin/src/components/form/components`。
- 菜单数据是否维护在 `backend/resource/data/menu/admin.php`。
## 后端开发流程
### 1. 数据库迁移
路径:
```text
backend/resource/database/migrations/{timestamp}_create_{table}_table.php
```
当前项目雪花 ID 主键写法:
```php
$table->bigInteger('id')->primary()->comment('主键');
```
不要使用 Laravel 默认的:
```php
$table->id();
```
字段设计要求:
- 主键使用雪花 ID。
- 按需增加唯一索引、普通索引。
- 状态字段常用 `enabled`,默认 `1`。
- 时间字段按项目迁移写法使用 `timestamps()` 或项目已有封装。
- 如果模型使用软删除,迁移需包含软删除字段。
### 2. Model
路径:
```text
backend/app/model/{module}/{Model}.php
```
要求:
- 继承 `core\base\BaseModel`。
- 设置 `$table`。
- 设置 `$fillable`。
- 设置 `$casts`。
- 如项目同类模型使用软删除,则保持一致。
### 3. DAO
路径:
```text
backend/app/dao/{module}/{Model}Dao.php
```
要求:
- 继承 `core\base\BaseDao`。
- `setModel()` 返回模型类。
- 如需关键字搜索,按项目已有 DAO 实现 `getKeywordFields()`。
### 4. Service
路径:
```text
backend/app/service/admin/{module}/{Model}Service.php
```
要求:
- 继承 `core\base\BaseService`。
- 构造函数注入 DAO。
- 自定义业务逻辑放在 Service。
- 自动生成字段在 `save()` / `create()` 入口处理。
- 自定义操作单独暴露方法,例如 `resetSecret()`。
应用授权类功能示例:
- 新增时自动生成 `access_key`。
- 新增时自动生成 `secret_key`。
- 重置时只更新 `secret_key`。
- 密钥生成应使用安全随机源,例如 `random_bytes()`。
### 5. Validate
路径:
```text
backend/app/adminapi/validate/{module}/{Model}Validate.php
```
要求:
- 继承 `core\base\BaseValidate`。
- 定义 `$rule`、`$message`、`$scene`。
- `store` 场景不要求 `id`。
- `update` 场景要求 `id`。
- 自动生成字段不要暴露为必填输入。
### 6. Request / Response Schema
请求路径:
```text
backend/app/adminapi/schema/request/{module}/{Model}FormRequest.php
backend/app/adminapi/schema/request/{module}/{Model}QueryRequest.php
```
响应路径:
```text
backend/app/adminapi/schema/response/{module}/{Model}Response.php
```
要求:
- FormRequest 继承项目实际存在的表单基类。
- QueryRequest 使用项目实际存在的 `BaseQueryRequest`。
- 不要引用不存在的 `PageRequest`。
- OpenAPI 属性需与控制器 `x: [SchemaConstants::X_SCHEMA_REQUEST => ...]` 对应。
### 7. Controller
路径:
```text
backend/app/adminapi/controller/{module}/{Model}Controller.php
```
要求:
- 继承 `Crud`。
- 构造函数注入 Service 和 Validate。
- 标准接口调用 `parent::index()`、`parent::show()`、`parent::store()`、`parent::update()`、`parent::destroy()`。
- 路由通过 OpenAPI Attribute 标注。
- 权限码与前端和菜单保持一致。
推荐路由格式:
```text
GET /{module}/{resource}
GET /{module}/{resource}/{id}
POST /{module}/{resource}
PUT /{module}/{resource}/{id}
DELETE /{module}/{resource}/{id}
POST /{module}/{resource}/{id}/{custom-action}
```
应用授权示例:
```text
/plugin/application-auth
/plugin/application-auth/{id}
/plugin/application-auth/{id}/reset-secret
```
## 前端开发流程
### 1. API
路径参考用户指定目录结构。
例如参考 `/tools/delegation` 时:
```text
frontend/admin/src/api/tools/{feature}/index.ts
frontend/admin/src/api/tools/{feature}/types.ts
```
要求:
- 使用 `BaseService`。
- 使用 `@/utils/http`。
- 基础路径与后端路由一致。
- 自定义接口单独封装方法。
示例:
```ts
const baseUrl = '/plugin/application-auth'
```
### 2. 页面
路径参考用户指定目录结构。
例如参考 `/tools/delegation` 时:
```text
frontend/admin/src/views/tools/{feature}/index.vue
frontend/admin/src/views/tools/{feature}/schemas/index.tsx
```
要求:
- 使用 `useCrud` 和 `BasicCrud`。
- CRUD 配置写在 `schemas/index.tsx`。
- 不要使用项目未注册的表单组件。
- 当前项目日期时间选择使用 `DatePicker` + `componentProps.type = 'datetime'`。
- 当前项目 `CrudSchema` 列类型不支持 `type: 'operate'`,操作列由 `BasicCrud` 根据 `tableActions` / `dropDownActions` 自动生成。
### 3. 列表字段
按照用户需求定义:
- 序号:`type: 'index'`
- 名称:普通列
- Access Key:建议列表遮罩显示
- Secret Key:建议列表遮罩显示
- 失效时间:使用 `formatDateTime`
- 状态:使用 `ApiDict`
- 描述:`showOverflowTooltip`
- 创建时间:优先使用后端格式化字段,或 `formatDateTime(created_at)`
- 操作:通过 CRUD 操作配置生成
### 4. 表单字段
自动生成字段不要出现在新增表单中。
应用授权示例:
- `name`:必填
- `expired_at`:可选
- `enabled`:必填,默认 `1`
- `description`:可选
- `access_key`:后端自动生成,不手填
- `secret_key`:后端自动生成,不手填
### 5. 自定义操作
例如重置 Secret Key:
- 前端按钮放在 `dropDownActions` 或项目实际支持的操作配置中。
- 按钮增加 `auth`。
- 操作前使用 `ElMessageBox.confirm` 二次确认。
- 成功后提示新密钥并刷新列表。
## 菜单与权限
菜单数据路径:
```text
backend/resource/data/menu/admin.php
```
页面菜单示例字段:
```php
"title" => "应用授权",
"code" => "plugin:application-auth:list",
"type" => 2,
"path" => "/tools/application-auth",
"component" => "/tools/application-auth/index",
"methods" => "GET",
```
按钮权限建议补齐:
```text
plugin:application-auth:create
plugin:application-auth:delete
plugin:application-auth:update
plugin:application-auth:read
plugin:application-auth:reset-secret
```
按钮节点:
- `type` 使用 `3`。
- `is_show` 使用 `0`。
- `path`、`component`、`icon` 通常为 `null`。
- `methods` 与接口方法保持一致。
## 迁移执行流程
当前项目没有 Laravel `artisan` 文件,不使用:
```bash
php artisan migrate
php artisan migrate:status
```
当前项目使用:
```bash
php webman madong-migrate status
php webman madong-migrate up --dry-run
php webman madong-migrate up
```
### 执行前必须 dry-run
```bash
php webman madong-migrate up --dry-run
```
确认输出中将执行的迁移文件。
注意:
- `madong-migrate up` 会执行所有未执行迁移。
- 当前命令不支持 Laravel 的 `--path=` 单文件迁移参数。
- 如果只允许迁移单个文件,不要直接执行 `madong-migrate up`。
### 事务异常注意事项
如果执行迁移时出现:
```text
There is no active transaction
```
常见原因:
- MySQL DDL 会隐式提交事务。
- 迁移命令将多个 DDL 包在事务中,可能导致事务管理器提交时发现事务已结束。
处理流程:
1. 先不要重复执行迁移。
2. 检查数据表是否已经创建成功。
3. 执行 `php webman madong-migrate status` 查看迁移日志状态。
4. 如果表已创建但日志未写入,需要谨慎补齐迁移日志或修复迁移命令事务逻辑后再处理。
5. 不要盲目回滚或再次执行全部迁移,避免表已存在导致二次错误。
## 校验清单
### 后端校验
对新增 PHP 文件执行语法检查:
```bash
php -l app/model/{module}/{Model}.php
php -l app/dao/{module}/{Model}Dao.php
php -l app/service/admin/{module}/{Model}Service.php
php -l app/adminapi/validate/{module}/{Model}Validate.php
php -l app/adminapi/schema/request/{module}/{Model}FormRequest.php
php -l app/adminapi/schema/request/{module}/{Model}QueryRequest.php
php -l app/adminapi/schema/response/{module}/{Model}Response.php
php -l app/adminapi/controller/{module}/{Model}Controller.php
php -l resource/database/migrations/{migration_file}.php
php -l resource/data/menu/admin.php
```
### 前端校验
只检查新增文件:
```bash
pnpm exec eslint src/views/{module}/{feature}/index.vue src/views/{module}/{feature}/schemas/index.tsx src/api/{module}/{feature}/index.ts src/api/{module}/{feature}/types.ts
```
### 路径一致性校验
检查以下内容必须一致:
- 后端 Controller OpenAPI path。
- 前端 API `baseUrl`。
- 菜单 `path`。
- 菜单 `component`。
- 前端 view 文件路径。
- 权限码。
## Skill 自动进化规则
开发过程中如果发现 skill 与真实项目不一致,必须先更新 skill,再继续使用。
需要自动进化的典型情况:
- 迁移主键写法与项目不一致。
- 前端 API 模板导入路径不一致。
- 表单组件名不存在。
- CRUD 列类型与 TypeScript 类型不一致。
- 菜单权限结构与项目实际不一致。
- 路由路径或权限码规则与项目实际不一致。
更新要求:
- 只更新已验证的不一致点。
- 不做无关重写。
- 最终总结中说明更新了哪些 skill。
## 完整输出清单
完成一个 Admin 全栈 CRUD 功能后,应至少包含:
```text
backend/resource/database/migrations/{migration}.php
backend/app/model/{module}/{Model}.php
backend/app/dao/{module}/{Model}Dao.php
backend/app/service/admin/{module}/{Model}Service.php
backend/app/adminapi/validate/{module}/{Model}Validate.php
backend/app/adminapi/schema/request/{module}/{Model}FormRequest.php
backend/app/adminapi/schema/request/{module}/{Model}QueryRequest.php
backend/app/adminapi/schema/response/{module}/{Model}Response.php
backend/app/adminapi/controller/{module}/{Model}Controller.php
frontend/admin/src/api/{section}/{feature}/index.ts
frontend/admin/src/api/{section}/{feature}/types.ts
frontend/admin/src/views/{section}/{feature}/index.vue
frontend/admin/src/views/{section}/{feature}/schemas/index.tsx
backend/resource/data/menu/admin.php
```
## 最终回复模板
完成后向用户说明:
- 新增了哪些后端文件。
- 新增了哪些前端文件。
- 菜单和权限是否已配置。
- 迁移是否执行,执行结果如何。
- 校验命令和结果。
- 是否自动进化了 skill。
- 是否有需要用户后续处理的事项。
@@ -0,0 +1,536 @@
---
name: cross-plugin-fullstack-development
description: Full-stack development workflow for Madong plugins. Covers plugin structure, backend CRUD and Swagger, frontend template source, menu import, install copy behavior, i18n, verification, and anti-regression rules for uninstall/reinstall scenarios.
globs:
- "backend/plugin/**/*.php"
- "backend/plugin/**/config/**/*.php"
- "backend/plugin/**/resource/**/*.php"
- "backend/plugin/**/resource/template/admin/**/*"
- "frontend/admin/src/apps/**/*.vue"
- "frontend/admin/src/apps/**/*.tsx"
- "frontend/admin/src/apps/**/*.ts"
- ".windsurf/skills/**/*.md"
---
# Madong 插件全流程开发技能
用于开发类似 `article` 这样的完整插件,覆盖从目录结构、后端 CRUD、Swagger、菜单导入、前端模板源、安装复制机制、国际化,到卸载重装后的防回退验证。
## 适用场景
- 新建一个完整业务插件。
- 为已有插件补齐后台 CRUD、前台模板、菜单和语言文件。
- 修复插件卸载再安装后的页面回退、菜单异常、Swagger 冲突。
- 将一次真实插件开发过程沉淀为可复用工作流。
## 总原则
1. 先核对项目真实实现,再使用 skill 或模板。
2. 插件前端的权威源目录是 `backend/plugin/{plugin}/resource/template/admin`,不是运行目录。
3. 安装后复制出来的 `frontend/admin/src/apps/{plugin}` 属于运行产物,不作为正式开发目录;若只改这里,卸载重装会回退。
4. 后端 Swagger 路由必须唯一,旧演示接口不可与新 CRUD 路由重复。
5. 插件菜单以安装导入的系统菜单为准,不依赖前端自动扫描插件路由生成侧边栏。
6. 前后端路径、权限码、菜单 component、i18n key 必须一致。
7. 插件 skill 若与真实项目不一致,先更新 skill 再继续生成或修改。
8. 插件开发顺序默认采用 template-first:后端完成后直接开发 `resource/template/admin`,不再单独维护运行目录。
## 先理解插件安装与复制机制
### 1. 模板源与运行目录
**模板源目录:**
```text
backend/plugin/{plugin}/resource/template/admin/
```
**安装后目标目录:**
```text
frontend/admin/src/apps/{plugin}/
```
当前项目会在插件安装时通过 `core\plugin\traits\TemplateTrait::copyTemplateDir('admin')`
把模板源整体复制到前端运行目录。
### 2. 关键结论
- 改 `frontend/admin/src/apps/{plugin}` 只会影响当前运行结果。
- 改 `backend/plugin/{plugin}/resource/template/admin` 才能影响未来安装结果。
- 只要模板源还是旧版,卸载再安装就会把旧版重新覆盖回来。
### 3. 正确维护顺序
修改插件前端时遵循:
1. 先完成后端开发与接口联调。
2. 后端完成后,直接在 `backend/plugin/{plugin}/resource/template/admin` 下开发前端模板源。
3. 不要把 `frontend/admin/src/apps/{plugin}` 作为正式开发目录。
4. 完成模板源后,通过插件安装或复制流程生成运行目录并验证结果。
5. 仅当用户明确要求排查运行态问题时,才临时查看运行目录,但不要把它作为主维护对象。
## 开发前检查
### 1. 确认插件真实目录结构
以 `article` 为例:
```text
backend/plugin/article/
├── app/
├── config/
├── resource/
│ ├── menu/
│ ├── template/admin/
│ └── ...
```
至少核对:
- `app/adminapi/controller`
- `app/service/admin`
- `app/dao`
- `app/model`
- `app/validate` 或 `app/adminapi/validate`
- `resource/template/admin/api`
- `resource/template/admin/views`
- `resource/template/admin/lang`
- `resource/menu/admin.php`
- `config/translation.php`
- `config/route.php`
### 2. 确认参考实现
优先寻找以下真实参考:
- 同类已完成插件
- 主项目现有 `BasicCrud` 页面
- 主项目已接好 Swagger 的 `Crud` 控制器
- 现有插件菜单定义
- 现有插件语言文件加载方式
### 3. Windows 开发环境检查
在 Windows 下开发插件时,注意新插件目录创建本身未必会立即触发外层监控重启。
如果新插件接口返回 404,需要核对监控是否覆盖 `backend/plugin` 根目录变更。
## 插件目录与产物约定
## 后端
```text
backend/plugin/{plugin}/app/
├── adminapi/controller/
├── adminapi/validate/
├── api/controller/
├── dao/
├── model/
├── schema/request/
├── schema/response/
└── service/admin/
```
## 模板源前端
```text
backend/plugin/{plugin}/resource/template/admin/
├── api/index.ts
├── lang/
│ ├── zh-cn/zh.json
│ └── en/en.json
└── views/{plugin_key}/{module}/
├── index.vue
└── schemas/index.tsx
```
## 安装后前端运行目录
```text
frontend/admin/src/apps/{plugin}/
├── api/index.ts
├── lang/
│ ├── zh-cn/zh.json
│ └── en/en.json
└── views/{plugin_key}/{module}/
├── index.vue
└── schemas/index.tsx
```
## 后端开发流程
### 1. 数据库迁移
路径:
```text
backend/plugin/{plugin}/resource/database/migrations/
```
要求:
- 主键优先使用雪花 ID:`bigInteger('id')->primary()`。
- 按真实项目风格补齐索引、时间字段、软删除字段。
- 状态字段优先使用 `enabled` 或业务已有枚举字段。
- 不要直接照搬 Laravel 默认 `id()`。
- 不要从旧项目或旧插件盲目继承 `tenant_id`、多租户索引、租户作用域;必须先核对当前项目是否仍为单租户结构。
### 2. Model / DAO / Service / Validate
要求:
- Model 继承插件或项目当前实际使用的 `BaseModel`。
- DAO 继承 `core\base\BaseDao`。
- Service 继承 `core\base\BaseService`。
- Validate 继承 `core\base\BaseValidate`。
- 自定义业务逻辑放在 Service,不要塞进 Controller。
本项目额外要求:
- DAO / Service 文件中必须显式 `use core\base\BaseDao;`、`use core\base\BaseService;`,不能只写 `extends BaseDao/BaseService` 而省略导入。
- Service 构造函数使用容器注入 DAO,例如 `__construct(FooDao $dao)`,不要在 Service 内部手动 `new FooDao()`。
- 如果 DAO 重写 `selectList()`,方法签名必须与当前项目 `BaseDao` 保持一致,包括:
- `string|array $field = '*'`
- `int $page = 0`
- `int $limit = 0`
- `bool $search = false`
- `?array $withoutScopes = null`
- `getKeywordFields()` 优先使用 `protected`,与当前项目已落地 DAO 风格保持一致。
- 若页面会通过 `Crud` 的 `format=select` 复用下拉,模型应提供 `getNameAttribute()`,把业务显示字段映射到 `name`,以兼容 `title / name / real_name / id` 标签解析顺序。
- JSON 字段应同时在 Model 中配置 `casts`,并在 Validate 中支持“数组或 JSON 字符串”两种输入形态。
### 3. Admin 控制器与 CRUD
控制器通常放在:
```text
backend/plugin/{plugin}/app/adminapi/controller/
```
要求:
- 继承 `app\adminapi\controller\Crud`。
- 构造函数注入 Service 和 Validate。
- 标准方法走 `parent::index()`、`parent::show()`、`parent::store()`、`parent::update()`、`parent::destroy()`。
- 路由路径与前端 `BaseService` 的 `baseUrl` 一致。
### 4. Swagger / OpenAPI 规范
插件最容易踩的坑之一是 Swagger 路由冲突。
必须遵守:
- 每个 `OA\Get`、`OA\Post`、`OA\Put`、`OA\Delete` 的 `path` 必须唯一。
- 新 CRUD 控制器不要和旧 demo Controller 使用相同路径。
- 若存在旧演示接口,迁移到 `legacy` 命名空间或改用不冲突路径。
- 详情、删除、批量删除等接口按项目现有 Swagger 属性写法统一。
- `IdRequest`、`BatchDeleteRequest`、`SchemaConstants::X_SCHEMA_REQUEST` 等参数约定要与项目现有控制器保持一致。
### 5. 路由与权限码
建议保持如下映射:
```text
GET /{plugin}/{resource}
GET /{plugin}/{resource}/{id}
POST /{plugin}/{resource}
PUT /{plugin}/{resource}/{id}
DELETE /{plugin}/{resource}/{id}
```
要求:
- `addAuth`、`editAuth`、`removeAuth`、`viewAuth` 与后端权限码一致。
- 菜单按钮权限码与控制器 `Permission` 属性一致。
- 不要出现前端权限码、后端权限码、菜单权限码三套不一致。
## 前端模板源开发流程
### 1. 后端完成后直接开发模板源
插件前端正式维护目录:
```text
backend/plugin/{plugin}/resource/template/admin/
```
后端完成后,直接在该目录开发插件前端。
默认不要单独开发 `frontend/admin/src/apps/{plugin}`。
运行目录只作为安装产物和验证结果使用。
### 2. index.vue 统一使用官方 BasicCrud
标准页面入口应为:
- `BasicCrud`
- `useCrud`
- `crudSchema`
不要继续保留插件 demo 里的:
- `el-card`
- 手写 `el-table`
- `fetchData()` 拉取列表
### 3. schemas/index.tsx
要求:
- 使用 `CrudSchema`。
- 列表接口优先接 `BaseService.list`。
- 标准 CRUD 页面使用 `crudApi.add/edit/remove/view`。
- `rowKey` 默认使用 `id`。
- 表头索引统一使用 `article.common.columns.index` 这类语言键。
- 查询条件使用项目真实后端支持的查询参数格式,例如 `LIKE_name`、`LIKE_title`。
### 4. API 层
插件模板 API 通常放在:
```text
backend/plugin/{plugin}/resource/template/admin/api/index.ts
```
要求:
- 使用 `BaseService` 生成标准 CRUD Service。
- 仍可保留兼容旧页面的 `pluginApi` 包装方法。
- `baseUrl` 必须与后端控制器路径一致。
- 自定义只读接口可继续使用 `request.get()` 封装。
- 如运行目录为调试临时新增了 API 类型、Service 或语言键,最终必须同步回模板源,否则插件重装后会回退。
### 5. i18n 语言文件
路径:
```text
backend/plugin/{plugin}/resource/template/admin/lang/zh-cn/zh.json
backend/plugin/{plugin}/resource/template/admin/lang/en/en.json
```
要求:
- 语言文件根对象使用插件名,例如 `article`。
- 如果 `schemas` 中使用了:
- `common.columns.index`
- `modules.*.search.*`
- `modules.*.form.*`
- `modules.*.columns.*`
则中英文都必须补齐。
- 不要只改中文不改英文,也不要只改运行目录不改模板源。
## 插件菜单与路由加载规则
### 1. 插件菜单来源
插件侧边栏应来自**安装时导入的系统菜单**,不是前端自动扫描插件 `routes/index.ts`。
因此:
- 不要依赖 `frontend/admin/src/apps/{plugin}/routes/index.ts` 自动显示菜单。
- 插件安装后应由 `sys_menu` 数据决定导航是否出现。
### 2. 菜单 component 路径
菜单中的 `component` 应填写相对 `views` 的组件路径,并包含插件 key 目录段。
例如页面文件:
```text
frontend/admin/src/apps/article/views/article/content/index.vue
```
则菜单 component 应使用类似:
```text
/article/content/index
```
### 3. meta.module
安装导入菜单时,`app` 字段应默认使用插件名 `{plugin}`,以便运行时生成:
```text
meta.module = {plugin}
```
这样组件加载器才会到:
```text
frontend/admin/src/apps/{plugin}/views...
```
去查找页面。
## 防回退规则
这是插件开发最重要的回归规则。
### 1. 何时会回退
满足以下条件时,卸载再安装会回退:
- 你只改了 `frontend/admin/src/apps/{plugin}`。
- 模板源 `backend/plugin/{plugin}/resource/template/admin` 仍是旧版。
- 重装时安装器重新复制旧模板源。
### 2. 如何避免回退
每次插件前端修改完成后,必须检查:
- `index.vue` 是否同步到模板源。
- `schemas/index.tsx` 是否同步到模板源。
- `api/index.ts` 是否同步到模板源。
- `lang/zh-cn/zh.json` 与 `lang/en/en.json` 是否同步到模板源。
### 3. 最终交付规则
若用户要求“以后重装不要丢”,交付标准不是“当前页面能看”,而是:
- 运行目录正常。
- 模板源正常。
- 卸载后重装仍正常。
## 推荐开发顺序
1. 明确插件名、模块名、路由、权限码。
2. 建迁移、模型、DAO、Service、Validate、Controller。
3. 接好 Swagger 注解,先排除路由冲突。
4. 配置菜单与权限。
5. 在模板源下实现前端 `api + index.vue + schemas + lang`。
6. 安装插件,复制模板到运行目录。
7. 在运行目录联调。
8. 如联调时临时改了运行目录,最终再同步回模板源。
9. 卸载再安装,确认不回退。
## 验证清单
### 后端验证
- 启动后无 Swagger `Unable to merge @OA\Get()` 等冲突。
- CRUD 路由与权限码可正常访问。
- 列表接口返回 `items/total`,与 `Crud::formatNormal()` 对齐。
- 删除接口支持单个 `id` 或批量 `ids`,与项目基类能力一致。
### 前端验证
- 页面使用官方 `BasicCrud` 外观。
- 列表数据结构与 `BaseService` 返回兼容。
- 中英文切换无缺失 key。
- 菜单能正常打开到对应插件页面。
### 重装验证
- 卸载插件。
- 重新安装插件。
- 确认 `frontend/admin/src/apps/{plugin}` 被重新复制后,页面仍保持最新结构。
- 确认没有回退到旧 `el-table` demo 版本。
## 常见坑位
### 1. Swagger 冲突
原因:
- 新旧控制器共用了同一路径。
- 旧 `ArticleController` demo 接口未迁移。
处理:
- 迁移旧接口到 `legacy` 路径。
- 新 CRUD 控制器使用正式资源路径。
### 2. 重装后页面回退
原因:
- 只改运行目录,没改模板源。
处理:
- 以模板源为主,运行目录为辅。
### 3. 插件菜单显示异常
原因:
- 把插件前端 routes 自动扫描混进了静态路由。
- 未安装插件也显示菜单。
处理:
- 插件导航仅依赖后端导入菜单。
- 不把 apps 自动扫描结果混进后端菜单模式。
### 4. 模板目录编辑器报别名错误
现象:
- `@/components/crud`
- `@/locales`
- `@/enums`
原因:
- 模板源目录不是主前端 tsconfig 的真实编译入口。
结论:
- 这是编辑器静态提示,不等于安装后运行目录会构建失败。
- 但模板源文件内容仍要与运行目录保持一致。
### 5. 插件第二批模块可打开但接口 500
常见根因:
- Service 忘记显式导入 `core\base\BaseService`
- DAO 忘记显式导入 `core\base\BaseDao`
- Service 构造器手动 `new Dao()`,未按项目真实风格使用容器注入
- DAO 重写 `selectList()` 但方法签名与当前 `BaseDao` 不一致
处理:
- 优先对照同插件已可用一期模块的 DAO / Service 写法,不要只凭旧记忆生成。
- 修复顺序优先:`use` 导入 -> 构造注入 -> 方法签名 -> 关联字段 / validate。
### 6. 旧项目字段迁移后前端下拉不显示名称
常见根因:
- 模型没有提供 `name` 访问器
- `Crud::formatSelect()` 只能按 `title / name / real_name / id` 兜底取标签
处理:
- 为需要复用 `format=select` 的模型增加 `getNameAttribute()`,将实际显示字段映射到 `name`。
### 7. JSON 规则字段仅在后端可存,前端表单难以维护
处理建议:
- 后端 Validate 接受数组和 JSON 字符串双形态。
- 模型 `casts` 统一转为数组。
- 模板源 `schemas` 中优先使用 textarea 输入 JSON 文本,并同步中英文 placeholder。
### 8. Windows 下修改插件 Seeder / Install 后重装仍执行旧逻辑
现象:
- 已修改 `backend/plugin/{plugin}/resource/database/seeds/*.php` 或 `backend/plugin/{plugin}/Install.php`。
- 重新点击插件安装后,菜单等流程有变化,但 Seeder 结果仍像旧代码。
- 日志看不到新的种子插入 SQL。
常见根因:
- Windows 开发态 `config/process.php` 中 `process.monitor.constructor.options.enable_file_monitor` 被关闭。
- 运行中的 Webman 进程未重载,仍持有旧版类定义。
- `SeedTrait::runSeed()` 使用 `require_once`,同一进程内不会重新加载已声明 Seeder 类。
处理:
- 确认 `backend/config/process.php` 的 `enable_file_monitor` 在 Windows 开发态也开启。
- 修改插件 `Seeder / Install / Controller / Service / Dao` 后,若不确定是否已重载,先手动重启后端进程再验证。
- 排查“重装插件不生效”时,不要只盯安装接口日志,还要先确认当前进程是否加载了最新代码。
## 输出契约
当用户要求“完成一个插件开发”时,至少应交付:
- 后端 CRUD 可运行。
- Swagger 无冲突。
- 菜单、权限、路由一致。
- 模板源前端完整。
- 运行目录联调通过。
- 卸载再安装不回退。
若只完成运行目录修改,不能判定为插件开发真正完成。
@@ -0,0 +1,480 @@
---
name: cross-plugin-web-development
description: Madong 插件 Web 端开发规范,覆盖 Web 模板源、运行目录同步、页面路由、Web 菜单、API、认证、Element Plus 页面实现、验证与重装防回退。
globs:
- "backend/plugin/**/resource/menu/web.php"
- "backend/plugin/**/resource/template/web/**/*"
- "frontend/web/app/apps/**/pages/**/*.vue"
- "frontend/web/app/apps/**/api/**/*.ts"
- "frontend/web/app/apps/**/pages/routes.ts"
- ".windsurf/skills/**/*.md"
---
# Madong 插件 Web 端开发技能
用于开发和维护插件的 Web 前台功能。重点解决 Web 页面模板源、安装复制、插件路由、Web 菜单、API 调用和卸载重装后的防回退问题。
## 适用场景
- 为已有 Madong 插件增加 Web 页面。
- 将移动端或后台已有业务迁移为 Web 页面。
- 为插件补齐 Web API、路由和 Web 菜单。
- 修复插件卸载重装后 Web 页面、路由或菜单回退。
- 维护插件 Web 模板源与安装后的 Web 运行目录一致性。
## 总原则
1. 先核对项目真实实现,再编写或修改 Web 插件代码。
2. 插件 Web 的权威模板源是 `backend/plugin/{plugin}/resource/template/web`。
3. 安装后的 Web 运行目录是 `frontend/web/app/apps/{plugin}`,属于模板复制产物。
4. 正式修改优先落在模板源,完成后再同步到 Web 运行目录。
5. 页面、API、路由、菜单和后端接口的路径必须一致。
6. 优先复用当前项目的 `~/utils/request`、Element Plus、Pinia 和既有页面风格,不直接照搬移动端 UI。
7. 先解决业务根因,不通过页面层伪造数据或绕过后端校验。
8. 不删除未经确认的内容;新增页面时不要覆盖已有业务页面。
## 目录与复制关系
### 模板源
```text
backend/plugin/{plugin}/resource/template/web/
├── api/
│ ├── data-entry.ts
│ └── exception.ts
├── pages/
│ ├── routes.ts
│ └── {module}/
│ ├── index.vue
│ ├── tasks.vue
│ └── components/
└── .gitkeep
```
### 安装后的运行目录
```text
frontend/web/app/apps/{plugin}/
├── api/
├── pages/
│ ├── routes.ts
│ └── {module}/
└── ...
```
当前项目的实际对应关系:
```text
backend/plugin/produce/resource/template/web/
frontend/web/app/apps/produce/
```
修改 Web 插件时,必须核对以下文件是否同步:
- `api/**/*.ts`
- `pages/**/*.vue`
- `pages/routes.ts`
- 页面使用的组件和辅助文件
## 开发前检查
开始开发前至少核对:
1. 插件名和 Web 运行目录:`frontend/web/app/apps/{plugin}`。
2. 模板源是否存在:`backend/plugin/{plugin}/resource/template/web`。
3. Web 主应用是否通过 `router.options.ts` 加载插件路由。
4. 后端接口的真实路径、请求方法、参数和响应结构。
5. 登录用户信息来源和认证请求头。
6. `resource/menu/web.php` 的真实菜单字段格式。
7. 可参考的 Web 页面,例如:
- `frontend/web/app/apps/produce/pages/data-entry/index.vue`
- `frontend/web/app/apps/produce/pages/data-entry/components/OrderEntryPanel.vue`
8. 移动端页面仅作为业务流程参考,不作为 Web 布局和组件规范来源。
## Web 路由规范
### 1. 插件路由入口
当前 Web 主应用通过以下方式加载插件路由:
```text
frontend/web/app/router.options.ts
```
该文件使用 `import.meta.glob('~/apps/**/pages/routes.ts', { eager: true })` 加载插件路由。因此插件页面应在以下文件注册:
```text
backend/plugin/{plugin}/resource/template/web/pages/routes.ts
frontend/web/app/apps/{plugin}/pages/routes.ts
```
### 2. 路由写法
```typescript
export default [
{
path: '/{plugin}/{module}',
component: () => import('~/apps/{plugin}/pages/{module}/index.vue'),
meta: {
title: '页面标题',
layout: 'default',
},
},
]
```
要求:
- 页面路径使用业务语义,保持与 Web 菜单 URL 一致。
- 组件路径使用 `~/apps/{plugin}/pages/...`。
- 默认使用 `layout: 'default'`,除非已核对项目存在其他布局。
- 一个页面只能注册一次,避免主站路由和插件路由重复。
- 带参数页面使用 Nuxt 约定的 `[param].vue`,但当前插件也可按项目路由表显式注册页面。
### 3. 认证与权限
Web 路由是否需要登录由当前项目权限中间件和路由元信息共同决定。涉及用户数据、异常提报、维修提交等业务时:
- 页面必须使用真实的登录用户信息。
- 不要在前端硬编码用户工号作为提交值。
- 需要登录的页面应按当前项目实际权限配置 `meta.permissions`,先核对现有菜单和中间件实现。
- 如果后端接口本身标记为公共接口,也不能因此在页面中跳过业务必填校验。
## Web 菜单规范
### 1. 菜单文件
插件 Web 菜单文件:
```text
backend/plugin/{plugin}/resource/menu/web.php
```
Web 菜单不是 Admin 菜单,不能直接复制 `resource/menu/admin.php` 的结构。当前项目 Web 菜单使用记录数组,例如:
```php
return [
[
'app' => 'web',
'category' => 1,
'source' => 'plugin',
'code' => null,
'name' => '业务页面',
'url' => '/{plugin}/{module}',
'icon' => 'ri:apps-line',
'level' => 1,
'type' => 2,
'sort' => 10,
'target' => 1,
'is_show' => 1,
'enabled' => 1,
'created_at' => time(),
'updated_at' => time(),
'deleted_at' => null,
],
];
```
字段要求以当前项目和菜单导入实现为准,至少核对:`app`、`source`、`name`、`url`、`type`、`sort`、`is_show`、`enabled`。
### 2. 菜单与路由一致
新增 Web 页面时必须同时检查:
- `resource/menu/web.php` 是否有对应菜单。
- 菜单 `url` 是否等于路由 `path`。
- 路由组件是否真实存在。
- 菜单是否需要显示为一级菜单或子菜单。
- 图标是否使用项目已有图标格式。
不要只添加路由而不添加菜单,也不要只添加菜单而不注册路由。
### 3. 菜单安装行为
菜单通常在插件安装时导入系统菜单。修改 `resource/menu/web.php` 后,当前数据库中已存在的菜单不一定会自动刷新。验证菜单时应区分:
- 文件配置是否正确。
- 安装导入后的系统菜单是否已更新。
- 当前用户是否有访问该菜单的权限。
不要通过前端硬编码菜单代替插件菜单导入机制。
## Web API 规范
### 1. API 文件位置
插件 Web API 正式放在模板源:
```text
backend/plugin/{plugin}/resource/template/web/api/
```
运行目录对应:
```text
frontend/web/app/apps/{plugin}/api/
```
当前项目的实际写法是业务 API 直接放在插件目录,例如:
```typescript
import request from '~/utils/request'
export function getExampleList(params: Record<string, unknown> = {}) {
return request.get('/{plugin}/{resource}', params)
}
```
### 2. 请求约定
- 使用 `~/utils/request`,不要在业务 API 中直接调用 axios 或 fetch。
- 传入后端业务路径,例如 `/produce/process`,不要手动拼接 `/api/`。
- GET 参数作为第二个参数传入。
- POST 数据作为第二个参数传入。
- 默认响应已经解包为 `data`。
- 只有确实需要状态码、消息或完整响应时,才使用 `returnFullResponse: true`。
- 上传使用 `request.upload` 和 `FormData`,统一由 API 层封装。
- API 层负责类型声明、路径和参数;页面负责交互状态和展示。
- 错误提示优先使用请求层统一处理,页面避免重复弹出相同错误。
### 3. API 类型
应根据后端真实返回结构定义接口,例如:
```typescript
export interface ExampleItem {
id: string | number
name?: string
status?: number
}
export function getExampleDetail(id: string | number) {
return request.get<ExampleItem>(`/example/${id}`)
}
```
列表接口要确认返回的是数组还是 `{ items, total }`,不能凭页面名称猜测。
### 4. API 与页面解耦
页面中不要重复实现:
- 基础 URL 拼接。
- Authorization 认证头。
- 响应解包。
- 上传认证。
- 通用错误弹窗。
如果多个页面共享业务请求,应提取到插件 Web API 文件,而不是复制请求代码。
## 页面开发规范
### 1. 技术栈
当前项目 Web 端使用:
- Vue 3 `<script setup lang="ts">`。
- Nuxt 4。
- Element Plus。
- Pinia。
- 当前项目已有的 SCSS / CSS 变量和布局。
### 2. 页面结构
建议结构:
```vue
<template>
<div class="produce-work-page">
<div class="page-shell">
<section class="page-hero">...</section>
<div class="content-grid">
<main class="main-column">...</main>
<aside class="side-column">...</aside>
</div>
</div>
</div>
</template>
```
要求:
- 页面应适配 `layouts/default.vue` 的滚动区域。
- 页面主体避免依赖移动端 `view`、`text`、`u-*` 组件。
- Web 页面使用 `div`、`button`、Element Plus 组件和原生表单语义。
- 页面需要考虑 1440px、1024px、768px 等宽度下的响应式布局。
- 重要操作按钮必须有 loading、禁用或重复提交保护。
- 空数据、加载中、提交失败和成功状态要有明确反馈。
- 不要为了追求视觉效果引入项目没有使用的新 UI 框架。
### 3. 业务迁移
从移动端迁移时:
- 保留业务流程、校验规则、接口参数和状态含义。
- 将 `u-popup` 改为 `el-dialog`、`el-drawer`、`el-select` 或页面内面板。
- 将 `u-input` 改为 `el-input`。
- 将 `u-button` 或自绘点击区域改为 `el-button` 或符合语义的按钮。
- 将移动端图片选择改为 `el-upload`,上传由 API 层完成。
- 将 `uni.showToast` 改为 `ElMessage` 或 `ElNotification`。
- 将移动端本地用户信息读取改为 `useMemberStore()`,但要以当前项目真实 Store 结构为准。
- 不要把移动端的 `uni`、`rpx`、原生扫码插件直接带入 Web 页面。
### 4. 图片上传
推荐流程:
1. 使用 `el-upload` 设置 `:auto-upload="false"`。
2. 读取 `UploadUserFile.raw`。
3. 调用插件 API 的上传方法。
4. 将返回的 URL 放入业务提交参数。
5. 限制文件类型、数量和必要的大小。
6. 上传失败时停止提交并保留用户选择,方便重试。
## 模板源优先与同步流程
标准流程:
1. 核对插件后端接口和业务返回结构。
2. 在 `backend/plugin/{plugin}/resource/template/web` 中创建或修改 Web API。
3. 在模板源中创建或修改页面及页面组件。
4. 在模板源 `pages/routes.ts` 注册路由。
5. 在 `resource/menu/web.php` 增加或更新 Web 菜单。
6. 核对模板源文件之间的导入路径和路由路径。
7. 将模板源同步到 `frontend/web/app/apps/{plugin}`。
8. 核对模板源与运行目录内容一致。
9. 启动 Web 端,验证菜单、路由、页面和接口。
10. 如菜单由安装流程导入,重新安装或执行项目规定的菜单刷新流程后再验证菜单。
禁止只修改以下运行目录而不更新模板源:
```text
frontend/web/app/apps/{plugin}
```
## 验证清单
### 文件与同步
- [ ] 模板源 Web API 存在。
- [ ] 模板源页面存在。
- [ ] 模板源 `pages/routes.ts` 已注册。
- [ ] `resource/menu/web.php` 已配置对应菜单。
- [ ] 运行目录已同步。
- [ ] 模板源与运行目录文件内容一致。
### 页面与交互
- [ ] 页面使用 Web 组件,不包含移动端 `uni`、`u-*`、`rpx`。
- [ ] 加载、空数据、错误和提交状态完整。
- [ ] 提交按钮有重复提交保护。
- [ ] 关键字段有前端校验,后端校验仍然保留。
- [ ] 用户信息来自真实 Store 或当前项目认证机制。
- [ ] 页面在窄屏下仍可用。
### API
- [ ] API 使用 `~/utils/request`。
- [ ] 请求路径未重复拼接 `/api/`。
- [ ] 请求方法、参数名称和响应结构与后端一致。
- [ ] 文件上传通过 API 层完成。
- [ ] 错误提示没有重复弹出。
### 路由与菜单
- [ ] 菜单 URL 与路由 path 一致。
- [ ] 路由 component 路径真实存在。
- [ ] 菜单排序、图标、显示状态符合项目实际格式。
- [ ] 菜单导入后的系统数据已经刷新或重新安装验证。
### 构建与运行
- [ ] Web 端定向 ESLint 检查通过,或已区分本次新增问题与项目历史问题。
- [ ] Web 端类型检查通过,或已记录项目历史类型错误。
- [ ] 页面可以正常打开。
- [ ] API 请求返回预期数据。
- [ ] 提交、上传、跳转和返回流程正常。
## 防回退规则
卸载重装插件会重新从模板源复制 Web 文件。以下情况会导致回退:
- 只修改了 `frontend/web/app/apps/{plugin}`。
- 模板源缺少新增页面或 API。
- 模板源 `pages/routes.ts` 没有同步。
- `resource/menu/web.php` 没有同步。
- 运行目录临时修复后没有回写模板源。
最终交付必须同时满足:
- 模板源版本正确。
- Web 运行目录版本正确。
- 路由和菜单版本正确。
- 重新安装插件后页面不会回退。
## 常见问题
### 1. 页面 404
排查顺序:
1. 页面文件是否位于 `frontend/web/app/apps/{plugin}/pages`。
2. 插件 `pages/routes.ts` 是否存在。
3. `router.options.ts` 是否仍使用插件路由 glob。
4. 路由 `path` 是否与访问地址一致。
5. import 路径大小写是否正确。
6. 模板源和运行目录是否都包含该页面。
### 2. 菜单不显示
排查:
- `resource/menu/web.php` 是否新增记录。
- `app` 是否为 `web`。
- `enabled`、`is_show` 是否启用。
- 菜单是否已重新导入系统表。
- 当前登录用户是否具备对应菜单访问权限。
### 3. 页面打开但请求 404
常见原因:
- API 路径多拼了 `/api/`。
- 请求路径与后端控制器路由不一致。
- 后端插件未加载或运行进程未重载。
- API 文件只同步到了运行目录,重装后被旧模板覆盖。
### 4. 模板源文件出现模块找不到
模板源不一定是主 Web 项目的 TypeScript 编译入口。若编辑器提示 `vue`、`element-plus`、`~/stores/member` 或 `~/apps/{plugin}/api` 找不到,先确认运行目录中的同一文件可以解析,再区分编辑器静态提示和真实构建错误。
### 5. 页面使用了公共接口但无法提交
公共接口不等于无需业务校验。应继续检查:
- 当前用户工号是否存在。
- 必填字段是否完整。
- 业务状态是否允许操作。
- 后端是否要求先校验订单或维修任务。
### 6. Windows 下模板修改未生效
如果修改模板源、菜单、安装逻辑后运行结果仍是旧版本:
- 确认 Webman 进程是否已重新加载插件代码。
- 确认插件安装流程是否重新复制模板。
- 确认菜单数据是否重新导入。
- 不要只根据浏览器缓存判断模板是否更新。
## 输出契约
完成一个插件 Web 功能时,至少交付:
- Web API 模板源。
- Web 页面模板源。
- 插件 Web 路由模板源。
- `resource/menu/web.php` 菜单配置。
- 同步后的 `frontend/web/app/apps/{plugin}` 运行目录。
- 页面、接口、菜单和路由一致性验证。
- 构建或定向检查结果。
- 如发现既有 Web skill 与项目真实实现不一致,应同步更新对应 skill,并说明更新内容。
@@ -0,0 +1,46 @@
---
name: frontend-admin-api
description: Admin 前端 API 层规范,基于项目 request/BaseService 的 API 调用模式
globs:
- "frontend/admin/src/api/**/*.ts"
- "apps/**/src/api/**/*.ts"
---
## 文件位置
```
frontend/admin/src/api/{module}/index.ts
```
## 代码模板
```typescript
import BaseService from '@/api/core/base';
import request from '@/utils/http';
const baseUrl = '/{module}/{model}';
export const {Model}Service = {
...BaseService<{Model}Row>({ baseUrl, allowedMethods: ['list', 'get', 'delete'] }),
customAction(id: string | number) {
return request.post({ url: `${baseUrl}/${id}/custom-action` });
},
};
```
## 关键约定
- API 服务命名:`{Model}Service`
- CRUD 路径:`/{module}/{model}`
- 通用 CRUD 优先复用 `@/api/core/base` 的 `BaseService`
- 自定义动作使用 `@/utils/http` 的 `request.get/post/put/del`
- 响应格式:`{code: 0, msg: 'ok', data: {...}}`
- 列表返回:`{items: [], total: N}`
## 检查清单
- [ ] 函数命名是否小驼峰
- [ ] 路径是否与后端路由一致
- [ ] 参数类型是否正确
- [ ] 是否处理了异常情况
@@ -0,0 +1,44 @@
---
name: frontend-admin-common
description: Admin 前端公共规范,Monorepo 结构、目录组织、命名约定
globs:
- "frontend/admin/**/*.ts"
- "frontend/admin/**/*.vue"
---
## Monorepo 结构
```
frontend/admin/
├── src/
│ ├── api/ # API 层
│ ├── components/ # 公共组件
│ ├── hooks/ # 组合式函数
│ ├── store/ # Pinia 状态
│ ├── router/ # 路由
│ ├── views/ # 页面
│ ├── utils/ # 工具
│ ├── types/ # 类型定义
│ ├── styles/ # 样式
│ └── lang/ # 国际化
├── apps/ # 子应用
├── public/
└── vite.config.ts
```
## 命名约定
| 场景 | 规范 | 示例 |
|------|------|------|
| 目录名 | kebab-case | `system/menu/` |
| Vue 组件 | PascalCase | `MenuModal.vue` |
| TS 文件 | kebab-case | `use-crud.ts` |
| API 函数 | camelCase | `getMenuList()` |
| 接口类型 | PascalCase | `MenuRecord` |
## 检查清单
- [ ] 目录名是否 kebab-case
- [ ] 文件名是否 kebab-case
- [ ] 组件名是否 PascalCase
- [ ] API 函数是否 camelCase
@@ -0,0 +1,32 @@
---
name: frontend-admin-component
description: Admin 前端公共组件规范,基于 Element Plus 组件封装
globs:
- "frontend/admin/src/components/**/*.vue"
---
## 组件分类
| 分类 | 说明 | 示例 |
|------|------|------|
| core/ | 核心组件 | Page, SearchBar, TableAction |
| crud/ | CRUD 组件 | FormDialog, CrudSchema |
| form/ | 表单组件 | FormDialog, FormItem |
| dialog/ | 弹窗组件 | DialogModal |
| view/ | 视图组件 | ViewComponent 注册表 |
| business/ | 业务组件 | MemberSelect, UploadImage |
| terminal/ | 终端组件 | TerminalMain |
## 关键约定
- 组件使用 `defineOptions({ name: '...' })` 命名
- Props 使用 TypeScript 类型定义
- 组件名使用 PascalCase
- 公共组件通过 `components/` 按目录分类
## 检查清单
- [ ] 组件名是否 PascalCase
- [ ] Props 是否有类型声明
- [ ] 组件是否有默认导出
- [ ] 是否使用 Vue 3 Composition API
@@ -0,0 +1,625 @@
---
name: frontend-admin-gen
description: Generate Vue frontend pages for madong module. Supports both app (main project) and plugin targets. Creates index.vue with BasicCrud, schemas, and API service files following madong frontend conventions.
---
# Step 7: Generate Frontend Pages
Create Vue frontend pages and API service for module.
## File Locations
**App (主项目):**
```
frontend/admin/src/apps/{module}/
├── views/{model}/index.vue
├── views/{model}/schemas/index.tsx
├── api/{module}/index.ts
└── locales/lang/
├── zh-cn/{module}.json
└── en/{module}.json
```
**Plugin (插件):**
```
backend/plugin/{plugin}/resource/template/admin/
├── api/{module}/index.ts
├── lang/
│ ├── zh-cn/{module}.json
│ └── en/{module}.json
└── views/{plugin_key}/{model}/
├── index.vue
└── schemas/index.tsx
安装插件时会由 `core\plugin\traits\TemplateTrait` 将上述 `resource/template/admin/*`
整体复制到:
frontend/admin/src/apps/{plugin}/
├── api/{module}/index.ts
├── lang/
│ ├── zh-cn/{module}.json
│ └── en/{module}.json
└── views/{plugin_key}/{model}/
├── index.vue
└── schemas/index.tsx
```
插件菜单必须通过 `backend/plugin/{plugin}/resource/menu/admin.php` 在安装时导入系统菜单,
不要在插件前端模板中生成 `routes/index.ts` 作为侧边栏菜单入口,避免未安装插件也被前端自动扫描并显示导航。
插件菜单中的 `component` 应包含插件 key 目录段,确保与生成器输出路径一致,
例如页面为 `views/{plugin_key}/index/index.vue` 时填写 `/{plugin_key}/index/index`。
插件 API 文件应从 `@/utils/http` 导入默认请求对象,并使用 `request.get()`、`request.post()` 等方法;
不要从 `@/api/request` 默认导入,因为该文件只转发命名导出,不提供 default export。
如果插件页面直接使用 `$t('{plugin}.title')`、`$t('{plugin}.description')` 等根级 key,
语言文件应生成到 `lang/zh-cn/zh.json` 和 `lang/en/en.json`,文件内容包含 `{plugin}` 根对象;
不要只生成 `index.json`,否则 apps 语言加载器会把 key 包装为 `{plugin}.index.*`。
后端插件必须生成 `config/translation.php`,因为插件请求上下文中 `__()` 会读取 `plugin.{plugin}.translation`,
缺少该配置会导致 `Undefined array key "locale"`。
## i18n Key Pattern
| Target | Pattern | Example |
|--------|---------|---------|
| App | `{module}.{model}.*` | `ask.question.*` |
| Plugin | `{plugin}.{module}.{model}.*` | `official.ask.question.*` |
## index.vue Template
```vue
<template>
<div class="{model}-page art-full-height">
<BasicCrud />
</div>
</template>
<script setup lang="tsx">
import { useCrud } from "@/components/crud";
import { crudSchema } from "./schemas";
import { $t } from "@/locales";
const [BasicCrud, crudApi] = useCrud({
...crudSchema(),
beforeFetch: (params: any) => {
return params;
},
// Custom table actions
tableActions: [
{
label: $t('{i18n_prefix}.{model}.actions.detail'),
type: "primary",
sort: -1,
link: true,
icon: "ant-design:eye-outlined",
auth: "{permission}:read",
onClick(e: Event, record: any) {},
},
],
// Custom dropdown actions
dropDownActions: [
{
label: $t('{i18n_prefix}.{model}.actions.edit'),
type: "primary",
sort: -1,
link: true,
icon: "ant-design:edit-outlined",
auth: "{permission}:update",
onClick(e: Event, record: any) {},
},
{
label: $t('{i18n_prefix}.{model}.actions.delete'),
type: "danger",
sort: -1,
link: true,
icon: "ant-design:delete-outlined",
auth: "{permission}:delete",
onClick(e: Event, record: any) {},
},
],
});
</script>
```
## schemas/index.tsx Template
```typescript
import { {Model}Service } from '@/apps/{app_name}/api/{module}'
import { DictEnum } from '@/apps/{app_name}/enums/dict-enum'
import { CrudSchema } from '@/components/crud'
import { formatDateTime } from '@/utils'
import { h } from 'vue'
import { ElTag } from 'element-plus'
import { $t } from '@/locales'
// CRUD配置
export const crudSchema = (): CrudSchema => {
return {
// 接口地址
api: {Model}Service.list,
dialogTitle: $t('{i18n_prefix}.{model}.dialog_title'),
crudApi: {
remove: {Model}Service.remove,
view: {Model}Service.get,
add: {Model}Service.create,
edit: {Model}Service.update,
},
useCrud: true,
hasAdd: true,
hasRemove: true,
hasEdit: true,
hasView: true,
addAuth: '{permission}:create',
removeAuth: '{permission}:delete',
editAuth: '{permission}:update',
viewAuth: '{permission}:read',
rowKey: 'id',
columns: [
{
type: 'selection'
},
{
prop: 'id',
label: $t('{i18n_prefix}.{model}.table.columns.id'),
align: 'center',
width: 80,
visible: false
},
{
prop: 'name',
label: $t('{i18n_prefix}.{model}.table.columns.name'),
align: 'left',
minWidth: 150,
showOverflowTooltip: true
},
{
prop: 'sort',
label: $t('{i18n_prefix}.{model}.table.columns.sort'),
align: 'center',
width: 100,
},
{
prop: 'status',
label: $t('{i18n_prefix}.{model}.table.columns.status'),
align: 'center',
width: 100,
component: 'ApiDict',
componentProps: {
code: DictEnum.SYS_NORMAL_DISABLE
}
},
{
prop: 'created_at',
label: $t('{i18n_prefix}.{model}.table.columns.created_at'),
minWidth: 170,
align: 'center',
formatter: (row: any) => {
return formatDateTime(row.created_at)
}
},
],
// 当前项目 CrudSchema 列类型不支持 `type: 'operate'`,操作列由 BasicCrud 根据 tableActions/dropDownActions 自动生成。
dialogProps: {
width: '50%',
},
searchFormSchema: {
labelWidth: '100px',
schema: [
{
label: $t('{i18n_prefix}.{model}.search_form.name'),
prop: 'name',
component: 'Input',
colSpan: 6,
componentProps: {
placeholder: $t('{i18n_prefix}.{model}.search_form.name_placeholder')
}
},
{
label: $t('{i18n_prefix}.{model}.search_form.status'),
prop: 'status',
component: 'ApiDict',
colSpan: 6,
componentProps: {
code: DictEnum.SYS_NORMAL_DISABLE,
placeholder: $t('{i18n_prefix}.{model}.search_form.status_placeholder')
}
}
]
},
formSchema: {
labelWidth: 120,
gutter: 20,
schema: [
{
label: $t('{i18n_prefix}.{model}.form.name'),
prop: 'name',
component: 'Input',
componentProps: {
placeholder: $t('{i18n_prefix}.{model}.form.name_placeholder'),
maxlength: 50
},
colSpan: 24,
rules: [{ required: true, message: $t('{i18n_prefix}.{model}.validate.name_required') }]
},
{
label: $t('{i18n_prefix}.{model}.form.sort'),
prop: 'sort',
component: 'InputNumber',
componentProps: {
placeholder: $t('{i18n_prefix}.{model}.form.sort_placeholder'),
min: 0,
max: 9999
},
colSpan: 24,
},
{
label: $t('{i18n_prefix}.{model}.form.status'),
prop: 'status',
component: 'ApiDict',
componentProps: {
code: DictEnum.SYS_NORMAL_DISABLE
},
colSpan: 24,
rules: [{ required: true, message: $t('{i18n_prefix}.{model}.validate.status_required') }]
}
]
}
}
}
```
## API Service Template
Current project convention: prefer `BaseService` from `@/api/core/base` for standard CRUD endpoints and `request` from `@/utils/http` for custom methods. Do not use the old `@/support/http` string-only service style when extending the current admin project.
```typescript
import BaseService from '@/api/core/base'
import request from '@/utils/http'
/**
* {Model} API 服务
*/
const baseUrl = '/{api_prefix}'
export const {Model}Service = {
...BaseService<any>({ baseUrl }),
/**
* 自定义方法 - 设置状态
*/
setStatus: (id: number, status: number) => {
return request.post({ url: `${baseUrl}/${id}/status`, data: { status } })
},
}
export default {Model}Service
```
## Complete Example (App)
### File: frontend/admin/src/apps/ask/api/question/index.ts
```typescript
import http from "@/support/http";
export const QuestionService = {
list: '/ask/question',
get: '/ask/question/{id}',
create: '/ask/question',
update: '/ask/question/{id}',
remove: '/ask/question/{id}',
batchRemove: '/ask/question',
select: '/ask/question',
};
```
### i18n: frontend/admin/src/apps/ask/locales/lang/zh-cn/question.json
```json
{
"ask": {
"question": {
"dialog_title": "问题管理",
"table": {
"columns": {
"id": "ID",
"title": "标题",
"category_id": "分类",
"price": "价格",
"status": "状态",
"created_at": "创建时间"
}
},
"search_form": {
"title": "标题",
"title_placeholder": "请输入标题",
"status": "状态",
"status_placeholder": "请选择状态"
},
"form": {
"title": "标题",
"title_placeholder": "请输入标题",
"category_id": "分类",
"price": "价格",
"status": "状态"
},
"actions": {
"detail": "详情",
"edit": "编辑",
"delete": "删除"
},
"validate": {
"title_required": "请输入标题"
}
}
}
}
```
## Complete Example (Plugin)
### File: frontend/admin/src/apps/official/api/question/index.ts
```typescript
import http from "@/support/http";
export const QuestionService = {
list: '/ask/question',
get: '/ask/question/{id}',
create: '/ask/question',
update: '/ask/question/{id}',
remove: '/ask/question/{id}',
batchRemove: '/ask/question',
select: '/ask/question',
};
```
### i18n: frontend/admin/src/apps/official/locales/lang/zh-cn/question.json
```json
{
"official": {
"ask": {
"question": {
"dialog_title": "问题管理",
"table": {
"columns": {
"id": "ID",
"title": "标题",
"category_id": "分类",
"price": "价格",
"status": "状态",
"created_at": "创建时间"
}
},
"search_form": {
"title": "标题",
"title_placeholder": "请输入标题",
"status": "状态",
"status_placeholder": "请选择状态"
},
"form": {
"title": "标题",
"title_placeholder": "请输入标题",
"category_id": "分类",
"price": "价格",
"status": "状态"
},
"actions": {
"detail": "详情",
"edit": "编辑",
"delete": "删除"
},
"validate": {
"title_required": "请输入标题"
}
}
}
}
}
```
## Custom Form Component (自定义表单)
When you need custom form logic (e.g., rich text editor with upload, conditional fields), disable built-in CRUD forms and use custom form component:
### 1. Disable Built-in Forms in schemas/index.tsx
```typescript
export const crudSchema = (): CrudSchema => {
return {
// ... other config
useCrud: true,
hasAdd: false, // Disable built-in add
hasEdit: false, // Disable built-in edit
hasView: false, // Disable built-in view
hasRemove: true, // Keep built-in delete
formSchema: {}, // Empty form schema
// ...
}
}
```
### 2. Create Custom Form Component (form.vue)
```vue
<template>
<Dialog>
<BasicForm />
</Dialog>
</template>
<script setup lang="tsx">
import { FormItemSchema, useForm } from "@/components/form";
import { useDialog } from "@/components/dialog";
import { CrudMethods } from "@/components/crud";
import { ref, nextTick } from "vue";
import { ElMessage } from "element-plus";
import { $t } from "@/locales";
const record = ref<any>({});
const crudApi = ref<CrudMethods>();
const formType = ref("add");
// Define form schema with JSON
const schema: FormItemSchema[] = [
{
label: $t('{i18n_prefix}.{model}.form.title'),
prop: "title",
component: "Input",
colSpan: 24,
rules: [{ required: true, message: "请输入标题" }],
},
{
label: $t('{i18n_prefix}.{model}.form.content'),
prop: "content",
component: "Editor", // Rich text editor
colSpan: 24,
componentProps: {
uploadConfig: {
server: `${import.meta.env.VITE_API_URL}/files/common/wangeditor`,
meta: { sub_dir: "content" },
},
},
},
];
const [BasicForm, formApi] = useForm({
labelPosition: "right",
labelWidth: "100px",
schema: schema,
});
const [Dialog, dialogApi] = useDialog({
title: $t('{i18n_prefix}.{model}.dialog_title'),
width: "80%",
dialogType: "drawer",
async onConfirm() {
formApi.validate().then(async () => {
const values = await formApi.getValues();
if (formType.value === "add") {
await {Model}Service.create(values);
crudApi?.value?.refreshCreate();
ElMessage.success("新增成功");
dialogApi.close();
} else if (formType.value === "edit") {
await {Model}Service.update(record.value.id, values);
crudApi?.value?.refreshCreate();
ElMessage.success("更新成功");
dialogApi.close();
}
});
},
});
defineExpose({
async show({ data, getCrudApi, type }: any) {
record.value = data;
formType.value = type || "add";
crudApi.value = getCrudApi();
dialogApi.open();
nextTick(() => {
formApi.setValues(record.value);
});
},
});
</script>
```
### 3. Use Custom Form in index.vue
```vue
<template>
<div class="{model}-page art-full-height">
<BasicCrud />
<Form ref="formRef" />
</div>
</template>
<script setup lang="tsx">
import { useCrud } from "@/components/crud";
import { crudSchema } from "./schemas";
import Form from "./form.vue";
const formRef = ref<any>(null);
const [BasicCrud, crudApi] = useCrud({
...crudSchema(),
toolbarActions: [
{
label: "新增",
type: "primary",
auth: "{permission}:create",
dialogRef: formRef, // Reference to custom form
dialogParams() {
return {
data: {},
getCrudApi() { return crudApi; },
type: "add",
};
},
},
],
tableActions: [
{
label: "编辑",
type: "primary",
link: true,
auth: "{permission}:update",
dialogRef: formRef, // Reference to custom form
dialogParams(record: any) {
return {
data: record,
getCrudApi() { return crudApi; },
type: "edit",
};
},
},
],
});
</script>
```
## Column Component Types
| Component | Description |
|-----------|-------------|
| `ApiDict` | Dictionary select (uses DictEnum codes) |
| `ApiSelect` | API-based select dropdown |
| `Input` | Text input |
| `InputNumber` | Number input |
| `DatePicker` | Date picker |
| `DatePicker` + `componentProps.type = 'datetime'` | Date time picker |
| `Switch` | Toggle switch |
| `RadioGroup` | Radio group |
| `CheckboxGroup` | Checkbox group |
| `Upload` | File upload |
| `ImageUpload` | Image upload |
| `Editor` | Rich text editor |
## DictEnum Common Codes
| Code | Description |
|------|-------------|
| `DictEnum.SYS_NORMAL_DISABLE` | Status: Normal/Disable |
| `DictEnum.YES_NO` | Yes/No |
| `DictEnum.USER_SEX` | User gender |
## Auto-generation Checklist
When generating frontend:
- [ ] Create directory structure based on target (app/plugin)
- [ ] Generate index.vue with BasicCrud
- [ ] Generate schemas/index.tsx with CRUD config
- [ ] Generate API service file
- [ ] Generate i18n JSON files (zh-cn and en)
- [ ] Use $t() for all text
- [ ] Use proper i18n key pattern based on target
- [ ] Use proper DictEnum codes
- [ ] Configure proper permissions
### Custom Form Checklist (when using custom form)
- [ ] Set `hasAdd: false, hasEdit: false, hasView: false` in schemas
- [ ] Set `formSchema: {}` in schemas
- [ ] Create form.vue with `useForm` + `useDialog`
- [ ] Import custom Form in index.vue
- [ ] Pass `dialogRef: formRef` in toolbarActions/tableActions
- [ ] Add required i18n keys: `add_title`, `edit_title`, `detail_title`, `validate.*`, `tips.*`
@@ -0,0 +1,294 @@
---
name: frontend-admin-i18n
description: Generate frontend i18n translation files for madong plugin module. Creates JSON translation files with proper key patterns for both zh-cn and en languages.
---
# Step 9: Generate Frontend i18n
Generate JSON translation files for plugin module frontend.
## File Locations
### Plugin Mode (插件模式)
```
frontend/admin/src/apps/{plugin}/lang/zh-cn/{module}.{feature}.json
frontend/admin/src/apps/{plugin}/lang/en/{module}.{feature}.json
```
**Key prefix**: `{plugin}.{module}.{feature}`
- Example: `official/lang/zh-cn/rich_text.template.json` → key prefix is `official.rich_text.template`
### Built-in Mode (内置模式)
```
frontend/admin/src/lang/zh-cn/{module}.{feature}.json
frontend/admin/src/lang/en/{module}.{feature}.json
```
**Key prefix**: `{module}.{feature}` (NO plugin prefix)
- Example: `admin/src/lang/zh-cn/rich_text.template.json` → key prefix is `rich_text.template`
**Important**:
- File path itself defines the key prefix
- Inside the JSON file, do NOT include the prefix again
- Use nested object structure, NOT dot notation in keys
## Translation File Template (zh-cn)
```json
{
"dialog_title": "富文本模板",
"table": {
"columns": {
"sort": "排序",
"title": "标题",
"template": "模板类型",
"is_show": "显示状态",
"status": "状态",
"created_at": "创建时间"
}
},
"search_form": {
"title": "标题",
"template": "模板类型"
},
"form": {
"title": "标题",
"template": "模板类型",
"sort": "排序",
"is_show": "显示状态",
"enabled": "启用状态",
"content": "内容",
"meta": "元数据",
"extra": "扩展数据"
},
"validate": {
"title_required": "请输入标题",
"content_required": "请输入内容"
},
"tips": {
"add_success": "添加成功",
"edit_success": "编辑成功",
"delete_confirm": "确认删除吗?",
"delete_success": "删除成功"
},
"actions": {
"add": "新增",
"edit": "编辑",
"delete": "删除",
"detail": "详情",
"submit": "提交",
"cancel": "取消",
"confirm": "确定"
}
}
```
## Translation File Template (en)
```json
{
"dialog_title": "Rich Text Template",
"table": {
"columns": {
"sort": "Sort",
"title": "Title",
"template": "Template Type",
"is_show": "Show Status",
"status": "Status",
"created_at": "Created At"
}
},
"search_form": {
"title": "Title",
"template": "Template Type"
},
"form": {
"title": "Title",
"template": "Template Type",
"sort": "Sort",
"is_show": "Show Status",
"enabled": "Enabled Status",
"content": "Content",
"meta": "Meta Data",
"extra": "Extra Data"
},
"validate": {
"title_required": "Please enter title",
"content_required": "Please enter content"
},
"tips": {
"add_success": "Added successfully",
"edit_success": "Edited successfully",
"delete_confirm": "Are you sure to delete?",
"delete_success": "Deleted successfully"
},
"actions": {
"add": "Add",
"edit": "Edit",
"delete": "Delete",
"detail": "Detail",
"submit": "Submit",
"cancel": "Cancel",
"confirm": "Confirm"
}
}
```
## Usage in Frontend Code
### Plugin Mode
```typescript
import { $t } from '@/locales'
// File: official/lang/zh-cn/rich_text.template.json
// Key in file: "dialog_title"
// Full key: official.rich_text.template.dialog_title
const title = $t('official.rich_text.template.dialog_title')
const columnLabel = $t('official.rich_text.template.table.columns.title')
// In Vue component
<el-button>{{ $t('official.rich_text.template.actions.add') }}</el-button>
```
### Built-in Mode
```typescript
import { $t } from '@/locales'
// File: admin/src/lang/zh-cn/rich_text.template.json
// Key in file: "dialog_title"
// Full key: rich_text.template.dialog_title (NO plugin prefix)
const title = $t('rich_text.template.dialog_title')
const columnLabel = $t('rich_text.template.table.columns.title')
// In Vue component
<el-button>{{ $t('rich_text.template.actions.add') }}</el-button>
```
## Auto-generation Checklist
When generating frontend i18n:
- [ ] Create lang directory structure if not exists: `lang/zh-cn/` and `lang/en/`
- [ ] Filename format: `{module}.{feature}.json` (e.g., `rich_text.template.json`)
- [ ] Generate `zh-cn/{module}.{feature}.json` with nested object structure
- [ ] Generate `en/{module}.{feature}.json` with nested object structure
- [ ] Do NOT include key prefix in JSON content (prefix comes from filename)
- [ ] Add all column labels, placeholders, and validation messages
## Key Naming Convention
### Plugin Mode
| Type | File Location | Key in JSON | Full Key Used in Code |
|------|---------------|-------------|----------------------|
| Title | `apps/official/lang/zh-cn/rich_text.template.json` | `"dialog_title"` | `official.rich_text.template.dialog_title` |
| Column | `apps/official/lang/zh-cn/rich_text.template.json` | `"table.columns.title"` (nested) | `official.rich_text.template.table.columns.title` |
| Search | `apps/official/lang/zh-cn/rich_text.template.json` | `"search_form.title"` (nested) | `official.rich_text.template.search_form.title` |
| Form | `apps/official/lang/zh-cn/rich_text.template.json` | `"form.title"` (nested) | `official.rich_text.template.form.title` |
| Action | `apps/official/lang/zh-cn/rich_text.template.json` | `"actions.add"` (nested) | `official.rich_text.template.actions.add` |
### Built-in Mode
| Type | File Location | Key in JSON | Full Key Used in Code |
|------|---------------|-------------|----------------------|
| Title | `admin/src/lang/zh-cn/rich_text.template.json` | `"dialog_title"` | `rich_text.template.dialog_title` |
| Column | `admin/src/lang/zh-cn/rich_text.template.json` | `"table.columns.title"` (nested) | `rich_text.template.table.columns.title` |
| Search | `admin/src/lang/zh-cn/rich_text.template.json` | `"search_form.title"` (nested) | `rich_text.template.search_form.title` |
| Form | `admin/src/lang/zh-cn/rich_text.template.json` | `"form.title"` (nested) | `rich_text.template.form.title` |
| Action | `admin/src/lang/zh-cn/rich_text.template.json` | `"actions.add"` (nested) | `rich_text.template.actions.add` |
## File Naming Rules
1. **Module name in filename uses underscore**: `rich_text.template.json` (NOT `rich-text.template.json`)
2. **Feature name in filename uses lowercase**: `template` (NOT `Template`)
3. **File extension**: `.json`
4. **Directory structure**: `lang/{locale}/{module}.{feature}.json`
## Integration with Frontend Code
The generated i18n keys should match the schemas/index.tsx:
```typescript
// schemas/index.tsx
import { $t } from '@/locales'
export const crudSchema = (): CrudSchema => {
return {
// File: official/lang/zh-cn/rich_text.template.json
// Key: "dialog_title"
dialogTitle: $t('official.rich_text.template.dialog_title'),
columns: [
{
prop: 'title',
// Key: "table.columns.title"
label: $t('official.rich_text.template.table.columns.title'),
},
],
searchFormSchema: {
schema: [
{
// Key: "search_form.title"
label: $t('official.rich_text.template.search_form.title'),
prop: 'title',
},
],
},
formSchema: {
schema: [
{
// Key: "form.title"
label: $t('official.rich_text.template.form.title'),
prop: 'title',
},
],
},
}
}
```
## Common Mistakes to Avoid
1. ❌ **Wrong**: Including prefix in JSON keys
```json
{
"official.rich_text.template.dialog_title": "富文本模板"
}
```
2. ✅ **Correct**: Nested object without prefix
```json
{
"dialog_title": "富文本模板"
}
```
3. ❌ **Wrong**: Using hyphen in filename
```
lang/zh-cn/rich-text.template.json
```
4. ✅ **Correct**: Using underscore in filename
```
lang/zh-cn/rich_text.template.json
```
5. ❌ **Wrong**: Flat keys with dots
```json
{
"table.columns.sort": "排序"
}
```
6. ✅ **Correct**: Nested objects
```json
{
"table": {
"columns": {
"sort": "排序"
}
}
}
```
@@ -0,0 +1,41 @@
---
name: frontend-admin-router
description: Admin 前端路由规范,基于 Vue Router
globs:
- "frontend/admin/src/router/**/*.ts"
---
## 路由结构
```typescript
import type { RouteRecordRaw } from 'vue-router';
const routes: RouteRecordRaw[] = [
{
path: '/{module}',
component: () => import('#/layouts/DefaultLayout.vue'),
children: [
{
path: '{model}',
name: '{App}{Model}List',
component: () => import('#/views/{module}/{model}/index.vue'),
meta: { title: '{title}', permission: '{module}:{model}:list' },
},
],
},
];
```
## 关键约定
- 路由 name:`{App}{Model}List`(PascalCase)
- 路由 path:`/{module}/{model}`(kebab-case)
- 所有页面使用懒加载(dynamic import)
- 权限通过 `meta.permission` 配置
## 检查清单
- [ ] 路由 name 是否 PascalCase
- [ ] 是否使用懒加载
- [ ] 权限配置是否正确
- [ ] 父子路由嵌套是否合理
@@ -0,0 +1,45 @@
---
name: frontend-admin-store
description: Admin 前端状态管理规范,基于 Pinia
globs:
- "frontend/admin/src/store/**/*.ts"
- "apps/**/src/stores/**/*.ts"
---
## 代码模板
```typescript
import { defineStore } from 'pinia';
interface {Model}State {
list: any[];
total: number;
}
export const use{Model}Store = defineStore('{module}-{model}', {
state: (): {Model}State => ({
list: [],
total: 0,
}),
actions: {
async fetchList(params?: any) {
const res = await get{Model}List(params);
this.list = res.data.items;
this.total = res.data.total;
},
},
});
```
## 关键约定
- Store 命名:`use{Name}Store`
- Store ID:`kebab-case`
- State 使用类型定义
- Actions 使用 async/await
## 检查清单
- [ ] Store 名称是否唯一
- [ ] State 是否有类型定义
- [ ] Actions 是否有错误处理
@@ -0,0 +1,50 @@
---
name: frontend-admin-view
description: Admin 前端视图层规范,Vue 3 + Element Plus + VxeTable 页面模板
globs:
- "frontend/admin/src/views/**/*.vue"
- "apps/**/src/views/**/*.vue"
---
## 文件位置
```
frontend/admin/src/views/{module}/{name}/
├── index.vue # 主项目列表页面
└── schemas/index.tsx # CRUD Schema 定义
# 插件 Admin 视图(例如 produce)
# 模板源:必须优先修改
backend/plugin/{plugin}/resource/template/admin/views/{module}/{name}/
├── index.vue
└── schemas/index.tsx
# 运行目录:模板源完成后必须同步并用于验证
frontend/admin/src/apps/{app}/views/{module}/{name}/
├── index.vue
└── schemas/index.tsx
```
## 技术栈
- Vue 3 + TypeScript + Vite
- Element Plus(UI 组件库)
- VxeTable(表格组件)
- Pinia(状态管理)
- Tailwind CSS(样式)
## 关键约定
- 每个页面使用 `defineOptions({ name: '{App}{Model}List' })` 设置组件名
- CRUD 页面使用 `BasicCrud` 与 `useCrud({ ...crudSchema() })` 组合
- 查询条件、列表列和表单字段统一定义在同目录 `schemas/index.tsx` 的 `CrudSchema` 中
- `crudApi` 绑定 API service 的 `create`、`update`、`remove`、`get` 方法
- 页面保留 `art-full-height` 容器,并使用 `defineOptions({ name: '{App}{Model}List' })` 命名
## 检查清单
- [ ] 组件名是否按规范命名
- [ ] API 导入路径是否正确
- [ ] 表单校验规则是否完整
- [ ] `CrudSchema` 是否配置 `crudApi`、权限码、查询字段、列表列与表单规则
- [ ] 页面是否保留 BasicCrud 默认操作入口,未以空动作数组覆盖
@@ -0,0 +1,248 @@
---
name: frontend-app-common
description: scanapp 移动端公共开发规范,基于 uni-app、Vue 和 uView Pro,覆盖页面组织、路由、组件、扫码交互、视觉字号和交付检查
globs:
- "frontend/scanapp/**/*.vue"
- "frontend/scanapp/**/*.js"
- "frontend/scanapp/pages.json"
---
## 技术基线
当前 APP 位于 `frontend/scanapp/`,使用以下技术约定:
- uni-app 页面运行模式。
- 页面采用 Vue 单文件组件(`.vue`)。
- `main.js` 注册 `@/uni_modules/uview-pro`,并注入全局主题 mixin;每个页面可直接通过 `themeMode` 获取 `light`/`dark`,根容器必须绑定 `:class="`theme-${themeMode}`"`。
- 全局主题状态由 `utils/theme.js` 管理:`applyAppTheme(mode)` 会持久化 `scanapp_theme_mode`、同步导航栏颜色并广播 `scanapp-theme-change`;设置页切换主题时必须调用它,不能只修改本页 class。
- `pages.json` 开启 uView Pro Easycom 自动组件扫描。
- uView Pro 组件使用 `u-*` 命名,例如 `u-icon`、`u-input`、`u-form`、`u-button`、`u-switch`。
- 兼容 Vue 2 和 Vue 3 入口,新增页面优先遵循现有 Options API 写法,除非项目整体迁移。
## 目录与页面组织
```text
frontend/scanapp/
├── App.vue
├── main.js
├── pages.json
├── uni.scss
├── pages/
│ ├── auth/
│ │ ├── login.vue
│ │ └── register.vue
│ ├── entry/
│ │ ├── order.vue
│ │ └── machine.vue
│ ├── exception/
│ │ └── index.vue
│ ├── repair/
│ │ ├── index.vue
│ │ └── tasks.vue
│ ├── query/
│ │ ├── index.vue
│ │ ├── index copy.vue
│ │ └── index.vue
│ ├── settings/
│ │ └── index.vue
│ ├── statistics/
│ │ └── index.vue
│ └── index/
│ └── index.vue
└── uni_modules/
```
新增页面时:
1. 页面文件放入 `pages/<module>/`。
2. 页面入口优先使用 `index.vue`,业务模式页面可使用明确名称,例如 `entry/order.vue`。
3. 必须在 `pages.json` 的 `pages` 数组中注册路由。
4. `pages.json` 中的路径不带 `.vue` 后缀,例如 `pages/settings/index`;设置模块也允许使用与路由同名的平铺页面文件,例如 `pages/settings/feedback.vue` 对应 `pages/settings/feedback`。
5. 页面导航标题通过路由项的 `style.navigationBarTitleText` 配置。
6. 不要把未注册的副本页面当作运行页面维护;例如 `index copy.vue` 不属于正式路由。
7. `pages.json` 的首项保持为 `pages/index/index`,APP 启动默认进入首页,不把登录页放在最前面。
## 页面结构规范
页面组件按以下顺序组织:
1. `<template>`:页面结构和交互绑定。
2. `<script>`:`data`、`computed`、`watch`、`methods` 和生命周期。
3. `<style lang="scss" scoped>`:页面局部样式。
页面模板建议采用以下结构:
- 页面根节点使用语义化页面类名,例如 `.settings-page`、`.query-page`、`.entry-page`。
- 页面按业务区块拆分为卡片、列表、表单和操作区域。
- 重复数据使用 `v-for`,并使用稳定的业务字段作为 `:key`。
- 交互统一使用 `@tap` 或现有组件支持的事件写法。
- 对话框、提示和扫码等平台能力统一通过 `uni.*` API 调用。
## 组件使用规范
优先使用项目已有的 uView Pro 组件,不重复实现同类基础控件:
| 场景 | 组件或能力 |
|------|------------|
| 图标 | `u-icon` |
| 文本输入 | `u-input` |
| 表单 | `u-form`、`u-form-item` |
| 按钮 | `u-button` |
| 开关 | `u-switch` |
| 空状态 | `u-empty` |
| 页面提示 | `uni.showToast` |
| 操作选择 | `uni.showActionSheet` |
| 页面跳转 | `uni.navigateTo`、`uni.navigateBack`、`uni.reLaunch` |
| 基础相机扫码 | `uni.scanCode` |
| 相册识图 / 原生扫码体验 | `@/uni_modules/mas-code-scan` 的 `openCodeScan`(仅 App-Plus) |
使用 uView Pro 组件时:
- 通过 Easycom 直接使用 `u-*` 标签,不在页面内重复注册组件。
- 沿用项目当前属性命名和事件写法。
- 按钮、输入框、表单等控件保持页面间的尺寸、圆角和状态表现一致。
- 图标使用 `u-icon`,不手写重复的 SVG 图标。
## 页面交互规范
### 页面跳转
使用 `uni` 导航 API,并与 `pages.json` 中已注册路径保持一致:
```js
uni.navigateTo({
url: '/pages/settings/index'
})
```
APP 默认允许直接进入首页和访问普通页面;登录页只在用户点击需要登录的能力时出现。需要登录的入口先检查本地登录态,未登录时使用 `uni.showModal` 提示,再跳转 `/pages/auth/login?redirect=<目标页>`;登录成功后写入登录态并使用 `uni.redirectTo` 回到 `redirect` 指定页面。普通功能页之间使用 `uni.navigateTo`。
### 扫码
基础相机扫码可使用 `uni.scanCode`:
```js
uni.scanCode({
onlyFromCamera: true,
scanType: ['barCode', 'qrCode'],
success: (res) => {
this.form.code = String(res.result || '').trim()
},
fail: () => {
uni.showToast({ title: '未完成扫码', icon: 'none' })
}
})
```
要求:
- 扫码结果先转为字符串并去除首尾空格。
- 同时支持条码和二维码时使用 `['barCode', 'qrCode']`。
- 扫码失败必须给出中文提示。
- 扫码后的结果应回填当前字段,不要跨页面隐式修改无关状态。
- 若业务需要相册识图、图片预览加载态、智能变焦或多码点选,使用 `@/uni_modules/mas-code-scan` 的 `openCodeScan`,并以 `// #ifdef APP-PLUS` 包裹导入与调用;`success` 中读取 `result.content` 后回填字段,`9021003` 视为用户取消不提示,其他错误显示中文提示。
- 有效识别区域通过 `options.scanRegion` 传给 `openCodeScan`,结构为 `fullScreen` 或 `width`、`height`、`centerX`、`centerY` 的 0~1 比例;业务页面统一调用 `@/utils/scan-settings.js` 的 `getScanRegionSetting()`,设置按 `scanapp_settings_<userCode>` 持久化。未配置时返回空对象,保持插件原有整帧识别行为。
- APP-PLUS Android 原生 `uni.showModal` 的按钮视觉顺序与 H5 不一致时,统一由 `main.js` 的平台适配层交换显示文本,并还原 `success`/Promise 的 `confirm`、`cancel` 语义;业务页面继续按标准语义编写,不要逐个页面交换按钮或回调判断。
### 表单与提示
- 提交前先执行页面级校验。
- 校验失败通过 `uni.showToast({ title, icon: 'none' })` 提示。
- 提交期间使用 `loading` 或 `submitting` 状态防止重复提交。
- 成功、失败和异常状态使用明确的中文提示。
- 原型阶段可以使用本地 mock 数据,但要保持数据结构与真实业务字段一致,并将接口接入限制在 service/API 层,不在模板中直接拼接请求。
### 接口封装
- scanapp 对接后端时统一通过 `frontend/scanapp/utils/request.js` 发起 `uni.request`,页面中不要直接调用 `uni.request`。
- 请求配置集中放在 `frontend/scanapp/config/index.js`,当前生产插件 APP 业务端默认 `baseURL` 为 `/appapi`;主项目系统 API(例如 `/file/image` 上传)实际注册在 `/api`,调用时必须显式传入 `baseURL: config.apiBaseURL`,不能沿用默认基址。
- 业务接口按模块放入 `frontend/scanapp/api/<module>/`,例如生产扫码接口放在 `api/produce/data-entry.js`。
- API 函数命名按页面语义表达,例如 `getProcessOptions`、`submitOrderRecord`、`getOrderStatistics`,页面只调用这些函数。
- 业务表单中的图片统一采用“本地预览、提交时上传”:选图后仅保存 `tempFilePaths` 用于立即预览;提交时调用 `@/api/upload.js` 的 `uploadImage(filePath, subDir)`,将返回的服务器 URL 放入业务请求。上传失败时中止提交并保留本地图片,便于用户重试。
- 图片上传使用 `/api/file/image` 和 `config.apiBaseURL`;公共 `normalizeImageUrl()` 会将后端本地存储返回的 `127.0.0.1` / `localhost` URL 改写为配置的局域网后端地址,避免 Android 真机将回环地址解析为手机自身。历史记录中的服务器图片地址也必须经过该方法规范化。
- 响应格式沿用后端 `Json::success` / `Json::fail`:`code === 0` 时默认返回 `data`,失败使用 `msg` 做中文提示。
- token 从本地 `scanapp_token` 读取并写入 `Authorization` 请求头;后续接真实登录接口时替换原型 token 来源,不改页面调用方式。
## 样式与视觉规范
### 基础单位
- 移动端尺寸统一使用 `rpx`。
- 不在同一页面混用无必要的 `px`、`rem` 和 `rpx`。
- 页面根节点设置 `min-height: 100vh`,并统一使用 `box-sizing: border-box`。
- 卡片、按钮和输入区域的尺寸应保持稳定,避免文字变化造成布局跳动。
### 字号层级
正式运行页面统一使用以下字号档位,不新增其他字号:
| 字号 | 使用场景 |
|------|----------|
| `38rpx` | 页面主标题、品牌主标题 |
| `32rpx` | 核心数据、关键数字、弹层主编号 |
| `28rpx` | 区块标题、记录主标题、主要设置项 |
| `26rpx` | 字段值、重点内容、次级编号 |
| `24rpx` | 操作文字、重要次级内容 |
| `22rpx` | 正文、按钮文字、表单标签 |
| `20rpx` | 说明文字、筛选项、辅助信息 |
| `18rpx` | 状态、时间、标签、脚注 |
字号要求:
- 最大字号不得超过 `38rpx`。
- 最小字号不得低于 `18rpx`。
- 不使用 `23rpx`、`30rpx`、`34rpx`、`36rpx`、`40rpx` 等非标准档位。
- 同一语义在不同页面使用相同字号,例如所有页面的卡片标题统一为 `28rpx`。
- 只因字号调整而产生的视觉问题,优先调整容器宽度或换行,不随意增加新的字号档位。
### 颜色与层级
沿用当前 APP 的清爽生产工具风格:
- 页面背景使用浅色中性背景,例如 `#f4f6fb`、`#f5f7fb`。
- 内容卡片使用白色背景和轻量阴影。
- 主色优先使用项目现有蓝色 `#1677ff` 或 `#2f6cf6`。
- 成功状态使用绿色,失败或危险状态使用红色,警告状态使用橙色或黄色。
- 文字颜色按主标题、正文、说明、辅助信息分级,不使用过多高饱和颜色。
- 不在单个页面引入与现有页面不一致的主题色、渐变方向或状态颜色。
### 布局一致性
- 页面内容使用统一的左右内边距和卡片间距,除非现场扫码等业务场景确实需要特殊布局。
- 卡片圆角、边框、阴影应与同类页面保持一致。
- 列表项使用分隔线或稳定卡片层级,不将多个卡片无意义地嵌套。
- 右侧操作统一使用开关、值文本、箭头或图标表达,不重复增加装饰性按钮。
- 小屏幕上长单号、商品编码等业务文本允许换行或使用 `word-break: break-all`。
- 安卓端需要控制图标、文字、开关之间的横向距离时,优先使用显式 `margin-left` / `margin-right` 或固定 `flex-shrink: 0`,不要把关键间距只交给 `gap`;涉及 `u-switch`、自定义开关和扫码结果卡片时尤其如此。
- 如果同一行里有图标+文字+开关这类组合,先确认安卓端真机表现,再决定是否保留 `gap`,默认把分离距离写在紧邻元素上。
## 状态与数据规范
- 页面临时状态放在当前页面 `data` 中;跨页面共享状态需要先确认项目是否已有 store 方案。
- 加载、空数据、提交中、成功、失败和禁用状态必须有明确表现。
- mock 数据仅用于原型展示,不应伪装成已接入后端。
- 不在本次样式任务中删除已有功能、字段、交互或路由。
- 修改中心样式或公共能力前,先搜索所有页面调用方。
## 维护与演进规则
当项目实际实现与本技能不一致时:
1. 先读取真实入口、路由、组件配置和页面调用方式。
2. 以项目代码为准,不强行套用旧模板。
3. 将已验证的新约定同步到本技能文件。
4. 技能更新只描述确认过的差异,不进行无关重写。
## 开发检查清单
- [ ] 页面文件位于 `frontend/scanapp/pages/` 合适目录。
- [ ] 新页面已注册到 `pages.json`。
- [ ] 导航路径与 `pages.json` 完全一致。
- [ ] 已复用 uView Pro 组件和现有 `uni.*` 能力。
- [ ] 扫码结果已清理空格,并处理成功和失败状态。
- [ ] 表单已处理空值、重复提交和失败提示。
- [ ] 字号只使用 `18/20/22/24/26/28/32/38rpx`。
- [ ] 页面没有超过 `38rpx` 或低于 `18rpx` 的字号。
- [ ] 同类页面的标题、正文、说明、状态字号一致。
- [ ] 未修改与需求无关的功能、数据和路由。
- [ ] 真机或移动端预览已检查长文本、空状态、弹层和底部操作区域。
@@ -0,0 +1,44 @@
---
name: frontend-web-api
description: Web 前台 API 层规范(Nuxt 4),基于 $fetch 或 useFetch
globs:
- "frontend/web/app/api/**/*.ts"
- "frontend/web/app/apps/**/api/**/*.ts"
---
## 文件位置
```
frontend/web/app/api/{module}/index.ts
```
## 代码模板
当前项目 Web 端统一通过 `~/utils/request` 封装请求,自动处理 `/api/` 基础路径、认证头、响应解包和错误提示:
```typescript
import request from '~/utils/request'
export function get{Model}List(params: Record<string, any> = {}) {
return request.get('/{module}/{model}', params)
}
export function get{Model}Detail(id: string | number) {
return request.get(`/{module}/{model}/${id}`)
}
```
## 关键约定
- 优先使用 `~/utils/request`,不要在业务 API 中直接使用 axios 或 `$fetch`
- 传给 `request` 的路径使用后端业务路径(如 `/produce/process`),由请求封装统一拼接 `/api/`
- 默认响应已解包为 `data`;需要完整响应时传递 `{ returnFullResponse: true }`
- 上传使用 `request.upload` 并传入 `FormData`,不要在页面中重复实现认证和基础 URL 逻辑
- 错误由请求封装统一提示,业务层只处理状态和页面交互
## 检查清单
- [ ] 是否使用 $fetch/useFetch 而非 axios
- [ ] 路径是否以 /api/ 开头
- [ ] SSR 场景是否使用 useFetch
- [ ] 是否处理了请求错误
@@ -0,0 +1,40 @@
---
name: frontend-web-common
description: Web 前台公共规范(Nuxt 4),项目结构、命名约定、工具函数
globs:
- "frontend/web/**/*.ts"
- "frontend/web/**/*.vue"
---
## 项目结构
```
frontend/web/
├── app/
│ ├── api/ # API 层
│ ├── components/ # 组件
│ ├── composables/ # 组合式函数
│ ├── layouts/ # 布局
│ ├── middleware/ # 中间件
│ ├── pages/ # 页面
│ ├── plugins/ # 插件
│ ├── stores/ # Pinia 状态
│ ├── types/ # 类型定义
│ └── utils/ # 工具函数
├── public/
├── nuxt.config.ts
└── package.json
```
## 关键约定
- 使用 Nuxt 4 目录约定(`app/` 目录结构)
- Composables 使用 `use` 前缀
- 工具函数放在 `utils/` 下
- 中间件放在 `middleware/` 下
## 检查清单
- [ ] 目录结构是否符合 Nuxt 4 约定
- [ ] composable 命名是否以 use 开头
- [ ] 工具函数是否归类到 utils/
@@ -0,0 +1,31 @@
---
name: frontend-web-component
description: Web 前台组件规范(Nuxt 4),Element Plus 组件使用与封装
globs:
- "frontend/web/app/components/**/*.vue"
---
## 组件分类
| 分类 | 说明 |
|------|------|
| advertisement/ | 广告组件 |
| icon/ | 图标组件 |
| login-dialog/ | 登录弹窗 |
| search-box/ | 搜索组件 |
| seo/ | SEO 组件 |
| sms-code/ | 短信验证码 |
## 关键约定
- Nuxt auto-imports `components/` 下的所有组件
- 组件名与文件名一致(PascalCase)
- 公共组件放在 `components/`,页面私有组件放在 `components/{page}/`
- 使用 Element Plus 组件时直接使用 `<el-*>` 标签
## 检查清单
- [ ] 组件名是否 PascalCase
- [ ] 是否利用 Nuxt auto-import
- [ ] 是否使用 Composition API
- [ ] Props 是否有默认值
@@ -0,0 +1,40 @@
---
name: frontend-web-layout
description: Web 前台布局规范(Nuxt 4),布局文件组织与使用
globs:
- "frontend/web/app/layouts/**/*.vue"
---
## 布局文件
| 布局 | 文件 | 说明 |
|------|------|------|
| default | `layouts/default.vue` | 默认布局(含 header/footer) |
| blank | `layouts/blank.vue` | 空白布局 |
| container | `layouts/container.vue` | 容器布局 |
| member | `layouts/member.vue` | 会员中心布局 |
## 代码模板
```vue
<template>
<div>
<Header />
<slot />
<Footer />
</div>
</template>
```
## 关键约定
- Nuxt 自动加载 `layouts/` 目录下的布局文件
- 页面通过 `definePageMeta({ layout: 'name' })` 指定布局
- 默认布局为 `default.vue`
- 布局中可使用 `<slot />` 嵌入页面内容
## 检查清单
- [ ] 布局文件是否放在 layouts/ 目录
- [ ] 页面是否指定了正确的布局
- [ ] 布局中是否包含必要的公共元素
@@ -0,0 +1,38 @@
---
name: frontend-web-store
description: Web 前台状态管理规范(Nuxt 4),基于 Pinia
globs:
- "frontend/web/app/stores/**/*.ts"
---
## 代码模板
```typescript
import { defineStore } from 'pinia';
export const use{Model}Store = defineStore('{model}', () => {
const list = ref<any[]>([]);
const total = ref(0);
async function fetchList(params?: any) {
const res = await get{Model}List(params);
list.value = res.data.items;
total.value = res.data.total;
}
return { list, total, fetchList };
});
```
## 关键约定
- 使用 Pinia `setup store` 语法(Composition API)
- Store 命名:`use{Name}Store`
- Store ID:`kebab-case`
- Nuxt 项目中 store 支持 SSR
## 检查清单
- [ ] 是否使用 setup store 语法
- [ ] Store 名称是否唯一
- [ ] SSR 场景是否正确处理 hydration
@@ -0,0 +1,47 @@
---
name: frontend-web-view
description: Web 前台页面规范(Nuxt 4),页面文件组织与组件使用
globs:
- "frontend/web/app/pages/**/*.vue"
- "frontend/web/app/apps/**/pages/**/*.vue"
---
## 文件位置
主站页面:
```
frontend/web/app/pages/{module}/
├── index.vue # 列表/首页
├── [id].vue # 详情页
└── create.vue # 创建页
```
插件页面:
```
backend/plugin/{plugin}/resource/template/web/pages/{module}/{name}/
frontend/web/app/apps/{plugin}/pages/{module}/{name}/
```
插件开发必须先修改模板源,再同步到前端运行目录,并核对两者实现一致。
## 技术栈
- Vue 3 + Nuxt 4
- Element Plus
- Pinia
- UnoCSS
## 关键约定
- 页面使用 Nuxt 文件路由(`pages/` 目录自动生成路由)
- 动态路由使用 `[param].vue` 命名
- 布局文件使用 `layouts/` 目录
- 组件使用 `components/` 目录,自动全局注册
## 检查清单
- [ ] 页面文件命名是否符合 Nuxt 文件路由规范
- [ ] 是否使用了正确的布局
- [ ] API 调用是否通过 composable 封装
+279
View File
@@ -0,0 +1,279 @@
---
description: Madong 插件全流程开发执行工作流
---
# 插件开发执行 Workflow
用于按步骤执行一个 Madong 插件的完整开发流程,覆盖需求确认、目录核对、后端 CRUD、Swagger、菜单、前端模板源、安装验证,以及卸载重装防回退验证。
执行本 workflow 前,优先参考:
- `.windsurf/skills/cross-plugin-fullstack-development/SKILL.md`
## Step 1. 明确输入信息
先向用户确认以下信息,不完整时不要直接生成代码:
- 插件名,例如 `article`
- 插件显示名称
- 插件 key / 业务目录名
- 需要开发的模块列表,例如 `content`、`category`、`tag`
- 每个模块的数据库表名
- 字段定义、索引、是否软删除
- 后端路由前缀
- 权限码前缀
- 是否需要前台 API
- 是否需要菜单导入
- 是否要求卸载再安装后仍保留最新前端结构
如果用户给的是表结构或截图,先整理成统一规格后再继续。
## Step 2. 核对现有插件与参考实现
优先读取并核对以下路径:
- `backend/plugin/{plugin}/app`
- `backend/plugin/{plugin}/config`
- `backend/plugin/{plugin}/resource/menu`
- `backend/plugin/{plugin}/resource/template/admin`
- `.windsurf/skills/cross-plugin-fullstack-development/SKILL.md`
同时寻找至少一个真实参考实现:
- 同类已完成插件
- 主项目现有 `BasicCrud` 页面
- 现有 CRUD 控制器
- 现有插件菜单与语言文件
如果发现 skill 与真实项目结构不一致,先修正 skill,再继续开发。
## Step 3. 建立开发计划
将任务拆成 3 到 5 个里程碑,并保持只有一个 `in_progress`:
- 后端结构与数据层
- 控制器、Swagger 与菜单
- 前端模板源开发与安装验证
- 重装验证与收口
如任务只是补模板源或补语言文件,可用更轻量计划。
## Step 4. 搭建或核对后端数据层
按模块逐项处理:
1. 迁移文件
2. Model
3. DAO
4. Service
5. Validate
6. Request / Response Schema
检查点:
- 主键是否使用项目当前雪花 ID 写法
- 字段、索引、软删除是否与需求一致
- DAO / Service / Validate 基类是否与项目一致
- 命名空间是否在插件路径下正确展开
- DAO / Service 是否显式导入 `core\base\BaseDao` / `core\base\BaseService`
- Service 是否使用构造注入 DAO,而不是手动 `new Dao()`
- 如 DAO 重写 `selectList()`,方法签名是否与当前项目 `BaseDao` 一致
- 如果模块来自旧项目迁移,是否已确认当前项目是否仍需要 `tenant_id`
## Step 5. 实现 Admin 控制器与标准 CRUD 路由
为每个模块实现 Admin 控制器,并保证:
- 继承 `app\adminapi\controller\Crud`
- 构造函数注入 Service 与 Validate
- 标准方法优先复用父类
- 路由前缀与前端 API 一致
- 权限码与菜单按钮权限一致
如存在旧 demo 控制器,先确认是否会与新 CRUD 冲突。
## Step 6. 处理 Swagger / OpenAPI
在继续前端之前,必须先排除 Swagger 冲突。
逐项核对:
- 新旧控制器是否使用重复 `path`
- `OA\Get`、`OA\Post`、`OA\Put`、`OA\Delete` 是否唯一
- QueryRequest / FormRequest / Response Schema 是否对应
- 批量删除、详情、列表等接口注解是否与项目现有写法一致
如果旧演示接口与新接口冲突:
- 改到 `legacy` 路径
- 或迁移到单独演示控制器
- 不要直接删除未经确认的旧内容
## Step 7. 配置插件菜单与权限
核对并维护:
- `backend/plugin/{plugin}/resource/menu/admin.php`
- 菜单 `component` 路径
- 菜单 `app` 字段
- 权限码与控制器一致性
必须保证:
- 插件菜单来源于安装导入的系统菜单
- 不依赖前端自动扫描插件 routes 作为导航入口
- `component` 指向插件 views 实际路径
## Step 8. 在模板源实现前端
正式开发目录应放在:
- `backend/plugin/{plugin}/resource/template/admin/api`
- `backend/plugin/{plugin}/resource/template/admin/views`
- `backend/plugin/{plugin}/resource/template/admin/lang`
按模块实现:
1. `api/index.ts`
2. `views/{plugin_key}/{module}/index.vue`
3. `views/{plugin_key}/{module}/schemas/index.tsx`
4. `lang/zh-cn/zh.json`
5. `lang/en/en.json`
要求:
- 页面入口统一使用 `BasicCrud`
- `schemas` 使用 `CrudSchema`
- 标准接口优先接 `BaseService`
- i18n key 中英文成对补齐
- 表头索引等公共 key 必须统一
- 默认不要单独开发 `frontend/admin/src/apps/{plugin}`
- 通过安装流程生成运行目录并验证结果,而不是双份维护
## Step 9. 安装插件并生成运行目录
当模板源完成后,安装插件或执行相应同步流程,确认以下目录被生成或更新:
- `frontend/admin/src/apps/{plugin}/api`
- `frontend/admin/src/apps/{plugin}/views`
- `frontend/admin/src/apps/{plugin}/lang`
如果运行目录已存在旧文件,重点检查是否已被模板源最新内容覆盖。
## Step 10. 基于安装结果验证页面
联调时重点检查:
- 页面是否为官方 `BasicCrud` 外观
- 列表接口返回是否与 `items/total` 结构兼容
- 增删改查是否可用
- 查询参数是否与后端真实支持格式一致
- 中英文是否存在缺词
- 菜单点击是否能打开正确页面
- `format=select` 下拉是否能显示正确标签;若不显示,优先检查模型是否提供 `getNameAttribute()`
- JSON 规则字段是否支持前端 textarea 文本输入与后端数组/字符串双形态校验
默认不要直接修改运行目录。
只有在用户明确要求排查安装产物问题时,才临时查看或修补运行目录,并且必须把最终改动落实回模板源。
## Step 11. 执行模板源一致性检查
在交付前强制执行以下比对:
- 安装生成后的运行目录是否与模板源一致
- `index.vue` 是否按模板源生成
- `schemas/index.tsx` 是否按模板源生成
- `api/index.ts` 是否按模板源生成
- `lang/zh-cn/zh.json` 是否按模板源生成
- `lang/en/en.json` 是否按模板源生成
只改运行目录但未落实模板源,不算完成。
## Step 12. 卸载再安装验证
若用户明确要求“重装后不要回退”,必须做这一步。
验证目标:
- 卸载插件后重新安装
- 确认模板源再次复制到运行目录后,页面仍是最新结构
- 确认未回退到旧 demo 样式
- 确认菜单、权限、Swagger 依旧正常
如果没有时间完整执行,也要明确告诉用户:
- 当前完成的是代码层同步
- 仍建议再做一次卸载重装验证
## Step 13. 收口与交付
最终汇报时至少说明:
- 改了哪些后端文件
- 改了哪些模板源文件
- 是否通过安装方式验证了运行目录产物
- 是否补齐了菜单与语言
- 是否处理了 Swagger 冲突
- 是否完成重装防回退验证
- 是否顺手更新了相关 skill
## Step 14. 常见二次排错顺序
当插件页面已打开但接口返回 500 时,优先按以下顺序排查:
1. Service 是否显式导入 `core\base\BaseService`
2. DAO 是否显式导入 `core\base\BaseDao`
3. Service 是否错误地手动 `new Dao()`
4. DAO 重写方法签名是否与当前 `BaseDao` 一致
5. 模型关联名是否与 DAO `with([...])` 一致
6. Validate 对 JSON 字段和枚举字段的写法是否与当前项目兼容
## 常见分支处理
### 分支 A. 仅修复插件前端页面样式
执行:
- Step 2
- Step 3
- Step 8
- Step 9
- Step 10
- Step 11
- Step 13
### 分支 B. 仅修复重装后回退
执行:
- Step 2
- Step 3
- Step 8
- Step 11
- Step 12
- Step 13
### 分支 C. 修复 Swagger 冲突
执行:
- Step 2
- Step 3
- Step 5
- Step 6
- Step 13
### 分支 D. 新建完整插件模块
执行全部步骤。
## 完成判定
满足以下条件,才可判定“插件开发完成”:
- 后端 CRUD 可运行
- Swagger 无冲突
- 菜单与权限可用
- 模板源前端完整
- 运行目录联调通过
- 用户要求重装防回退时,已完成重装验证或已明确说明未验证部分
+22
View File
@@ -0,0 +1,22 @@
---
auto_execution_mode: 0
description: Review code changes for bugs, security issues, and improvements
---
You are a senior software engineer performing a thorough code review to identify potential bugs.
Your task is to find all potential bugs and code improvements in the code changes. Focus on:
1. Logic errors and incorrect behavior
2. Edge cases that aren't handled
3. Null/undefined reference issues
4. Race conditions or concurrency issues
5. Security vulnerabilities
6. Improper resource management or resource leaks
7. API contract violations
8. Incorrect caching behavior, including cache staleness issues, cache key-related bugs, incorrect cache invalidation, and ineffective caching
9. Violations of existing code patterns or conventions
Make sure to:
1. If exploring the codebase, call multiple tools in parallel for increased efficiency. Do not spend too much time exploring.
2. If you find any pre-existing bugs in the code, you should also report those since it's important for us to maintain general code quality for the user.
3. Do NOT report issues that are speculative or low-confidence. All your conclusions should be based on a complete understanding of the codebase.
4. Remember that if you were given a specific git commit, it may not be checked out and local code states may be different.
+69
View File
@@ -0,0 +1,69 @@
# Windsurf Project Rules
以提升代码质量和开发效率为宗旨,持续保证代码的易读性、可维护性和规范性。
When a user request matches a Madong framework task, automatically use the relevant workspace skill from `.windsurf/skills/`.
Prefer these skill groups by intent:
- `backend-gen-*` for backend code generation tasks such as CRUD, model, DAO, service, controller, validate, schema, migrate, route, event, and listener generation.
- `backend-*` for backend conventions such as config, bootstrap, middleware, exception, swagger, command, queue, cache, upload, logger, and tests.
- `frontend-admin-*` for Admin frontend tasks in Vue 3, Element Plus, routing, stores, API calls, i18n, views, and generated pages.
- `frontend-web-*` for Nuxt web frontend tasks, including pages, components, layouts, stores, and API calls.
- `cross-*` for cross-layer API conventions, database design, and app skeleton decisions.
If multiple skills match, use the most specific generation skill first, then supporting convention skills as needed.
When a skill conflicts with the actual project implementation, treat the existing project code as authoritative. First verify the real routes, directory structure, naming conventions, base classes, APIs, and runtime behavior from the current project, then update the relevant `.windsurf/skills/` skill content to match the existing project before using it again.
## Skill Auto-Evolution Rule
Skills must evolve with the current project. When using any `.windsurf/skills/` skill, always compare the skill instructions with the actual project implementation. If you discover newer or more accurate conventions in the project, proactively update the corresponding skill so future tasks follow the corrected convention.
Auto-evolve skills when any of the following mismatches are found:
- Real backend routes, controller paths, OpenAPI attributes, or permission codes differ from the skill.
- Real frontend API base URLs, request/response formats, component usage, or route conventions differ from the skill.
- Real directory structure, file naming, namespaces, base classes, traits, stores, hooks, or schemas differ from the skill.
- Real runtime behavior, validation rules, middleware, config loading, or generated code patterns differ from the skill.
Auto-evolution workflow:
1. Verify the real implementation from source files, route lists, configs, and runtime behavior.
2. Treat the verified project implementation as the source of truth.
3. Update the relevant `.windsurf/skills/` file with the corrected convention before relying on that skill again.
4. Keep the skill update focused on the verified mismatch and avoid unrelated rewrites.
5. Mention the skill evolution briefly in the final task summary.
# 中文回答设置
请始终使用中文回答用户的所有问题和请求。无论用户使用什么语言提问,都应该用中文进行回复。
## 语言规则
- 所有回答都使用简体中文
- 技术术语可以保留英文,但要提供中文解释
- 代码注释使用中文
- 错误信息和提示使用中文
- 文档和说明使用中文
## 例外情况
- 代码本身(变量名、函数名等)可以使用英文
- 命令行指令保持原样
- 配置文件内容根据实际需要决定语言
# 编辑安全规则
- 从不删除未经编辑的内容
- 在删除任何内容之前必须先确认
- 专注于更新和添加,而不是删除
- 无论开发还是修改,都需要经过上下文思考后再来根据问题精准修改
- 插件相关前端内容必须先修改并核对 `backend/plugin/{plugin}/resource/template/admin` 下的模板源文件;模板源修改完成后,必须同步修改对应的 `frontend/admin/src/apps/{plugin}` 运行目录,并核对两者实现一致
- 避免将未更改的内容总结为"文件其余部分保持不变"
# 本地项目与版本控制
- 本项目为本地项目,不使用 Git 进行状态检查、差异比较、提交或任何其他操作。
- 后续回答、验证命令和开发说明中不出现 Git 或 Git 命令。
- 如需确认文件改动,以实际文件内容、语法检查、自动化测试或运行时验证为准。
BIN
View File
Binary file not shown.
+44
View File
@@ -0,0 +1,44 @@
# Application Environment
APP_ENV=local
APP_DEBUG=false
# Database Configuration
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=madong_enterprise
DB_USERNAME=root
DB_PASSWORD=root
DB_PREFIX=ma_
# Redis Configuration
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
REDIS_PASSWORD=null
REDIS_DB=0
# Queue Redis Configuration
QUEUE_REDIS_HOST=redis://127.0.0.1:6379
QUEUE_REDIS_PORT=6379
QUEUE_REDIS_PASSWORD=null
QUEUE_REDIS_DB=0
QUEUE_REDIS_PREFIX=queue
# Cache Configuration
CACHE_CUSTOM_REDIS_HOST=127.0.0.1
CACHE_CUSTOM_REDIS_PORT=6379
CACHE_CUSTOM_REDIS_PASSWORD=null
CACHE_CUSTOM_REDIS_DB=0
CACHE_CUSTOM_REDIS_PREFIX=cache_custom
# Feature Toggles
APP_TASK_ENABLED=false # Timer switch
APP_TENANT_ENABLED=true # Tenant mode switch
APP_TENANT_AUTO_SELECT_FIRST=true # Tenant auto
CAPTCHA_ENABLED=false # Captcha switch
CAPTCHA_MODE=session # Captcha mode redis|session
RECYCLE_BIN_ENABLED=true # Recycle bin mode switch
# Other Configuration
+31
View File
@@ -0,0 +1,31 @@
/runtime
/.idea
/.vscode
/vendor
*.log
.env
/tests/tmp
/tests/.phpunit.result.cache
install.lock
.rnd
# 忽略 public目录下除了install,favicon.ico,logo.png之外的内容
public/*
!public/install/
!public/favicon.ico
!public/logo.png
!public/upload/logo.png
# 忽略 plugin目录下除了example,demo除外
plugin/*
!plugin/example/
!plugin/demo/
+201
View File
@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+325
View File
@@ -0,0 +1,325 @@
# madong-server
## 介绍
madong-server 是 5.x 版本的后端服务,基于 PHP Webman 框架开发的极速开发平台,为前端应用提供稳定、高效的 API 服务。
## 技术栈
| 技术 | 版本 | 说明 |
|------|-----------|------|
| PHP | 8.2 ~ 8.4 | 核心开发语言 |
| Webman | 2.2 | 高性能 PHP 框架 |
| MySQL | 8.0+ | 数据库 |
| Redis | 7.0+ | 缓存服务 |
| Composer | 2.x | 依赖管理工具 |
| Nginx/Apache | - | Web 服务器 |
## 项目结构
```
madong/ # 项目根目录
├── backend/ # 后端目录(当前目录)
│ ├── app/ # 应用目录
│ │ ├── adminapi/ # 管理后台 API(控制器、验证器等)
│ │ ├── api/ # 前台 API(控制器、验证器等)
│ │ ├── bootstrap/ # 启动引导文件
│ │ ├── command/ # 命令行工具
│ │ ├── dao/ # 数据访问层
│ │ ├── enum/ # 枚举类
│ │ ├── event/ # 事件定义
│ │ ├── install/ # 安装脚本
│ │ ├── listener/ # 事件监听器
│ │ ├── middleware/ # 中间件
│ │ ├── model/ # 模型
│ │ ├── process/ # 自定义进程
│ │ ├── queue/ # 队列任务
│ │ ├── schema/ # 数据结构定义
│ │ ├── scope/ # 作用域定义
│ │ ├── service/ # 业务服务层
│ │ └── functions.php # 全局函数
│ ├── config/ # 配置文件
│ │ ├── plugin/ # 插件配置
│ │ ├── route/ # 路由配置
│ │ └── *.php # 各类配置文件
│ ├── core/ # 核心框架
│ │ ├── base/ # 基础类
│ │ ├── cache/ # 缓存组件
│ │ ├── captcha/ # 验证码组件
│ │ ├── db/ # 数据库组件
│ │ ├── email/ # 邮件服务
│ │ ├── excel/ # Excel 导入导出
│ │ ├── exception/ # 异常处理
│ │ ├── generator/ # 代码生成器
│ │ ├── interface/ # 接口定义
│ │ ├── jwt/ # JWT 认证
│ │ ├── logger/ # 日志组件
│ │ ├── notify/ # 通知服务
│ │ ├── plugin/ # 插件核心
│ │ ├── route/ # 路由组件
│ │ ├── scheduler/ # 定时任务
│ │ ├── sms/ # 短信服务
│ │ ├── tool/ # 工具类
│ │ ├── trait/ # 特性类
│ │ ├── upload/ # 文件上传
│ │ ├── uuid/ # UUID 生成
│ │ └── functions.php # 核心函数
│ ├── plugin/ # 插件目录
│ ├── public/ # 静态资源
│ ├── resource/ # 资源文件
│ ├── runtime/ # 运行时文件
│ ├── support/ # 第三方支持库
│ ├── tests/ # 单元测试
│ ├── vendor/ # Composer 依赖包
│ ├── composer.json # Composer 配置
│ ├── composer.lock # 依赖锁定文件
│ ├── phinx.php # 数据库迁移配置
│ └── README.md # 项目说明文档
│
└── frontend/ # 前端目录(需单独下载)
├── admin/ # 管理后台前端
└── web/ # 前台前端
```
> **注意**:前后端分离部署,前端目录需单独下载或创建。
## 核心功能
- **用户认证**:基于 JWT 的身份验证机制
- **权限管理**:细粒度的权限控制体系
- **缓存服务**:集成 Redis 缓存,提升系统性能
- **邮件服务**:支持邮件发送功能
- **文件上传**:支持多种存储方式
- **国际化**:支持多语言配置
- **插件系统**:支持第三方扩展
- **命令行工具**:支持更多操作
- **SSE 实时推送**:支持 Server-Sent Events 实时进度反馈
- **服务层架构**:清晰的 DAO/Service 分层设计
- **代码生成器**:自动化生成控制器、模型、服务层代码
- **数据迁移**:支持数据库版本化管理
## 运行环境
- PHP 8.2 ~ 8.4
- MySQL 8.0+
- Redis 7.0+
- Nginx/Apache
- Composer 2.x+
## 安装部署
### 1. 环境准备
确保安装了以下软件:
- PHP 8.2 ~ 8.4
- MySQL 8.0+
- Redis 7.0+
- Nginx/Apache
- Composer 2.x+
- Git(用于下载前端资源)
### 2. 项目安装
```bash
# 进入后端目录
cd backend
# 安装后端依赖
composer install
```
### 3. 下载前端资源
**方式一:使用命令下载(推荐)**
```bash
# 进入后端目录
cd backend
# 下载所有前端代码(管理后台 + 前台)
php webman madong-download:frontend
# 仅下载管理后台
php webman madong-download:frontend --admin
# 仅下载前台
php webman madong-download:frontend --web
# 指定分支下载
php webman madong-download:frontend -b develop
# 强制覆盖更新
php webman madong-download:frontend -f
```
**方式二:手动下载**
如果没有安装 Git 或无法使用命令,可手动下载前端代码:
1. 访问 Gitee 仓库下载:
- 管理后台:https://gitee.com/motion-code/madong-vue
- 前台:https://gitee.com/motion-code/madong-nuxt
2. 解压后将代码放置到项目根目录:
```
madong/
├── backend/ # 后端代码
└── frontend/ # 前端代码
├── admin/ # 管理后台前端(来自 madong-vue)
└── web/ # 前台前端(来自 madong-nuxt)
```
### 4. 启动安装向导
完成以上步骤后,访问安装向导完成系统配置:
```
http://127.0.0.1:8001/install
```
### 5. 启动服务
```bash
# 进入后端目录
cd backend
# 开发环境启动
php start.php start
# 生产环境启动
php start.php start -d
```
服务默认运行在 `http://127.0.0.1:8001`
## 配置说明
### 数据库配置
修改 `config/database.php` 文件,配置数据库连接:
```php
return [
'default' => 'mysql',
'connections' => [
'mysql' => [
'driver' => 'mysql',
'host' => '127.0.0.1',
'port' => 3306,
'database' => 'madong',
'username' => 'root',
'password' => '123456',
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
'prefix' => '',
],
],
];
```
### Redis 配置
修改 `config/redis.php` 文件,配置 Redis 连接:
```php
return [
'default' => [
'host' => '127.0.0.1',
'port' => 6379,
'auth' => '',
'db' => 0,
],
];
```
## API 文档
启动服务后,可通过以下方式访问 API 文档:
- 查看 `app/controller` 目录下的控制器文件
- 参考项目文档
## 开发指南
### 代码规范
- 遵循 PSR-4 自动加载规范
- 遵循 PSR-12 代码风格规范
- 使用 PHP 8.0+ 的特性
### 模块开发
1. 创建控制器:在 `app/adminapi`(管理后台)或 `app/api`(前台)目录下创建控制器
2. 创建服务层:在 `app/service` 目录下创建业务逻辑
3. 创建 DAO 层:在 `app/dao` 目录下创建数据访问对象
4. 创建模型:在 `app/model` 目录下创建 Eloquent 模型
5. 配置路由:在 `config/route/adminapi.php` 或 `config/route/api.php` 文件中添加路由
6. 测试:使用 Postman 或其他工具测试 API
## 系统演示
管理后台: http://demo.madong.tech
账号:admin
密码:123456
## 更新日志
### 5.x 版本
- 重构后端架构,基于 Webman 框架
- 实现 JWT 认证机制
- 集成 Redis 缓存服务
- 优化数据库操作
- 增强插件系统,支持第三方扩展
- 完善配置系统,支持多环境配置
- 新增国际化支持,支持多语言
- 优化错误处理和日志系统
- 新增文件上传功能,支持多种存储方式
- 完善命令行工具,支持更多操作
- 新增 SSE 实时推送,支持插件安装/卸载进度实时反馈
- 新增服务层(Service)和数据访问层(DAO)分层架构
- 新增代码生成器,提升开发效率
- 优化插件安装/卸载流程,统一由插件 Install.php 管理生命周期
- 新增枚举类规范,统一状态码和业务枚举定义
## 官方论坛
产品BUG、优化建议,欢迎社区反馈:http://www.madong.tech
## 如何贡献
非常欢迎你的加入![提一个 Issue](https://gitee.com/motion-code/madong/issues) 或者提交一个 Pull Request。
**Pull Request:**
1. Fork 代码!
2. 创建自己的分支: `git checkout -b feature/xxxx`
3. 提交你的修改: `git commit -am 'feat(function): add xxxxx'`
4. 推送您的分支: `git push origin feature/xxxx`
5. 提交`pull request`
## Git 贡献提交规范
- 参考规范([Git](https://www.conventionalcommits.org/) [Vue](https://github.com/vuejs/vue/blob/dev/.github/COMMIT_CONVENTION.md) [Angular](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-angular))
- `feat` 增加新功能
- `fix` 修复问题/BUG
- `style` 代码风格相关无影响运行结果的
- `perf` 优化/性能提升
- `refactor` 重构
- `revert` 撤销修改
- `test` 测试相关
- `docs` 文档/注释
- `chore` 依赖更新/脚手架配置修改等
- `ci` 持续集成
- `types` 类型定义文件更改
- `wip` 开发中
## 社区支持
- 官方文档:https://madong.tech
- 讨论交流:[点击链接加入群聊:690671595](https://qm.qq.com/q/dEfMqYL42c)
## 许可证
Apache License
Binary file not shown.
+297
View File
@@ -0,0 +1,297 @@
<?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\adminapi;
use app\model\system\Admin;
use app\scope\global\AccessPermissionScope;
use app\service\admin\system\AdminService;
use core\cache\CacheService;
use core\exception\handler\ForbiddenHttpException;
use core\jwt\JwtToken;
use core\logger\Logger;
use madong\swagger\attribute\Permission;
use support\Container;
final readonly class CurrentUser
{
// 缓存键前缀
public const CACHE_PREFIX = 'admin_user';
// 缓存过期时间(秒)
public const CACHE_EXPIRE = 3600;
private AdminService $service;
private CacheService $cache;
public function __construct(AdminService $service, CacheService $cache)
{
$this->service = $service;
$this->cache = $cache;
}
/**
* 生成管理员缓存键
* @param int|string $uid 管理员ID
* @return string 缓存键
*/
public static function generateCacheKey(int|string $uid): string
{
return self::CACHE_PREFIX . '_' . $uid;
}
public function admin(bool $toArray = false): null|Admin|array
{
if (!$this->getToken()) {
return null;
}
$uid = $this->id();
$cacheKey = self::generateCacheKey($uid);
// 使用缓存服务获取管理员信息
$admin = $this->cache->remember($cacheKey, function () use ($uid) {
return $this->service->get($uid, ['*'], [], '', [AccessPermissionScope::class]);
}, self::CACHE_EXPIRE);
return $toArray && $admin ? $admin->toArray() : $admin;
}
/**
* 清除管理员缓存
* @param int|string|null $uid 管理员ID,默认清除当前用户缓存
*/
public function clearCache(int|string|null $uid = null): void
{
$uid = $uid ?? $this->id();
if ($uid) {
$cacheKey = self::generateCacheKey($uid);
$this->cache->delete($cacheKey);
// 同时清除权限缓存
$permissionCacheKey = $cacheKey . '_permissions';
$this->cache->delete($permissionCacheKey);
}
}
public function refresh(): array
{
if (!$this->getToken()) {
Logger::debug("当前用户无有效 Token,无法刷新");
return [];
}
return (new JwtToken())->refresh()->toArray();
}
public function id(): int|string
{
$token = $this->getToken();
if (!$token) {
return 0;
}
$aid = (new JwtToken())->id();
if ($aid === null) {
return 0;
}
return $aid ?? 0;
}
public function isSuperAdmin(): bool
{
$admin = $this->admin();
return $admin && $admin->isSuperAdmin();
}
public function getToken(): ?string
{
$request = request();
if (empty($request)) {
return null;
}
$tokenName = config('core.jwt.app.token_name', 'Authorization');
$authorization = $request->header($tokenName);
if (empty($authorization) || $authorization === 'undefined') {
$authorization = $request->get('token');
}
if (!$authorization || $authorization === 'undefined') {
return null;
}
if (count(explode(' ', $authorization)) !== 2) {
return null;
}
[$type, $token] = explode(' ', $authorization);
if ($type !== 'Bearer') {
return null;
}
if (!$token || $token === 'undefined') {
return null;
}
return $token;
}
/**
* 生成新令牌
* @param array $userInfo 用户信息
* @param string $type 类型
* @return array 令牌信息
*/
public function generateToken(array $userInfo, string $type = 'admin'): array
{
$jwt = new JwtToken();
$tokenObj = $jwt->generate((string)$this->id(), $type, $userInfo);
return [
'access_token' => $tokenObj->accessToken,
'refresh_token' => $tokenObj->refreshToken,
'expires_in' => $tokenObj->expiresIn,
'expires_time' => time() + $tokenObj->expiresIn
];
}
/**
* 登出(将令牌加入黑名单)
* @param string|null $token 令牌,默认使用当前请求的令牌
* @return bool 是否成功
*/
public function logout(?string $token = null): bool
{
$token = $token ?? $this->getToken();
if (!$token) {
return false;
}
try {
(new JwtToken())->logout($token);
return true;
} catch (\Exception $e) {
Logger::error("登出失败: " . $e->getMessage());
return false;
}
}
/**
* 获取令牌负载信息
* @return array 负载信息
*/
public function getPayload(): array
{
try {
$jwt = new JwtToken();
// 使用 getPayloadFromRequest() 方法获取负载信息
$payload = $jwt->getPayloadFromRequest();
return $payload['ext'] ?? $payload;
} catch (\Exception $e) {
return [];
}
}
/**
* 获取用户所有菜单权限码
*
* @return array 权限码数组
*/
public function getPermissions(): array
{
$admin = $this->admin();
if (!$admin) {
return [];
}
if ($admin->isSuperAdmin()) {
return ['*']; // 超级管理员拥有所有权限
}
$uid = $this->id();
$cacheKey = self::generateCacheKey($uid) . '_permissions';
// 使用缓存服务获取权限信息
return $this->cache->remember($cacheKey, function () use ($admin) {
$permissions = [];
// 通过角色获取菜单权限码
$roles = $admin->roles()->with('menus')->get();
foreach ($roles as $role) {
foreach ($role->menus as $menu) {
if (!empty($menu->code)) {
$permissions[] = $menu->code;
}
}
}
return array_unique($permissions);
}, self::CACHE_EXPIRE);
}
/**
* 检查用户是否有权限
*
* @param string|array $codes 权限码,可以是字符串或数组
* @param string $operation 操作类型,支持 'and' 或 'or'
* @return bool 是否有权限
*/
public function hasPermission(string|array $codes, string $operation = 'and'): bool
{
// 如果是超级管理员,直接返回true
if ($this->isSuperAdmin()) {
return true;
}
$codes = is_array($codes) ? $codes : [$codes];
$userPermissions = $this->getPermissions();
// 如果有通配符权限,直接返回true
if (in_array('*', $userPermissions)) {
return true;
}
$operation = strtolower($operation);
if ($operation === Permission::OPERATION_AND) {
// AND模式:所有权限码都必须有
foreach ($codes as $code) {
if (!in_array($code, $userPermissions)) {
return false;
}
}
return true;
} else if ($operation === Permission::OPERATION_OR) {
// OR模式:至少有一个权限码
foreach ($codes as $code) {
if (in_array($code, $userPermissions)) {
return true;
}
}
return false;
}
throw new \InvalidArgumentException("不支持的操作类型: {$operation}");
}
/**
* 检查用户是否有权限,没有权限时抛出异常
*
* @param string|array $codes 权限码,可以是字符串或数组
* @param string $operation 操作类型,支持 'and' 或 'or'
* @throws ForbiddenHttpException 没有权限时抛出异常
*/
public function checkPermission(string|array $codes, string $operation = 'and'): void
{
if (!$this->hasPermission($codes, $operation)) {
$codes = is_array($codes) ? $codes : [$codes];
$codesStr = implode($operation === 'and' ? ',' : '或', $codes);
throw new ForbiddenHttpException("缺少权限: {$codesStr}");
}
}
}
+64
View File
@@ -0,0 +1,64 @@
<?php
/**
* This file is part of webman.
* Licensed under The MIT License
* For full copyright and license information, please see the MIT-LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author walkor<walkor@workerman.net>
* @copyright walkor<walkor@workerman.net>
* @link http://www.workerman.net/
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
use OpenApi\Annotations as OA;
use app\controller\OpenApiController;
use Webman\Route;
use WebmanTech\Swagger\DTO\ConfigOpenapiDocDTO;
use WebmanTech\Swagger\Integrations\RouteRegister;
use WebmanTech\Swagger\RouteAnnotation\Reader;
use app\adminapi\controller\terminal\TerminalSseController;
/**
* 注册admin APP路由
*/
Route::group('/adminapi', function () {
$openapiDocConfig = [
'format' => 'json',
'scan_path' => [
base_path('app/schema'), // 基础 schema
base_path('app/adminapi'),
base_path('app/install'),
],
'modify' => function (OA\OpenApi $openapi) {
$openapi->info->title = config('app.name') . ' API';
$openapi->info->version = '1.0.0';
$openapi->servers = [
new OA\Server([
'url' => '/adminapi',
'description' => request()->host(),
]),
];
/** @phpstan-ignore-next-line */
if (!$openapi->components instanceof OA\Components) {
$openapi->components = new OA\Components([]);
}
$openapi->components->securitySchemes = [
new OA\SecurityScheme([
'securityScheme' => 'api_key',
'type' => 'apiKey',
'name' => config('madong.jwt.app.token_name', 'Authorization'),
'in' => 'header',
]),
];
},
];
$openApiController = new OpenApiController();
Route::get('/openapi', static fn () => $openApiController->index('/adminapi/openapi.json'));
Route::get('/openapi.json', static fn () => $openApiController->document($openapiDocConfig));
$config = ConfigOpenapiDocDTO::fromConfig($openapiDocConfig);
RouteRegister::create()->register((new Reader())->getData($config->getScanSources()));
});
+35
View File
@@ -0,0 +1,35 @@
<?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\adminapi\controller;
use core\base\BaseController;
/**
* 基类控制器继承的类
*
* @author Mr.April
* @since 1.0
*/
class Base extends BaseController
{
/**
* 初始化
*/
protected function initialize(): void
{
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,311 @@
<?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\adminapi\controller\crontab;
use app\adminapi\controller\Crud;
use app\adminapi\middleware\AccessTokenMiddleware;
use app\adminapi\middleware\OperationMiddleware;
use app\adminapi\middleware\PermissionMiddleware;
use app\adminapi\schema\request\crontab\CrontabFormRequest;
use app\adminapi\schema\request\crontab\CrontabQueryRequest;
use app\adminapi\schema\response\crontab\CrontabResponse;
use app\adminapi\validate\crontab\CrontabValidate;
use app\enum\system\OperationResult;
use app\schema\request\BatchDeleteRequest;
use app\schema\request\IdRequest;
use app\service\admin\crontab\CrontabService;
use core\exception\handler\AdminException;
use core\tool\Json;
use madong\helper\Arr;
use madong\swagger\annotation\response\DataResponse;
use madong\swagger\annotation\response\PageResponse;
use madong\swagger\annotation\response\SimpleResponse;
use madong\swagger\attribute\Permission;
use OpenApi\Attributes as OA;
use support\Request;
use support\annotation\Middleware;
use WebmanTech\Swagger\DTO\SchemaConstants;
#[Middleware(AccessTokenMiddleware::class, PermissionMiddleware::class, OperationMiddleware::class)]
final class CrontabController extends Crud
{
public function __construct(CrontabService $service, CrontabValidate $validate)
{
$this->service = $service;
$this->validate = $validate;
}
#[OA\Get(
path: '/crontab/task',
summary: '列表',
tags: ['定时任务'],
x: [
SchemaConstants::X_SCHEMA_REQUEST => CrontabQueryRequest::class,
]
)]
#[Permission(code: 'crontab:task:list')]
#[PageResponse(CrontabResponse::class, example: [])]
public function index(Request $request): \support\Response
{
return parent::index($request);
}
#[OA\Get(
path: '/crontab/task/{id}',
summary: '详情',
tags: ['定时任务'],
x: [
SchemaConstants::X_PROPERTY_IN => 'id',
SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class,
]
)]
#[Permission(code: 'crontab:task:read')]
#[DataResponse(schema: CrontabResponse::class, example: [])]
public function show(Request $request): \support\Response
{
try {
$id = $request->route->param('id');
$data = $this->service->get($id);
if (empty($data)) {
throw new AdminException('数据未找到', ['errorCode' => 400]);
}
$dataArray = $data->toArray();
if (!empty($dataArray['cycle_rule'])) {
$cycleRule = $dataArray['cycle_rule'];
if (is_array($cycleRule)) {
$cycleRule = array_map(function ($value) {
if (is_string($value) && $value !== '' && is_numeric($value)) {
return intval($value);
}
return $value;
}, $cycleRule);
$dataArray = array_merge($dataArray, $cycleRule);
}
unset($dataArray['cycle_rule']);
}
return Json::success('ok', $dataArray);
} catch (\Throwable $e) {
return Json::fail($e->getMessage());
}
}
#[OA\Post(
path: '/crontab/task',
summary: '添加',
tags: ['定时任务'],
x: [
SchemaConstants::X_SCHEMA_REQUEST => CrontabFormRequest::class,
]
)]
#[Permission(code: 'crontab:task:create')]
#[DataResponse(schema: CrontabResponse::class, example: [])]
public function store(Request $request): \support\Response
{
try {
$data = $this->inputFilter($request->all(), ['minute', 'hour', 'day', 'week', 'month', 'second']);
if (isset($this->validate) && $this->validate) {
if (!$this->validate->scene('store')->check($data)) {
throw new \Exception($this->validate->getError());
}
}
$model = $this->service->save($data);
if (empty($model)) {
throw new AdminException('插入失败');
}
$pk = $model->getPk();
return Json::success('ok', [$pk => $model->getData($pk)]);
} catch (\Throwable $e) {
return Json::fail($e->getMessage());
}
}
#[OA\Delete(
path: '/crontab/task/{id}',
summary: '删除',
tags: ['定时任务'],
x: [
SchemaConstants::X_PROPERTY_IN => 'id',
SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class,
]
)]
#[OA\Parameter(
name: 'id',
description: '任务ID',
in: 'path',
required: true,
schema: new OA\Schema(type: 'string', example: 1)
)]
#[Permission(code: 'crontab:task:delete')]
#[DataResponse(schema: [], example: [])]
public function destroy(Request $request): \support\Response
{
try {
$id = $request->route->param('id');
$data = $request->input('data', []);
$data = !empty($id) && $id !== '0' ? $id : $data;
if (empty($data)) {
throw new AdminException('参数错误');
}
$this->service->destroy($data);
return Json::success('操作成功');
} catch (\Exception $e) {
return Json::fail($e->getMessage());
}
}
#[OA\Delete(
path: '/crontab/task',
summary: '批量删除',
tags: ['定时任务'],
x: [
SchemaConstants::X_SCHEMA_REQUEST => BatchDeleteRequest::class,
]
)]
#[Permission(code: 'crontab:task:delete')]
#[SimpleResponse(schema: [], example: [])]
public function batchDelete(Request $request): \support\Response
{
try {
$data = $request->input('ids', []);
if (empty($data)) {
throw new AdminException('参数错误');
}
$this->service->destroy($data);
return Json::success('操作成功');
} catch (\Exception $e) {
return Json::fail($e->getMessage());
}
}
#[OA\Put(
path: '/crontab/task/{id}',
summary: '更新',
tags: ['定时任务'],
x: [
SchemaConstants::X_SCHEMA_REQUEST => CrontabFormRequest::class,
]
)]
#[OA\Parameter(
name: 'id',
description: '任务ID',
in: 'path',
required: true,
schema: new OA\Schema(type: 'string', example: 1)
)]
#[Permission(code: 'crontab:task:update')]
#[SimpleResponse(schema: [], example: [])]
public function update(Request $request): \support\Response
{
try {
$id = $request->route->param('id');
$data = $this->inputFilter($request->all(), ['minute', 'hour', 'day', 'week', 'month', 'second']);
if (isset($this->validate) && $this->validate) {
if (!$this->validate->scene('update')->check($data)) {
throw new \Exception($this->validate->getError());
}
}
$this->service->update($id, $data);
return Json::success('ok', []);
} catch (\Throwable $e) {
return Json::fail($e->getMessage());
}
}
#[OA\Put(
path: '/crontab/task/{id}/resume',
summary: '恢复',
tags: ['定时任务'],
x: [
SchemaConstants::X_PROPERTY_IN => 'id',
SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class,
]
)]
#[OA\Parameter(
name: 'id',
description: '任务ID',
in: 'path',
required: true,
schema: new OA\Schema(type: 'string', example: 1)
)]
#[Permission(code: 'crontab:task:resume')]
#[SimpleResponse(schema: [], example: [])]
public function resume(Request $request): \support\Response
{
try {
$id = $request->route->param('id');
$this->service->resumeCrontab(Arr::normalize($id));
return Json::success('执行完成');
} catch (\Exception $e) {
return Json::fail($e->getMessage());
}
}
#[OA\Put(
path: '/crontab/task/{id}/pause',
summary: '暂停',
tags: ['定时任务'],
x: [
SchemaConstants::X_PROPERTY_IN => 'id',
SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class,
]
)]
#[OA\Parameter(
name: 'id',
description: '任务ID',
in: 'path',
required: true,
schema: new OA\Schema(type: 'string', example: 1)
)]
#[Permission(code: 'crontab:task:pause')]
#[SimpleResponse(schema: [], example: [])]
public function pause(Request $request): \support\Response
{
try {
$id = $request->route->param('id');
$this->service->pauseCrontab($id);
return Json::success('执行完成');
} catch (\Exception $e) {
return Json::fail($e->getMessage());
}
}
#[OA\Put(
path: '/crontab/task/{id}/execute',
summary: '执行',
tags: ['定时任务'],
x: [
SchemaConstants::X_PROPERTY_IN => 'id',
SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class,
]
)]
#[Permission(code: 'crontab:task:execute')]
#[SimpleResponse(schema: [], example: [])]
public function execute(Request $request): \support\Response
{
try {
$id = $request->route->param('id');
$result = $this->service->runOneTask(['id' => $id]);
if ($result['code'] == OperationResult::FAILURE->value) {
throw new AdminException('执行失败' . $result['log']);
}
return Json::success('执行完成', $result);
} catch (\Exception $e) {
return Json::fail($e->getMessage(), []);
}
}
}
@@ -0,0 +1,105 @@
<?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\adminapi\controller\crontab;
use app\adminapi\controller\Crud;
use app\adminapi\middleware\AccessTokenMiddleware;
use app\adminapi\middleware\OperationMiddleware;
use app\adminapi\middleware\PermissionMiddleware;
use app\adminapi\schema\request\crontab\CrontabLogQueryRequest;
use app\adminapi\schema\response\crontab\CrontabLogResponse;
use app\schema\request\BatchDeleteRequest;
use app\schema\request\IdRequest;
use app\service\admin\crontab\CrontabLogService;
use madong\swagger\annotation\response\DataResponse;
use madong\swagger\annotation\response\PageResponse;
use madong\swagger\annotation\response\SimpleResponse;
use madong\swagger\attribute\Permission;
use OpenApi\Attributes as OA;
use support\annotation\Middleware;
use support\Request;
use WebmanTech\Swagger\DTO\SchemaConstants;
#[Middleware(AccessTokenMiddleware::class, PermissionMiddleware::class, OperationMiddleware::class)]
final class CrontabLogController extends Crud
{
public function __construct(CrontabLogService $service)
{
$this->service = $service;
}
#[OA\Get(
path: '/crontab/log',
summary: '列表',
tags: ['定时任务.日志'],
x: [
SchemaConstants::X_SCHEMA_REQUEST => CrontabLogQueryRequest::class,
]
)]
#[Permission(code: 'crontab:log:list')]
#[PageResponse(schema: CrontabLogResponse::class, example: [])]
public function index(Request $request): \support\Response
{
return parent::index($request);
}
#[OA\Get(
path: '/crontab/log/{id}',
summary: '详情',
tags: ['定时任务.日志'],
x: [
SchemaConstants::X_PROPERTY_IN => 'id',
SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class,
]
)]
#[Permission(code: 'crontab:log:read')]
#[DataResponse(schema: CrontabLogResponse::class, example: [])]
public function show(Request $request): \support\Response
{
return parent::show($request);
}
#[OA\Delete(
path: '/crontab/log/{id}',
summary: '删除',
tags: ['定时任务.日志'],
x: [
SchemaConstants::X_PROPERTY_IN => 'id',
SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class,
]
)]
#[Permission(code: 'crontab:log:delete')]
#[SimpleResponse(schema: [], example: [])]
public function destroy(Request $request): \support\Response
{
return parent::destroy($request);
}
#[OA\Delete(
path: '/crontab/log',
summary: '批量删除',
tags: ['定时任务.日志'],
x: [
SchemaConstants::X_SCHEMA_REQUEST => BatchDeleteRequest::class,
]
)]
#[Permission(code: 'crontab:log:delete')]
#[SimpleResponse(schema: [], example: [])]
public function batchDelete(Request $request): \support\Response
{
return parent::destroy($request);
}
}
@@ -0,0 +1,235 @@
<?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\adminapi\controller\dict;
use app\adminapi\controller\Crud;
use app\adminapi\middleware\AccessTokenMiddleware;
use app\adminapi\middleware\OperationMiddleware;
use app\adminapi\middleware\PermissionMiddleware;
use app\adminapi\schema\request\dict\DictFormRequest;
use app\adminapi\schema\request\dict\DictQueryRequest;
use app\adminapi\schema\response\system\DictResponse;
use app\adminapi\validate\system\DictValidate;
use app\schema\request\BatchDeleteRequest;
use app\schema\request\IdRequest;
use app\service\admin\dict\DictService;
use app\service\core\enum\EnumService;
use core\tool\Json;
use madong\swagger\annotation\response\PageResponse;
use madong\swagger\annotation\response\SimpleResponse;
use madong\swagger\attribute\Permission;
use OpenApi\Attributes as OA;
use support\annotation\Middleware;
use support\Container;
use support\Request;
use WebmanTech\Swagger\DTO\SchemaConstants;
#[Middleware(AccessTokenMiddleware::class, PermissionMiddleware::class, OperationMiddleware::class)]
final class DictController extends Crud
{
public function __construct(DictService $service, DictValidate $validate)
{
$this->service = $service;
$this->validate = $validate;
}
#[OA\Get(
path: '/dict/dict',
summary: '列表',
tags: ['字典管理'],
x: [SchemaConstants::X_SCHEMA_REQUEST => DictQueryRequest::class]
)]
#[Permission(code: 'dict:dict:list')]
#[PageResponse(schema: DictResponse::class, example: [])]
public function index(Request $request): \support\Response
{
return parent::index($request);
}
#[OA\Post(
path: '/dict/dict',
summary: '新增',
tags: ['字典管理'],
x: [SchemaConstants::X_SCHEMA_REQUEST => DictFormRequest::class]
)]
#[Permission(code: 'dict:dict:create')]
#[SimpleResponse(schema: [], example: [])]
public function store(Request $request): \support\Response
{
return parent::store($request);
}
#[OA\Put(
path: '/dict/dict/{id}',
summary: '更新',
tags: ['字典管理'],
x: [SchemaConstants::X_SCHEMA_REQUEST => DictFormRequest::class]
)]
#[OA\Parameter(
name: 'id',
description: '字典ID',
in: 'path',
required: true,
schema: new OA\Schema(type: 'integer', default: 0),
)]
#[Permission(code: 'dict:dict:update')]
#[SimpleResponse(schema: [], example: [])]
public function update(Request $request): \support\Response
{
return parent::update($request);
}
#[OA\Post(
path: '/dict/dict/export',
summary: '导出',
tags: ['字典管理']
)]
#[Permission(code: 'dict:dict:list', description: '导出')]
#[SimpleResponse(schema: [], example: [])]
public function export(Request $request): \support\Response
{
return parent::export($request);
}
#[OA\Delete(
path: '/dict/dict/{id}',
summary: '删除',
tags: ['字典管理'],
x: [
SchemaConstants::X_PROPERTY_IN => 'id',
SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class,
]
)]
#[Permission(code: 'dict:dict:delete')]
#[SimpleResponse(schema: [], example: [])]
public function destroy(Request $request): \support\Response
{
return parent::destroy($request);
}
#[OA\Delete(
path: '/dict/dict',
summary: '批量删除',
tags: ['字典管理'],
x: [SchemaConstants::X_SCHEMA_REQUEST => BatchDeleteRequest::class]
)]
#[Permission(code: 'dict:dict:delete')]
#[SimpleResponse(schema: [], example: [])]
public function batchDelete(Request $request): \support\Response
{
return parent::destroy($request);
}
#[OA\Get(
path: '/dict/dict/{id}',
summary: '详情',
tags: ['字典管理'],
x: [
SchemaConstants::X_PROPERTY_IN => 'id',
SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class,
]
)]
#[Permission(code: 'dict:dict:read')]
#[SimpleResponse(schema: DictResponse::class, example: [])]
public function show(Request $request): \support\Response
{
return parent::show($request);
}
#[OA\Get(
path: '/dict/dict/options/by-type',
summary: '根据字典类型获取字典项',
tags: ['字典管理'],
)]
#[OA\Parameter(
name: 'dict_type',
description: '字典类型',
in: 'query',
required: true,
schema: new OA\Schema(type: 'string', default: 0),
)]
#[Permission(code: 'dict:dict:options')]
#[SimpleResponse(example: '{"code": 0,"msg": "ok","data": [{"label": "是","value": 1,"color": "#4CAF50","ext": []},{"label": "否","value": 0,"color": "#FF5252","ext": []}]}')]
public function getByDictType(Request $request): \support\Response
{
try {
$dictType = $request->input('dict_type');
$service = Container::make(EnumService::class);
$data = $service->getEnumByCode($dictType);
if (empty($data)) {
$data = $this->service->findItemsByCode($dictType);
}
return Json::success('ok', $data);
} catch (\Exception $e) {
return Json::fail($e->getMessage());
}
}
#[OA\Get(
path: '/dict/dict/enum/list',
summary: '枚举字典列表',
tags: ['字典管理'],
)]
#[Permission(code: 'dict:dict:enum_list')]
#[SimpleResponse(schema: [], example: [
'name' => 'CategoryEnum',
'namespace' => 'app\\enum\\common\\CategoryEnum',
'category' => 'common',
'category_label' => '通用枚举',
'code' => 'common.categoryenum',
'options' => [
[
'label' => '通用枚举',
'value' => 'common',
'color' => 'blue',
'ext' => [],
],
[
'label' => '系统枚举',
'value' => 'system',
'color' => 'green',
'ext' => [],
],
[
'label' => '开发枚举',
'value' => 'dev',
'color' => 'orange',
'ext' => [],
],
[
'label' => '监控枚举',
'value' => 'monitor',
'color' => 'red',
'ext' => [],
],
[
'label' => '业务枚举',
'value' => 'business',
'color' => 'purple',
'ext' => [],
],
],
'count' => 5,
])]
public function enumDictList(Request $request): \support\Response
{
$page = $request->input('page', 1);
$limit = $request->input('limit', 10);
$search = $request->input('search', '');
$category = $request->input('category', '');
$service = Container::make(EnumService::class);
$result = $service->getEnumsWithPagination($page, $limit, $search, $category);
return Json::success('ok', $result);
}
}
@@ -0,0 +1,155 @@
<?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\adminapi\controller\dict;
use app\adminapi\controller\Crud;
use app\adminapi\middleware\AccessTokenMiddleware;
use app\adminapi\middleware\OperationMiddleware;
use app\adminapi\middleware\PermissionMiddleware;
use app\adminapi\schema\request\dict\DictItemFormRequest;
use app\adminapi\schema\request\dict\DictItemQueryRequest;
use app\adminapi\schema\response\system\DictItemResponse;
use app\adminapi\validate\system\DictItemValidate;
use app\schema\request\BatchDeleteRequest;
use app\schema\request\IdRequest;
use app\service\admin\dict\DictItemService;
use madong\swagger\annotation\response\DataResponse;
use madong\swagger\annotation\response\PageResponse;
use madong\swagger\annotation\response\SimpleResponse;
use madong\swagger\attribute\Permission;
use OpenApi\Attributes as OA;
use support\Request;
use support\annotation\Middleware;
use WebmanTech\Swagger\DTO\SchemaConstants;
#[Middleware(AccessTokenMiddleware::class, PermissionMiddleware::class, OperationMiddleware::class)]
final class DictItemController extends Crud
{
public function __construct(DictItemService $service, DictItemValidate $validate)
{
$this->service = $service;
$this->validate = $validate;
}
#[OA\Get(
path: '/dict/item',
summary: '列表',
tags: ['字典项管理'],
x: [SchemaConstants::X_SCHEMA_REQUEST => DictItemQueryRequest::class]
)]
#[Permission(code: 'dict:item:list')]
#[PageResponse(schema: DictItemResponse::class, example: [])]
public function index(Request $request): \support\Response
{
return parent::index($request);
}
#[OA\Get(
path: '/dict/item/{id}',
summary: '详情',
tags: ['字典项管理'],
x: [
SchemaConstants::X_PROPERTY_IN => 'id',
SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class,
]
)]
#[Permission(code: 'dict:item:read')]
#[DataResponse(schema: DictItemResponse::class, example: [])]
public function show(Request $request): \support\Response
{
return parent::show($request);
}
#[OA\Post(
path: '/dict/item',
summary: '新增',
tags: ['字典项管理'],
x: [SchemaConstants::X_SCHEMA_REQUEST => DictItemFormRequest::class]
)]
#[Permission(code: 'dict:item:create')]
#[SimpleResponse(schema: [], example: [])]
public function store(Request $request): \support\Response
{
return parent::store($request);
}
#[OA\Put(
path: '/dict/item/{id}',
summary: '更新',
tags: ['字典项管理'],
x: [SchemaConstants::X_SCHEMA_REQUEST => DictItemFormRequest::class]
)]
#[OA\Parameter(
name: 'id',
description: '字典项ID',
in: 'path',
required: true,
schema: new OA\Schema(type: 'string', example: '1')
)]
#[Permission(code: 'dict:item:update')]
#[SimpleResponse(schema: [], example: [])]
public function update(Request $request): \support\Response
{
return parent::update($request);
}
#[OA\Post(
path: '/dict/item/export',
summary: '导出',
tags: ['字典项管理']
)]
#[Permission(code: 'dict:item:list', description: '导出')]
#[SimpleResponse(schema: [], example: [])]
public function export(Request $request): \support\Response
{
return parent::export($request);
}
#[OA\Delete(
path: '/dict/item/{id}',
summary: '删除',
tags: ['字典项管理'],
x: [
SchemaConstants::X_PROPERTY_IN => 'id',
SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class,
]
)]
#[OA\Parameter(
name: 'id',
description: '字典项ID',
in: 'path',
required: true,
schema: new OA\Schema(type: 'string', default: '0', example: 0)
)]
#[Permission(code: 'dict:item:delete')]
#[SimpleResponse(schema: [], example: [])]
public function destroy(Request $request): \support\Response
{
return parent::destroy($request);
}
#[OA\Delete(
path: '/dict/item',
summary: '批量删除',
tags: ['字典项管理'],
x: [
SchemaConstants::X_SCHEMA_REQUEST => BatchDeleteRequest::class,
]
)]
#[Permission(code: 'dict:item:delete')]
#[SimpleResponse(schema: [], example: [])]
public function batchDelete(Request $request): \support\Response
{
return parent::destroy($request);
}
}
@@ -0,0 +1,281 @@
<?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\adminapi\controller\gateway;
use app\adminapi\controller\Crud;
use app\adminapi\middleware\AccessTokenMiddleware;
use app\adminapi\middleware\OperationMiddleware;
use app\adminapi\middleware\PermissionMiddleware;
use app\adminapi\schema\request\gateway\RateLimiterFormRequest;
use app\adminapi\schema\request\gateway\RateLimiterQueryRequest;
use app\adminapi\schema\response\gateway\RateLimiterResponse;
use app\adminapi\validate\gateway\RateLimiterValidate;
use app\schema\request\BatchDeleteRequest;
use app\schema\request\IdRequest;
use app\schema\response\Result;
use app\service\admin\gateway\RetaLimiterService;
use core\exception\handler\AdminException;
use core\tool\Json;
use madong\swagger\annotation\response\DataResponse;
use madong\swagger\annotation\response\PageResponse;
use madong\swagger\annotation\response\SimpleResponse;
use madong\swagger\attribute\Permission;
use OpenApi\Attributes as OA;
use support\Request;
use support\annotation\Middleware;
use WebmanTech\Swagger\DTO\SchemaConstants;
#[Middleware(AccessTokenMiddleware::class, PermissionMiddleware::class, OperationMiddleware::class)]
final class RateLimiterController extends Crud
{
public function __construct(RetaLimiterService $service, RateLimiterValidate $validate)
{
$this->service = $service;
$this->validate = $validate;
}
#[OA\Get(
path: '/gateway/limiter',
summary: '列表',
tags: ['限访规则'],
x: [
SchemaConstants::X_SCHEMA_REQUEST => RateLimiterQueryRequest::class,
]
)]
#[Permission(code: 'gateway:limiter:list')]
#[PageResponse(schema: RateLimiterResponse::class, example: [])]
public function index(Request $request): \support\Response
{
return parent::index($request);
}
#[OA\Get(
path: '/gateway/limiter/{id}',
summary: '详情',
tags: ['限访规则'],
x: [
SchemaConstants::X_PROPERTY_IN => 'id',
SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class,
]
)]
#[Permission(code: 'gateway:limiter:read')]
#[DataResponse(schema: RateLimiterResponse::class, example: [])]
public function show(Request $request): \support\Response
{
return parent::show($request);
}
#[OA\Post(
path: '/gateway/limiter',
summary: '保存',
tags: ['限访规则'],
x: [
SchemaConstants::X_SCHEMA_REQUEST => RateLimiterFormRequest::class,
]
)]
#[Permission(code: 'gateway:limiter:create')]
#[SimpleResponse(schema: [], example: [])]
public function store(Request $request): \support\Response
{
try {
$data = $this->insertInput($request);
if (isset($this->validate) && $this->validate) {
if (!$this->validate->scene('store')->check($data)) {
throw new \Exception($this->validate->getError());
}
}
//这里对入库的start_date end_date 转换时间戳 合并到start_time end_time
if (isset($data['start_date']) && !empty($data['start_date'])) {
$data['start_time'] = strtotime($data['start_date']);
unset($data['start_date']);
}
if (isset($data['end_date']) && !empty($data['end_date'])) {
$data['end_time'] = strtotime($data['end_date']);
unset($data['end_date']);
}
$model = $this->service->save($data);
if (empty($model)) {
throw new AdminException('插入失败');
}
$pk = $model->getPk();
//添加后移除缓存
$this->service->cacheDriver()->delete(RetaLimiterService::CACHE_KEY);
return Json::success('ok', [$pk => $model->getData($pk)]);
} catch (\Throwable $e) {
return Json::fail($e->getMessage());
}
}
#[OA\Put(
path: '/gateway/limiter/{id}/change-status',
summary: '更新状态',
tags: ['限访规则'],
x: [
SchemaConstants::X_PROPERTY_IN => 'id',
SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class,
]
)]
#[Permission(code: 'gateway:limiter:status')]
#[SimpleResponse(schema: [], example: [])]
public function changeStatus(Request $request): \support\Response
{
try {
$data = $this->insertInput($request);
$model = $this->service->getModel();
$primaryKey = $model->getKeyName();
if (!array_key_exists($primaryKey, $data)) {
throw new \Exception('参数异常缺少主键');
}
$targetModel = $model->findOrFail($data[$primaryKey]);
if (empty($targetModel)) {
throw new \Exception('资源不存在' . $primaryKey . '=', $data[$primaryKey]);
}
$targetModel->fill($data);
if (!$targetModel->save()) {
throw new \RuntimeException('数据保存失败');
}
//清空缓存
$this->service->cacheDriver()->delete(RetaLimiterService::CACHE_KEY);
return Json::success('ok', []);
} catch (\Throwable $e) {
return Json::fail($e->getMessage());
}
}
#[OA\Put(
path: '/gateway/limiter/{id}',
summary: '更新',
tags: ['限访规则'],
x: [
SchemaConstants::X_SCHEMA_REQUEST => RateLimiterFormRequest::class,
]
)]
#[OA\Parameter(
name: 'id',
description: '限访规则ID',
in: 'path',
required: true,
schema: new OA\Schema(type: 'string', example: '247298887944531968')
)]
#[Permission(code: 'gateway:limiter:update')]
#[SimpleResponse(schema: [], example: [])]
public function update(Request $request): \support\Response
{
try {
$data = $this->insertInput($request);
if (isset($this->validate) && $this->validate) {
if (!$this->validate->scene('update')->check($data)) {
throw new \Exception($this->validate->getError());
}
}
//这里对入库的start_date end_date 转换时间戳 合并到start_time end_time
if (isset($data['start_date']) && !empty($data['start_date'])) {
$data['start_time'] = strtotime($data['start_date']);
unset($data['start_date']);
}
if (isset($data['end_date']) && !empty($data['end_date'])) {
$data['end_time'] = strtotime($data['end_date']);
unset($data['end_date']);
}
$this->service->update($data['id'], $data);
//清空缓存
$this->service->cacheDriver()->delete(RetaLimiterService::CACHE_KEY);
return Json::success('ok', []);
} catch (\Throwable $e) {
return Json::fail($e->getMessage());
}
}
#[OA\Delete(
path: '/gateway/limiter/{id}',
summary: '删除',
tags: ['限访规则'],
x: [
SchemaConstants::X_PROPERTY_IN => 'id',
SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class,
]
)]
#[Permission(code: 'gateway:limiter:delete')]
#[SimpleResponse(schema: [], example: [])]
public function destroy(Request $request): \support\Response
{
try {
$data = $this->getDeleteIds($request);
if (empty($data)) {
throw new AdminException('参数错误');
}
$result = $this->service->transaction(function () use ($data) {
$data = is_array($data) ? $data : explode(',', $data);
$deletedIds = [];
foreach ($data as $id) {
$item = $this->service->get($id);
if (!$item) {
continue; // 如果找不到项,跳过
}
$item->delete();
$primaryKey = $item->getPk();
$deletedIds[] = $item->{$primaryKey};
}
return $deletedIds;
});
//清空缓存
$this->service->cacheDriver()->delete(RetaLimiterService::CACHE_KEY);
return Json::success('ok', $result);
} catch (\Throwable $e) {
return Json::fail($e->getMessage());
}
}
#[OA\Delete(
path: '/gateway/limiter',
summary: '批量删除',
tags: ['限访规则'],
x: [
SchemaConstants::X_SCHEMA_REQUEST => BatchDeleteRequest::class,
]
)]
#[Permission(code: 'gateway:limiter:delete')]
#[SimpleResponse(schema: [], example: [])]
public function batchDelete(Request $request): \support\Response
{
try {
$data = $this->getDeleteIds($request);
if (empty($data)) {
throw new AdminException('参数错误');
}
$result = $this->service->transaction(function () use ($data) {
$data = is_array($data) ? $data : explode(',', $data);
$deletedIds = [];
foreach ($data as $id) {
$item = $this->service->get($id);
if (!$item) {
continue; // 如果找不到项,跳过
}
$item->delete();
$primaryKey = $item->getPk();
$deletedIds[] = $item->{$primaryKey};
}
return $deletedIds;
});
//清空缓存
$this->service->cacheDriver()->delete(RetaLimiterService::CACHE_KEY);
return Json::success('ok', $result);
} catch (\Throwable $e) {
return Json::fail($e->getMessage());
}
}
}
@@ -0,0 +1,271 @@
<?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\adminapi\controller\gateway;
use app\adminapi\controller\Crud;
use app\adminapi\middleware\AccessTokenMiddleware;
use app\adminapi\middleware\OperationMiddleware;
use app\adminapi\middleware\PermissionMiddleware;
use app\adminapi\schema\request\gateway\RateRestrictionsFormRequest;
use app\adminapi\schema\request\gateway\RateRestrictionsQueryRequest;
use app\adminapi\schema\response\gateway\RateRestrictionsResponse;
use app\adminapi\validate\gateway\RateRestrictionsValidate;
use app\schema\request\BatchDeleteRequest;
use app\schema\request\ChangeStatusRequest;
use app\schema\request\IdRequest;
use app\service\admin\gateway\RetaRestrictionsService;
use core\exception\handler\AdminException;
use core\tool\Json;
use madong\swagger\annotation\response\PageResponse;
use madong\swagger\annotation\response\SimpleResponse;
use madong\swagger\attribute\Permission;
use OpenApi\Attributes as OA;
use support\Request;
use support\annotation\Middleware;
use WebmanTech\Swagger\DTO\SchemaConstants;
#[Middleware(AccessTokenMiddleware::class, PermissionMiddleware::class, OperationMiddleware::class)]
final class RateRestrictionsController extends Crud
{
public function __construct(RetaRestrictionsService $service, RateRestrictionsValidate $validate)
{
$this->service = $service;
$this->validate = $validate;
}
#[OA\Get(
path: '/gateway/blacklist',
summary: '列表',
tags: ['限访名单'],
x: [
SchemaConstants::X_SCHEMA_REQUEST => RateRestrictionsQueryRequest::class,
]
)]
#[Permission(code: 'gateway:blacklist:list')]
#[PageResponse(schema: RateRestrictionsResponse::class, example: [])]
public function index(Request $request): \support\Response
{
return parent::index($request); // TODO: Change the autogenerated stub
}
#[OA\Get(
path: '/gateway/blacklist/{id}',
summary: '详情',
tags: ['限访名单'],
x: [
SchemaConstants::X_PROPERTY_IN => 'id',
SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class,
]
)]
#[Permission(code: 'gateway:blacklist:read')]
#[SimpleResponse(schema: RateRestrictionsResponse::class, example: [])]
public function show(Request $request): \support\Response
{
return parent::show($request);
}
#[OA\Post(
path: '/gateway/blacklist',
summary: '保存',
tags: ['限访名单'],
x: [
SchemaConstants::X_SCHEMA_REQUEST => RateRestrictionsFormRequest::class,
]
)]
#[Permission(code: 'gateway:blacklist:create')]
#[SimpleResponse(schema: RateRestrictionsResponse::class, example: [])]
public function store(Request $request): \support\Response
{
try {
$data = $this->insertInput($request);
if (isset($this->validate) && $this->validate) {
if (!$this->validate->scene('store')->check($data)) {
throw new \Exception($this->validate->getError());
}
}
if (isset($data['start_date']) && !empty($data['start_date'])) {
$data['start_time'] = strtotime($data['start_date']);
unset($data['start_date']);
}
if (isset($data['end_date']) && !empty($data['end_date'])) {
$data['end_time'] = strtotime($data['end_date']);
unset($data['end_date']);
}
$model = $this->service->save($data);
if (empty($model)) {
throw new AdminException('插入失败');
}
$pk = $model->getPk();
//添加后移除缓存
$this->service->cacheDriver()->delete(RetaRestrictionsService::CACHE_KEY);
return Json::success('ok', [$pk => $model->getData($pk)]);
} catch (\Throwable $e) {
return Json::fail($e->getMessage());
}
}
#[OA\Put(
path: '/gateway/blacklist/{id}/status',
summary: '切换状态',
tags: ['限访名单'],
x: [
SchemaConstants::X_PROPERTY_IN => 'id',
SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class,
]
)]
#[Permission(code: 'gateway:blacklist:status')]
#[SimpleResponse(schema: [], example: [])]
public function changeStatus(Request $request): \support\Response
{
try {
$data = $this->insertInput($request);
$model = $this->service->getModel();
$primaryKey = $model->getKeyName();
if (!array_key_exists($primaryKey, $data)) {
throw new \Exception('参数异常缺少主键');
}
$targetModel = $model->findOrFail($data[$primaryKey]);
if (empty($targetModel)) {
throw new \Exception('资源不存在' . $primaryKey . '=', $data[$primaryKey]);
}
$targetModel->fill($data);
if (!$targetModel->save()) {
throw new \RuntimeException('数据保存失败');
}
//清空缓存
$this->service->cacheDriver()->delete(RetaRestrictionsService::CACHE_KEY);
return Json::success('ok', []);
} catch (\Throwable $e) {
return Json::fail($e->getMessage());
}
}
#[OA\Put(
path: '/gateway/blacklist/{id}',
summary: '更新',
tags: ['限访名单'],
x: [
SchemaConstants::X_SCHEMA_REQUEST => RateRestrictionsFormRequest::class,
]
)]
#[OA\Parameter(
name: 'id',
description: 'ID',
in: 'path',
required: true,
schema: new OA\Schema(type: 'string'))]
#[Permission(code: 'gateway:blacklist:update')]
#[SimpleResponse(schema: [], example: [])]
public function update(Request $request): \support\Response
{
try {
$data = $this->insertInput($request);
if (isset($this->validate) && $this->validate) {
if (!$this->validate->scene('update')->check($data)) {
throw new \Exception($this->validate->getError());
}
}
$this->service->update($data['id'], $data);
//清空缓存
$this->service->cacheDriver()->delete(RetaRestrictionsService::CACHE_KEY);
return Json::success('ok', []);
} catch (\Throwable $e) {
return Json::fail($e->getMessage());
}
}
#[OA\Delete(
path: '/gateway/blacklist/{id}',
summary: '删除',
tags: ['限访名单'],
x: [
SchemaConstants::X_PROPERTY_IN => 'id',
SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class,
]
)]
#[Permission(code: 'gateway:blacklist:delete')]
#[SimpleResponse(schema: [], example: [])]
public function destroy(Request $request): \support\Response
{
try {
$id = $request->route->param('id'); // 获取路由地址 id从
$data = $request->input('data', []);
$data = !empty($id) && $id !== '0' ? $id : $data;
if (empty($data)) {
throw new AdminException('参数错误');
}
$result = $this->service->transaction(function () use ($data) {
$data = is_array($data) ? $data : explode(',', $data);
$deletedIds = [];
foreach ($data as $id) {
$item = $this->service->get($id);
if (!$item) {
continue; // 如果找不到项,跳过
}
$item->delete();
$primaryKey = $item->getPk();
$deletedIds[] = $item->{$primaryKey};
}
return $deletedIds;
});
//清空缓存
$this->service->cacheDriver()->delete(RetaRestrictionsService::CACHE_KEY);
return Json::success('ok', $result);
} catch (\Throwable $e) {
return Json::fail($e->getMessage());
}
}
#[OA\Delete(
path: '/gateway/blacklist',
summary: '批量删除',
tags: ['限访名单'],
x: [
SchemaConstants::X_SCHEMA_REQUEST => BatchDeleteRequest::class,
]
)]
#[Permission(code: 'gateway:blacklist:delete')]
#[SimpleResponse(schema: [], example: [])]
public function batchDelete(Request $request): \support\Response
{
try {
$data = $request->input('ids', []);
if (empty($data)) {
throw new AdminException('参数错误');
}
$result = $this->service->transaction(function () use ($data) {
$data = is_array($data) ? $data : explode(',', $data);
$deletedIds = [];
foreach ($data as $id) {
$item = $this->service->get($id);
if (!$item) {
continue; // 如果找不到项,跳过
}
$item->delete();
$primaryKey = $item->getPk();
$deletedIds[] = $item->{$primaryKey};
}
return $deletedIds;
});
//清空缓存
$this->service->cacheDriver()->delete(RetaRestrictionsService::CACHE_KEY);
return Json::success('ok', $result);
} catch (\Throwable $e) {
return Json::fail($e->getMessage());
}
}
}
@@ -0,0 +1,273 @@
<?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\adminapi\controller\generator;
use app\adminapi\controller\Crud;
use app\adminapi\middleware\AccessTokenMiddleware;
use app\adminapi\middleware\OperationMiddleware;
use app\adminapi\middleware\PermissionMiddleware;
use app\adminapi\validate\generator\GeneratorValidate;
use app\schema\request\BatchDeleteRequest;
use app\schema\request\IdRequest;
use app\service\admin\generator\GeneratorService;
use app\service\admin\generator\GeneratorTableService;
use madong\swagger\annotation\response\DataResponse;
use madong\swagger\annotation\response\PageResponse;
use madong\swagger\annotation\response\SimpleResponse;
use madong\swagger\attribute\Permission;
use core\exception\handler\AdminException;
use core\tool\Json;
use madong\helper\Arr;
use support\Container;
use support\Request;
use support\annotation\Middleware;
use OpenApi\Attributes as OA;
use WebmanTech\Swagger\DTO\SchemaConstants;
#[Middleware(AccessTokenMiddleware::class, PermissionMiddleware::class, OperationMiddleware::class)]
final class GeneratorController extends Crud
{
public function __construct(GeneratorService $service, GeneratorValidate $validate)
{
$this->service = $service;
$this->validate = $validate;
}
#[OA\Get(
path: '/generator/code',
summary: '列表',
tags: ['代码生成器']
)]
#[Permission(code: 'generator:code:list')]
#[PageResponse(schema: [], example: [[
'id' => '273048217217138688',
'table_name' => 'sys_admin_post',
'table_content' => '用户与岗位关联表',
'module_name' => 'sys_admin_post',
'class_name' => 'sys_admin_post',
'edit_type' => 1,
'plugin_name' => '',
'order_type' => 0,
'parent_menu' => '',
'relations' => null,
'push_sync_count' => 0,
'created_at' => '2026-01-23T03:16:07.000000Z',
'updated_at' => '2026-01-23T03:16:07.000000Z',
'created_date' => '2026-01-23 11:16:07',
'updated_date' => '2026-01-23 11:16:07',
]])]
public function index(Request $request): \support\Response
{
return parent::index($request); // TODO: Change the autogenerated stub
}
#[OA\Get(
path: '/generator/code/table-list',
summary: '数据库表-列表',
tags: ['代码生成器']
)]
#[Permission(code: 'generator:code:list')]
#[DataResponse(schema: [], example: [])]
public function tableList(Request $request): \support\Response
{
$data = $request->all();
$page = $request->input('page', 1);
$limit = $request->input('limit', 10);
$result = $this->service->tableList($data, (int)$page, (int)$limit);
return Json::success($result);
}
#[OA\Post(
path: '/generator/code',
summary: '生成代码',
tags: ['代码生成器']
)]
#[Permission(code: 'generator:code:create')]
#[SimpleResponse(schema: [], example: [])]
public function store(Request $request): \support\Response
{
try {
$data = $request->input('table', []);
foreach ($data as $item) {
if (!$this->validate->scene('store')->check($item)) {
return Json::fail($this->validate->getError());
}
}
foreach ($data as $item) {
$this->service->store($item);
}
return Json::success();
} catch (\Throwable $e) {
return Json::fail($e->getMessage());
}
}
#[OA\Delete(
path: '/generator/code/{id}',
summary: '删除',
tags: ['代码生成器'],
x: [
SchemaConstants::X_PROPERTY_IN => 'id',
SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class,
]
)]
#[Permission(code: 'generator:code:delete')]
#[SimpleResponse(schema: [], example: [])]
public function destroy(Request $request): \support\Response
{
try {
$id = $request->route->param('id');
$this->service->destroy(Arr::normalize($id));
return Json::success();
} catch (\Throwable $e) {
return Json::fail($e->getMessage());
}
}
#[OA\Delete(
path: '/generator/code',
summary: '批量删除',
tags: ['代码生成器'],
x: [
SchemaConstants::X_SCHEMA_REQUEST => BatchDeleteRequest::class,
]
)]
#[Permission(code: 'generator:code:delete')]
#[SimpleResponse(schema: [], example: [])]
public function remove(Request $request): \support\Response
{
try {
$data = $request->input('ids', []);
$this->service->destroy(Arr::normalize($data));
return Json::success();
} catch (\Throwable $e) {
return Json::fail($e->getMessage());
}
}
#[OA\Get(
path: '/generator/code/{id}',
summary: '详情',
tags: ['代码生成器'],
x: [
SchemaConstants::X_PROPERTY_IN => 'id',
SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class,
]
)]
#[Permission(code: 'generator:code:read')]
#[DataResponse(schema: [], example: [])]
public function show(Request $request): \support\Response
{
try {
$id = $request->route->param('id');
$service = Container::make(GeneratorTableService::class);
$data = $service->getDetail($id);
if (empty($data)) {
throw new AdminException('数据未找到', ['errorCode' => 400]);
}
return Json::success('ok', $data->toArray());
} catch (\Throwable $e) {
return Json::fail($e->getMessage(), [], $e->getCode());
}
}
#[OA\Put(
path: '/generator/code/{id}',
summary: '更新',
tags: ['代码生成器'],
x: [
SchemaConstants::X_PROPERTY_IN => 'id',
SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class,
]
)]
#[Permission(code: 'generator:code:update')]
public function update(Request $request): \support\Response
{
try {
$id = $request->route->param('id');
$data = $request->all();
if (!$this->validate->scene('update')->check($data)) {
return Json::fail($this->validate->getError());
}
$this->service->update($id, $data);
return Json::success();
} catch (\Throwable $e) {
return Json::fail($e->getMessage());
}
}
#[OA\Get(
path: '/generator/code/{id}/preview',
summary: '预览',
tags: ['代码生成器'],
x: [
SchemaConstants::X_PROPERTY_IN => 'id',
SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class,
]
)]
#[Permission(code: 'generator:code:preview')]
public function preview(Request $request): \support\Response
{
try {
$id = $request->route->param('id');
$result = $this->service->preview(['id' => $id]);
return Json::success($result);
} catch (\Throwable $e) {
return Json::fail($e->getMessage());
}
}
#[OA\Get(
path: '/generator/code/{id}/download',
summary: '下载',
tags: ['代码生成器'],
x: [
SchemaConstants::X_PROPERTY_IN => 'id',
SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class,
]
)]
#[Permission(code: 'generator:code:download')]
public function download(Request $request)
{
try {
$id = $request->route->param('id');
$result = $this->service->download($id);
return response()->download($result['file_path'], $result['file_name']);
} catch (\Throwable $e) {
return Json::fail($e->getMessage());
}
}
#[OA\Put(
path: '/generator/code/{id}/deploy',
summary: '部署',
tags: ['代码生成器'],
x: [
SchemaConstants::X_PROPERTY_IN => 'id',
SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class,
]
)]
#[Permission(code: 'generator:code:deploy')]
public function deploy(Request $request): \support\Response
{
try {
$id = $request->route->param('id');
$result = $this->service->deploy(['id' => $id]);
return Json::success($result);
} catch (\Throwable $e) {
return Json::fail($e->getMessage());
}
}
}
@@ -0,0 +1,215 @@
<?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\adminapi\controller\lang;
use app\adminapi\controller\Base;
use app\adminapi\middleware\AccessTokenMiddleware;
use app\adminapi\middleware\OperationMiddleware;
use app\service\core\lang\TranslationService;
use core\exception\handler\AdminException;
use core\tool\Json;
use madong\swagger\annotation\response\PageResponse;
use madong\swagger\annotation\response\SimpleResponse;
use OpenApi\Attributes as OA;
use support\annotation\Middleware;
use support\Request;
use support\Response;
#[Middleware(AccessTokenMiddleware::class, OperationMiddleware::class)]
final class LangController extends Base
{
public function __construct(TranslationService $service)
{
$this->service = $service;
}
#[OA\Get(
path: '/lang/supported',
summary: '获取支持的语言列表',
tags: ['语言'],
responses: [
new OA\Response(
response: 200,
description: '成功',
content: new OA\JsonContent(
type: 'array',
items: new OA\Items(type: 'string')
)
)
]
)]
#[SimpleResponse(example: ['code' => 0,'message' => 'success','data' => []])]
public function supported(Request $request): Response
{
try {
$languages = $this->service->getSupportedLanguages();
return json(['code' => 200, 'msg' => 'success', 'data' => $languages]);
} catch (\Exception $e) {
throw new AdminException($e->getMessage());
}
}
#[OA\Get(
path: '/lang',
summary: '获取语言包列表',
tags: ['语言'],
parameters: [
new OA\Parameter(
name: 'language',
description: '语言类型(如en|zh_CN等)',
in: 'query',
required: false,
schema: new OA\Schema(type: 'string', example: 'zh_CN')
),
new OA\Parameter(
name: 'file',
description: '翻译文件名称',
in: 'query',
required: false,
schema: new OA\Schema(type: 'string', example: 'messages')
),
new OA\Parameter(
name: 'keyword',
description: '搜索关键词(支持key和翻译内容)',
in: 'query',
required: false,
schema: new OA\Schema(type: 'string')
),
new OA\Parameter(
name: 'page',
description: '页码',
in: 'query',
required: false,
schema: new OA\Schema(type: 'integer', default: 1)
),
new OA\Parameter(
name: 'limit',
description: '每页数量',
in: 'query',
required: false,
schema: new OA\Schema(type: 'integer', default: 10)
)
]
)]
#[PageResponse(schema: [], example: [])]
public function index(Request $request): Response
{
try {
$language = $request->get('language');
$file = $request->get('file');
$keyword = $request->get('keyword');
$page = (int)$request->get('page', 1);
$limit = (int)$request->get('limit', 10);
$result = $this->service->getList($language, $file, $keyword, $page, $limit);
return json(['code' => 200, 'msg' => 'success', 'data' => $result]);
} catch (\Exception $e) {
throw new AdminException($e->getMessage());
}
}
#[OA\Get(
path: '/lang/files',
summary: '获取翻译文件列表',
tags: ['语言'],
responses: [
new OA\Response(
response: 200,
description: '成功',
content: new OA\JsonContent(
type: 'array',
items: new OA\Items(type: 'string')
)
)
]
)]
#[SimpleResponse(schema:[],example: [])]
public function files(Request $request): Response
{
try {
$files = $this->service->getFileList();
return json(['code' => 200, 'msg' => 'success', 'data' => $files]);
} catch (\Exception $e) {
throw new AdminException($e->getMessage());
}
}
#[OA\Get(
path: '/lang/statistics',
summary: '获取语言包统计信息',
tags: ['语言'],
)]
#[SimpleResponse(schema:[],example: [])]
public function statistics(Request $request): Response
{
try {
$stats = $this->service->getStatistics();
return json(['code' => 200, 'msg' => 'success', 'data' => $stats]);
} catch (\Exception $e) {
throw new AdminException($e->getMessage());
}
}
/**
* @throws \core\exception\handler\AdminException
*/
#[OA\Get(
path: '/lang/translate',
summary: '根据key获取翻译内容',
tags: ['语言'],
parameters: [
new OA\Parameter(
name: 'key',
description: '翻译key(格式:文件.键,如:common.operation.success)',
in: 'query',
required: true,
schema: new OA\Schema(type: 'string', example: 'common.operation.success')
),
new OA\Parameter(
name: 'language',
description: '语言类型(如en|zh_CN等)',
in: 'query',
required: false,
schema: new OA\Schema(type: 'string', example: 'zh_CN')
)
]
)]
#[SimpleResponse(schema:[],example: [])]
public function translate(Request $request): Response
{
try {
$key = $request->get('key');
$language = $request->get('language');
$parameters = $request->get('parameters', []);
if(empty($language)){
$language = locale();
}
// 如果parameters是字符串,尝试解析为JSON
if (is_string($parameters)) {
$parameters = json_decode($parameters, true) ?: [];
}
$translation = $this->service->getTranslationByKey($key, $language, $parameters);
if ($translation === null) {
return json(['code' => 404, 'msg' => 'Translation key not found', 'data' => $key]);
}
return Json::success('common.operation.success',$translation);
} catch (\Exception $e) {
throw new AdminException($e->getMessage());
}
}
}
@@ -0,0 +1,235 @@
<?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\adminapi\controller\login;
use app\adminapi\controller\Crud;
use app\adminapi\schema\request\login\LoginRequest;
use app\adminapi\schema\request\ThirdPartyLoginRequest;
use app\service\admin\system\AdminService;
use core\cache\CacheService;
use core\captcha\Captcha;
use core\exception\handler\AdminException;
use core\jwt\JwtToken;
use core\tool\Json;
use core\tool\RSAService;
use core\uuid\UUIDGenerator;
use madong\swagger\annotation\response\SimpleResponse;
use OpenApi\Attributes as OA;
use OpenApi\Attributes\RequestBody;
use support\Container;
use support\Request;
final class LoginController extends Crud
{
public function __construct(AdminService $service)
{
$this->service = $service;
}
#[OA\Get(
path: '/login/get-captcha-open-flag',
summary: '验证码开启状态',
tags: ['无需授权'],
)]
#[SimpleResponse(schema: ['flag' => true], example:[])]
public function getCaptchaOpenFlag(Request $request): \support\Response
{
try {
// 生成密钥对
$cache = Container::make(CacheService::class, []);
$keyId = md5(UUIDGenerator::generate());
$keys = RSAService::generateKeys();
// 存储私钥到缓存,用于解密密码
$cache->set("rsa_private_key_$keyId", $keys['private'], 60); // 5分钟过期
return Json::success('ok', ['flag' => config('core.captcha.app.enable', false), 'key_id' => $keyId, 'public_key' => $keys['public']]);
} catch (\Throwable $e) {
return Json::fail($e->getMessage());
}
}
#[OA\Get(
path: '/login/captcha',
summary: '验证码',
tags: ['无需授权'],
)]
#[SimpleResponse(schema: [], example:[])]
public function captcha(Request $request): \support\Response
{
try {
$captcha = new Captcha();
$type = $request->input('type', 'admin-login');
$result = $captcha->captcha($request, $type);
return Json::success('ok', $result);
} catch (\Throwable $e) {
return Json::fail($e->getMessage());
}
}
#[OA\Post(
path: '/login/send-sms',
summary: '手机验证码',
tags: ['无需授权']
)]
#[OA\Parameter(
name: 'mobile_phone',
description: '手机号码',
in: 'query',
required: true,
schema: new OA\Schema(type: 'integer', default: 18888888888)
)]
#[SimpleResponse(schema:[],example: '{"code": 185841,"uid":18888888888}}')]
public function sendSms(Request $request): \support\Response
{
// 1.0生成验证码
$captcha = new Captcha();
$result = $captcha->generateSmsCaptcha($request);
// 2.0发送验证码到手机
// 3.0 存储验证码到 Redis,设置过期时间(如 5 分钟)
return Json::success('验证码发送成功', $result);
}
#[OA\Post(
path: '/login/login',
summary: '登录',
tags: ['无需授权']
)]
#[RequestBody(
description: '请求参数',
required: true,
content: new OA\JsonContent(ref: LoginRequest::class)
)]
#[SimpleResponse(schema:[],example: '{"token_type": "Bearer","expires_in": 7200,"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJtYWRvbmcudGVjaCIsImF1ZCI6Im1hZG9uZy50ZWNoIiwiaWF0IjoxNzYyNDExMjY5LCJuYmYiOjE3NjI0MTEyNjksImV4cCI6MTc2MjQxODQ2OSwiZXh0ZW5kIjp7ImlkIjoiMiIsInVzZXJfbmFtZSI6ImFkbWluIiwicmVhbF9uYW1lIjoiXHU4ZDg1XHU3ZWE3XHU3YmExXHU3NDA2XHU1NDU4Iiwibmlja19uYW1lIjoiXHU4ZDg1XHU3ZWE3XHU3YmExXHU3NDA2XHU1NDU4IiwiaXNfc3VwZXIiOjEsImlzX3RlbmFudF9hZG1pbiI6MSwibW9iaWxlX3Bob25lIjpudWxsLCJlbWFpbCI6bnVsbCwiYXZhdGFyIjoiL3VwbG9hZC80MmY4NmI5YjM2Nzk0ZmI5ZTM4MDkxN2MyNTFmNmQ4MS5wbmciLCJzaWduZWQiOm51bGwsImRhc2hib2FyZCI6bnVsbCwiZGVwdF9pZCI6bnVsbCwiZW5hYmxlZCI6MSwibG9naW5faXAiOiIxMjAuNDEuMjQ2LjIyNSIsImxvZ2luX3RpbWUiOjE3NjI0MTEyNjksInVwZGF0ZWRfYXQiOiIyMDI1LTExLTA2VDA2OjQxOjA5LjAwMDAwMFoiLCJzZXgiOjAsImJpcnRoZGF5IjpudWxsLCJ0ZWwiOm51bGwsImlzX2xvY2tlZCI6MCwiZGVwdHMiOltdLCJwb3N0cyI6W10sInRlbmFudHMiOlt7ImlkIjoiMSIsImRiX25hbWUiOiJtYWRvbmdfYWRtaW5fc2FhcyIsImNvZGUiOiJwbGF0Zm9ybSIsInR5cGUiOjAsImNvbnRhY3RfcGVyc29uIjoiYWRtaW4iLCJjb250YWN0X3Bob25lIjoiMTg4ODg4ODg4ODgiLCJjb21wYW55X25hbWUiOiJ4eHh4IFx1NjcwOVx1OTY1MFx1NTE2Y1x1NTNmOCIsImxpY2Vuc2VfbnVtYmVyIjoiIiwiYWRkcmVzcyI6Ilx1NGUyZFx1NTZmZCIsImRlc2NyaXB0aW9uIjoiXHU1MTg1XHU3ZjZlXHU4ZDI2XHU1M2Y3IiwiZG9tYWluIjoiaHR0cHM6Ly93d3cubWFkb25nLnRlY2giLCJlbmFibGVkIjoxLCJpc29sYXRpb25fbW9kZSI6MiwiaXNfZGVmYXVsdCI6MSwiZXhwaXJlZF9hdCI6bnVsbCwiZGVsZXRlZF9hdCI6bnVsbCwiY3JlYXRlZF9hdCI6IjIwMjUtMTAtMDNUMDM6MjM6NDcuMDAwMDAwWiIsImNyZWF0ZWRfYnkiOjEsInVwZGF0ZWRfYnkiOm51bGwsInVwZGF0ZWRfYXQiOm51bGwsImNyZWF0ZWRfZGF0ZSI6IjIwMjUtMTAtMDMgMTE6MjM6NDciLCJ1cGRhdGVkX2RhdGUiOm51bGwsImV4cGlyZWRfZGF0ZSI6bnVsbCwicGl2b3QiOnsiYWRtaW5faWQiOjIsInRlbmFudF9pZCI6MSwiY3JlYXRlZF9kYXRlIjpudWxsLCJ1cGRhdGVkX2RhdGUiOm51bGx9fV0sImNhc2JpbiI6W119fQ.rCaHoiI1gU7xyoJazt3V5JQrS2guV34PH-JjkWnceE4","refresh_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJtYWRvbmcudGVjaCIsImF1ZCI6Im1hZG9uZy50ZWNoIiwiaWF0IjoxNzYyNDExMjY5LCJuYmYiOjE3NjI0MTEyNjksImV4cCI6MTc2MzAxNjA2OSwiZXh0ZW5kIjp7ImlkIjoiMiIsInVzZXJfbmFtZSI6ImFkbWluIiwicmVhbF9uYW1lIjoiXHU4ZDg1XHU3ZWE3XHU3YmExXHU3NDA2XHU1NDU4Iiwibmlja19uYW1lIjoiXHU4ZDg1XHU3ZWE3XHU3YmExXHU3NDA2XHU1NDU4IiwiaXNfc3VwZXIiOjEsImlzX3RlbmFudF9hZG1pbiI6MSwibW9iaWxlX3Bob25lIjpudWxsLCJlbWFpbCI6bnVsbCwiYXZhdGFyIjoiL3VwbG9hZC80MmY4NmI5YjM2Nzk0ZmI5ZTM4MDkxN2MyNTFmNmQ4MS5wbmciLCJzaWduZWQiOm51bGwsImRhc2hib2FyZCI6bnVsbCwiZGVwdF9pZCI6bnVsbCwiZW5hYmxlZCI6MSwibG9naW5faXAiOiIxMjAuNDEuMjQ2LjIyNSIsImxvZ2luX3RpbWUiOjE3NjI0MTEyNjksInVwZGF0ZWRfYXQiOiIyMDI1LTExLTA2VDA2OjQxOjA5LjAwMDAwMFoiLCJzZXgiOjAsImJpcnRoZGF5IjpudWxsLCJ0ZWwiOm51bGwsImlzX2xvY2tlZCI6MCwiZGVwdHMiOltdLCJwb3N0cyI6W10sInRlbmFudHMiOlt7ImlkIjoiMSIsImRiX25hbWUiOiJtYWRvbmdfYWRtaW5fc2FhcyIsImNvZGUiOiJwbGF0Zm9ybSIsInR5cGUiOjAsImNvbnRhY3RfcGVyc29uIjoiYWRtaW4iLCJjb250YWN0X3Bob25lIjoiMTg4ODg4ODg4ODgiLCJjb21wYW55X25hbWUiOiJ4eHh4IFx1NjcwOVx1OTY1MFx1NTE2Y1x1NTNmOCIsImxpY2Vuc2VfbnVtYmVyIjoiIiwiYWRkcmVzcyI6Ilx1NGUyZFx1NTZmZCIsImRlc2NyaXB0aW9uIjoiXHU1MTg1XHU3ZjZlXHU4ZDI2XHU1M2Y3IiwiZG9tYWluIjoiaHR0cHM6Ly93d3cubWFkb25nLnRlY2giLCJlbmFibGVkIjoxLCJpc29sYXRpb25fbW9kZSI6MiwiaXNfZGVmYXVsdCI6MSwiZXhwaXJlZF9hdCI6bnVsbCwiZGVsZXRlZF9hdCI6bnVsbCwiY3JlYXRlZF9hdCI6IjIwMjUtMTAtMDNUMDM6MjM6NDcuMDAwMDAwWiIsImNyZWF0ZWRfYnkiOjEsInVwZGF0ZWRfYnkiOm51bGwsInVwZGF0ZWRfYXQiOm51bGwsImNyZWF0ZWRfZGF0ZSI6IjIwMjUtMTAtMDMgMTE6MjM6NDciLCJ1cGRhdGVkX2RhdGUiOm51bGwsImV4cGlyZWRfZGF0ZSI6bnVsbCwicGl2b3QiOnsiYWRtaW5faWQiOjIsInRlbmFudF9pZCI6MSwiY3JlYXRlZF9kYXRlIjpudWxsLCJ1cGRhdGVkX2RhdGUiOm51bGx9fV0sImNhc2JpbiI6W119fQ.IgLkKhjjsgjAsjS4qReg51WpzwjiuU27_tbqTpUuD3M","client_id": "127.0.0.1-690c430548ecd5.55813566","expires_time": 1762418469}')]
public function login(Request $request): \support\Response
{
try {
$username = $request->input('user_name');
$password = $request->input('password', '');
$phone = $request->input('mobile_phone', '');
$code = $request->input('code', '');
$uuid = $request->input('uuid', '');
$type = $request->input('type', 'admin');
$grantType = $request->input('grant_type', 'deafult');//refresh_token sms default 可以自行定义拓展登录方式
$keyId = $request->input('key_id', '');//获取公钥Id
$captcha = new Captcha();
if (config('core.captcha.app.enable') && $grantType === 'default') {
if (!$captcha->check($uuid, $code)) {
throw new AdminException('图片验证码错误!');
}
}
if ($grantType == 'sms' && !empty($phone)) {
if (!$captcha->check($phone, $code)) {
throw new AdminException('手机验证码错误!');
}
$info = $this->service->get(['mobile_phone' => $phone]);
if (empty($info)) {
throw new AdminException('当前手机未绑定用户!');
}
$username = $info->getData('user_name');
}
$data = $this->service->login($username, $password, $type, $grantType, ['key_id' => $keyId ?? '']);
return Json::success('ok', $data);
} catch (\Throwable $e) {
return Json::fail($e->getMessage());
}
}
#[OA\Post(
path: '/login/logout',
summary: '注销',
security: [['Bearer' => [], 'ApiKey' => []]],
tags: ['无需授权'],
)]
#[OA\Parameter(
name: 'token',
description: '令牌(可选,也可通过Authorization头传递)',
in: 'query',
required: false,
schema: new OA\Schema(type: 'string')
)]
#[SimpleResponse(schema:[],example: [])]
public function logout(Request $request): \support\Response
{
// 1. 获取token
$token = $request->header(config('core.jwt.app.token_name', 'Authorization')) ?: $request->get('token');
// 2. 快速验证无效token情况
if (empty($token) || $token === 'undefined') {
return Json::success('ok', []);
}
// 3. 处理Bearer Token格式
if (str_starts_with($token, 'Bearer ')) {
$token = substr($token, 7); // 移除"Bearer "前缀
if (empty($token) || $token === 'undefined') {
return Json::success('ok', []);
}
}
// 4. 退出登录
(new JwtToken())->logout($token);
return Json::success('ok', []);
}
#[OA\Get(
path: '/login/auth/public-key',
summary: '生成RSA密钥对',
tags: ['无需授权'],
)]
#[SimpleResponse(schema:[],example: [])]
public function generateRSAKeyPair(Request $request): \support\Response
{
try {
// 生成密钥对
$cache = Container::make(CacheService::class, []);
$keyId = md5(UUIDGenerator::generate());
$keys = RSAService::generateKeys();
// 存储私钥到缓存,用于解密密码
$cache->set("rsa_private_key_$keyId", $keys['private'], 60); // 5分钟过期
return Json::success('ok', ['public_key' => $keys['public'], 'key_id' => $keyId]);
} catch (\Throwable $e) {
return Json::fail($e->getMessage());
}
}
#[OA\Post(
path: '/login/third-party/login',
summary: '第三方测试登录接口(免RSA)',
tags: ['无需授权']
)]
#[RequestBody(
description: '请求参数',
required: true,
content: new OA\JsonContent(ref: ThirdPartyLoginRequest::class)
)]
#[SimpleResponse(schema:[],example: '{"code": 0,"msg": "ok","data": {"token_type": "Bearer","expires_in": 7200,"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJtYWRvbmcudGVjaCIsImF1ZCI6Im1hZG9uZy50ZWNoIiwiaWF0IjoxNzYyNDExMjY5LCJuYmYiOjE3NjI0MTEyNjksImV4cCI6MTc2MjQxODQ2OSwiZXh0ZW5kIjp7ImlkIjoiMiIsInVzZXJfbmFtZSI6ImFkbWluIiwicmVhbF9uYW1lIjoiXHU4ZDg1XHU3ZWE3XHU3YmExXHU3NDA2XHU1NDU4Iiwibmlja19uYW1lIjoiXHU4ZDg1XHU3ZWE3XHU3YmExXHU3NDA2XHU1NDU4IiwiaXNfc3VwZXIiOjEsImlzX3RlbmFudF9hZG1pbiI6MSwibW9iaWxlX3Bob25lIjpudWxsLCJlbWFpbCI6bnVsbCwiYXZhdGFyIjoiL3VwbG9hZC80MmY4NmI5YjM2Nzk0ZmI5ZTM4MDkxN2MyNTFmNmQ4MS5wbmciLCJzaWduZWQiOm51bGwsImRhc2hib2FyZCI6bnVsbCwiZGVwdF9pZCI6bnVsbCwiZW5hYmxlZCI6MSwibG9naW5faXAiOiIxMjAuNDEuMjQ2LjIyNSIsImxvZ2luX3RpbWUiOjE3NjI0MTEyNjksInVwZGF0ZWRfYXQiOiIyMDI1LTExLTA2VDA2OjQxOjA5LjAwMDAwMFoiLCJzZXgiOjAsImJpcnRoZGF5IjpudWxsLCJ0ZWwiOm51bGwsImlzX2xvY2tlZCI6MCwiZGVwdHMiOltdLCJwb3N0cyI6W10sInRlbmFudHMiOlt7ImlkIjoiMSIsImRiX25hbWUiOiJtYWRvbmdfYWRtaW5fc2FhcyIsImNvZGUiOiJwbGF0Zm9ybSIsInR5cGUiOjAsImNvbnRhY3RfcGVyc29uIjoiYWRtaW4iLCJjb250YWN0X3Bob25lIjoiMTg4ODg4ODg4ODgiLCJjb21wYW55X25hbWUiOiJ4eHh4IFx1NjcwOVx1OTY1MFx1NTE2Y1x1NTNmOCIsImxpY2Vuc2VfbnVtYmVyIjoiIiwiYWRkcmVzcyI6Ilx1NGUyZFx1NTZmZCIsImRlc2NyaXB0aW9uIjoiXHU1MTg1XHU3ZjZlXHU4ZDI2XHU1M2Y3IiwiZG9tYWluIjoiaHR0cHM6Ly93d3cubWFkb25nLnRlY2giLCJlbmFibGVkIjoxLCJpc29sYXRpb25fbW9kZSI6MiwiaXNfZGVmYXVsdCI6MSwiZXhwaXJlZF9hdCI6bnVsbCwiZGVsZXRlZF9hdCI6bnVsbCwiY3JlYXRlZF9hdCI6IjIwMjUtMTAtMDNUMDM6MjM6NDcuMDAwMDAwWiIsImNyZWF0ZWRfYnkiOjEsInVwZGF0ZWRfYnkiOm51bGwsInVwZGF0ZWRfYXQiOm51bGwsImNyZWF0ZWRfZGF0ZSI6IjIwMjUtMTAtMDMgMTE6MjM6NDciLCJ1cGRhdGVkX2RhdGUiOm51bGwsImV4cGlyZWRfZGF0ZSI6bnVsbCwicGl2b3QiOnsiYWRtaW5faWQiOjIsInRlbmFudF9pZCI6MSwiY3JlYXRlZF9kYXRlIjpudWxsLCJ1cGRhdGVkX2RhdGUiOm51bGx9fV0sImNhc2JpbiI6W119fQ.rCaHoiI1gU7xyoJazt3V5JQrS2guV34PH-JjkWnceE4","refresh_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJtYWRvbmcudGVjaCIsImF1ZCI6Im1hZG9uZy50ZWNoIiwiaWF0IjoxNzYyNDExMjY5LCJuYmYiOjE3NjI0MTEyNjksImV4cCI6MTc2MzAxNjA2OSwiZXh0ZW5kIjp7ImlkIjoiMiIsInVzZXJfbmFtZSI6ImFkbWluIiwicmVhbF9uYW1lIjoiXHU4ZDg1XHU3ZWE3XHU3YmExXHU3NDA2XHU1NDU4Iiwibmlja19uYW1lIjoiXHU4ZDg1XHU3ZWE3XHU3YmExXHU3NDA2XHU1NDU4IiwiaXNfc3VwZXIiOjEsImlzX3RlbmFudF9hZG1pbiI6MSwibW9iaWxlX3Bob25lIjpudWxsLCJlbWFpbCI6bnVsbCwiYXZhdGFyIjoiL3VwbG9hZC80MmY4NmI5YjM2Nzk0ZmI5ZTM4MDkxN2MyNTFmNmQ4MS5wbmciLCJzaWduZWQiOm51bGwsImRhc2hib2FyZCI6bnVsbCwiZGVwdF9pZCI6bnVsbCwiZW5hYmxlZCI6MSwibG9naW5faXAiOiIxMjAuNDEuMjQ2LjIyNSIsImxvZ2luX3RpbWUiOjE3NjI0MTEyNjksInVwZGF0ZWRfYXQiOiIyMDI1LTExLTA2VDA2OjQxOjA5LjAwMDAwMFoiLCJzZXgiOjAsImJpcnRoZGF5IjpudWxsLCJ0ZWwiOm51bGwsImlzX2xvY2tlZCI6MCwiZGVwdHMiOltdLCJwb3N0cyI6W10sInRlbmFudHMiOlt7ImlkIjoiMSIsImRiX25hbWUiOiJtYWRvbmdfYWRtaW5fc2FhcyIsImNvZGUiOiJwbGF0Zm9ybSIsInR5cGUiOjAsImNvbnRhY3RfcGVyc29uIjoiYWRtaW4iLCJjb250YWN0X3Bob25lIjoiMTg4ODg4ODg4ODgiLCJjb21wYW55X25hbWUiOiJ4eHh4IFx1NjcwOVx1OTY1MFx1NTE2Y1x1NTNmOCIsImxpY2Vuc2VfbnVtYmVyIjoiIiwiYWRkcmVzcyI6Ilx1NGUyZFx1NTZmZCIsImRlc2NyaXB0aW9uIjoiXHU1MTg1XHU3ZjZlXHU4ZDI2XHU1M2Y3IiwiZG9tYWluIjoiaHR0cHM6Ly93d3cubWFkb25nLnRlY2giLCJlbmFibGVkIjoxLCJpc29sYXRpb25fbW9kZSI6MiwiaXNfZGVmYXVsdCI6MSwiZXhwaXJlZF9hdCI6bnVsbCwiZGVsZXRlZF9hdCI6bnVsbCwiY3JlYXRlZF9hdCI6IjIwMjUtMTAtMDNUMDM6MjM6NDcuMDAwMDAwWiIsImNyZWF0ZWRfYnkiOjEsInVwZGF0ZWRfYnkiOm51bGwsInVwZGF0ZWRfYXQiOm51bGwsImNyZWF0ZWRfZGF0ZSI6IjIwMjUtMTAtMDMgMTE6MjM6NDciLCJ1cGRhdGVkX2RhdGUiOm51bGwsImV4cGlyZWRfZGF0ZSI6bnVsbCwicGl2b3QiOnsiYWRtaW5faWQiOjIsInRlbmFudF9pZCI6MSwiY3JlYXRlZF9kYXRlIjpudWxsLCJ1cGRhdGVkX2RhdGUiOm51bGx9fV0sImNhc2JpbiI6W119fQ.IgLkKhjjsgjAsjS4qReg51WpzwjiuU27_tbqTpUuD3M","client_id": "127.0.0.1-690c430548ecd5.55813566","expires_time": 1762418469}}')]
public function thirdPartyLogin(Request $request): \support\Response
{
try {
$acctId = $request->get('acct_id');
$appId = $request->get('app_id');
$appSecret = $request->get('app_secret');
$userName = $request->get('user_name');
$data = $this->service->thirdPartyLogin($acctId, $appId, $appSecret, $userName);
return Json::success('登录成功', $data);
} catch (\Throwable $e) {
return Json::fail($e->getMessage());
}
}
}
@@ -0,0 +1,134 @@
<?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\adminapi\controller\logs;
use app\adminapi\controller\Crud;
use app\adminapi\middleware\AccessTokenMiddleware;
use app\adminapi\middleware\OperationMiddleware;
use app\adminapi\middleware\PermissionMiddleware;
use app\adminapi\schema\request\logs\LoginLogQueryRequest;
use app\adminapi\schema\response\system\LoginLogResponse;
use app\schema\request\BatchDeleteRequest;
use app\schema\request\IdRequest;
use app\service\admin\logs\LoginLogService;
use core\exception\handler\AdminException;
use core\tool\Json;
use madong\swagger\annotation\response\PageResponse;
use madong\swagger\annotation\response\SimpleResponse;
use madong\swagger\attribute\Permission;
use OpenApi\Attributes as OA;
use support\Request;
use support\annotation\Middleware;
use WebmanTech\Swagger\DTO\SchemaConstants;
#[Middleware(AccessTokenMiddleware::class, PermissionMiddleware::class, OperationMiddleware::class)]
final class LoginLogController extends Crud
{
public function __construct(LoginLogService $service)
{
$this->service = $service;
}
#[OA\Get(
path: '/logs/login',
summary: '列表',
tags: ['日志管理.登录'],
x: [
SchemaConstants::X_SCHEMA_REQUEST => LoginLogQueryRequest::class,
]
)]
#[Permission(code: 'logs:login:list')]
#[PageResponse(schema: LoginLogResponse::class, example: [])]
public function index(Request $request): \support\Response
{
try {
[$where, $format, $limit, $field, $order, $page] = $this->selectInput($request);
$methods = [
'select' => 'formatSelect',
'tree' => 'formatTree',
'table_tree' => 'formatTableTree',
'normal' => 'formatNormal',
];
$format_function = $methods[$format] ?? 'formatNormal';
$total = $this->service->getCount($where);
$list = $this->service->selectList($where, $field, $page, $limit, $order, ['account' => function ($query) {
$query->select(['id', 'user_name', 'real_name']);
}], false);
return call_user_func([$this, $format_function], $list, $total);
} catch (\Throwable $e) {
return Json::fail($e->getMessage());
}
}
#[OA\Get(
path: '/logs/login/{id}',
summary: '详情',
tags: ['日志管理.登录'],
)]
#[OA\Parameter(
name: 'id',
description: '日志ID',
in: 'path',
required: true,
schema: new OA\Schema(type: 'string', example: 1)
)]
#[Permission(code: 'logs:login:read')]
#[SimpleResponse(schema: LoginLogResponse::class, example: [])]
public function show(Request $request): \support\Response
{
try {
$id = $request->route->param('id');
$data = $this->service->get($id,['*'],['account' => function ($query) {
$query->select(['id', 'user_name', 'real_name']);
}]);
if (empty($data)) {
throw new AdminException('数据未找到');
}
return Json::success('ok', $data->toArray());
} catch (\Throwable $e) {
return Json::fail($e->getMessage(), [], $e->getCode());
}
}
#[OA\Delete(
path: '/logs/login/{id}',
summary: '删除',
tags: ['日志管理.登录'],
x: [
SchemaConstants::X_PROPERTY_IN => 'id',
SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class,
]
)]
#[Permission(code: 'logs:login:delete')]
#[SimpleResponse(schema: [], example: [])]
public function destroy(Request $request): \support\Response
{
return parent::destroy($request);
}
#[OA\Delete(
path: '/logs/login',
summary: '批量删除',
tags: ['日志管理.登录'],
x: [
SchemaConstants::X_SCHEMA_REQUEST => BatchDeleteRequest::class,
]
)]
#[Permission(code: 'logs:login:delete')]
#[SimpleResponse(schema: [], example: [])]
public function batchDelete(Request $request): \support\Response
{
return parent::destroy($request);
}
}
@@ -0,0 +1,109 @@
<?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\adminapi\controller\logs;
use app\adminapi\controller\Crud;
use app\adminapi\middleware\AccessTokenMiddleware;
use app\adminapi\middleware\OperationMiddleware;
use app\adminapi\middleware\PermissionMiddleware;
use app\adminapi\schema\request\logs\OperateLogQueryRequest;
use app\adminapi\schema\response\system\OperateLogResponse;
use app\schema\request\BatchDeleteRequest;
use app\schema\request\IdRequest;
use app\service\admin\logs\OperateLogService;
use madong\swagger\annotation\response\PageResponse;
use madong\swagger\annotation\response\SimpleResponse;
use madong\swagger\attribute\Permission;
use OpenApi\Attributes as OA;
use support\Request;
use support\annotation\Middleware;
use WebmanTech\Swagger\DTO\SchemaConstants;
#[Middleware(AccessTokenMiddleware::class, PermissionMiddleware::class, OperationMiddleware::class)]
final class OperateLogController extends Crud
{
public function __construct(OperateLogService $service)
{
$this->service = $service;
}
#[OA\Get(
path: '/logs/operate',
summary: '列表',
tags: ['日志管理.操作'],
x: [
SchemaConstants::X_SCHEMA_REQUEST => OperateLogQueryRequest::class,
]
)]
#[Permission(code: 'logs:operate:list')]
#[PageResponse(schema: OperateLogResponse::class, example: [])]
public function index(Request $request): \support\Response
{
return parent::index($request);
}
#[OA\Get(
path: '/logs/operate/{id}',
summary: '详情',
tags: ['日志管理.操作'],
x: [
SchemaConstants::X_PROPERTY_IN => 'id',
SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class,
]
)]
#[Permission(code: 'logs:operate:read')]
#[SimpleResponse(schema: OperateLogResponse::class, example: [])]
public function show(Request $request): \support\Response
{
return parent::show($request);
}
#[OA\Delete(
path: '/logs/operate/{id}',
summary: '删除',
tags: ['日志管理.操作'],
x: [
SchemaConstants::X_PROPERTY_IN => 'id',
SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class,
]
)]
#[OA\Parameter(
name: 'id',
description: 'ID',
in: 'path',
required: true,
schema: new OA\Schema(type: 'string', default: '123456789012345678'),
)]
#[Permission(code: 'logs:operate:delete')]
#[SimpleResponse(schema: [], example: [])]
public function destroy(Request $request): \support\Response
{
return parent::destroy($request);
}
#[OA\Delete(
path: '/logs/operate',
summary: '批量删除',
tags: ['日志管理.操作'],
x: [
SchemaConstants::X_SCHEMA_REQUEST => BatchDeleteRequest::class,
]
)]
#[Permission(code: 'logs:operate:delete')]
#[SimpleResponse(schema: [], example: [])]
public function batchDelete(Request $request): \support\Response
{
return parent::destroy($request);
}
}
@@ -0,0 +1,141 @@
<?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\adminapi\controller\member;
use app\adminapi\controller\Crud;
use app\adminapi\middleware\AccessTokenMiddleware;
use app\adminapi\middleware\OperationMiddleware;
use app\adminapi\middleware\PermissionMiddleware;
use app\adminapi\validate\member\MemberLevelValidate;
use app\schema\request\BatchDeleteRequest;
use app\schema\request\IdRequest;
use app\service\admin\member\MemberLevelService;
use madong\swagger\annotation\response\SimpleResponse;
use madong\swagger\attribute\Permission;
use OpenApi\Attributes as OA;
use support\Request;
use support\annotation\Middleware;
use WebmanTech\Swagger\DTO\SchemaConstants;
#[Middleware(AccessTokenMiddleware::class, PermissionMiddleware::class, OperationMiddleware::class)]
final class LevelController extends Crud
{
public function __construct(MemberLevelService $service, MemberLevelValidate $validate)
{
$this->service = $service;
$this->validate = $validate;
}
#[OA\Get(
path: '/member/level',
summary: '列表',
tags: ['会员等级'],
parameters: [
new OA\Parameter(name: "name", description: "等级名称", in: "query", schema: new OA\Schema(type: "string")),
new OA\Parameter(name: "status", description: "状态", in: "query", schema: new OA\Schema(type: "integer")),
new OA\Parameter(name: "page", description: "页码", in: "query", schema: new OA\Schema(type: "integer")),
new OA\Parameter(name: "limit", description: "每页数量", in: "query", schema: new OA\Schema(type: "integer")),
]
)]
#[Permission("member:level:list")]
public function index(Request $request): \support\Response
{
return parent::index($request);
}
#[OA\Post(
path: '/member/level',
summary: '创建',
requestBody: new OA\RequestBody(
required: true,
content: new OA\JsonContent(properties: [
new OA\Property(property: "name", type: "string", example: "普通会员"),
new OA\Property(property: "min_points", type: "integer", example: 0),
new OA\Property(property: "max_points", type: "integer", example: 999),
new OA\Property(property: "discount", type: "number", example: 1),
new OA\Property(property: "status", type: "integer", example: 1),
new OA\Property(property: "sort", type: "integer", example: 100),
])
),
tags: ['会员等级'],
)]
#[Permission("member:level:create")]
#[SimpleResponse(example: ['code' => 0, 'message' => 'success', 'data' => []])]
public function store(Request $request): \support\Response
{
return parent::store($request);
}
#[OA\Put(
path: '/member/level/{id}',
summary: '更新',
tags: ['会员等级'],
x: [
SchemaConstants::X_PROPERTY_IN => 'id',
SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class,
]
)]
#[Permission("member:level:update")]
#[SimpleResponse(example: ['code' => 0, 'message' => 'success', 'data' => []])]
public function update(Request $request): \support\Response
{
return parent::update($request);
}
#[OA\Delete(
path: '/member/level/{id}',
summary: '删除会员等级',
tags: ['会员等级'],
x: [
SchemaConstants::X_PROPERTY_IN => 'id',
SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class,
]
)]
#[Permission("member:level:delete")]
#[SimpleResponse(example: ['code' => 0, 'message' => 'success', 'data' => []])]
public function destroy(Request $request): \support\Response
{
return parent::destroy($request);
}
#[OA\Delete(
path: '/member/level',
summary: '批量删除',
tags: ['会员等级'],
x: [SchemaConstants::X_SCHEMA_REQUEST => BatchDeleteRequest::class],
)]
#[Permission("member:level:delete")]
#[SimpleResponse(example: ['code' => 0, 'message' => 'success', 'data' => []])]
public function batchDelete(Request $request): \support\Response
{
return parent::destroy($request);
}
#[OA\Get(
path: '/member/level/{id}',
summary: '详情',
tags: ['会员等级'],
x: [
SchemaConstants::X_PROPERTY_IN => 'id',
SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class,
]
)]
#[Permission("member:level:show")]
public function show(Request $request): \support\Response
{
return parent::show($request);
}
}
@@ -0,0 +1,280 @@
<?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\adminapi\controller\member;
use app\adminapi\controller\Crud;
use app\adminapi\CurrentUser;
use app\adminapi\middleware\AccessTokenMiddleware;
use app\adminapi\middleware\OperationMiddleware;
use app\adminapi\middleware\PermissionMiddleware;
use app\adminapi\validate\member\MemberAuthValidate;
use app\service\admin\member\MemberAuthService;
use app\service\admin\member\MemberService;
use app\service\admin\member\MemberTagPermissionService;
use app\service\admin\member\MemberTagRelationService;
use madong\swagger\attribute\Permission;
use core\tool\Json;
use madong\swagger\annotation\response\SimpleResponse;
use OpenApi\Attributes as OA;
use support\Container;
use support\Request;
use support\annotation\Middleware;
#[Middleware(AccessTokenMiddleware::class, PermissionMiddleware::class, OperationMiddleware::class)]
final class MemberAuthController extends Crud
{
public function __construct(MemberAuthService $service, MemberAuthValidate $validate)
{
$this->service = $service;
$this->validate = $validate;
}
#[OA\Get(
path: '/member/auth/permissions',
summary: '权限菜单-包含所有',
security: [['Bearer' => [], 'ApiKey' => []]],
tags: ['权限相关-会员'],
)]
#[Permission(code: 'member:auth:permissions')]
#[SimpleResponse(schema: [], example: [])]
public function getPermissions(Request $request): \support\Response
{
try {
$format = input('format', 'table_tree');
$collection = $this->service->getAllMenus(true);
$methods = [
'select' => 'formatSelect',
'tree' => 'formatTree',
'table_tree' => 'formatTableTree',
'normal' => 'formatNormal',
];
$format_function = $methods[$format] ?? 'formatNormal';
$items = $collection->toArray();
return call_user_func([$this, $format_function], $collection, count($items));
} catch (\Throwable $e) {
return Json::fail($e->getMessage(), [], $e->getCode());
}
}
#[OA\Get(
path: '/member/auth/{id}/tag-permissions',
summary: '权限菜单-包含所有',
security: [['Bearer' => [], 'ApiKey' => []]],
tags: ['权限相关-会员'],
)]
#[Permission(code: 'member:auth:tag_permissions')]
#[SimpleResponse(example: '{"code": 0,"msg": "ok","data": []}')]
public function getPermissionsByTag(Request $request, $id): \support\Response
{
try {
$format = input('format', 'tree');
$collection = $this->service->getMenusByMemberTags($id, true);
$methods = [
'select' => 'formatSelect',
'tree' => 'formatTree',
'table_tree' => 'formatTableTree',
'normal' => 'formatNormal',
];
$format_function = $methods[$format] ?? 'formatNormal';
$items = $collection->toArray();
return call_user_func([$this, $format_function], $items, count($items));
} catch (\Throwable $e) {
return Json::fail($e->getMessage(), [], $e->getCode());
}
}
#[OA\Get(
path: '/member/auth/perm-code',
summary: '获取用户-权限码',
security: [['Bearer' => [], 'ApiKey' => []]],
tags: ['权限相关-会员'],
)]
#[Permission(code: 'member:auth:perm_code')]
#[SimpleResponse(example: ['code' => 0, 'message' => 'success', 'data' => ['admin']])]
public function getUserCodes(Request $request): \support\Response
{
try {
/** @var CurrentUser $currUser */
$currUser = Container::make(CurrentUser::class);
$data = $this->service->getCodesByUserRoles($currUser);
return Json::success('ok', $data);
} catch (\Throwable $e) {
return Json::fail($e->getMessage(), [], $e->getCode());
}
}
#[OA\Get(
path: '/member/auth/tag-menu-ids',
summary: '通过角色ID获取权限ID集合',
security: [['Bearer' => [], 'ApiKey' => []]],
tags: ['权限相关-会员'],
)]
#[Permission(code: 'member:auth:role_menu_ids')]
#[SimpleResponse(example: ['code' => 0, 'message' => 'success', 'data' => []])]
public function tagMenuIds(Request $request): \support\Response
{
try {
$tagId = $request->input('tag_id');
$result = $this->service->getMenusByTagId($tagId, true);
if (empty($result)) {
return Json::Success([]);
}
$data = [];
foreach ($result as $item) {
$data[] = $item->id;
}
return Json::success('ok', $data);
} catch (\Throwable $e) {
return Json::fail($e->getMessage(), [], $e->getCode());
}
}
#[OA\Post(
path: '/member/auth/save-tag-menu',
summary: '保存角色菜单关系',
security: [['Bearer' => [], 'ApiKey' => []]],
tags: ['权限相关-会员'],
)]
#[Permission(code: 'member:auth:role_menu')]
#[SimpleResponse( example: '{"code": 0,"msg": "ok"}')]
public function saveTagMenuRelation(Request $request): \support\Response
{
try {
$data = $request->all();
/** @var MemberTagPermissionService $systemRoleMenuService */
$systemRoleMenuService = Container::make(MemberTagPermissionService::class);
$systemRoleMenuService->save($data);
return Json::success('success');
} catch (\Throwable $e) {
return Json::fail($e->getMessage(), [], $e->getCode());
}
}
#[OA\Get(
path: '/member/auth/user-list-by-tag-id',
summary: '获取角色-关联用户列表',
security: [['Bearer' => [], 'ApiKey' => []]],
tags: ['权限相关-会员'],
)]
#[OA\Parameter(
name: 'tag_id',
description: '标签ID',
in: 'query',
required: true,
schema: new OA\Schema(type: 'string'),
)]
#[Permission(code: 'member:auth:tag_user_list')]
#[SimpleResponse(example: ['code' => 0, 'message' => 'success', 'data' => []])]
public function getUsersByTagId(Request $request): \support\Response
{
try {
[$where, $format, $limit, $field, $order, $page] = $this->selectInput($request, [
'allowed_fields' => ['username', 'nickname', 'email', 'mobile', 'tag_id'], // 使用 null 表示不限制字段,允许所有模型字段
'skip_fields' => [],
// 添加自定义参数会自动合并到 where 中
'custom_fields' => [],
]);
/** @var MemberService $systemUserService */
$systemUserService = Container::make(MemberService::class);
$data = $systemUserService->getUsersListByTagId($where, $field, $page, $limit);
return Json::success('ok', $data);
} catch (\Throwable $e) {
return Json::fail($e->getMessage(), [], $e->getCode());
}
}
#[OA\Post(
path: '/member/auth/save-user-tag',
summary: '保存用户-关联标签',
security: [['Bearer' => [], 'ApiKey' => []]],
tags: ['权限相关-会员'],
)]
#[Permission(code: 'member:auth:save_user_tag')]
#[SimpleResponse(example: '{"code": 0,"msg": "success"}')]
public function saveUserTags(Request $request): \support\Response
{
try {
$data = $request->all();
/** @var MemberTagRelationService $memberTagRelationService */
$memberTagRelationService = Container::make(MemberTagRelationService::class);
$memberTagRelationService->saveUserTags($data);
return Json::success('success');
} catch (\Throwable $e) {
return Json::fail($e->getMessage(), [], $e->getCode());
}
}
#[OA\Post(
path: '/member/auth/remove-user-tag',
summary: '移除用户-关联角色',
security: [['Bearer' => [], 'ApiKey' => []]],
tags: ['权限相关-会员'],
)]
#[Permission(code: 'member:auth:remove_user_tag')]
#[SimpleResponse(example: '{"code": 0,"msg": "ok"}')]
public function removeUserRole(Request $request): \support\Response
{
try {
$data = $request->all();
/** @var MemberTagRelationService $MemberTagRelationService */
$MemberTagRelationService = Container::make(MemberTagRelationService::class);
$MemberTagRelationService->removeUserTag($data);
return Json::success('ok');
} catch (\Throwable $e) {
return Json::fail($e->getMessage(), [], $e->getCode());
}
}
#[OA\Get(
path: '/member/auth/user-list-exclude-tag-id',
summary: '获取用户列表-排除指定角色id',
security: [['Bearer' => [], 'ApiKey' => []]],
tags: ['权限相关-会员'],
)]
#[OA\Parameter(
name: 'tag_id',
description: '标签ID',
in: 'query',
required: true,
schema: new OA\Schema(type: 'string'),
)]
#[Permission(code: 'member:auth:user_list_exclude_tag')]
#[SimpleResponse(example: ['code' => 0, 'message' => 'success', 'data' => []])]
public function getUsersExcludingTag(Request $request): \support\Response
{
try {
// 自定义参数
$tagId = $request->input('tag_id');
[$where, $format, $limit, $field, $order, $page] = $this->selectInput($request, [
'allowed_fields' => null, // 使用 null 表示不限制字段,允许所有模型字段
'skip_fields' => ['token', 'deleted_at'],
// 添加自定义参数 tag_id,会自动合并到 where 中
'custom_fields' => [
'tag_id' => $tagId,
],
]);
/** @var MemberService $memberUserService */
$memberUserService = Container::make(MemberService::class);
$data = $memberUserService->getUsersExcludingTag($where, $field, $page, $limit);
return Json::success('ok', $data);
} catch (\Throwable $e) {
return Json::fail($e->getMessage(), [], $e->getCode());
}
}
}
@@ -0,0 +1,409 @@
<?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\adminapi\controller\member;
use app\adminapi\controller\Crud;
use app\adminapi\middleware\AccessTokenMiddleware;
use app\adminapi\middleware\OperationMiddleware;
use app\adminapi\middleware\PermissionMiddleware;
use app\adminapi\validate\member\MemberValidate;
use app\api\CurrentMember;
use app\schema\request\BatchDeleteRequest;
use app\schema\request\IdRequest;
use app\service\admin\member\MemberService;
use core\tool\Json;
use madong\swagger\annotation\response\SimpleResponse;
use madong\swagger\attribute\Permission;
use OpenApi\Attributes as OA;
use support\annotation\Middleware;
use support\Container;
use support\Request;
use WebmanTech\Swagger\DTO\SchemaConstants;
#[Middleware(AccessTokenMiddleware::class, PermissionMiddleware::class, OperationMiddleware::class)]
final class MemberController extends Crud
{
public function __construct(MemberService $service, MemberValidate $validate)
{
$this->service = $service;
$this->validate = $validate;
}
#[OA\Get(
path: '/member/user',
summary: '会员用户列表',
tags: ['会员用户'],
parameters: [
new OA\Parameter(name: "username", description: "用户名", in: "query", schema: new OA\Schema(type: "string")),
new OA\Parameter(name: "nickname", description: "昵称", in: "query", schema: new OA\Schema(type: "string")),
new OA\Parameter(name: "phone", description: "手机号", in: "query", schema: new OA\Schema(type: "string")),
new OA\Parameter(name: "email", description: "邮箱", in: "query", schema: new OA\Schema(type: "string")),
new OA\Parameter(name: "level_id", description: "等级ID", in: "query", schema: new OA\Schema(type: "integer")),
new OA\Parameter(name: "status", description: "状态", in: "query", schema: new OA\Schema(type: "integer")),
new OA\Parameter(name: "page", description: "页码", in: "query", schema: new OA\Schema(type: "integer")),
new OA\Parameter(name: "limit", description: "每页数量", in: "query", schema: new OA\Schema(type: "integer")),
],
responses: [
new OA\Response(
response: 200,
description: "成功",
content: new OA\JsonContent(properties: [
new OA\Property(property: "code", type: "integer", example: 0),
new OA\Property(property: "msg", type: "string", example: "获取成功"),
new OA\Property(property: "data", properties: [
new OA\Property(property: "total", type: "integer", example: 10),
new OA\Property(property: "list", type: "array", items: new OA\Items(properties: [
new OA\Property(property: "id", type: "integer", example: 1),
new OA\Property(property: "username", type: "string", example: "testuser"),
new OA\Property(property: "nickname", type: "string", example: "测试用户"),
new OA\Property(property: "phone", type: "string", example: "13800138000"),
new OA\Property(property: "email", type: "string", example: "test@example.com"),
new OA\Property(property: "avatar", type: "string", example: ""),
new OA\Property(property: "level_id", type: "integer", example: 1),
new OA\Property(property: "status", type: "integer", example: 1),
new OA\Property(property: "create_time", type: "integer", example: 1640995200),
])),
], type: "object"),
])
),
]
)]
#[Permission("member:user:list")]
public function index(Request $request): \support\Response
{
try {
[$where, $format, $limit, $field, $order, $page] = $this->selectInput($request);
$methods = [
'select' => 'formatSelect',
'tree' => 'formatTree',
'table_tree' => 'formatTableTree',
'normal' => 'formatNormal',
];
$format_function = $methods[$format] ?? 'formatNormal';
$total = $this->service->getCount($where);
$list = $this->service->selectList($where, $field, $page, $limit, $order, ['level', 'tags'], false);
return call_user_func([$this, $format_function], $list, $total);
} catch (\Throwable $e) {
return Json::fail($e->getMessage());
}
}
#[OA\Get(
path: '/member/user/{id}',
summary: '会员用户详情',
tags: ['会员用户'],
responses: [
new OA\Response(
response: 200,
description: "成功",
content: new OA\JsonContent(properties: [
new OA\Property(property: "code", type: "integer", example: 0),
new OA\Property(property: "msg", type: "string", example: "获取成功"),
new OA\Property(property: "data", properties: [
new OA\Property(property: "id", type: "integer", example: 1),
new OA\Property(property: "username", type: "string", example: "testuser"),
new OA\Property(property: "nickname", type: "string", example: "测试用户"),
new OA\Property(property: "phone", type: "string", example: "13800138000"),
new OA\Property(property: "email", type: "string", example: "test@example.com"),
new OA\Property(property: "avatar", type: "string", example: ""),
new OA\Property(property: "level_id", type: "integer", example: 1),
new OA\Property(property: "status", type: "integer", example: 1),
new OA\Property(property: "create_time", type: "integer", example: 1640995200),
], type: "object"),
])
),
],
x: [
SchemaConstants::X_PROPERTY_IN => 'id',
SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class,
]
)]
#[Permission("member:user:read")]
public function show(Request $request): \support\Response
{
return parent::show($request);
}
#[OA\Post(
path: '/member/user',
summary: '创建',
requestBody: new OA\RequestBody(
required: true,
content: new OA\JsonContent(properties: [
new OA\Property(property: "username", type: "string", example: "testuser"),
new OA\Property(property: "nickname", type: "string", example: "测试用户"),
new OA\Property(property: "phone", type: "string", example: "13800138000"),
new OA\Property(property: "email", type: "string", example: "test@example.com"),
new OA\Property(property: "password", type: "string", example: "123456"),
new OA\Property(property: "avatar", type: "string", example: ""),
new OA\Property(property: "level_id", type: "integer", example: 1),
new OA\Property(property: "status", type: "integer", example: 1),
])
),
tags: ['会员用户'],
responses: [
new OA\Response(
response: 200,
description: "成功",
content: new OA\JsonContent(properties: [
new OA\Property(property: "code", type: "integer", example: 0),
new OA\Property(property: "msg", type: "string", example: "创建成功"),
new OA\Property(property: "data", properties: [
new OA\Property(property: "id", type: "integer", example: 1),
], type: "object"),
])
),
],
)]
#[Permission("member:user:create")]
#[SimpleResponse( example: '{"code":0,"msg":"created","id":1}')]
public function store(Request $request): \support\Response
{
return parent::store($request);
}
#[OA\Put(
path: '/member/user/{id}',
summary: '更新',
requestBody: new OA\RequestBody(
required: true,
content: new OA\JsonContent(properties: [
new OA\Property(property: "nickname", type: "string", example: "新昵称"),
new OA\Property(property: "phone", type: "string", example: "13800138000"),
new OA\Property(property: "email", type: "string", example: "test@example.com"),
new OA\Property(property: "avatar", type: "string", example: ""),
new OA\Property(property: "level_id", type: "integer", example: 2),
new OA\Property(property: "status", type: "integer", example: 1),
])
),
tags: ['会员用户'],
parameters: [
new OA\Parameter(name: "id", description: "用户ID", in: "path", required: true, schema: new OA\Schema(type: "integer")),
],
responses: [
new OA\Response(
response: 200,
description: "成功",
content: new OA\JsonContent(properties: [
new OA\Property(property: "code", type: "integer", example: 0),
new OA\Property(property: "msg", type: "string", example: "更新成功"),
new OA\Property(property: "data", properties: [
new OA\Property(property: "id", type: "integer", example: 1),
], type: "object"),
])
),
]
)]
#[Permission("member:user:update")]
public function update(Request $request): \support\Response
{
return parent::update($request);
}
#[OA\Delete(
path: '/member/user/{id}',
summary: '删除',
tags: ['会员用户'],
parameters: [
new OA\Parameter(name: "id", description: "用户ID", in: "path", required: true, schema: new OA\Schema(type: "integer")),
],
responses: [
new OA\Response(
response: 200,
description: "成功",
content: new OA\JsonContent(properties: [
new OA\Property(property: "code", type: "integer", example: 0),
new OA\Property(property: "msg", type: "string", example: "删除成功"),
])
),
],
x: [
SchemaConstants::X_PROPERTY_IN => 'id',
SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class,
]
)]
#[Permission("member:user:delete")]
public function destroy(Request $request): \support\Response
{
return parent::destroy($request);
}
#[OA\Delete(
path: '/member/user',
summary: '批量删除',
tags: ['会员用户'],
x: [SchemaConstants::X_SCHEMA_REQUEST => BatchDeleteRequest::class],
)]
#[Permission("member:user:delete")]
public function batchDelete(Request $request): \support\Response
{
return parent::destroy($request);
}
#[OA\Put(
path: '/member/user/{id}/reset-password',
summary: '重置密码',
requestBody: new OA\RequestBody(
required: true,
content: new OA\JsonContent(properties: [
new OA\Property(property: "password", type: "string", example: "123456"),
])
),
tags: ['会员用户'],
parameters: [
new OA\Parameter(name: "id", description: "用户ID", in: "path", required: true, schema: new OA\Schema(type: "integer")),
],
responses: [
new OA\Response(
response: 200,
description: "成功",
content: new OA\JsonContent(properties: [
new OA\Property(property: "code", type: "integer", example: 0),
new OA\Property(property: "msg", type: "string", example: "密码重置成功"),
])
),
],
x: [
SchemaConstants::X_PROPERTY_IN => 'id',
SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class,
]
)]
#[Permission("member:user:reset_password")]
public function resetPassword(Request $request): \support\Response
{
try {
$id = $request->route->param('id');
$data = $this->insertInput($request);
if (empty($data['password'])) {
return Json::fail('密码不能为空');
}
$this->service->update($id, ['password' => $data['password']]);
return Json::success('密码重置成功');
} catch (\Exception $e) {
return Json::fail($e->getMessage());
}
}
#[OA\Put(
path: '/member/user/{id}/assign-tags',
summary: '分配标签',
requestBody: new OA\RequestBody(
required: true,
content: new OA\JsonContent(properties: [
new OA\Property(property: "tags", type: "array", example: [1, 2, 3]),
])
),
tags: ['会员用户'],
)]
#[SimpleResponse(example: ['code' => 0, 'message' => 'success', 'data' => []])]
#[Permission("member:user:assign_tags")]
public function assignTags(Request $request): \support\Response
{
try {
$id = $request->route->param('id');
$data = $request->input('tags', []);
if (empty($data)) {
return Json::fail('标签不能为空');
}
$this->service->assignTags($id, $data);
return Json::success('标签分配成功');
} catch (\Exception $e) {
return Json::fail($e->getMessage());
}
}
#[OA\Put(
path: '/member/user/{id}/adjust-points',
summary: '调整会员积分',
requestBody: new OA\RequestBody(
required: true,
content: new OA\JsonContent(properties: [
new OA\Property(property: "points", type: "integer", example: 100),
])
),
tags: ['会员用户'],
parameters: [
new OA\Parameter(name: "id", description: "用户ID", in: "path", required: true, schema: new OA\Schema(type: "integer")),
],
x: [
SchemaConstants::X_PROPERTY_IN => 'id',
SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class,
]
)]
#[SimpleResponse(example: ['code' => 0, 'message' => 'success', 'data' => []])]
#[Permission("member:user:adjust_points")]
public function adjustPoints(Request $request): \support\Response
{
try {
$id = $request->route->param('id');
$points = $request->input('points', 0);
$this->service->adjustPoints($id, (int)$points);
return Json::success('积分调整成功');
} catch (\Exception $e) {
return Json::fail($e->getMessage());
}
}
/**
* 格式化下拉列表
*
* @param $items
*
* @return \support\Response
*/
public function formatSelect($items): \support\Response
{
$formatted_items = [];
foreach ($items as $item) {
$formatted_items[] = [
'label' => $item->nickname ?? $item->id,
'value' => $item->id,
];
}
return Json::success('ok', $formatted_items);
}
// #[OA\Get(
// path: '/member/user/statistics',
// summary: '会员统计',
// tags: ['会员用户'],
// responses: [
// new OA\Response(
// response: 200,
// description: "成功",
// content: new OA\JsonContent(properties: [
// new OA\Property(property: "code", type: "integer", example: 0),
// new OA\Property(property: "msg", type: "string", example: "获取成功"),
// new OA\Property(property: "data", properties: [
// new OA\Property(property: "total", type: "integer", example: 100),
// new OA\Property(property: "active", type: "integer", example: 80),
// new OA\Property(property: "inactive", type: "integer", example: 20),
// ], type: "object"),
// ])
// ),
// ]
// )]
// #[Permission("member:user:statistics")]
public function statistics(Request $request): \support\Response
{
try {
$result = $this->service->getStatistics();
return Json::success('获取成功', $result);
} catch (\Exception $e) {
return Json::fail($e->getMessage());
}
}
}
@@ -0,0 +1,132 @@
<?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\adminapi\controller\member;
use app\adminapi\controller\Crud;
use app\adminapi\middleware\AccessTokenMiddleware;
use app\adminapi\middleware\OperationMiddleware;
use app\adminapi\middleware\PermissionMiddleware;
use app\adminapi\validate\member\MemberPointsValidate;
use app\service\admin\member\MemberPointsService;
use madong\swagger\attribute\Permission;
use core\exception\handler\AdminException;
use core\tool\Json;
use madong\swagger\annotation\response\PageResponse;
use madong\swagger\annotation\response\SimpleResponse;
use OpenApi\Attributes as OA;
use support\Request;
use support\annotation\Middleware;
#[Middleware(AccessTokenMiddleware::class, PermissionMiddleware::class, OperationMiddleware::class)]
final class PointsController extends Crud
{
public function __construct(MemberPointsService $service, MemberPointsValidate $validate)
{
$this->service = $service;
$this->validate = $validate;
}
#[OA\Get(
path: '/member/points',
summary: '列表',
tags: ['会员积分'],
parameters: [
new OA\Parameter(name: "member_id", description: "会员ID", in: "query", schema: new OA\Schema(type: "integer")),
new OA\Parameter(name: "type", description: "类型", in: "query", schema: new OA\Schema(type: "integer")),
new OA\Parameter(name: "start_time", description: "开始时间", in: "query", schema: new OA\Schema(type: "string")),
new OA\Parameter(name: "end_time", description: "结束时间", in: "query", schema: new OA\Schema(type: "string")),
new OA\Parameter(name: "page", description: "页码", in: "query", schema: new OA\Schema(type: "integer")),
new OA\Parameter(name: "limit", description: "每页数量", in: "query", schema: new OA\Schema(type: "integer")),
]
)]
#[Permission("member:points:list")]
public function index(Request $request): \support\Response
{
try {
[$where, $format, $limit, $field, $order, $page] = $this->selectInput($request);
$methods = [
'select' => 'formatSelect',
'tree' => 'formatTree',
'table_tree' => 'formatTableTree',
'normal' => 'formatNormal',
];
$format_function = $methods[$format] ?? 'formatNormal';
$total = $this->service->getCount($where);
$list = $this->service->selectList($where, $field, $page, $limit, $order, ['member'], false);
return call_user_func([$this, $format_function], $list, $total);
} catch (\Throwable $e) {
return Json::fail($e->getMessage());
}
}
#[OA\Get(
path: '/member/points/{id}',
summary: '详情',
tags: ['会员积分'],
x: [
SchemaConstants::X_PROPERTY_IN => 'id',
SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class,
]
)]
#[Permission("member:points:read")]
public function show(Request $request): \support\Response
{
try {
$id = $request->route->param('id');
$data = $this->service->get($id, ['*'], ['member'], 'created_at', []);
if (empty($data)) {
throw new AdminException('数据未找到', ['errorCode' => 400]);
}
return Json::success('ok', $data->toArray());
} catch (\Throwable $e) {
return Json::fail($e->getMessage(), [], $e->getCode());
}
}
#[OA\Post(
path: '/member/points',
summary: '新增',
requestBody: new OA\RequestBody(
required: true,
content: new OA\JsonContent(properties: [
new OA\Property(property: "member_id", type: "integer", example: 1),
new OA\Property(property: "points", type: "integer", example: 100),
new OA\Property(property: "type", type: "integer", example: 1),
new OA\Property(property: "source", type: "string", example: "admin"),
new OA\Property(property: "remark", type: "string", example: "管理员操作"),
new OA\Property(property: "operator", type: "string", example: "admin"),
])
),
tags: ['会员积分']
)]
#[Permission("member:points:create")]
public function store(Request $request): \support\Response
{
try {
$data = $request->all();
if (isset($this->validate) && $this->validate) {
if (!$this->validate->scene('store')->check($data)) {
throw new \Exception($this->validate->getError());
}
}
$this->service->operate($data);
return Json::success('操作成功');
} catch (\Throwable $e) {
return Json::fail($e->getMessage(), [], $e->getCode());
}
}
}
@@ -0,0 +1,259 @@
<?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\adminapi\controller\member;
use app\adminapi\controller\Crud;
use app\adminapi\middleware\AccessTokenMiddleware;
use app\adminapi\middleware\OperationMiddleware;
use app\adminapi\middleware\PermissionMiddleware;
use app\adminapi\validate\member\MemberTagValidate;
use app\schema\request\BatchDeleteRequest;
use app\schema\request\IdRequest;
use app\service\admin\member\MemberTagService;
use core\tool\Json;
use madong\swagger\annotation\response\SimpleResponse;
use madong\swagger\attribute\Permission;
use OpenApi\Attributes as OA;
use support\Request;
use support\annotation\Middleware;
use WebmanTech\Swagger\DTO\SchemaConstants;
#[Middleware(AccessTokenMiddleware::class, PermissionMiddleware::class, OperationMiddleware::class)]
final class TagController extends Crud
{
public function __construct(MemberTagService $service, MemberTagValidate $validate)
{
$this->service = $service;
$this->validate = $validate;
}
#[OA\Get(
path: '/member/tag',
summary: '列表',
tags: ['会员标签'],
parameters: [
new OA\Parameter(name: "name", description: "标签名称", in: "query", schema: new OA\Schema(type: "string")),
new OA\Parameter(name: "status", description: "状态", in: "query", schema: new OA\Schema(type: "integer")),
new OA\Parameter(name: "page", description: "页码", in: "query", schema: new OA\Schema(type: "integer")),
new OA\Parameter(name: "limit", description: "每页数量", in: "query", schema: new OA\Schema(type: "integer")),
]
)]
#[Permission("member:tag:list")]
public function index(Request $request): \support\Response
{
return parent::index($request);
}
#[OA\Get(
path: '/member/tag/{id}',
summary: '详情',
tags: ['会员标签'],
responses: [
new OA\Response(
response: 200,
description: "成功",
content: new OA\JsonContent(properties: [
new OA\Property(property: "code", type: "integer", example: 0),
new OA\Property(property: "msg", type: "string", example: "获取成功"),
new OA\Property(property: "data", properties: [
new OA\Property(property: "id", type: "integer", example: 1),
new OA\Property(property: "name", type: "string", example: "活跃会员"),
new OA\Property(property: "color", type: "string", example: "#409EFF"),
new OA\Property(property: "status", type: "integer", example: 1),
new OA\Property(property: "sort", type: "integer", example: 100),
new OA\Property(property: "create_time", type: "integer", example: 1640995200),
], type: "object"),
])
),
],
x: [
SchemaConstants::X_PROPERTY_IN => 'id',
SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class,
]
)]
#[Permission("member:tag:read")]
public function show(Request $request): \support\Response
{
return parent::show($request);
}
#[OA\Post(
path: '/member/tag',
summary: '新增',
requestBody: new OA\RequestBody(
required: true,
content: new OA\JsonContent(properties: [
new OA\Property(property: "name", type: "string", example: "活跃会员"),
new OA\Property(property: "color", type: "string", example: "#409EFF"),
new OA\Property(property: "status", type: "integer", example: 1),
new OA\Property(property: "sort", type: "integer", example: 100),
])
),
tags: ['会员标签']
)]
#[Permission("member:tag:create")]
public function store(Request $request): \support\Response
{
return parent::store($request);
}
#[OA\Put(
path: '/member/tag/{id}',
summary: '更新',
requestBody: new OA\RequestBody(
required: true,
content: new OA\JsonContent(properties: [
new OA\Property(property: "name", type: "string", example: "活跃会员"),
new OA\Property(property: "color", type: "string", example: "#409EFF"),
new OA\Property(property: "status", type: "integer", example: 1),
new OA\Property(property: "sort", type: "integer", example: 100),
])
),
tags: ['会员标签'],
parameters: [
new OA\Parameter(name: "id", description: "标签ID", in: "path", required: true, schema: new OA\Schema(type: "integer")),
],
responses: [
new OA\Response(
response: 200,
description: "成功",
content: new OA\JsonContent(properties: [
new OA\Property(property: "code", type: "integer", example: 0),
new OA\Property(property: "msg", type: "string", example: "更新成功"),
new OA\Property(property: "data", properties: [
new OA\Property(property: "id", type: "integer", example: 1),
], type: "object"),
])
),
]
)]
#[Permission("member:tag:update")]
public function update(Request $request): \support\Response
{
return parent::update($request);
}
#[OA\Delete(
path: '/member/tag/{id}',
summary: '删除',
tags: ['会员标签'],
parameters: [
new OA\Parameter(name: "id", description: "标签ID", in: "path", required: true, schema: new OA\Schema(type: "integer")),
],
x: [
SchemaConstants::X_PROPERTY_IN => 'id',
SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class,
]
)]
#[Permission("member:tag:delete")]
public function destroy(Request $request): \support\Response
{
return parent::destroy($request);
}
#[OA\Delete(
path: '/member/tag',
summary: '批量删除',
tags: ['会员标签'],
x: [SchemaConstants::X_SCHEMA_REQUEST => BatchDeleteRequest::class],
)]
#[Permission("member:tag:delete")]
public function batchDelete(Request $request): \support\Response
{
return parent::destroy($request);
}
#[OA\Put(
path: '/member/{id}/permissions',
summary: '为标签分配权限',
requestBody: new OA\RequestBody(
required: true,
content: new OA\JsonContent(properties: [
new OA\Property(property: "permissions", type: "array", items: new OA\Items(type: "string"), example: ["developer", "api_access"]),
])
),
tags: ['会员标签'],
parameters: [
new OA\Parameter(name: "id", description: "标签ID", in: "path", required: true, schema: new OA\Schema(type: "integer")),
],
)]
#[SimpleResponse(example: ['code' => 0, 'message' => 'success', 'data' => []])]
#[Permission("member:tag:assign_permissions")]
public function assignPermissions(Request $request, $id): \support\Response
{
try {
$permissions = $request->input('permissions', []);
$this->service->assignPermissions($id, $permissions);
return Json::success('权限分配成功');
} catch (\Exception $e) {
return Json::fail($e->getMessage());
}
}
#[OA\Get(
path: '/member/{id}/permissions',
summary: '获取标签权限',
tags: ['会员标签'],
parameters: [
new OA\Parameter(name: "id", description: "标签ID", in: "path", required: true, schema: new OA\Schema(type: "integer")),
],
)]
#[Permission("member:tag:show_permissions")]
public function getPermissions(Request $request, $id): \support\Response
{
try {
$tag = $this->service->get($id, ['*'], ['permissions']);
if (!$tag) {
return Json::fail('标签不存在');
}
return Json::success('ok', $tag->permissions->toArray());
} catch (\Exception $e) {
return Json::fail($e->getMessage());
}
}
#[OA\Get(
path: '/member/all-with-permissions',
summary: '获取所有启用的标签(含权限)',
tags: ['会员标签'],
)]
#[Permission("member:tag:all")]
public function getAllWithPermissions(Request $request): \support\Response
{
try {
$result = $this->service->getEnabledTagsWithPermissions();
return Json::success('ok', $result);
} catch (\Exception $e) {
return Json::fail($e->getMessage());
}
}
#[OA\Get(
path: '/member/available-permissions',
summary: '获取所有可用的权限码(从菜单表)',
tags: ['会员标签'],
)]
#[Permission("member:tag:all")]
public function getAvailablePermissions(Request $request): \support\Response
{
try {
$result = $this->service->getAvailablePermissions();
return Json::success('ok', $result);
} catch (\Exception $e) {
return Json::fail($e->getMessage());
}
}
}
@@ -0,0 +1,254 @@
<?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\adminapi\controller\message;
use app\adminapi\CurrentUser;
use app\adminapi\controller\Crud;
use app\adminapi\middleware\AccessTokenMiddleware;
use app\adminapi\middleware\OperationMiddleware;
use app\adminapi\middleware\PermissionMiddleware;
use app\adminapi\schema\request\message\MessageFormRequest;
use app\adminapi\schema\request\message\MessageQueryRequest;
use app\adminapi\schema\response\system\MessageResponse;
use app\adminapi\validate\system\MessageValidate;
use app\enum\system\MessageStatus;
use app\schema\request\BatchDeleteRequest;
use app\schema\request\IdRequest;
use app\service\admin\message\MessageService;
use core\tool\Json;
use madong\swagger\annotation\response\PageResponse;
use madong\swagger\annotation\response\SimpleResponse;
use madong\swagger\attribute\AllowAnonymous;
use madong\swagger\attribute\Permission;
use OpenApi\Attributes as OA;
use OpenApi\Attributes\RequestBody;
use support\Request;
use support\annotation\Middleware;
use support\Container;
use WebmanTech\Swagger\DTO\SchemaConstants;
#[Middleware(AccessTokenMiddleware::class, PermissionMiddleware::class, OperationMiddleware::class)]
final class MessageController extends Crud
{
public function __construct(MessageService $service, MessageValidate $validate)
{
$this->service = $service;
$this->validate = $validate;
}
#[OA\Get(
path: '/message',
summary: '列表',
tags: ['消息管理'],
x: [
SchemaConstants::X_SCHEMA_REQUEST => MessageQueryRequest::class,
]
)]
#[Permission(code: 'network:message:list')]
#[PageResponse(MessageResponse::class)]
public function index(Request $request): \support\Response
{
$uid = Container::make(CurrentUser::class)->id();
$data = $request->get();
$data['receiver_id'] = $uid;//消息接收人
$request->setGet($data);
return parent::index($request);
}
#[OA\Get(
path: '/message/{id}',
summary: '详情',
tags: ['消息管理'],
x: [
SchemaConstants::X_PROPERTY_IN => 'id',
SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class,
]
)]
#[Permission(code: 'network:message:read')]
#[SimpleResponse(schema: MessageResponse::class, example: [])]
public function show(Request $request): \support\Response
{
return parent::show($request);
}
#[OA\Post(
path: '/message',
summary: '创建',
tags: ['消息管理'],
x: [
SchemaConstants::X_SCHEMA_REQUEST => MessageFormRequest::class,
]
)]
#[Permission(code: 'network:message:create')]
#[SimpleResponse(example: ['code' => 0, 'message' => 'success', 'data' => []])]
public function store(Request $request): \support\Response
{
return parent::store($request);
}
#[OA\Put(
path: '/message/{id}',
summary: '更新',
tags: ['消息管理'],
x: [
SchemaConstants::X_SCHEMA_REQUEST => MessageFormRequest::class,
]
)]
#[OA\Parameter(
name: 'id',
description: '消息ID(雪花ID)',
in: 'path',
required: true,
schema: new OA\Schema(
type: 'string',
example: '123456789012345678'
)
)]
#[Permission(code: 'network:message:update')]
#[SimpleResponse(example: ['code' => 0, 'message' => 'success', 'data' => []])]
public function update(Request $request): \support\Response
{
return parent::update($request);
}
#[OA\Post(
path: '/message/export',
summary: '导出',
tags: ['消息管理']
)]
#[Permission(code: 'network:message:list', description: '导出')]
#[SimpleResponse(schema: [], example: [])]
public function export(Request $request): \support\Response
{
return parent::export($request);
}
#[OA\Delete(
path: '/message/{id}',
summary: '删除',
tags: ['消息管理'],
parameters: [
new OA\Parameter(
name: 'id',
description: '消息ID',
in: 'path',
required: true,
schema: new OA\Schema(
type: 'string',
default: 0,
example: '123456789012345678',
)
),
],
x: [
SchemaConstants::X_PROPERTY_IN => 'id',
SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class,
]
)]
#[Permission(code: 'network:message:delete')]
#[SimpleResponse(example: ['code' => 0, 'message' => 'success', 'data' => []])]
public function destroy(Request $request): \support\Response
{
return parent::destroy($request);
}
#[OA\Delete(
path: '/message',
summary: '批量删除',
tags: ['消息管理'],
x: [
SchemaConstants::X_SCHEMA_REQUEST => BatchDeleteRequest::class,
]
)]
#[Permission(code: 'network:message:delete')]
#[SimpleResponse(example: ['code' => 0, 'message' => 'success', 'data' => []])]
public function batchDelete(Request $request): \support\Response
{
return parent::destroy($request);
}
#[OA\Put(
path: '/message/{id}/update-read',
summary: '消息状态标记',
tags: ['消息管理'],
)]
#[OA\Parameter(
name: 'id',
description: '消息ID',
in: 'path',
required: true,
schema: new OA\Schema(
type: 'string',
default: 0,
example: '123456789012345678',
)
)]
#[RequestBody(required: true, content: new OA\JsonContent(
required: ['status'],
properties: [
new OA\Property(
property: 'status',
description: '消息状态(0=未读,1=已读)',
type: 'integer',
enum: [0, 1],
example: 1
),
]
))]
#[Permission(code: 'network:message:update_read')]
#[SimpleResponse(example: ['code' => 0, 'message' => 'success', 'data' => []])]
public function updateRead(Request $request): \support\Response
{
try {
$data = $request->all();
if ($this->validate) {
$this->validate->scene('update-read')->check($data);
}
$actionMap = [
MessageStatus::READ->value => 'isRead',
MessageStatus::UNREAD->value => 'isUnread',
];
if (!isset($actionMap[$data['status']])) {
throw new \InvalidArgumentException('Invalid message status');
}
$this->service->{$actionMap[$data['status']]}(
$data['id'] ?? throw new \InvalidArgumentException('Missing message ID')
);
return Json::success('ok');
} catch (\Throwable $e) {
// 统一异常处理(生产环境建议记录日志)
return Json::fail($e->getMessage());
}
}
#[OA\Post(
path: '/message/notify-on-first-login-to-all',
summary: '推送广播&消息',
tags: ['消息管理'],
)]
#[Permission(code: 'network:message:push')]
#[AllowAnonymous(requireToken: true,requirePermission: false)]
#[SimpleResponse(example: ['code' => 0, 'message' => 'success', 'data' => []])]
public function notifyOnFirstLoginToAll(Request $request): \support\Response
{
try {
$id = Container::make(CurrentUser::class)->id();
$this->service->notifyOnFirstLoginToAll($id);
return Json::success('ok');
} catch (\Throwable $e) {
return Json::fail($e->getMessage());
}
}
}
@@ -0,0 +1,93 @@
<?php
declare(strict_types=1);
namespace app\adminapi\controller\monitor;
use app\adminapi\controller\Crud;
use app\adminapi\middleware\AccessTokenMiddleware;
use app\adminapi\middleware\OperationMiddleware;
use app\adminapi\middleware\PermissionMiddleware;
use app\service\admin\monitor\OnlineUserService;
use core\tool\Json;
use madong\swagger\annotation\response\SimpleResponse;
use madong\swagger\attribute\Permission;
use OpenApi\Attributes as OA;
use support\Container;
use support\Request;
use support\annotation\Middleware;
#[Middleware(AccessTokenMiddleware::class, PermissionMiddleware::class, OperationMiddleware::class)]
final class OnlineUserController extends Crud
{
#[OA\Get(
path: '/monitor/online-user',
summary: '在线用户列表',
tags: ['系统监控']
)]
#[Permission(code: 'monitor:online_user:list')]
#[SimpleResponse(schema: [], example: [])]
public function index(Request $request): \support\Response
{
try {
/** @var OnlineUserService $service */
$service = Container::get(OnlineUserService::class);
return Json::success('ok', $service->list($request->all()));
} catch (\Throwable $e) {
return Json::fail($e->getMessage(), [], $e->getCode());
}
}
#[OA\Delete(
path: '/monitor/online-user/{jti}',
summary: '踢下线',
tags: ['系统监控']
)]
#[OA\Parameter(
name: 'jti',
description: '会话JTI',
in: 'path',
required: true,
schema: new OA\Schema(type: 'string')
)]
#[Permission(code: 'monitor:online_user:kickout')]
#[SimpleResponse(schema: [], example: [])]
public function destroy(Request $request): \support\Response
{
try {
$jti = (string)$request->route->param('jti', '');
if ($jti === '') {
return Json::fail('会话JTI不能为空');
}
/** @var OnlineUserService $service */
$service = Container::get(OnlineUserService::class);
if (!$service->kickout($jti)) {
return Json::fail('会话不存在或已失效');
}
return Json::success('ok');
} catch (\Throwable $e) {
return Json::fail($e->getMessage(), [], $e->getCode());
}
}
#[OA\Post(
path: '/monitor/online-user/{jti}/revoke',
summary: '撤销Token',
tags: ['系统监控']
)]
#[OA\Parameter(
name: 'jti',
description: '会话JTI',
in: 'path',
required: true,
schema: new OA\Schema(type: 'string')
)]
#[Permission(code: 'monitor:online_user:revoke')]
#[SimpleResponse(schema: [], example: [])]
public function revoke(Request $request): \support\Response
{
return $this->destroy($request);
}
}
@@ -0,0 +1,53 @@
<?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\adminapi\controller\monitor;
use app\adminapi\controller\Crud;
use app\adminapi\middleware\AccessTokenMiddleware;
use app\adminapi\middleware\OperationMiddleware;
use app\adminapi\middleware\PermissionMiddleware;
use app\adminapi\schema\response\monitor\RedisMonitorResponse;
use core\monitor\ServerMonitor;
use core\tool\Json;
use madong\swagger\annotation\response\SimpleResponse;
use madong\swagger\attribute\Permission;
use OpenApi\Attributes as OA;
use support\Container;
use support\Request;
use support\annotation\Middleware;
#[Middleware(AccessTokenMiddleware::class, PermissionMiddleware::class, OperationMiddleware::class)]
final class RedisController extends Crud
{
#[OA\Get(
path: '/monitor/redis',
summary: 'Redis监控',
tags: ['系统监控']
)]
#[Permission(code: 'monitor:redis:read')]
#[SimpleResponse(example: ['code' => 0, 'message' => 'success', 'data' => RedisMonitorResponse::class])]
public function index(Request $request): \support\Response
{
try {
/** @var ServerMonitor $service */
$service = Container::get(ServerMonitor::class);
$data = $service->getRedisInfo();
return Json::success('ok', $data);
} catch (\Exception $e) {
return Json::fail($e->getMessage(), [], $e->getCode());
}
}
}
@@ -0,0 +1,59 @@
<?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\adminapi\controller\monitor;
use app\adminapi\controller\Crud;
use app\adminapi\middleware\AccessTokenMiddleware;
use app\adminapi\middleware\OperationMiddleware;
use app\adminapi\middleware\PermissionMiddleware;
use app\adminapi\schema\response\monitor\ServerMonitorResponse;
use core\monitor\ServerMonitor;
use core\tool\Json;
use madong\swagger\annotation\response\SimpleResponse;
use madong\swagger\attribute\Permission;
use OpenApi\Attributes as OA;
use support\Container;
use support\Request;
use support\annotation\Middleware;
#[Middleware(AccessTokenMiddleware::class, PermissionMiddleware::class, OperationMiddleware::class)]
final class ServerController extends Crud
{
#[OA\Get(
path: '/monitor/server',
summary: '性能监控',
tags: ['系统监控']
)]
#[Permission(code: 'monitor:server:read')]
#[SimpleResponse(schema:ServerMonitorResponse::class, example: ['code' => 0, 'message' => 'success', 'data' => []])]
public function index(Request $request): \support\Response
{
try {
/** @var ServerMonitor $service */
$service = Container::get(ServerMonitor::class);
$serverInfo = [
'cpu' => $service->getCpuInfo(),
'memory' => $service->getMemoryInfo(),
'disk' => $service->getDiskInfo(),
'php' => $service->getPhpInfo(),
];
return Json::success('ok', $serverInfo);
} catch (\Exception $e) {
return Json::fail($e->getMessage(), [], $e->getCode());
}
}
}
@@ -0,0 +1,178 @@
<?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\adminapi\controller\notice;
use app\adminapi\controller\Crud;
use app\adminapi\middleware\AccessTokenMiddleware;
use app\adminapi\middleware\OperationMiddleware;
use app\adminapi\middleware\PermissionMiddleware;
use app\adminapi\schema\request\notice\NoticeFormRequest;
use app\adminapi\schema\request\notice\NoticeQueryRequest;
use app\adminapi\schema\response\system\NoticeResponse;
use app\adminapi\validate\system\NoticeValidate;
use app\schema\request\BatchDeleteRequest;
use app\schema\request\IdRequest;
use app\service\admin\notice\NoticeService;
use core\tool\Json;
use madong\swagger\annotation\response\PageResponse;
use madong\swagger\annotation\response\SimpleResponse;
use madong\swagger\attribute\Permission;
use OpenApi\Attributes as OA;
use support\Request;
use support\annotation\Middleware;
use Webman\RedisQueue\Client;
use WebmanTech\Swagger\DTO\SchemaConstants;
#[Middleware(AccessTokenMiddleware::class, PermissionMiddleware::class, OperationMiddleware::class)]
final class NoticeController extends Crud
{
// 注入服务和验证器
public function __construct(NoticeService $service, NoticeValidate $validate)
{
$this->service = $service;
$this->validate = $validate;
}
#[OA\Get(
path: '/notice',
summary: '列表',
tags: ['公告管理'],
x: [
SchemaConstants::X_SCHEMA_REQUEST => NoticeQueryRequest::class,
]
)]
#[Permission(code: 'network:notice:list')]
#[PageResponse(schema:NoticeResponse::class,example: [])]
public function index(Request $request): \support\Response
{
return parent::index($request); // 调用父类Crud的列表方法
}
#[OA\Get(
path: '/notice/{id}',
summary: '详情',
tags: ['公告管理'],
x: [
SchemaConstants::X_PROPERTY_IN => 'id',
SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class,
]
)]
#[Permission(code: 'network:notice:read')]
#[SimpleResponse(schema: NoticeResponse::class, example: ['code' => 0, 'message' => 'success', 'data' => []])] // 详情响应
public function show(Request $request): \support\Response
{
return parent::show($request); // 调用父类Crud的详情方法
}
#[OA\Post(
path: '/notice',
summary: '创建',
tags: ['公告管理'],
x: [
SchemaConstants::X_SCHEMA_REQUEST => NoticeFormRequest::class,
]
)]
#[Permission(code: 'network:notice:create')]
#[SimpleResponse(example: ['code' => 0, 'message' => 'success', 'data' => []])]
public function store(Request $request): \support\Response
{
return parent::store($request);
}
// 4. 更新公告
#[OA\Put(
path: '/notice/{id}',
summary: '更新',
tags: ['公告管理'],
x: [
SchemaConstants::X_SCHEMA_REQUEST => NoticeFormRequest::class,
]
)]
#[OA\Parameter(
name: 'id',
description: 'ID',
in: 'path',
required: true,
schema: new OA\Schema(type: 'string', example: '123456789012345678')
)]
#[Permission(code: 'network:notice:update')]
#[SimpleResponse(example: ['code' => 0, 'message' => 'success', 'data' => []])] // 更新响应
public function update(Request $request): \support\Response
{
return parent::update($request); // 调用父类Crud的更新方法
}
#[OA\Post(
path: '/notice/export',
summary: '导出',
tags: ['公告管理']
)]
#[Permission(code: 'network:notice:list', description: '导出')]
#[SimpleResponse(schema: [], example: [])]
public function export(Request $request): \support\Response
{
return parent::export($request);
}
#[OA\Delete(
path: '/notice/{id}',
summary: '删除',
tags: ['公告管理'],
x: [
SchemaConstants::X_PROPERTY_IN => 'id',
SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class,
]
)]
#[Permission(code: 'network:notice:delete')]
#[SimpleResponse(example: ['code' => 0, 'message' => 'success', 'data' => []])] // 删除响应
public function destroy(Request $request): \support\Response
{
return parent::destroy($request); // 调用父类Crud的删除方法
}
#[OA\Delete(
path: '/notice',
summary: '批量删除',
tags: ['公告管理'],
x: [
SchemaConstants::X_SCHEMA_REQUEST => BatchDeleteRequest::class,
]
)]
#[Permission(code: 'network:notice:delete')]
#[SimpleResponse(example: ['code' => 0, 'message' => 'success', 'data' => []])]
public function batchDelete(Request $request): \support\Response
{
return parent::destroy($request);
}
#[OA\Post(
path: '/notice/publish',
summary: '发布',
tags: ['公告管理'],
)]
#[Permission(code: 'network:notice:publish')]
#[SimpleResponse(example: ['code' => 0, 'message' => 'success', 'data' => []])]
public function publish(Request $request): \support\Response
{
try {
$id = $request->input('id', null);
$model = $this->service->get($id);
$queue = 'admin-announcement-push';
Client::send($queue, $model->makeVisible('tenant_id')->toArray(), 0);
return Json::success('ok', []);
} catch (\Throwable $e) {
return Json::fail($e->getMessage());
}
}
}
@@ -0,0 +1,159 @@
<?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\adminapi\controller\org;
use app\adminapi\controller\Crud;
use app\adminapi\middleware\AccessTokenMiddleware;
use app\adminapi\middleware\OperationMiddleware;
use app\adminapi\middleware\PermissionMiddleware;
use app\adminapi\schema\request\org\DeptFormRequest;
use app\adminapi\schema\request\org\DeptQueryRequest;
use app\adminapi\schema\response\system\DeptResponse;
use app\adminapi\validate\system\DeptValidate;
use app\schema\request\BatchDeleteRequest;
use app\schema\request\IdRequest;
use app\service\admin\org\DeptService;
use core\exception\handler\AdminException;
use core\tool\Json;
use madong\swagger\annotation\response\PageResponse;
use madong\swagger\annotation\response\SimpleResponse;
use madong\swagger\attribute\Permission;
use OpenApi\Attributes as OA;
use OpenApi\Attributes\RequestBody;
use support\Request;
use support\annotation\Middleware;
use WebmanTech\Swagger\DTO\SchemaConstants;
#[Middleware(AccessTokenMiddleware::class, PermissionMiddleware::class, OperationMiddleware::class)]
final class DeptController extends Crud
{
public function __construct(DeptService $service, DeptValidate $validate)
{
$this->service = $service;
$this->validate = $validate;
}
#[OA\Get(
path: '/org/dept',
summary: '列表',
tags: ['部门管理'],
x: [SchemaConstants::X_SCHEMA_REQUEST => DeptQueryRequest::class]
)]
#[Permission(code: 'org:dept:list')]
#[PageResponse(DeptResponse::class)]
public function index(Request $request): \support\Response
{
return parent::index($request);
}
#[OA\Post(
path: '/org/dept',
summary: '创建',
tags: ['部门管理'],
x: [
SchemaConstants::X_SCHEMA_REQUEST => DeptFormRequest::class,
]
)]
#[Permission(code: 'org:dept:create')]
#[SimpleResponse(DeptResponse::class, example: '{"code":0,"msg":"created","id":1}')]
public function store(Request $request): \support\Response
{
try {
$data = $this->inputFilter($request->all(), ['leader_id_list']);
if (isset($this->validate) && $this->validate) {
if (!$this->validate->scene('store')->check($data)) {
throw new \Exception($this->validate->getError());
}
}
$model = $this->service->save($data);
if (empty($model)) {
throw new AdminException('插入失败');
}
$pk = $model->getPk();
return Json::success('ok', [$pk => $model->getData($pk)]);
} catch (\Throwable $e) {
return Json::fail($e->getMessage());
}
}
#[OA\Delete(
path: '/org/dept/{id}',
summary: '删除',
tags: ['部门管理'],
x: [
SchemaConstants::X_PROPERTY_IN => 'id',
SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class,
]
)]
#[Permission(code: 'org:dept:delete')]
#[SimpleResponse( example: '{"code":0,"msg":"success","data":null}')]
public function destroy(Request $request): \support\Response
{
return parent::destroy($request);
}
#[OA\Delete(
path: '/org/dept',
summary: '批量删除',
tags: ['部门管理'],
x: [SchemaConstants::X_SCHEMA_REQUEST => BatchDeleteRequest::class]
)]
#[Permission(code: 'org:dept:delete')]
#[SimpleResponse(example: '{"code":0,"msg":"success","data":null}')]
public function batchDelete(Request $request): \support\Response
{
return parent::destroy($request);
}
#[OA\Put(
path: '/org/dept/{id}',
summary: '更新',
tags: ['部门管理'],
)]
#[Permission(code: 'org:dept:update')]
#[SimpleResponse( example: '{"code":0,"msg":"success","data":null}')]
public function update(Request $request): \support\Response
{
try {
$id = $request->route->param('id');
$data = $this->inputFilter($request->all(), ['leader_id_list']);
if (isset($this->validate) && $this->validate) {
if (!$this->validate->scene('update')->check($data)) {
throw new \Exception($this->validate->getError());
}
}
$this->service->update($id, $data);
return Json::success('ok');
} catch (\Throwable $e) {
return Json::fail($e->getMessage());
}
}
#[OA\Get(
path: '/org/dept/{id}',
summary: '详情',
tags: ['部门管理'],
x: [
SchemaConstants::X_PROPERTY_IN => 'id',
SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class,
]
)]
#[Permission(code: 'org:dept:read')]
#[SimpleResponse(DeptResponse::class, example: '{"code":0,"msg":"success","data":null}')]
public function show(Request $request): \support\Response
{
return parent::show($request);
}
}
@@ -0,0 +1,177 @@
<?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\adminapi\controller\org;
use app\adminapi\controller\Crud;
use app\adminapi\middleware\AccessTokenMiddleware;
use app\adminapi\middleware\OperationMiddleware;
use app\adminapi\middleware\PermissionMiddleware;
use app\adminapi\schema\request\org\PostFormRequest;
use app\adminapi\schema\request\org\PostQueryRequest;
use app\adminapi\schema\response\system\PostResponse;
use app\adminapi\validate\system\PostValidate;
use app\schema\request\BatchDeleteRequest;
use app\schema\request\IdRequest;
use app\service\admin\org\PostService;
use core\exception\handler\AdminException;
use core\tool\Json;
use madong\swagger\annotation\response\PageResponse;
use madong\swagger\annotation\response\SimpleResponse;
use madong\swagger\attribute\Permission;
use OpenApi\Attributes as OA;
use support\Request;
use support\annotation\Middleware;
use WebmanTech\Swagger\DTO\SchemaConstants;
#[Middleware(AccessTokenMiddleware::class, PermissionMiddleware::class, OperationMiddleware::class)]
final class PostController extends Crud
{
public function __construct(PostService $service, PostValidate $validate)
{
$this->service = $service;
$this->validate = $validate;
}
#[OA\Get(
path: '/org/post',
summary: '列表',
tags: ['岗位管理'],
x: [
SchemaConstants::X_SCHEMA_REQUEST => PostQueryRequest::class,
]
)]
#[Permission(code: 'org:post:list')]
#[PageResponse(PostResponse::class, example: '{"code":0,"msg":"success","data":[]}')] // 分页响应关联岗位Schema
public function index(Request $request): \support\Response
{
try {
[$where, $format, $limit, $field, $order, $page] = $this->selectInput($request);
$methods = [
'select' => 'formatSelect',
'tree' => 'formatTree',
'table_tree' => 'formatTableTree',
'normal' => 'formatNormal',
];
$format_function = $methods[$format] ?? 'formatNormal';
$total = $this->service->getCount($where);
$list = $this->service->selectList($where, $field, $page, $limit, $order, ['dept']);
return call_user_func([$this, $format_function], $list, $total);
} catch (\Throwable $e) {
return Json::fail($e->getMessage());
}
}
#[OA\Get(
path: '/org/post/{id}',
summary: '详情',
tags: ['岗位管理'],
x: [
SchemaConstants::X_PROPERTY_IN => 'id',
SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class,
]
)]
#[Permission(code: 'org:post:read')]
#[SimpleResponse(PostResponse::class, example: [])] // 详情响应
public function show(Request $request): \support\Response
{
try {
$id = $request->route->param('id');
$data = $this->service->get($id,['*'],['dept']);
if (empty($data)) {
throw new AdminException('数据未找到');
}
return Json::success('ok', $data->toArray());
} catch (\Throwable $e) {
return Json::fail($e->getMessage());
}
}
#[OA\Post(
path: '/org/post',
summary: '创建',
tags: ['岗位管理'],
x: [
SchemaConstants::X_SCHEMA_REQUEST => PostFormRequest::class,
]
)]
#[Permission(code: 'org:post:create')]
#[SimpleResponse(example: ['code' => 0, 'message' => 'success', 'data' => []])]
public function store(Request $request): \support\Response
{
return parent::store($request);
}
#[OA\Put(
path: '/org/post/{id}',
summary: '更新',
tags: ['岗位管理'],
x: [
SchemaConstants::X_PROPERTY_IN => 'id',
SchemaConstants::X_SCHEMA_REQUEST => PostFormRequest::class,
]
)]
#[Permission(code: 'org:post:update')]
#[SimpleResponse(example: ['code' => 0, 'message' => 'success', 'data' => []])]
public function update(Request $request): \support\Response
{
return parent::update($request);
}
#[OA\Delete(
path: '/org/post/{id}',
summary: '删除',
tags: ['岗位管理'],
x: [
SchemaConstants::X_PROPERTY_IN => 'id',
SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class,
]
)]
#[OA\Parameter(
name: 'id',
description: 'ID',
in: 'path',
required: true,
schema: new OA\Schema(type: 'string', example: '123456789012345678')
)]
#[Permission(code: 'org:post:delete')]
#[SimpleResponse(example: ['code' => 0, 'message' => 'success', 'data' => []])]
public function destroy(Request $request): \support\Response
{
return parent::destroy($request);
}
#[OA\Delete(
path: '/org/post',
summary: '批量删除',
tags: ['岗位管理'],
x: [
SchemaConstants::X_SCHEMA_REQUEST => BatchDeleteRequest::class,
]
)]
#[OA\Parameter(
name: 'id',
description: 'ID',
in: 'path',
required: true,
schema: new OA\Schema(type: 'string', example: '123456789012345678')
)]
#[Permission(code: 'org:post:delete')]
#[SimpleResponse(example: ['code' => 0, 'message' => 'success', 'data' => []])]
public function batchDelete(Request $request): \support\Response
{
return parent::destroy($request);
}
}
@@ -0,0 +1,158 @@
<?php
declare(strict_types=1);
namespace app\adminapi\controller\plugin;
use app\adminapi\controller\Crud;
use app\adminapi\middleware\AccessTokenMiddleware;
use app\adminapi\middleware\OperationMiddleware;
use app\adminapi\middleware\PermissionMiddleware;
use app\adminapi\schema\request\plugin\ApplicationAuthFormRequest;
use app\adminapi\schema\request\plugin\ApplicationAuthQueryRequest;
use app\adminapi\schema\response\plugin\ApplicationAuthResponse;
use app\adminapi\validate\plugin\ApplicationAuthValidate;
use app\schema\request\BatchDeleteRequest;
use app\schema\request\IdRequest;
use app\service\admin\plugin\ApplicationAuthService;
use core\tool\Json;
use madong\swagger\annotation\response\PageResponse;
use madong\swagger\annotation\response\SimpleResponse;
use madong\swagger\attribute\Permission;
use OpenApi\Attributes as OA;
use support\annotation\Middleware;
use support\Request;
use WebmanTech\Swagger\DTO\SchemaConstants;
#[Middleware(AccessTokenMiddleware::class, PermissionMiddleware::class, OperationMiddleware::class)]
final class ApplicationAuthController extends Crud
{
public function __construct(ApplicationAuthService $service, ApplicationAuthValidate $validate)
{
$this->service = $service;
$this->validate = $validate;
}
#[OA\Get(
path: '/plugin/application-auth',
summary: '列表',
tags: ['应用授权'],
x: [SchemaConstants::X_SCHEMA_REQUEST => ApplicationAuthQueryRequest::class]
)]
#[Permission(code: 'plugin:application-auth:list')]
#[PageResponse(schema: ApplicationAuthResponse::class, example: [])]
public function index(Request $request): \support\Response
{
return parent::index($request);
}
#[OA\Get(
path: '/plugin/application-auth/{id}',
summary: '详情',
tags: ['应用授权'],
x: [
SchemaConstants::X_PROPERTY_IN => 'id',
SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class,
]
)]
#[Permission(code: 'plugin:application-auth:read')]
#[SimpleResponse(schema: ApplicationAuthResponse::class, example: [])]
public function show(Request $request): \support\Response
{
return parent::show($request);
}
#[OA\Post(
path: '/plugin/application-auth',
summary: '创建',
tags: ['应用授权'],
x: [SchemaConstants::X_SCHEMA_REQUEST => ApplicationAuthFormRequest::class]
)]
#[Permission(code: 'plugin:application-auth:create')]
#[SimpleResponse(example: ['code' => 0, 'message' => 'success', 'data' => []])]
public function store(Request $request): \support\Response
{
return parent::store($request);
}
#[OA\Put(
path: '/plugin/application-auth/{id}',
summary: '更新',
tags: ['应用授权'],
x: [SchemaConstants::X_SCHEMA_REQUEST => ApplicationAuthFormRequest::class]
)]
#[OA\Parameter(
name: 'id',
description: 'ID',
in: 'path',
required: true,
schema: new OA\Schema(type: 'string', example: '123456789012345678')
)]
#[Permission(code: 'plugin:application-auth:update')]
#[SimpleResponse(example: ['code' => 0, 'message' => 'success', 'data' => []])]
public function update(Request $request): \support\Response
{
return parent::update($request);
}
#[OA\Post(
path: '/plugin/application-auth/export',
summary: '导出',
tags: ['应用授权']
)]
#[Permission(code: 'plugin:application-auth:list', description: '导出')]
#[SimpleResponse(schema: [], example: [])]
public function export(Request $request): \support\Response
{
return parent::export($request);
}
#[OA\Delete(
path: '/plugin/application-auth/{id}',
summary: '删除',
tags: ['应用授权'],
x: [
SchemaConstants::X_PROPERTY_IN => 'id',
SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class,
]
)]
#[Permission(code: 'plugin:application-auth:delete')]
#[SimpleResponse(example: ['code' => 0, 'message' => 'success', 'data' => []])]
public function destroy(Request $request): \support\Response
{
return parent::destroy($request);
}
#[OA\Delete(
path: '/plugin/application-auth',
summary: '批量删除',
tags: ['应用授权'],
x: [SchemaConstants::X_SCHEMA_REQUEST => BatchDeleteRequest::class]
)]
#[Permission(code: 'plugin:application-auth:delete')]
#[SimpleResponse(example: ['code' => 0, 'message' => 'success', 'data' => []])]
public function batchDelete(Request $request): \support\Response
{
return parent::destroy($request);
}
#[OA\Post(
path: '/plugin/application-auth/{id}/reset-secret',
summary: '重置密钥',
tags: ['应用授权'],
x: [
SchemaConstants::X_PROPERTY_IN => 'id',
SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class,
]
)]
#[Permission(code: 'plugin:application-auth:reset-secret')]
#[SimpleResponse(example: ['code' => 0, 'message' => 'success', 'data' => ['id' => '123456789012345678', 'secret_key' => 'SK...']])]
public function resetSecret(Request $request): \support\Response
{
try {
$id = $request->route->param('id');
return Json::success('ok', $this->service->resetSecret($id));
} catch (\Throwable $e) {
return Json::fail($e->getMessage());
}
}
}
@@ -0,0 +1,482 @@
<?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\adminapi\controller\plugin;
use app\adminapi\controller\Base;
use app\adminapi\middleware\AccessTokenMiddleware;
use app\adminapi\middleware\OperationMiddleware;
use app\adminapi\middleware\PermissionMiddleware;
use app\service\core\plugin\PluginDownloadService;
use app\service\core\plugin\PluginInstallService;
use app\service\core\plugin\PluginRemoteService;
use app\service\core\plugin\PluginUninstallService;
use app\service\core\plugin\PluginService;
use core\tool\Sse;
use madong\swagger\annotation\response\PageResponse;
use madong\swagger\attribute\Permission;
use core\tool\Json;
use madong\swagger\annotation\response\SimpleResponse;
use OpenApi\Attributes as OA;
use support\annotation\Middleware;
use support\Container;
use support\Request;
use support\Response;
#[Middleware(AccessTokenMiddleware::class, PermissionMiddleware::class, OperationMiddleware::class)]
final class PluginController extends Base
{
#[OA\Get(
path: '/plugin',
summary: '列表',
tags: ['模块市场'],
parameters: [
new OA\Parameter(
name: 'page',
description: '页码',
in: 'query',
required: false,
schema: new OA\Schema(type: 'integer', default: 1)
),
new OA\Parameter(
name: 'limit',
description: '每页数量',
in: 'query',
required: false,
schema: new OA\Schema(type: 'integer', default: 10)
),
new OA\Parameter(
name: 'type',
description: '模块类型(module: 系统模块, plugin: 插件)',
in: 'query',
required: false,
schema: new OA\Schema(type: 'string', enum: ['module', 'plugin'])
),
new OA\Parameter(
name: 'status',
description: '状态(0:禁用,1:启用)',
in: 'query',
required: false,
schema: new OA\Schema(type: 'integer', enum: [0, 1])
),
new OA\Parameter(
name: 'keyword',
description: '搜索关键词',
in: 'query',
required: false,
schema: new OA\Schema(type: 'string')
),
new OA\Parameter(
name: 'installed',
description: '是否已安装(0:未安装,1:已安装)',
in: 'query',
required: false,
schema: new OA\Schema(type: 'integer', enum: [0, 1])
),
],
)]
#[PageResponse(schema: [], example: [[
'id' => 3,
'code' => 'official',
'name' => 'official',
'type' => 'madong',
'version' => '1.0.0',
'status' => 1,
'description' => '官方应用插件',
'detail_description' => '',
'author' => 'Madong',
'cover' => '',
'poster' => '',
'price' => 0,
'downloads' => 0,
'rating' => 0,
'created_at' => '2026-03-19 16:26:19',
'updated_at' => '2026-03-19 16:26:19',
'update_time' => '',
'update_logs' => [],
'category' => null,
'category_name' => '',
'tags' => [],
'is_new' => 0,
'is_hot' => 0,
'purchased' => 0,
'installed' => 0,
'manual_uninstall' => 0,
'composer_dependencies' => [],
'npm_dependencies' => [],
'is_installed' => 1,
'installed_version' => '1.0.0',
'has_update' => 0,
'is_purchased' => 1,
'is_downloaded' => 1,
'can_uninstall' => 1,
'can_download' => 1,
'undeletable' => 1,
'version_info' => [
'remote' => [
'version' => '1.0.0',
'release_date' => '2026-03-19 16:26:19',
],
'local' => [
'version' => '1.0.0',
'install_date' => '2026-03-19 16:26:19',
],
'comparison' => [
'needs_update' => 0,
'is_latest' => 1,
'version_difference' => null,
],
],
]])]
#[Permission(code: 'plugin:module:list')]
public function index(Request $request): \support\Response
{
try {
$category = $request->input('category', null);
$pageInput = $request->input('current', $request->input('page', 1));
$limitInput = $request->input('size', $request->input('limit', 15));
$page = max(1, (int)$pageInput);
$limit = max(1, (int)$limitInput);
$type = $request->input('type');
$keyword = $request->input('keyword');
/** @var PluginService $service */
$service = Container::make(PluginService::class);
$result = $service->getList($category, $type, $keyword, $page, $limit);
return Json::success('common.operation.success', $result);
} catch (\Throwable $e) {
return Json::fail($e->getMessage(), []);
}
}
#[OA\Get(
path: '/plugin/{name}/download',
summary: '下载模块',
tags: ['模块市场'],
)]
#[OA\Parameter(
name: 'name',
description: '模块标识',
in: 'path',
required: true,
schema: new OA\Schema(type: 'string')
)]
#[SimpleResponse(schema: [], example: [])]
public function download(Request $request): \support\Response
{
try {
$pluginCode = $request->route->param('name');
// 1. 获取下载链接
$remoteService = Container::make(PluginRemoteService::class);
$authCode = config('madong.auth_code');
$secretKey = config('madong.auth_secret');
$downloadInfo = $remoteService->getRemoteDownloadService($authCode, $secretKey, $pluginCode, 'latest');
if (empty($downloadInfo['url'])) {
return Json::fail('获取下载链接失败');
}
// 2. 下载并解压插件
$downloadService = Container::make(PluginDownloadService::class);
$result = $downloadService->downloadAndExtract($pluginCode, $downloadInfo['url']);
return Json::success('下载成功', $result);
} catch (\Throwable $e) {
return Json::fail($e->getMessage());
}
}
#[OA\Get(
path: '/plugin/detail/{name}',
summary: '获取模块详情',
tags: ['模块市场'],
)]
#[OA\Parameter(
name: 'name',
description: '模块标识',
in: 'path',
required: true,
schema: new OA\Schema(type: 'string')
)]
#[SimpleResponse(schema: [], example: [])]
public function detail(Request $request): \support\Response
{
try {
$moduleName = $request->route->param('name');
/** @var PluginService $service */
$service = Container::make(PluginService::class);
$result = $service->getList(null, null, $moduleName);
return Json::success('common.operation.success', $result['items'][0]);
} catch (\Throwable $e) {
return Json::fail($e->getMessage(), []);
}
}
#[OA\Get(
path: '/plugin/{name}/upgrade-logs',
summary: '获取模块升级日志',
tags: ['模块市场'],
)]
#[OA\Parameter(
name: 'name',
description: '模块标识',
in: 'path',
required: true,
schema: new OA\Schema(type: 'string')
)]
#[SimpleResponse(schema: [], example: [])]
public function upgradeLogs(Request $request): \support\Response
{
try {
$moduleName = $request->route->param('name');
/** @var PluginService $service */
$service = Container::make(PluginService::class);
$result = $service->getUpgradeLogs($moduleName);
return Json::success('common.operation.success', $result);
} catch (\Throwable $e) {
return Json::fail($e->getMessage(), []);
}
}
#[OA\Get(
path: '/plugin/check-environment',
summary: '环境检测',
tags: ['模块市场'],
)]
#[OA\Parameter(
name: 'name',
description: '模块标识',
in: 'query',
required: true,
schema: new OA\Schema(type: 'string')
)]
#[SimpleResponse(schema: [], example: [])]
public function checkEnvironment(Request $request): \support\Response
{
try {
$name = $request->input('name');
/** @var PluginInstallService $service */
$service = Container::make(PluginInstallService::class);
$result = $service->installCheck($name);
// 提取路径列表和状态
$paths = [];
foreach ($result['checks'] as $check) {
if (isset($check['path'])) {
// 只展示从madong开始的路径
$path = $check['path'];
$madongPos = strpos(strtolower($path), 'madong');
if ($madongPos !== false) {
// 截取从madong开始的路径
$path = substr($path, $madongPos);
// 确保路径格式正确,madong后面应该有目录分隔符
if (substr($path, 6, 1) !== DIRECTORY_SEPARATOR) {
$path = 'madong' . DIRECTORY_SEPARATOR . substr($path, 6);
}
}
$paths[] = [
'path' => $path,
'requirement' => $check['permission_type'] ?? 'readable',
'status' => $check['status'] === 'success' ? 'success' : 'error',
];
}
}
return Json::success('common.operation.success', [
'paths' => $paths,
'overall_status' => $result['overall_status'],
'message' => $result['message'],
]);
} catch (\Throwable $e) {
return Json::fail($e->getMessage(), []);
}
}
#[OA\Get(
path: '/plugin/install',
summary: '插件安装',
tags: ['模块市场'],
)]
#[SimpleResponse(schema: [], example: [])]
public function install(Request $request): void
{
$connection = $request->connection;
$code = $request->input('code');
$mode = $request->input('mode', 'local');
// 验证参数
if (empty($code)) {
$connection->send(new Response(200, [
'Content-Type' => 'text/event-stream',
'Cache-Control' => 'no-cache',
'Connection' => 'keep-alive',
'Access-Control-Allow-Origin' => '*',
'Access-Control-Allow-Credentials' => 'true',
'Access-Control-Expose-Headers' => 'Content-Type',
], "\r\n"));
$connection->send(\core\tool\Sse::error('参数错误:缺少code参数'));
return;
}
// 发送SSE响应头
$connection->send(new Response(200, [
'Content-Type' => 'text/event-stream',
'Cache-Control' => 'no-cache',
'Connection' => 'keep-alive',
'Access-Control-Allow-Origin' => '*',
'Access-Control-Allow-Credentials' => 'true',
'Access-Control-Expose-Headers' => 'Content-Type',
], "\r\n"));
try {
// 发送开始安装消息
$connection->send(Sse::progress('开始安装插件 ' . $code, 0));
// 调用安装服务
$generator = (new PluginInstallService())->install($code, $mode);
foreach ($generator as $chunk) {
$connection->send($chunk);
}
} catch (\Exception $e) {
// 发送错误消息
$connection->send(Sse::error('安装失败:' . $e->getMessage()));
}
}
#[OA\Get(
path: '/plugin/uninstall',
summary: '插件卸载(SSE流式)',
tags: ['模块市场'],
)]
#[OA\Parameter(
name: 'code',
description: '插件编码',
in: 'query',
required: true,
schema: new OA\Schema(type: 'string')
)]
#[SimpleResponse(schema: [], example: [])]
public function uninstall(Request $request): void
{
$connection = $request->connection;
$code = $request->input('code');
// 验证参数
if (empty($code)) {
$connection->send(new Response(200, [
'Content-Type' => 'text/event-stream',
'Cache-Control' => 'no-cache',
'Connection' => 'keep-alive',
'Access-Control-Allow-Origin' => '*',
'Access-Control-Allow-Credentials' => 'true',
'Access-Control-Expose-Headers' => 'Content-Type',
], "\r\n"));
$connection->send(Sse::error('参数错误:缺少code参数'));
return;
}
// 发送SSE响应头
$connection->send(new Response(200, [
'Content-Type' => 'text/event-stream',
'Cache-Control' => 'no-cache',
'Connection' => 'keep-alive',
'Access-Control-Allow-Origin' => '*',
'Access-Control-Allow-Credentials' => 'true',
'Access-Control-Expose-Headers' => 'Content-Type',
], "\r\n"));
try {
// 调用卸载服务(统一入口)
$generator = (new PluginUninstallService())->uninstall($code);
foreach ($generator as $chunk) {
$connection->send($chunk);
}
} catch (\Exception $e) {
// 发送错误消息
$connection->send(Sse::error('卸载失败:' . $e->getMessage()));
}
}
#[OA\Delete(
path: '/plugin/{name}',
summary: '插件删除',
tags: ['模块市场'],
)]
#[OA\Parameter(
name: 'name',
description: '插件名称',
in: 'path',
required: true,
schema: new OA\Schema(type: 'string')
)]
#[Permission(code: 'plugin:delete')]
#[SimpleResponse(schema: [], example: [])]
public function destroy(Request $request): \support\Response
{
try {
$name = $request->route->param('name');
if (empty($name)) {
return Json::fail('参数错误:缺少name参数', []);
}
/** @var PluginUninstallService $service */
$service = Container::get(PluginUninstallService::class);
$service->delete($name);
return Json::success('common.operation.delete.success', []);
} catch (\Throwable $e) {
return Json::fail($e->getMessage());
}
}
#[OA\Get(
path: '/plugin/select',
summary: '获取已安装插件列表(用于下拉框)',
tags: ['模块市场'],
)]
#[SimpleResponse(schema: [], example: [])]
public function select(Request $request): \support\Response
{
try {
/** @var PluginService $service */
$service = Container::make(PluginService::class);
// 获取本地插件列表(包括已安装和未安装的)
$localModules = $service->getLocalModules();
// 过滤出已安装的插件
$installedPlugins = array_filter($localModules, function ($module) {
return $module['is_installed'] === true && $module['status'] == 1;
});
// 构建下拉框格式数据
$selectItems = [];
foreach ($installedPlugins as $plugin) {
$selectItems[] = [
'value' => $plugin['name'],
'label' => $plugin['name'] . ' - ' . $plugin['description'],
'name' => $plugin['name'],
'description' => $plugin['description'],
'version' => $plugin['version'],
'type' => $plugin['type'] ?? 'plugin',
];
}
return Json::success('common.operation.success', array_values($selectItems));
} catch (\Throwable $e) {
return Json::fail($e->getMessage(), []);
}
}
}
@@ -0,0 +1,301 @@
<?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\adminapi\controller\plugin;
use app\adminapi\controller\Crud;
use app\adminapi\middleware\AccessTokenMiddleware;
use app\adminapi\middleware\OperationMiddleware;
use app\adminapi\middleware\PermissionMiddleware;
use app\adminapi\validate\plugin\PluginValidate;
use app\service\admin\plugin\PluginDevelopService;
use madong\swagger\annotation\response\DataResponse;
use madong\swagger\annotation\response\PageResponse;
use madong\swagger\annotation\response\SimpleResponse;
use madong\swagger\attribute\Permission;
use core\tool\Json;
use OpenApi\Attributes as OA;
use support\annotation\Middleware;
use support\Request;
#[Middleware(AccessTokenMiddleware::class, PermissionMiddleware::class, OperationMiddleware::class)]
final class PluginDevelopController extends Crud
{
public function __construct(PluginDevelopService $service, PluginValidate $validate)
{
$this->service = $service;
$this->validate = $validate;
}
/**
* 插件列表
* 扫描插件目录,获取type为madong:前缀的插件,同步到数据库并返回列表
*
* @param Request $request
*
* @return \support\Response
*/
#[OA\Get(
path: '/plugin/develop',
summary: '插件列表',
tags: ['插件开发'],
parameters: [
new OA\Parameter(
name: 'page',
description: '页码',
in: 'query',
required: false,
schema: new OA\Schema(type: 'integer', default: 1)
),
new OA\Parameter(
name: 'limit',
description: '每页数量',
in: 'query',
required: false,
schema: new OA\Schema(type: 'integer', default: 15)
),
new OA\Parameter(
name: 'title',
description: '插件标题',
in: 'query',
required: false,
schema: new OA\Schema(type: 'string')
),
new OA\Parameter(
name: 'author',
description: '作者',
in: 'query',
required: false,
schema: new OA\Schema(type: 'string')
),
new OA\Parameter(
name: 'status',
description: '状态',
in: 'query',
required: false,
schema: new OA\Schema(type: 'integer')
),
]
)]
#[Permission(code: 'plugin:develop:list')]
#[PageResponse(schema: [], example: [[
"id" => "2032735331152958596",
"name" => "官网管理",
"icon" => null,
"code" => "official",
"description" => "官网管理插件,用于搭建和管理企业官方网站",
"enabled" => 1,
"author" => "Mr.April",
"version" => "1.0.0",
"cover" => null,
"type" => "madong:plugin",
"support_app" => "admin",
"installed_at" => null,
"created_at" => 1773476844,
"updated_at" => 1773476844,
"variables" => null,
"created_date" => "2026-03-14 16:27:24",
"updated_date" => "2026-03-14 16:27:24",
]])]
public function index(Request $request): \support\Response
{
try {
// 使用标准模式:通过 selectInput 处理查询参数
[$where, , $limit, , , $page] = $this->selectInput($request);
// 调用服务层获取列表数据
$result = $this->service->getList($where, $page, $limit);
// 使用标准格式返回
return Json::success($result);
} catch (\Exception $e) {
return Json::fail($e->getMessage());
}
}
#[OA\Get(
path: '/plugin/develop/{id}',
summary: '插件详情',
tags: ['插件开发'],
parameters: [
new OA\Parameter(
name: 'id',
description: '插件ID',
in: 'path',
required: true,
schema: new OA\Schema(type: 'integer')
),
]
)]
#[Permission(code: 'plugin:develop:read')]
#[DataResponse(schema: [], example: [
"id" => "2032735331152958596",
"title" => "官网管理",
"icon" => null,
"key" => "official",
"desc" => "官网管理插件,用于搭建和管理企业官方网站",
"status" => 1,
"author" => "Mr.April",
"version" => "1.0.0",
"cover" => null,
"type" => "madong:plugin",
"support_app" => "admin",
"installed_at" => null,
"created_at" => 1773476844,
"updated_at" => 1773476844,
"variables" => null,
"created_date" => "2026-03-14 16:27:24",
"updated_date" => "2026-03-14 16:27:24",
])]
public function show(Request $request): \support\Response
{
try {
$id = $request->route->param('id');
$plugin = $this->service->read($id);
if (!$plugin) {
return Json::fail('插件不存在');
}
return Json::success($plugin->toArray());
} catch (\Exception $e) {
return Json::fail($e->getMessage());
}
}
/**
* 创建插件
*
* @param Request $request
*
* @return \support\Response
* @throws \Throwable
*/
#[OA\Post(
path: '/plugin/develop',
summary: '创建插件',
requestBody: new OA\RequestBody(
required: true,
content: new OA\JsonContent(
required: ['title', 'key'],
properties: [
new OA\Property(property: 'title', description: '插件标题', type: 'string'),
new OA\Property(property: 'key', description: '插件标识(唯一值)', type: 'string'),
new OA\Property(property: 'desc', description: '插件描述', type: 'string'),
new OA\Property(property: 'author', description: '作者', type: 'string'),
new OA\Property(property: 'version', description: '版本', type: 'string', default: '1.0.0'),
new OA\Property(property: 'type', description: '类型', type: 'string', default: 'madong:plugin'),
new OA\Property(property: 'icon', description: '图标(base64)', type: 'string'),
new OA\Property(property: 'cover', description: '封面(base64)', type: 'string'),
new OA\Property(property: 'frontend_type', description: '前端类型', type: 'string', default: 'admin'),
]
)
),
tags: ['插件开发']
)]
#[Permission(code: 'plugin:develop:create')]
#[SimpleResponse(schema: [], example: [])]
public function store(Request $request): \support\Response
{
try {
$this->validate->scene('store')->check($request->all());
$this->service->store($request->all());
return Json::success();
} catch (\Exception $e) {
return Json::fail($e->getMessage());
}
}
/**
* 打包插件
* 将后端、后台前端、前台前端复制到后端server/plugin/xxx/resource目录,并打包为zip
*
* @param Request $request
*
* @return \support\Response
*/
#[OA\Post(
path: '/plugin/develop/build',
summary: '打包插件',
requestBody: new OA\RequestBody(
required: true,
content: new OA\JsonContent(
required: ['plugin_key'],
properties: [
new OA\Property(property: 'plugin_key', description: '插件标识', type: 'string'),
]
)
),
tags: ['插件开发']
)]
#[Permission(code: 'plugin:develop:build')]
#[SimpleResponse(schema: [], example: [
"plugin_key" => "test",
"zip_path" => "D:\\MyProject\\private\\madong/server/runtime/adminapi/test.zip",
"has_frontend" => false,
])]
public function build(Request $request): \support\Response
{
try {
$pluginKey = $request->input('plugin_key');
if (empty($pluginKey)) {
return Json::fail('插件标识不能为空');
}
$result = $this->service->buildPlugin($pluginKey);
return Json::success('插件打包成功',$result);
} catch (\Exception $e) {
return Json::fail($e->getMessage());
}
}
#[OA\Put(
path: '/plugin/develop/{id}',
summary: '编辑插件',
requestBody: new OA\RequestBody(
required: true,
content: new OA\JsonContent(
properties: [
new OA\Property(property: 'title', description: '插件标题', type: 'string'),
new OA\Property(property: 'desc', description: '插件描述', type: 'string'),
new OA\Property(property: 'author', description: '作者', type: 'string'),
new OA\Property(property: 'version', description: '版本', type: 'string'),
new OA\Property(property: 'icon', description: '图标(base64)', type: 'string'),
new OA\Property(property: 'cover', description: '封面(base64)', type: 'string'),
]
)
),
tags: ['插件开发'],
parameters: [
new OA\Parameter(
name: 'id',
description: '插件ID',
in: 'path',
required: true,
schema: new OA\Schema(type: 'integer')
),
]
)]
#[Permission(code: 'plugin:develop:update')]
#[SimpleResponse(schema: [], example: [])]
public function update(Request $request): \support\Response
{
try {
$id = $request->route->param('id');
$this->service->update($id, $request->all());
return Json::success('更新成功');
} catch (\Throwable $e) {
return Json::fail($e->getMessage());
}
}
}
@@ -0,0 +1,118 @@
<?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\adminapi\controller\plugin;
use app\adminapi\controller\Base;
use app\adminapi\middleware\AccessTokenMiddleware;
use app\adminapi\middleware\OperationMiddleware;
use app\adminapi\middleware\PermissionMiddleware;
use app\service\core\plugin\PluginRemoteService;
use core\tool\Json;
use madong\swagger\annotation\auth\AllowAnonymous;
use madong\swagger\annotation\response\SimpleResponse;
use madong\swagger\attribute\Permission;
use OpenApi\Attributes as OA;
use support\annotation\Middleware;
use support\Request;
#[Middleware(AccessTokenMiddleware::class, PermissionMiddleware::class, OperationMiddleware::class)]
final class PluginMadongAuthController extends Base
{
public function __construct(PluginRemoteService $service)
{
$this->service = $service;
}
#[OA\Get(
path: '/madong/auth-info',
summary: '获取插件授权信息',
tags: ['应用授权']
)]
#[Permission('madong:delegation:read')]
#[AllowAnonymous(requireToken: true, requirePermission: true)]
#[SimpleResponse(example: '{"code": 0,"msg": "ok","data": {"company_name": "-","domain": "-","auth_code": "202601***********888888"}}')]
public function read(Request $request): \support\Response
{
$data = [
'company_name' => '-',
'domain' => '-',
'auth_code' => config('madong.auth_code'),
];
return Json::success($data);
}
#[OA\Post(
path: '/madong/auth-info',
summary: '设置插件授权信息',
tags: ['应用授权']
)]
#[Permission('madong:delegation:setting')]
#[SimpleResponse(schema: [], example: [])]
public function store(Request $request): \support\Response
{
try {
$param = $request->all();
if (empty($param['auth_code'])) {
throw new \Exception('auth_code 不能为空');
}
if (empty($param['auth_secret'])) {
throw new \Exception('auth_secret 不能为空');
}
$this->service->verifyRemoteAuthorization($param['auth_code'],$param['auth_secret']);
//追加到系统配置
$this->updateMadongConfig($param['auth_code'], $param['auth_secret']);
return Json::success('授权设置成功');
} catch (\Throwable $e) {
return Json::fail($e->getMessage());
}
}
/**
* 更新server/config/madong.php文件中的配置
*
* @param string $authCode 授权码
* @param string $authSecret 授权密钥
*
* @throws \Exception
*/
private function updateMadongConfig(string $authCode, string $authSecret): void
{
$configPath = base_path() . '/config/madong.php';
if (!file_exists($configPath)) {
throw new \Exception('madong.php配置文件不存在');
}
$configContent = file_get_contents($configPath);
// 定义需要替换的参数映射
$replacements = [
'auth_code' => $authCode,
'auth_secret' => $authSecret,
];
foreach ($replacements as $key => $value) {
$pattern = '/\'' . $key . '\'\s*=>\s*env\(\'madong\..*?\'\s*,\s*\'(.*?)\'\)/';
$replacement = "'$key' => env('madong." . ($key === 'auth_code' ? 'code' : 'secret') . "', '$value')";
$configContent = preg_replace($pattern, $replacement, $configContent);
}
file_put_contents($configPath, $configContent);
}
}
@@ -0,0 +1,305 @@
<?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\adminapi\controller\profile;
use app\adminapi\controller\Crud;
use app\adminapi\CurrentUser;
use app\adminapi\middleware\AccessTokenMiddleware;
use app\adminapi\middleware\OperationMiddleware;
use app\adminapi\middleware\PermissionMiddleware;
use app\adminapi\schema\request\profile\ProfileUpdateRequest;
use app\adminapi\schema\request\profile\PasswordUpdateRequest;
use app\adminapi\schema\request\profile\AvatarUpdateRequest;
use app\adminapi\validate\profile\ProfileValidate;
use app\dao\logs\LoginLogDao;
use app\schema\request\IdRequest;
use app\service\admin\logs\LoginLogService;
use app\service\admin\system\AdminService;
use core\tool\Json;
use madong\swagger\annotation\response\SimpleResponse;
use madong\swagger\attribute\AllowAnonymous;
use madong\swagger\attribute\Permission;
use OpenApi\Attributes as OA;
use support\annotation\Middleware;
use support\Container;
use support\Request;
use WebmanTech\Swagger\DTO\SchemaConstants;
#[OA\Tag(name: '个人中心')]
#[Middleware(AccessTokenMiddleware::class, PermissionMiddleware::class, OperationMiddleware::class)]
final class ProfileController extends Crud
{
public function __construct(AdminService $service, ProfileValidate $validate)
{
$this->service = $service;
$this->validate = $validate;
}
#[OA\Get(
path: '/profile',
summary: '获取当前用户信息',
security: [['Bearer' => [], 'ApiKey' => []]],
tags: ['个人中心']
)]
#[SimpleResponse(example: '{"id": 1, "user_name": "admin", "email": "test@example.com"}')]
#[Permission(code: 'admin:profile:info')]
#[AllowAnonymous(requireToken: true, requirePermission: false)]
public function show(Request $request): \support\Response
{
try {
$userId = Container::make(CurrentUser::class)->id();
$data = $this->service->get($userId);
return Json::success('ok', $data);
} catch (\Exception $e) {
return Json::fail($e->getMessage());
}
}
#[OA\Put(
path: '/profile',
summary: '更新个人信息',
security: [['Bearer' => [], 'ApiKey' => []]],
tags: ['个人中心']
)]
#[OA\RequestBody(
required: true,
content: new OA\JsonContent(ref: ProfileUpdateRequest::class)
)]
#[SimpleResponse(example: '{"code": 0, "msg": "success"}')]
#[Permission(code: 'admin:profile:update_info')]
#[AllowAnonymous(requireToken: true, requirePermission: false)]
public function update(Request $request): \support\Response
{
try {
$userId = Container::make(CurrentUser::class)->id();
$data = $this->inputFilter($request->all());
// 验证数据
if (!$this->validate->scene('update-profile')->check($data)) {
throw new \Exception($this->validate->getError());
}
$this->service->updateUserInfo($userId, $data);
return Json::success('个人信息更新成功');
} catch (\Exception $e) {
return Json::fail($e->getMessage());
}
}
#[OA\Put(
path: '/profile/password',
summary: '修改密码',
security: [['Bearer' => [], 'ApiKey' => []]],
tags: ['个人中心']
)]
#[OA\RequestBody(
required: true,
content: new OA\JsonContent(ref: PasswordUpdateRequest::class)
)]
#[SimpleResponse(example: '{"code": 0, "msg": "密码修改成功"}')]
#[Permission(code: 'admin:profile:password')]
#[AllowAnonymous(requireToken: true, requirePermission: false)]
public function updatePassword(Request $request): \support\Response
{
try {
$userId = Container::make(CurrentUser::class)->id();
$data = $this->inputFilter($request->all(), ['old_password', 'new_password', 'confirm_password']);
// 验证数据
if (!$this->validate->scene('update-password')->check($data)) {
throw new \Exception($this->validate->getError());
}
$this->service->updateUserPwd($userId, $data);
return Json::success('密码修改成功');
} catch (\Exception $e) {
return Json::fail($e->getMessage());
}
}
#[OA\Put(
path: '/profile/avatar',
summary: '更新头像',
security: [['Bearer' => [], 'ApiKey' => []]],
tags: ['个人中心']
)]
#[SimpleResponse(schema: [], example: [])]
#[Permission(code: 'admin:profile:avatar')]
#[AllowAnonymous(requireToken: true, requirePermission: false)]
public function updateAvatar(Request $request): \support\Response
{
try {
$userId = Container::make(CurrentUser::class)->id();
$avatar = $request->input('avatar');
// 验证数据
if (!$this->validate->scene('update-avatar')->check(['avatar' => $avatar])) {
throw new \Exception($this->validate->getError());
}
$this->service->updateAvatarUser($userId, $avatar);
return Json::success('头像更新成功');
} catch (\Exception $e) {
return Json::fail($e->getMessage());
}
}
#[OA\Get(
path: '/profile/sessions',
summary: '获取在线设备列表',
security: [['Bearer' => [], 'ApiKey' => []]],
tags: ['个人中心']
)]
#[OA\Parameter(
name: 'page',
description: '页码',
in: 'query',
required: false,
schema: new OA\Schema(type: 'integer', default: 1)
)]
#[OA\Parameter(
name: 'limit',
description: '每页数量',
in: 'query',
required: false,
schema: new OA\Schema(type: 'integer', default: 10)
)]
#[SimpleResponse(schema: [], example: [])]
#[Permission(code: 'admin:profile:sessions')]
#[AllowAnonymous(requireToken: true, requirePermission: false)]
public function getSessions(Request $request): \support\Response
{
try {
$page = (int) $request->input('page', 1);
$limit = (int) $request->input('limit', 10);
$currUser = Container::make(CurrentUser::class);
$jwt = new \core\jwt\JwtToken();
// 获取当前用户信息(用于兜底)
$adminInfo = $currUser->admin(true) ?? [];
$defaultUserName = $adminInfo['user_name'] ?? '';
$defaultRealName = $adminInfo['real_name'] ?? '';
$defaultAvatar = $adminInfo['avatar'] ?? '';
// 从 JWT 获取真实的会话列表
$allSessions = $jwt->getSessions($currUser->id());
$total = count($allSessions);
// 分页处理
$offset = ($page - 1) * $limit;
$sessions = array_slice($allSessions, $offset, $limit);
// 格式化数据
$items = array_map(function ($session) use ($defaultUserName, $defaultRealName, $defaultAvatar) {
$extra = $session['extra'] ?? [];
return [
'jti' => $session['jti'],
'client_type' => $session['client_type'] ?? 'admin',
'login_time' => $session['created_at'] ?? time(),
'ip' => $extra['ip'] ?? '',
'ip_location'=>$extra['ip_location'] ?? '',
'os' => $extra['os'] ?? '',
'browser' => $extra['browser'] ?? '',
'user_name' => $extra['user_name'] ?? $defaultUserName,
'real_name' => $extra['real_name'] ?? $defaultRealName,
'avatar' => $extra['avatar'] ?? $defaultAvatar,
];
}, $sessions);
return Json::success('ok', compact('total', 'items'));
} catch (\Exception $e) {
return Json::fail($e->getMessage());
}
}
/**
* @throws \Throwable
*/
#[OA\Delete(
path: '/profile/sessions/{jti}',
summary: '强制下线指定设备',
security: [['Bearer' => [], 'ApiKey' => []]],
tags: ['个人中心']
)]
#[OA\Parameter(
name: 'jti',
description: '会话JTI',
in: 'path',
required: true,
schema: new OA\Schema(type: 'string')
)]
#[SimpleResponse(schema: [], example: [])]
#[Permission(code: 'admin:profile:kickout')]
#[AllowAnonymous(requireToken: true, requirePermission: false)]
public function kickoutSession(Request $request): \support\Response
{
try {
$jti = $request->route->param('jti', null);
$jwt = new \core\jwt\JwtToken();
$result = $jwt->kickoutByJti($jti);
if (!$result) {
return Json::fail('会话不存在或已失效');
}
return Json::success('ok');
} catch (\Exception $e) {
return Json::fail($e->getMessage());
}
}
#[OA\Put(
path: '/profile/{id}/update-preferences',
summary: '更新用户前端偏好设置',
tags: ['用户管理'],
x: [
SchemaConstants::X_PROPERTY_IN => 'id',
SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class,
]
)]
#[OA\RequestBody(
required: true,
content: new OA\JsonContent(
properties: [
new OA\Property(
property: 'preferences',
description: '偏好设置',
type: 'string',
example: '{"theme": "dark"}'
),
]
)
)]
#[Permission(code: 'system:admin:update_preferences')]
#[SimpleResponse(schema: [], example: [])]
public function updatePreferences(Request $request): \support\Response
{
try {
$uid = Container::make(CurrentUser::class)->id();
$data = $request->all();
if (isset($this->validate) && $this->validate) {
$data['id'] = $uid;
if (!$this->validate->scene('update-preferences')->check($data)) {
throw new \Exception($this->validate->getError());
}
}
$this->service->updateUserPreferences($uid, $data);
return Json::success('ok');
} catch (\Throwable $e) {
return Json::fail($e->getMessage());
}
}
}
@@ -0,0 +1,320 @@
<?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\adminapi\controller\review;
use app\adminapi\controller\Crud;
use app\adminapi\middleware\AccessTokenMiddleware;
use app\adminapi\middleware\OperationMiddleware;
use app\adminapi\middleware\PermissionMiddleware;
use app\schema\request\IdRequest;
use app\service\admin\review\ReviewService;
use madong\swagger\annotation\response\DataResponse;
use madong\swagger\annotation\response\PageResponse;
use madong\swagger\annotation\response\SimpleResponse;
use madong\swagger\attribute\Permission;
use core\tool\Json;
use OpenApi\Attributes as OA;
use support\annotation\Middleware;
use support\Request;
use WebmanTech\Swagger\DTO\SchemaConstants;
#[OA\Tag(name: '审核管理', description: '通用审核模块')]
#[Middleware(AccessTokenMiddleware::class, PermissionMiddleware::class, OperationMiddleware::class)]
final class ReviewController extends Crud
{
public function __construct(ReviewService $service)
{
$this->service = $service;
}
#[OA\Get(
path: '/review',
summary: '审核列表',
tags: ['审核管理'],
parameters: [
new OA\Parameter(name: "status", description: "审核状态:0待审核,1已通过,2已拒绝", in: "query", schema: new OA\Schema(type: "integer")),
new OA\Parameter(name: "reviewable_type", description: "审核类型(morph_map别名)", in: "query", schema: new OA\Schema(type: "string")),
new OA\Parameter(name: "page", description: "页码", in: "query", schema: new OA\Schema(type: "integer")),
new OA\Parameter(name: "limit", description: "每页数量", in: "query", schema: new OA\Schema(type: "integer")),
new OA\Parameter(name: "LIKE_title", description: "标题模糊搜索", in: "query", schema: new OA\Schema(type: "string")),
new OA\Parameter(name: "LIKE_applicant", description: "申请人模糊搜索", in: "query", schema: new OA\Schema(type: "string")),
new OA\Parameter(name: "LIKE_content", description: "内容模糊搜索", in: "query", schema: new OA\Schema(type: "string")),
],
)]
#[Permission(code: "review:list")]
#[PageResponse(schema: [], example: [[
'id' => '230104551592014400',
'reviewable_type' => 'question',
'reviewable_id' => '230104551591014400',
'status' => 0,
'reason' => '同意',
'reviewer_id' => 1,
'reviewed_at' => 1758899600,
'flow_instance_id' => null,
'extra_data' => null,
'created_by' => 1,
'updated_by' => 1,
'created_at' => '2026-03-02T09:12:13.000000Z',
'updated_at' => '2026-03-02T09:08:38.000000Z',
'title' => '请问Madong框架可以免费商用吗?',
'content' => '我想用到项目上可以吗!阿打发斯蒂芬阿斯顿发生大法师',
'applicant' => '未知',
'reviewer_name' => '超级管理员',
'display_name' => 'question',
'morph_alias' => null,
'status_text' => '待审核',
'is_workflow_mode' => false,
'reviewer' => [
'id' => '1',
'user_name' => 'superAdmin',
'real_name' => '超级管理员',
'nick_name' => '超级管理员',
'created_date' => null,
'updated_date' => null,
],
'reviewable' => [
'id' => '230104551591014400',
'category_id' => '2',
'title' => '请问Madong框架可以免费商用吗?',
'content' => '我想用到项目上可以吗!阿打发斯蒂芬阿斯顿发生大法师',
'deleted_at' => null,
'created_at' => '2025-09-26T15:13:20.000000Z',
'updated_at' => '2026-03-03T06:39:01.000000Z',
'view_count' => 3869,
'like_count' => 0,
'collect_count' => 0,
'comment_count' => 0,
'member_id' => '277105476800872448',
'is_sticky' => 0,
'sticky_order' => 0,
'is_excellent' => 0,
'is_solved' => 0,
'solved_answer_id' => null,
'created_date' => '2025-09-26 23:13:20',
'updated_date' => '2026-03-03 14:39:01',
],
]])]
public function index(Request $request): \support\Response
{
try {
// 使用 selectInput 处理参数(现在 title/applicant/content 字段已被跳过)
[$where, $format, $limit, $field, $order, $page] = $this->selectInput($request);
// 准备关联预加载
$with = [
'reviewer' => function ($query) {
$query->select(['id', 'user_name', 'real_name', 'nick_name']);
},
];
// 获取映射后的审核列表(支持 extra_data 字段搜索)
$items = $this->service->getMappedList($where, $field, $page, $limit, $order, $with, false);
// 使用支持搜索的计数方法
$total = $this->service->getCountWithSearch($where);
return $this->formatNormal($items, $total);
} catch (\Throwable $e) {
return Json::fail($e->getMessage());
}
}
#[OA\Get(
path: '/review/{id}',
summary: '审核详情',
tags: ['审核管理'],
x: [
SchemaConstants::X_PROPERTY_IN => 'id',
SchemaConstants::X_SCHEMA_REQUEST => IdRequest::class,
]
)]
#[Permission("review:read")]
#[DataResponse(schema: [], example: [
'id' => '230104551592014400',
'reviewable_type' => 'question',
'reviewable_id' => '230104551591014400',
'status' => 0,
'reason' => '同意',
'reviewer_id' => 1,
'reviewed_at' => 1758899600,
'flow_instance_id' => null,
'extra_data' => null,
'created_by' => 1,
'updated_by' => 1,
'created_at' => '2026-03-02T09:12:13.000000Z',
'updated_at' => '2026-03-02T09:08:38.000000Z',
'title' => '请问Madong框架可以免费商用吗?',
'content' => '我想用到项目上可以吗!阿打发斯蒂芬阿斯顿发生大法师',
'applicant' => '未知',
'reviewer_name' => '超级管理员',
'display_name' => 'question',
'morph_alias' => null,
'status_text' => '待审核',
'is_workflow_mode' => false,
'reviewer' => [],
'reviewable' => [],
])]
public function show(Request $request): \support\Response
{
try {
$id = $request->route->param('id');
$detail = $this->service->getMappedReview($id);
if (!$detail) {
return Json::fail('审核记录不存在');
}
return Json::success('获取成功', $detail);
} catch (\Throwable $e) {
return Json::fail($e->getMessage());
}
}
#[OA\Put(
path: '/review/{id}/approve',
summary: '审核通过',
tags: ['审核管理'],
parameters: [
new OA\Parameter(name: "id", description: "审核ID", in: "path", required: true, schema: new OA\Schema(type: "integer")),
],
)]
#[Permission("review:approve")]
#[SimpleResponse(schema: [], example: [])]
public function approve(Request $request): \support\Response
{
try {
$id = $request->route->param('id');
$this->service->approve($id);
return Json::success('审核通过');
} catch (\Throwable $e) {
return Json::fail($e->getMessage());
}
}
#[OA\Put(
path: '/review/{id}/reject',
summary: '审核拒绝',
requestBody: new OA\RequestBody(
required: false,
content: new OA\JsonContent(properties: [
new OA\Property(property: "reason", type: "string", example: "内容违规"),
])
),
tags: ['审核管理'],
parameters: [
new OA\Parameter(name: "id", description: "审核ID", in: "path", required: true, schema: new OA\Schema(type: "integer")),
],
)]
#[Permission("review:reject")]
#[SimpleResponse(schema: [], example: [])]
public function reject(Request $request): \support\Response
{
try {
$id = $request->route->param('id');
$reason = $request->input('reason', '');
$this->service->reject($id, $reason);
return Json::success('审核拒绝');
} catch (\Throwable $e) {
return Json::fail($e->getMessage());
}
}
#[OA\Post(
path: '/review/batch/approve',
summary: '批量审核通过',
requestBody: new OA\RequestBody(
required: true,
content: new OA\JsonContent(properties: [
new OA\Property(property: "ids", type: "array", items: new OA\Items(type: "integer"), example: [1, 2, 3]),
])
),
tags: ['审核管理'],
)]
#[Permission("review:approve")]
#[SimpleResponse(schema: [], example: [])]
public function batchApprove(Request $request): \support\Response
{
try {
$ids = $request->input('ids', []);
$reason = $request->input('reason', '');
if (empty($ids)) {
return Json::fail('请选择要审核的记录');
}
$count = $this->service->batchApprove($ids, ['reason' => $reason]);
return Json::success('批量审核通过成功', ['count' => $count]);
} catch (\Throwable $e) {
return Json::fail($e->getMessage());
}
}
#[OA\Post(
path: '/review/batch/reject',
summary: '批量审核拒绝',
requestBody: new OA\RequestBody(
required: true,
content: new OA\JsonContent(properties: [
new OA\Property(property: "ids", type: "array", items: new OA\Items(type: "integer"), example: [1, 2, 3]),
new OA\Property(property: "reason", type: "string", example: "内容违规"),
])
),
tags: ['审核管理'],
)]
#[Permission("review:reject")]
#[SimpleResponse(schema: [],example: [])]
public function batchReject(Request $request): \support\Response
{
try {
$ids = $request->input('ids', []);
$reason = $request->input('reason', '');
if (empty($ids)) {
return Json::fail('请选择要审核的记录');
}
$count = $this->service->batchReject($ids, $reason, ['reason' => $reason]);
return Json::success('批量审核拒绝成功', ['count' => $count]);
} catch (\Throwable $e) {
return Json::fail($e->getMessage());
}
}
#[OA\Get(
path: '/review/review/statistics',
summary: '审核统计',
tags: ['审核管理'],
)]
#[Permission("review:list")]
#[SimpleResponse(schema: [],example: [])]
public function statistics(Request $request): \support\Response
{
try {
$result = $this->service->getStatistics();
return Json::success('获取成功', $result);
} catch (\Throwable $e) {
return Json::fail($e->getMessage());
}
}
#[OA\Delete(
path: '/review/{id}',
summary: '删除审核记录',
tags: ['审核管理'],
parameters: [
new OA\Parameter(name: "id", description: "审核ID", in: "path", required: true, schema: new OA\Schema(type: "integer")),
]
)]
#[Permission("review:delete")]
#[SimpleResponse(schema: [],example: [])]
public function destroy(Request $request): \support\Response
{
return parent::destroy($request);
}
}

Some files were not shown because too many files have changed in this diff Show More