onezan-admin 快速开发框架
壹站 onezan-admin 适用于核心接入应用实现多样化可定制、快速交付的场景。项目采用前后端分离结构,内置系统核心功能:用户、渠道多端、权限控制和基础配置能力,可作为新项目的统一底座继续扩展。
/docs/
后端采用框架核心 + 框架基础能力 + 应用 + 模块 / 插件结构,前端按框架基础能力、模块、插件三类后台页面组织。
框架内置 sample 示例应用(快速开发模板)应用独立拥有 manifest、event.php、菜单、权限、消息、设置,互不耦合。框架核心不包含任何应用专属逻辑。
技术栈
项目遵循前后端分离架构,服务端接管业务逻辑与数据持久化,管理后台负责页面渲染与交互,两端通过 RESTful API 通信。以下版本号为系统最低要求,生产环境建议采用更新版本以保证安全与性能。
语言:PHP 最低 ≥ 8.0,建议 ≥ 8.2(严格模式,声明类型)
框架:ThinkPHP 8.0(PSR-4)
ORM:ThinkORM 3.x / 4.x
认证:firebase/php-jwt Token 鉴权
队列:sync / database / redis 三驱动 + Supervisor 常驻
支付:yansongda/pay 3.x(微信 + 支付宝)
消息:w7corp/easywechat(公众号 / 小程序)、phpmailer
存储:think-filesystem(本地 + 云存储扩展)
运行环境:Nginx / Apache + PHP-FPM
数据库:MySQL 最低 ≥ 5.7,建议 ≥ 8.0
缓存:Redis 最低 ≥ 5.0,建议 ≥ 7.0
语言:TypeScript 5.x 严格模式
框架:Vue 3.5 Composition API
构建:Vite 6.x
UI 库:TDesign Vue Next 1.18
状态:Pinia 3.x + pinia-plugin-persistedstate
路由:Vue Router 4.x
HTTP:Axios 1.x(请求/响应拦截)
图表:ECharts 5.4
样式:Less 4.x + TDesign 设计变量
质量:ESLint 9 + Stylelint 16 + vue-tsc 类型检查
Node:最低 ≥ 20.19,建议 ≥ 20.x LTS / 22.x LTS
参考前端:暂无
route)→ 控制器(controller,参数校验 + 业务校验)→ 应用服务(service,参数收口 + 幂等)→ 核心服务(core,写流程 + 事务 + 状态流转)→ 仓储(repository,数据访问封装)→ 模型(model,映射数据表)。前端:页面(pages)→ 组件(components)→ API 层(api)→ 状态(store)→ 路由(router)。
目录结构
下面展示的是项目完整目录结构,所有开发都应围绕这套结构展开。
api/core;“框架基础能力”只指 api/app 中通用控制器、服务、仓储、模型、校验;“应用”只指 api/applications/{app};“模块”只指 modules/{module};“插件”指可关闭、可替换、可移除的扩展单元。
| 层级 | 目录 | 职责 |
|---|---|---|
| 核心框架 | api/core |
注册中心、聚合器、安装器、菜单在线覆盖、队列核心等能力。 |
| 框架基础能力 | api/app/adminapi/controller、api/app/api/controller、api/app/service、api/app/repository、api/app/model、api/app/validate |
框架公共接口、服务、仓储、模型、校验与中间件。 |
| 应用 | api/applications/{app} |
应用清单 manifest、事件注册 event.php、菜单、权限、消息、设置、桥接路由、应用专属控制器/事件/监听器。 |
| 模块 | api/applications/{app}/modules/{module} |
某一模块的 service、core、repository、model、validate、manifest、route、queue.php。 |
| 插件 | api/applications/{app}/plugins/{plugin} |
应用内可插拔扩展能力,如优惠券、会员、充值等。 |
| 框架基础页面 | admin/src/core/pages、admin/src/core/api |
框架内置了基础页面:如管理员、用户、渠道、系统配置、权限角色、菜单配置。 |
| 功能模块页面 | admin/src/applications/{app}/pages、admin/src/applications/{app}/api |
模块页面与接口,如商品、内容、订单、财务。 |
| 扩展插件页面 | admin/src/applications/{app}/plugins/{plugin}/pages、admin/src/applications/{app}/plugins/{plugin}/api |
插件页面与接口,如充值、签到。 |
| 路由层 | admin/src/router/core/entry.ts、admin/src/router/registry.ts、admin/src/applications/{app}/router/*.ts、admin/src/applications/{app}/plugins/router.ts |
框架路由组聚合;应用路由文件自动发现;插件路由由各应用自己的 plugins/router.ts 自动收集。 |
前端规范
前端的首要目标是让新增页面与框架内置页面保持同一套 UI、布局、按钮、弹窗、提示和交互风格。
框架基础能力页面放 admin/src/core/pages;模块页面放 admin/src/applications/{app}/pages/{biz}(如 applications/mall/pages/goods);插件页面放 admin/src/applications/{app}/plugins/{plugin}/pages。
框架基础能力接口放 admin/src/core/api;模块接口放 admin/src/applications/{app}/api;插件接口放 admin/src/applications/{app}/plugins/{plugin}/api。
强制 DOM 结构:t-card(:bordered="false") → t-form.search-form → t-table → t-dialog。禁止冗余包裹和 t-card title。
查询 → 重置 → 主操作。查询 theme="primary",重置 theme="default" variant="base"。
状态、类型、来源等枚举统一使用 t-tag,轻量边框或浅色填充,不额外造新的视觉体系。
菜单图标用本地 iconfont icon-menu-*;二级菜单不显示图标;列表页操作按钮统一纯文字无图标;卡片展示区仍可用 AppIcon 做装饰。
admin/src/components/ 提供框架级通用组件:AppIcon、UserTwoLine、RegionPicker(省市区三级地区选择,基于 element-china-area-data + t-cascader,应用可直接复用)。
命名惯例
| 用途 | 建议命名 | 说明 |
|---|---|---|
| 加载状态 | dataLoading |
所有页面统一。 |
| 查询表单 | searchForm |
用 reactive 包裹。 |
| 列表数据 | tableData |
单文件内保持一致。 |
| 列表请求 | fetchList() |
统一入口。 |
| 查询/重置 | handleSearch() / handleReset() |
查询重置到第一页。 |
| 分页 | pagination |
{ current, pageSize, total }。 |
| 保存 | handleSave() |
配合 saving 状态。 |
| 弹窗 | dialogVisible |
配合 openAdd() / openEdit(row)。 |
页面范式
1. 简单 CRUD:列表页 + 弹窗 2. 字段较多:列表页 + edit.vue 独立编辑页 3. 在线配置:顶部说明 + 可编辑表格 + 统一保存按钮 4. 详情展示:抽屉或描述列表,不额外引入陌生交互
应用设置页
应用可通过 menus.json 将设置页菜单自动注入到"系统设置"二级菜单下,无需修改核心框架代码。
| 步骤 | 文件 | 说明 |
|---|---|---|
| 1 | api/applications/{app}/menus.json | 新增菜单项,parent_code 设为 "system"(自动挂载到系统设置下) |
| 2 | admin/src/applications/{app}/router/settings.ts | 创建路由文件,用 Layout 包裹,path 必须以 / 开头。文件存在即生效,无需单独 entry |
| 3 | admin/src/applications/{app}/pages/settings/index.vue | 实现设置页组件,建议用 t-tabs 分 Tab 组织配置区 |
parent_code 固定为 "system",path 必须以 / 开头(如 "/mall/settings")。系统会自动将其作为"系统设置"的二级菜单展示,无需修改 Core/manifest/menus.json。
路由模板
// admin/src/applications/{app}/router/settings.ts
import type { RouteRecordRaw } from 'vue-router';
import Layout from '@/layouts/index.vue';
export default [
{
path: '/{app}/settings',
name: 'AppSettings',
component: Layout,
redirect: '/{app}/settings/index',
meta: {
title: { zh_CN: '应用设置', en_US: 'App Settings' },
icon: 'iconfont iconshezhi1',
orderNo: 50,
permission: 'system.view',
},
children: [
{
path: 'index',
name: 'AppSettingsIndex',
component: () => import('@/applications/{app}/pages/settings/index.vue'),
meta: {
title: { zh_CN: '应用设置', en_US: 'App Settings' },
permission: 'system.view',
},
},
],
},
] satisfies RouteRecordRaw[];
菜单层级约束
每个 parent_code 为空的主菜单项(一级菜单)必须至少有一个 parent_code 指向它的子菜单项。如果缺少子项,侧边栏会出现冗余的二级点击层级(左侧点击主菜单后右侧仅显示主菜单自身)。框架已提供运行时兜底:若服务端菜单树中某父节点无子项,前端会从路由中自动解析子菜单。
parent_code 指向该菜单的子菜单项。订单、财务等自带多个子路由的模块只需写 parent_code 子项清单。不可只写 parent 不写 child。
正确示例
// api/applications/mall/menus.json — 订单管理(一级菜单 + 子菜单)
[
{
"code": "order",
"title": "订单管理",
"title_short": "订单",
"path": "/order/index",
"icon": "iconfont icon-menu-order",
"parent_code": "",
"order_no": 50
},
{
"code": "order.index",
"title": "订单列表",
"path": "/order/index",
"icon": "iconfont iconliebiao",
"parent_code": "order",
"order_no": 10
}
]
财务示例(多子菜单)
// api/applications/mall/menus.json — 财务(一级菜单 + 三个子菜单)
[
{
"code": "finance",
"title": "财务管理",
"title_short": "财务",
"path": "/finance/payments",
"icon": "iconfont icon-menu-finance",
"parent_code": "",
"order_no": 60
},
{
"code": "finance.money-detail",
"title": "余额明细",
"path": "/finance/money-detail",
"parent_code": "finance",
"order_no": 10
},
{
"code": "finance.payments",
"title": "支付记录",
"path": "/finance/payments",
"parent_code": "finance",
"order_no": 20
},
{
"code": "finance.points",
"title": "积分明细",
"path": "/finance/points-detail",
"parent_code": "finance",
"order_no": 30
}
]
路由自动发现
应用模块路由和插件路由均通过 import.meta.glob 零配置自动发现,新增应用或模块无需修改核心框架代码。
// admin/src/router/registry.ts — 固定路由自动发现
const applicationModules = import.meta.glob(
['../applications/**/router/!(entry).ts', '../applications/**/plugins/router.ts'],
{ eager: true },
);
// 应用路由文件放 admin/src/applications/{app}/router/{module}.ts
// 插件聚合入口放 admin/src/applications/{app}/plugins/router.ts
// 文件存在即生效,无需手动 import
// admin/src/utils/route/index.ts — 全局页面自动扫描
const dynamicViewsModules = {
...import.meta.glob('../../core/pages/**/*.vue'),
...import.meta.glob('../../applications/*/pages/**/*.vue'),
...import.meta.glob('../../applications/*/plugins/pages/**/*.vue'),
};
admin/src/applications/{app}/router/{module}.ts,插件聚合入口放到 admin/src/applications/{app}/plugins/router.ts。框架自动 glob 加载,无需任何手动注册。页面自动扫描使用通配符 applications/*/pages/** 与 applications/*/plugins/pages/**。
后端规范
后端开发围绕框架核心放 api/core,框架基础能力放 api/app,应用、模块与插件放 api/applications。
Controller 应用 Service Core Service Repository Model DB。复杂查询下沉 Repository,复杂写入下沉 Core Service。
写操作必须执行格式校验 + 领域校验;读操作至少做格式校验。不要在控制器和 Service 里堆零散 if。
框架基础能力路由走 api/route/admin/*.php、api/route/app/*.php;模块与插件使用各自目录下的 route/admin.php、route/app.php。
菜单、权限、消息、设置、manifest 改动后,通过 POST /adminapi/sys/platform/sync 或后台更新缓存刷新聚合结果,并清理 api/runtime/cache、api/runtime/log 运行时文件。
校验结构
| 层 | 目录 | 职责 |
|---|---|---|
| 格式校验 | api/app/validate/admin、api/app/validate/api 以及模块 / 插件自身 validate/admin、validate/api |
字段类型、必填、长度、枚举等。 |
| 领域校验 | api/app/validate/domain 或模块 / 插件自身 validate/domain |
跨字段业务规则、场景规则。 |
| 自定义规则 | api/app/validate/rule |
手机号、证件号、小数等可复用规则。 |
应用开发
应用是一组完整业务能力的集合(如 mall)。框架设计上,框架核心与框架基础能力不变,每个应用独立部署,互不污染。
应用目录结构
api/applications/{app}/
├─ manifest.json ← 应用清单(名称、版本、依赖、路由入口)
├─ menus.json ← 后台左侧菜单树
├─ permissions.json ← 权限节点注册
├─ messages.json ← 消息模板注册
├─ settings.json ← 应用级配置项
├─ event.php ← 应用事件声明
├─ queue.php ← 应用队列声明
├─ route/admin.php ← 后台路由桥接入口
├─ route/app.php ← 前台路由桥接入口
├─ modules/{module}/ ← 模块(goods / order / user 等)
├─ plugins/{plugin}/ ← 插件(vip / coupon / recharge 等)
│ ├─ extension/ ← 扩展点处理器(同步 Pipeline)
│ ├─ queue/ ← 队列 handler(异步)
│ ├─ event.php ← 插件事件声明(可选)
│ └─ queue.php ← 插件队列声明(可选)
快速开始:一行命令创建应用
// === 创建应用(后端 + 前端 全部骨架) === php think make app myapp // === 创建模块(含 CRUD 骨架代码) === php think make module myapp goods // === 创建插件(含扩展点目录) === php think make plugin myapp vip
手动创建(了解原理)
// 1. 创建目录
mkdir -p api/applications/myapp
mkdir -p api/applications/myapp/modules
mkdir -p api/applications/myapp/plugins
mkdir -p api/applications/myapp/data
// 2. 创建 manifest.json
// api/applications/myapp/manifest.json
{
"code": "myapp",
"name": "我的应用",
"version": "1.0.0",
"description": "自定义应用",
"dependencies": ["core"],
"modules": [],
"plugins": [],
"route_bridges": {
"admin": "applications/myapp/route/admin.php",
"app": "applications/myapp/route/app.php"
}
}
// 3. 创建路由桥接
// api/applications/myapp/route/admin.php
onezan_load_php_tree(array_merge(
glob(dirname(__DIR__) . '/modules/*/route/admin.php') ?: [],
glob(dirname(__DIR__) . '/plugins/*/route/admin.php') ?: []
));
// 4. 创建安装 SQL
// api/applications/myapp/data/schema.sql
CREATE TABLE IF NOT EXISTS `onezan_myapp_config` (
`id` int unsigned NOT NULL AUTO_INCREMENT,
`key` varchar(64) NOT NULL DEFAULT '',
`value` text,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_key` (`key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
运行机制
| 步骤 | 操作 | 说明 |
|---|---|---|
| 1 | 创建应用目录与 manifest | 框架通过 manifest.json 识别应用 |
| 2 | 注册路由桥接 | route/admin.php 和 route/app.php 是本应用的总入口 |
| 3 | 添加模块或插件 | 在 modules/ 或 plugins/ 下按规范创建 |
| 4 | 同步菜单权限 | 修改 menus.json 和 permissions.json |
| 5 | 在后台系统设置中点击"更新缓存",使新应用生效 | |
| 📌 | php think make app {code} | 一键创建应用骨架(推荐,替代步骤 1-2) |
| 📌 | php think make module {应用} {编码} | 一键创建模块骨架(推荐,替代步骤 3) |
| 📌 | php think make plugin {应用} {编码} | 一键创建插件骨架(推荐,替代步骤 3) |
插件开发
插件是可关闭、可替换、可移除的扩展单元。它通过事件订阅、扩展点管道和 Provider 接口与模块解耦协作。
框架扩展点清单
所有可用的扩展点定义在 api/core/manifest/extension_points.json,插件开发者可查看此文件了解可挂载的钩子:
| 扩展点 | 说明 |
|---|---|
plugins.snapshot | 用户插件快照数据注入(VIP 状态、优惠券等) |
order.price.calculate | 订单价格计算 — 插件可在支付前修改价格 |
order.after_create | 订单创建后触发 — 锁定优惠券、发放积分、发送通知 |
goods.price.calculate | 商品价格计算 — VIP 会员折扣 |
order.shipping.calculate | 运费计算 — VIP 免邮、地区定价 |
插件目录结构
api/applications/mall/plugins/my_plugin/ ├─ manifest.json ← 声明插件信息、扩展点、依赖 ├─ event.php ← 插件事件声明(可选) ├─ queue.php ← 插件队列主题声明(可选) ├─ route/admin.php ← 后台路由 ├─ route/app.php ← 前台路由 ├─ controller/adminapi/ ← 后台控制器 ├─ controller/api/ ← 前台控制器 ├─ service/admin/ ← 后台 Service ├─ service/api/ ← 前台 Service ├─ core/ ← 核心业务逻辑(Core Service) ├─ repository/ ← 数据查询层 ├─ validate/ ← 校验层 ├─ extension/ ← 扩展点处理器(实现 ExtensionPointHandlerInterface) └─ queue/ ← 队列 handler(异步消费)
快速开始:创建一个插件
// 1. 创建 manifest.json
// api/applications/mall/plugins/my_plugin/manifest.json
{
"code": "mall.my_plugin",
"name": "我的插件",
"version": "1.0.0",
"type": "plugin",
"status": 1,
"is_removable": true,
"description": "演示插件",
"files": [
"api/applications/mall/plugins/my_plugin"
],
"tables": [],
"route_bridges": {
"admin": "applications/mall/plugins/my_plugin/route/admin.php",
"app": "applications/mall/plugins/my_plugin/route/app.php"
},
"permissions": ["plugins.my_plugin.view"],
"dependencies": ["core", "mall"],
"extension_points": [
{
"point": "order.price.calculate",
"handler": "Applications\\mall\\plugins\\my_plugin\\extension\\MyPriceHandler",
"priority": 30
}
],
"plugins_meta": {
"type": "other",
"description": "演示插件能力。",
"icon": "iconfont iconplugin",
"sort": 99
}
}
// 2. 实现扩展点处理器
// api/applications/mall/plugins/my_plugin/extension/MyPriceHandler.php
use Core\contract\ExtensionPointHandlerInterface;
class MyPriceHandler implements ExtensionPointHandlerInterface
{
public function point(): string
{
return 'order.price.calculate';
}
public function handle(array $ctx): array
{
// 读取管道上下文
$userId = (int)($ctx['user_id'] ?? 0);
$currentPrice = (float)($ctx['final_price'] ?? 0);
// 业务判断
if (!$this->isEligible($userId)) {
return $ctx; // 不修改,原样传递
}
// 计算折扣(满100减10)
$discount = $currentPrice >= 100.00 ? 10.00 : 0;
$ctx['discount'] = ((float)($ctx['discount'] ?? 0)) + $discount;
$ctx['final_price'] = round($currentPrice - $discount, 2);
$ctx['breakdown'][] = [
'type' => 'my_plugin',
'label' => '满减优惠',
'amount' => $discount,
];
return $ctx; // 传递给下一个处理器
}
}
// 3. 注册后台路由(扁平路由,与模块路由风格一致)
// api/applications/mall/plugins/my_plugin/route/admin.php
use think\facade\Route;
Route::get('mall/plugins/my_plugin/config', 'Applications\mall\plugins\my_plugin\controller\adminapi\Config@index');
插件与模块的解耦规则
| 通信方式 | 方向 | 机制 | 适用场景 |
|---|---|---|---|
| ExtensionPoint | 模块 → 插件 | 同步管道,按优先级串联执行,收集返回值 | 价格计算、折扣叠加、运费计算 |
| Event + Queue | 模块 → 插件 | 异步投递,不阻塞主流程 | 下单后发券、支付后通知、数据上报 |
| Provider 接口 | 插件 → 模块 | 插件通过接口调用模块的读写能力 | 插件查询用户等级、获取商品列表 |
| API 直连 | 前端 → 插件后台 | 插件独立的后台 CRUD 接口 | 插件自身的配置、列表、管理功能 |
通信处理
框架提供三种通信机制:事件(Event)、队列(Queue)和扩展点(ExtensionPoint),分别对应异步投递、异步消费和同步管道三种模式。
三种机制对比
| 特性 | 事件(Event) | 队列(Queue) | 扩展点(ExtensionPoint) |
|---|---|---|---|
| 执行方式 | 同步触发 Listener | 异步消费 Job | 同步 Pipeline |
| 阻塞主流程 | 是(Listener 内) | 否(独立进程) | 是(串联执行) |
| 返回值 | 不关心 | 不关心 | 收集并聚合 |
| 优先级 | 无 | 按入队顺序 | 按 priority 排序 |
| 典型场景 | 日志、缓存清理 | 发券、通知、上报 | 价格计算、数据聚合 |
事件投递
事件分两层注册:框架层在 core/manifest/event.php 注册通用事件(如用户注册),应用层在各 applications/{app}/event.php 注册应用专属事件(如订单创建)。模块写完数据库后抛出事件,Listener 接收后决定是同步处理还是投递队列。
// 1. 应用层定义事件类
// api/applications/mall/event/OrderCreated.php
namespace Applications\mall\event;
class OrderCreated
{
public int $orderId;
public array $orderData;
public function __construct(int $orderId, array $orderData) {
$this->orderId = $orderId;
$this->orderData = $orderData;
}
}
// 2. 应用 Core Service 中触发事件
// api/applications/mall/modules/order/core/CoreOrderLifecycleService.php
use Applications\mall\event\OrderCreated;
class CoreOrderLifecycleService
{
public function create(array $payload): array
{
$order = $this->repository->create(/* ... */);
// 抛事件(有已注册的 Listener 时自动触发)
app()->event->trigger(new OrderCreated((int)$order->id, $order->toArray()));
return ['code' => 0, 'data' => ['id' => (int)$order->id]];
}
}
// 3. 应用 event.php 注册事件-监听关系
// api/applications/mall/event.php
return [
'listen' => [
\Applications\mall\event\OrderCreated::class => [
\Applications\mall\listener\OrderCreatedListener::class,
],
],
];
队列消费
Listener 接收事件后通过 QueueDispatcher 投递队列,队列 handler 异步执行耗时逻辑。框架支持一个 topic 对应多个 handler(multi-cast),并通过 queue.php 声明式自动装载。
mall.order.created.audit、legacy.order.created.audit。框架层仅提供通用 topic(user.registered.audit、upgrade.execute 等)。
// 1. 应用 Listener 接收事件后投递队列
// api/applications/mall/listener/OrderCreatedListener.php
namespace Applications\mall\listener;
use Applications\mall\event\OrderCreated;
use Core\service\queue\QueueDispatcher;
class OrderCreatedListener
{
public function handle(OrderCreated $event): void
{
app()->make(QueueDispatcher::class)->dispatch(
'mall.order.created.audit',
['order_id' => $event->orderId, 'data' => $event->orderData],
['biz_key' => 'mall.order.created:' . $event->orderId]
);
}
}
// 2. 应用层/模块层声明 queue.php(LifecycleDefinitionLoader 自动装载)
// api/applications/mall/modules/order/queue.php
use Applications\mall\modules\order\handler\OrderCreatedAuditHandler;
return [
'mall.order.created.audit' => [
OrderCreatedAuditHandler::class,
],
];
// 3. 队列 handler 实现
// api/applications/mall/modules/order/handler/OrderCreatedAuditHandler.php
namespace Applications\mall\modules\order\handler;
use app\Core\service\queue\contract\QueueHandlerInterface;
class OrderCreatedAuditHandler implements QueueHandlerInterface
{
public function topic(): string
{
return 'mall.order.created.audit';
}
public function handle(array $payload): void
{
$orderId = (int)($payload['order_id'] ?? 0);
// 异步审计 / 通知逻辑
}
}
// 4. 插件也可以声明自己的 queue.php(多 Handler 消费同一 topic)
// api/applications/mall/plugins/coupon/queue.php
use Applications\mall\plugins\coupon\queue\CouponOrderCompletedHandler;
return [
'mall.order.completed.followup' => [
CouponOrderCompletedHandler::class,
],
];
扩展点管道
扩展点用于需要同步串联执行并收集返回值的场景。所有注册的处理器按 priority 从小到大依次执行,每个处理器的输出作为下一个的输入。
// 1. 模块调用扩展点
use app\Core\service\extension\ExtensionRegistry;
class CoreOrderService
{
public function calculatePrice(int $userId, float $price, array $goodsItems): array
{
$ctx = [
'user_id' => $userId,
'original_price' => $price,
'final_price' => $price,
'discount' => 0,
'breakdown' => [],
'goods_items' => $goodsItems,
];
$registry = app()->make(ExtensionRegistry::class);
// 管道计算:VIP(10) → Coupon(20) → Points(30)
$result = $registry->execute('order.price.calculate', $ctx);
return $result; // $result['final_price'] 已叠加所有折扣
}
}
// 2. 插件实现处理器(见上方"插件开发"章节的完整示例)
不关心返回值、允许延迟 → Event + Queue(如发券、通知);
不关心返回值、需同步完成 → Event + Listener(如日志记录)。
支付模块:Gateway Driver 模式
设计动机
支付模块需要对接多个第三方支付服务商(微信、支付宝、余额、线下转账),且不同应用的支付逻辑各有不同。为避免在每个应用中重复编写 SDK 对接代码,框架采用 Gateway Driver 模式 实现框架层复用。
分层架构
| 层级 | 组件 | 职责 | 目录 |
|---|---|---|---|
| 应用层 | MallPayBridge | 电商特有业务(余额扣减、订单完成、退款审核) | applications/mall/modules/pay/core/ |
| 框架层 | CorePayOrchestrator | 支付编排(网关注册、分发、路由、可用选项查询) | app/service/domain/pay/ |
| CoreRefundOrchestrator | 退款编排(退款提交、退款回调路由) | ||
| 网关层 | PaymentGatewayInterface | 统一契约(9个方法) | app/service/domain/pay/gateway/ |
| BaseGateway | 抽象基类(金额转换、测试 Mock) | ||
| WechatGateway | 微信支付 SDK 封装 | ||
| AlipayGateway | 支付宝 SDK 封装 | ||
| BalanceGateway | 余额支付(无需 SDK) | ||
| OfflineGateway | 线下转账(无需 SDK) |
核心接口
interface PaymentGatewayInterface
{
public function create(array $payment, array $order, string $terminal, array $context): array;
public function notify(Request $request, string $terminal): array;
public function refund(array $refund, array $payment): array;
public function refundNotify(Request $request, string $terminal): array;
public function provider(): string;
public function name(): string;
public function supportsTerminal(string $terminal): bool;
public function isAllocatable(): bool;
public function successResponse(): array;
}
支付方式配置控制
| Gateway | isAllocatable | 支持终端 | 说明 |
|---|---|---|---|
WechatGateway | true | pc, h5, wechat, weapp, app | 需DB 配置 |
AlipayGateway | true | pc, h5, app, aliapp | 需DB 配置 |
BalanceGateway | false | 全部终端 | 始终可用 |
OfflineGateway | false | 全部终端 | 始终可用 |
开关联动逻辑
channel_profiles.status = 0→ 该终端所有支付方式不可用channel_payment_configs.status = 0→ 该支付方式在该终端不可用Gateway::supportsTerminal() = false→ 该支付方式不支持该终端APP_DEBUG = true→ 自动启用 wechatpay/alipay 测试模式(Mock SDK 边界)
php api/tests/seed_channel_payment.php 插入种子数据,status=1 即启用。
开闭原则:新增支付方式
class StripeGateway extends BaseGateway
{
public function provider(): string { return 'stripe'; }
public function name(): string { return 'Stripe支付'; }
public function supportsTerminal(...): bool { return ...; }
public function isAllocatable(): bool { return true; }
public function create(...): array { /* Stripe SDK */ }
public function notify(...): array { /* 回调验证 */ }
public function refund(...): array { /* 退款 */ }
public function refundNotify(...): array { return []; }
public function successResponse(): array { return []; }
}
$payOrchestrator->register(new StripeGateway(...));
// 无需任何其他修改
应用复用
class MallPayBridge extends BaseCoreService /* 已有 */
{
public function getOrderPayOptions(...) { /* 验证订单 → 编排器 */ }
public function createOrderPayment(...) { /* 支付单 → 扣余额 → 编排器 */ }
}
class EduPayBridge extends BaseCoreService /* 将来 */
{
public function getCoursePayOptions(...) { /* 验证课程 → 同一编排器 */ }
public function createCoursePayment(...) { /* 选课锁定 → 同一编排器 */ }
}
安全过滤:Security Filter
框架层提供 CoreSecurityFilterService,封装微信小程序内容安全接口(msgSecCheck / imgSecCheck),用于用户生成内容(UGC)的文本和图片安全检测。应用通过注入调用接入,开关关闭时零开销。
职责划分
| 角色 | 文件 | 职责 |
|---|---|---|
| 框架层 | api/app/service/domain/security/CoreSecurityFilterService.php | 对接微信 API、开关控制、降级兜底 |
| 应用 | api/applications/mall/modules/content/core/CoreContentService.php | 在内容发布流程中注入调用 |
| 框架层主动调用 | api/app/service/domain/media/CoreMediaService.php | 图片上传时自动检测 |
核心接口
// api/app/service/domain/security/CoreSecurityFilterService.php
class CoreSecurityFilterService extends BaseCoreService
{
// 检查 weapp 渠道是否开启内容安全
public function isEnabled(): bool;
// 文本安全过滤,违规返回 code=1
public function filterText(string $content): array;
// 图片安全过滤,违规返回 code=1
public function filterImage(string $imagePath): array;
}
开关控制
- 管理员在后台 渠道管理 → 小程序设置 中切换
content_security_enabledswitch - 配置存储在 weapp 渠道的
base配置组中 APP_DEBUG = true时自动 Mock,不调用真实微信 API- 微信接口异常时降级放行,不阻塞业务流程
应用集成方式
1. 内容发布时过滤文本(以 mall content 为例):
// api/applications/mall/modules/content/core/CoreContentService.php
use app\service\domain\security\CoreSecurityFilterService;
public function save(array $payload): array
{
// ... 参数校验 ...
// 注入安全过滤
$checkText = $title . ' ' . ((string)($payload['description'] ?? ''));
$securityResult = $this->app->make(CoreSecurityFilterService::class)
->filterText($checkText);
if ($securityResult['code'] !== 0) {
return $securityResult;
}
// ... 写库 ...
}
2. 用户昵称变更时过滤:
$result = $this->app->make(CoreSecurityFilterService::class)
->filterText($newNickname);
if ($result['code'] !== 0) {
return ['code' => 1, 'msg' => $result['msg']];
}
3. 图片上传自动检测:框架层 CoreMediaService::saveUpload() 已内置图片安全检测,上传行为无需额外接入。检测不通过时抛出异常,删除已存储文件。
关键设计决策
| 决策 | 原因 |
|---|---|
| 开关优先检查 | isEnabled() 在前,关闭时零开销跳过所有 HTTP 调用 |
| 异常降级放行 | 微信 API 超时或故障时不阻塞正常业务流程 |
命名使用 security/filter | 避免与应用的 content 模块名歧义 |
返回统一 code / msg 结构 | 应用可直接透传给前端 |
安装说明
api/data/install/framework.sql,应用表结构在各自 api/applications/{app}/data/schema.sql,手动导入即可。框架不提供自动建表或 ORM migrate。
数据库导入
1. api/data/install/framework.sql ← 框架基础表(必导)
2. api/applications/{app}/data/schema.sql ← 应用表结构(在应用根目录下,自行开发的应用也需要符合一样的SQL命名规范)
3. admin/src/applications/{app} ← 后台端应用结构
示例应用安装
框架提供了 sample 示例应用,将示例应用前后端源码分别放到 /applications/sample/ 目录后,在后台"应用管理"中点击安装即可自动注册菜单、权限、路由。
| 步骤 | 操作 |
|---|---|
| 1 | 将示例应用 copy 到 api/applications/sample/ |
| 2 | 导入 api/data/install/framework.sql(如已导入则跳过) |
| 3 | 导入 api/applications/sample/data/schema.sql |
| 4 | 登录后台 → 应用管理 → 点击安装 |
api/applications/{app}/,在后台安装即可。
部署说明
宝塔部署 ThinkPHP 8
1. 宝塔创建站点 - 项目目录:/www/wwwroot/xxx.com/api - 运行目录:/www/wwwroot/xxx.com/api/public - PHP 版本:PHP 8.0 2. PHP 扩展建议 - 必装:pdo_mysql、mbstring、curl、fileinfo、openssl、gd、zip - 使用 Redis 队列时额外安装:redis 3. 安装服务端依赖 cd /www/wwwroot/xxx.com/api composer install --no-dev 4. 数据库配置 编辑 api/.env: [DATABASE] TYPE = mysql HOSTNAME = 127.0.0.1 HOSTPORT = 3306 DATABASE = onezan USERNAME = onezan PASSWORD = your-password CHARSET = utf8mb4 PREFIX = onezan_ 5. 导库(参见上方安装导库章节) 6. 首次检查 - 登录后台检查系统设置 - 菜单、权限、manifest 改动后执行"更新缓存"
伪静态配置
Nginx:
# 后台前端 /admin/ 静态站点
location ^~ /admin/ {
try_files $uri $uri/ /admin/index.html;
}
# 禁止 admin 目录下执行 PHP
location ~ ^/admin/.*\.php$ {
return 403;
}
# 常规静态资源缓存
location ~ .*\.(gif|jpg|jpeg|png|bmp|swf|css|js|woff|woff2|ttf|svg|ico|eot)$
{
expires 30d;
access_log off;
error_log /dev/null;
}
# ThinkPHP 入口
location / {
if (!-e $request_filename) {
rewrite ^(.*)$ /index.php?s=$1 last;
break;
}
}
# PHP 解析 后缀 82 对应 PHP8.2 按你的环境配置对应的
location ~ [^/]\.php(/|$)
{
try_files $uri =404;
fastcgi_pass unix:/tmp/php-cgi-82.sock;
fastcgi_index index.php;
include fastcgi.conf;
include pathinfo.conf;
}
# 禁止访问敏感文件
location ~ /\.(htaccess|git|svn|project|env)
{
return 404;
}
Apache /.htaccess:
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ index.php?s=/$1 [QSA,PT,L]
说明:
- 网站运行目录必须指向 api/public
Redis 与队列设置
1. 队列配置文件 - api/config/queue.php - 控制台命令:php think queue:work 2. 驱动说明 - sync:本地开发或关闭异步时使用 - database:推荐的生产方案 - redis:高并发异步场景使用 3. 后台系统设置中需要维护 - driver / default_queue / retry_delay_seconds - reserve_seconds / max_attempts / work_sleep_seconds 4. 使用 Redis 时补充配置 - host、port、password、select、timeout、persistent、prefix
Supervisor 长驻示例
[program:onezan_queue_default] command=/usr/bin/php /www/wwwroot/xxx.com/api/think queue:work --queue=default --sleep=3 directory=/www/wwwroot/xxx.com/api autostart=true autorestart=true 说明: - 驱动为 sync 或队列未启用时不需要启动 worker - 多队列场景可按 queue 名复制多份 program 配置
生产环境路由缓存
生产环境部署完成后,需编译路由缓存以提升性能: # 编译路由缓存 php think route:cache # 添加/删除/安装/卸载应用后,需重新执行 php think route:cache 说明: - 编译后将所有应用路由合并为单个文件 runtime/route_*.php - 生产环境自动加载缓存,跳过文件系统扫描 - 源文件修改后缓存自动失效(基于 mtime)
Manifest 校验说明
框架会对所有 manifest.json 进行结构校验: - 必填字段(code、name、type)缺失或类型错误 → 抛异常 - debug 模式(APP_DEBUG=true):异常直接抛出,便于开发排查 - 生产模式(APP_DEBUG=false):异常记入 error 日志,不中断运行 开发建议: - 保持 APP_DEBUG=true 直到 manifest 全部修正 - 上线前确认所有应用/模块/插件的 manifest.json 字段完整
前端开发环境
目录:admin/ 1. 安装依赖 npm install 2. 开发配置 文件:admin/.env.development VITE_BASE_URL = / VITE_IS_REQUEST_PROXY = true VITE_API_URL = http://xxx.com VITE_API_URL_PREFIX = /adminapi 3. 本地开发 npm run dev
前端生产打包
1. 生产配置 文件:admin/.env.release VITE_BASE_URL=/admin/ VITE_IS_REQUEST_PROXY=false VITE_API_URL= VITE_API_URL_PREFIX=/adminapi 2. 打包 npm run build 3. 发布产物 - 打包目录:admin/dist/ - 发布目标:api/public/admin/
升级说明
框架升级:api/data/upgrade/framework/YYYYMMDD_xxx.sql
应用升级:api/applications/{app}/data/upgrade/YYYYMMDD_xxx.sql
发生表结构变更时:
1. 同步更新应用 data/schema.sql 基线
2. 补升级 SQL
3. 如涉及菜单/权限/manifest,再更新缓存
技能包使用
项目内置技能包规则,帮助 AI 编码助手按项目结构和规范协同开发。
1. onezan-project-context(项目上下文) 2. onezan-admin-frontend 或 onezan-backend(专项规则) 3. onezan-module(新增模块/插件时) 4. onezan-docs(修改文档时) 5. onezan-delivery-checklist(交付前)
请先加载 onezan-project-context 和 onezan-admin-frontend。 我在 mall 应用新增 notice 页面和 API 接口, 文件放到 applications/mall/pages、applications/mall/api、applications/mall/router, 页面风格与 goods 页面保持一致, 完成后执行前端构建检查。
请先加载 onezan-project-context 和 onezan-backend。 我在 mall 应用新增 notice 模块, 包含 manifest、route、controller、service、core、repository、validate、permissions、menus, 同步 schema.sql 与升级 SQL,最后跑接口验证。
让 AI 编码助手按项目实际结构、UI 风格和交付要求协同开发,不偏离规范目录。