onezan-admin 快速开发框架
壹站 onezan-admin 适用于核心接入行业应用实现多样化可定制、快速交付的场景。项目采用前后端分离结构,内置系统核心功能:用户、渠道多端、权限控制和基础配置能力,可作为新项目的统一底座继续扩展。
/docs/
后端采用平台核心 + 平台基础能力 + 行业应用 + 业务模块 / 扩展插件结构,前端按平台基础能力、业务模块、扩展插件三类后台页面组织。
框架默认内置行业应用为 mall,行业清单位于 api/applications/mall/manifest.json,菜单、权限、消息、设置定义也都在该目录聚合。
框架的开发规范、开发示例与交付要求统一维护在文档中心。
接口要实际运行通过,前端要编译成功,最终交付必须打包成功,不能只停留在静态代码层。
技术栈
项目遵循前后端分离架构,服务端接管业务逻辑与数据持久化,管理后台负责页面渲染与交互,两端通过 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
国际化:vue-i18n 9.x
样式: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/app/Core;“平台基础能力”只指 api/app 中通用控制器、服务、仓储、模型、校验;“行业应用”只指 api/applications/{industry};“业务模块”只指 modules/{module};“扩展插件”指可关闭、可替换、可移除的扩展单元。
| 层级 | 目录 | 职责 |
|---|---|---|
| 平台核心 | api/app/Core |
平台注册中心、聚合器、安装器、菜单在线覆盖、队列核心等能力。 |
| 平台基础能力 | api/app/adminapi/controller、api/app/api/controller、api/app/service、api/app/repository、api/app/model、api/app/validate |
框架公共接口、服务、仓储、模型、校验与中间件。 |
| 行业应用 | api/applications/{industry} |
行业级清单、菜单、权限、消息、设置、桥接路由、行业控制器。 |
| 业务模块 | api/applications/{industry}/modules/{module} |
某一业务模块的 service、core、repository、model、validate、manifest、route。 |
| 扩展插件 | api/applications/{industry}/plugins/{plugin} |
行业内可插拔扩展能力,如优惠券、会员、充值等。 |
| 平台基础能力后台 | admin/src/core/pages、admin/src/core/api |
框架核心页面与平台聚合接口,如管理员、用户、渠道、系统配置、菜单配置。 |
| 业务模块后台 | admin/src/applications/{industry}/pages、admin/src/applications/{industry}/api |
业务模块页面与接口,如商品、内容、订单、财务。 |
| 扩展插件后台 | admin/src/applications/{industry}/plugins/{plugin}/pages、admin/src/applications/{industry}/plugins/{plugin}/api |
扩展插件页面与接口,如优惠券、充值、会员。 |
| 路由层 | admin/src/router/core/entry.ts、admin/src/router/registry.ts、admin/src/applications/{industry}/router/*.ts、admin/src/applications/{industry}/plugins/router.ts |
框架路由组聚合;行业路由文件自动发现;插件路由由各行业自己的 plugins/router.ts 自动收集。 |
| 数据层 | data/install、data/seeds、data/upgrade |
安装 SQL、测试数据、升级 SQL;运行时菜单、权限、消息、设置定义统一由代码与清单文件维护。 |
admin/src/core、admin/src/applications/{industry}、api/app、api/applications/{industry}、data/install 与 data/upgrade。
前端规范
前端的首要目标是让新增页面与框架内置页面保持同一套 UI、布局、按钮、弹窗、提示和交互风格。
平台基础能力页面放 admin/src/core/pages;业务模块页面放 admin/src/applications/{industry}/pages/{biz}(如 applications/mall/pages/goods);扩展插件页面放 admin/src/applications/{industry}/plugins/{plugin}/pages。
平台基础能力接口放 admin/src/core/api;业务模块接口放 admin/src/applications/{industry}/api;扩展插件接口放 admin/src/applications/{industry}/plugins/{plugin}/api。
优先使用 t-card :bordered="false",顶部内联搜索区,下面是表格区,操作列统一用纯文字 t-link。
统一为 查询 重置 主操作。查询按钮主色,重置按钮默认风格,主操作按钮按权限展示。
状态、类型、来源等枚举统一使用 t-tag,轻量边框或浅色填充,不额外造新的视觉体系。
菜单图标用本地 iconfont icon-menu-*;二级菜单不显示图标;列表页操作按钮统一纯文字无图标;卡片展示区仍可用 AppIcon 做装饰。
admin/src/components/ 提供平台级通用组件:AppIcon、UserTwoLine、RegionPicker(省市区三级地区选择,基于 element-china-area-data + t-cascader,行业应用可直接复用)。
命名惯例
| 用途 | 建议命名 | 说明 |
|---|---|---|
| 查询表单 | searchForm |
所有列表页统一。 |
| 列表数据 | tableData / data / listData |
可以三选一,但单文件内必须保持一致。 |
| 列表请求 | fetchList() |
统一入口,页面列表请求统一使用这个命名。 |
| 查询/重置 | handleSearch() / handleReset() |
查询通常重置到第一页后重新拉取。 |
| 分页 | pagination |
统一使用 current、pageSize、total。 |
| 保存 | handleSave() |
弹窗、配置页、编辑页都统一。 |
页面范式
1. 简单 CRUD:列表页 + 弹窗 2. 字段较多:列表页 + edit.vue 独立编辑页 3. 在线配置:顶部说明 + 可编辑表格 + 统一保存按钮 4. 详情展示:抽屉或描述列表,不额外引入陌生交互
行业设置页
行业应用可通过 menus.json 将设置页菜单自动注入到"系统设置"二级菜单下,无需修改核心框架代码。
| 步骤 | 文件 | 说明 |
|---|---|---|
| 1 | api/applications/{industry}/menus.json | 新增菜单项,parent_code 设为 "system"(自动挂载到系统设置下) |
| 2 | admin/src/applications/{industry}/router/settings.ts | 创建路由文件,用 Layout 包裹,path 必须以 / 开头。文件存在即生效,无需单独 entry |
| 3 | admin/src/applications/{industry}/pages/settings/index.vue | 实现设置页组件,建议用 t-tabs 分 Tab 组织配置区 |
parent_code 固定为 "system",path 必须以 / 开头(如 "/mall/settings")。系统会自动将其作为"系统设置"的二级菜单展示,无需修改 Core/manifest/menus.json。
路由模板
// admin/src/applications/{industry}/router/settings.ts
import type { RouteRecordRaw } from 'vue-router';
import Layout from '@/layouts/index.vue';
export default [
{
path: '/{industry}/settings',
name: 'IndustrySettings',
component: Layout,
redirect: '/{industry}/settings/index',
meta: {
title: { zh_CN: '行业设置', en_US: 'Industry Settings' },
icon: 'iconfont iconshezhi1',
orderNo: 50,
permission: 'system.view',
},
children: [
{
path: 'index',
name: 'IndustrySettingsIndex',
component: () => import('@/applications/{industry}/pages/settings/index.vue'),
meta: {
title: { zh_CN: '行业设置', en_US: 'Industry Settings' },
permission: 'system.view',
},
},
],
},
] satisfies RouteRecordRaw[];
行业统计页
行业统计页通过 menus.json 自动注入到"概况"二级菜单下,展示本行业的 KPI 指标、趋势图表与快捷入口。
| 步骤 | 文件 | 说明 |
|---|---|---|
| 1 | api/applications/{industry}/menus.json | 新增菜单项,parent_code 设为 "dashboard"(自动挂载到概况下) |
| 2 | admin/src/applications/{industry}/router/stats.ts | 创建路由文件,与设置页同一模板结构,无需单独 entry |
| 3 | admin/src/applications/{industry}/pages/stats/index.vue | 实现统计页。系统只提供用户、支付金额等框架基础统计,行业自行开发 KPI、趋势图、待办提醒、快捷入口 |
parent_code 固定为 "dashboard",path 必须以 / 开头(如 "/mall/stats")。多个行业应用可以各自挂载统计页到"概况"菜单下。
菜单层级约束
每个 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/{industry}/router/{module}.ts
// 插件聚合入口放 admin/src/applications/{industry}/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/{industry}/router/{module}.ts,插件聚合入口放到 admin/src/applications/{industry}/plugins/router.ts。框架自动 glob 加载,无需任何手动注册。页面自动扫描使用通配符 applications/*/pages/** 与 applications/*/plugins/pages/**。
后端规范
后端开发围绕平台核心放 api/app/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 运行时文件。
框架基础表在 data/install/sys.sql;行业安装基线在 data/install/{industry}/schema.sql;升级 SQL 在 data/upgrade。
使用 api/app、api/app/Core、api/applications/{industry}、data/install 与 data/upgrade。
校验结构
| 层 | 目录 | 职责 |
|---|---|---|
| 格式校验 | 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/{industry}/
├─ 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 industry myindustry // === 创建业务模块(含 CRUD 骨架代码) === php think make module myindustry goods // === 创建扩展插件(含扩展点目录) === php think make plugin myindustry vip
手动创建(了解原理)
// 1. 创建目录
mkdir -p api/applications/myindustry
mkdir -p api/applications/myindustry/modules
mkdir -p api/applications/myindustry/plugins
mkdir -p data/install/myindustry
// 2. 创建 manifest.json
// api/applications/myindustry/manifest.json
{
"code": "myindustry",
"name": "我的行业",
"version": "1.0.0",
"description": "自定义行业应用",
"dependencies": ["core"],
"modules": [],
"plugins": [],
"route_bridges": {
"admin": "applications/myindustry/route/admin.php",
"app": "applications/myindustry/route/app.php"
}
}
// 3. 创建路由桥接
// api/applications/myindustry/route/admin.php
onezan_load_php_tree(array_merge(
glob(__DIR__ . '/modules/*/route/admin.php') ?: [],
glob(__DIR__ . '/plugins/*/route/admin.php') ?: []
));
// 4. 创建安装 SQL
// data/install/myindustry/schema.sql
CREATE TABLE IF NOT EXISTS `onezan_myindustry_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 module list --init | 刷新框架聚合清单,使新行业生效 |
| 📌 | php think make industry {code} | 一键创建行业骨架(推荐,替代步骤 1-2) |
| 📌 | php think make module {行业} {编码} | 一键创建业务模块骨架(推荐,替代步骤 3) |
| 📌 | php think make plugin {行业} {编码} | 一键创建扩展插件骨架(推荐,替代步骤 3) |
插件开发
扩展插件是可关闭、可替换、可移除的扩展单元。它通过事件订阅、扩展点管道和 Provider 接口与业务模块解耦协作。
插件目录结构
api/applications/mall/plugins/my_plugin/ ├─ manifest.json ← 声明插件信息、扩展点、依赖 ├─ event.php ← 插件事件声明(可选) ├─ queue.php ← 插件队列主题声明(可选) ├─ route/admin.php ← 后台路由 ├─ route/app.php ← 前台路由 ├─ adminapi/controller/ ← 后台控制器 ├─ api/controller/ ← 前台控制器 ├─ 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 app\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::group('plugins/my_plugin', function () {
Route::get('config', 'adminapi/controller/Config@index');
});
插件与业务模块的解耦规则
| 通信方式 | 方向 | 机制 | 适用场景 |
|---|---|---|---|
| ExtensionPoint | 业务模块 → 插件 | 同步管道,按优先级串联执行,收集返回值 | 价格计算、折扣叠加、运费计算 |
| Event + Queue | 业务模块 → 插件 | 异步投递,不阻塞主流程 | 下单后发券、支付后通知、数据上报 |
| Provider 接口 | 插件 → 业务模块 | 插件通过接口调用业务模块的读写能力 | 插件查询用户等级、获取商品列表 |
| API 直连 | 前端 → 插件后台 | 插件独立的后台 CRUD 接口 | 插件自身的配置、列表、管理功能 |
通信处理
框架提供三种通信机制:事件(Event)、队列(Queue)和扩展点(ExtensionPoint),分别对应异步投递、异步消费和同步管道三种模式。
三种机制对比
| 特性 | 事件(Event) | 队列(Queue) | 扩展点(ExtensionPoint) |
|---|---|---|---|
| 执行方式 | 同步触发 Listener | 异步消费 Job | 同步 Pipeline |
| 阻塞主流程 | 是(Listener 内) | 否(独立进程) | 是(串联执行) |
| 返回值 | 不关心 | 不关心 | 收集并聚合 |
| 优先级 | 无 | 按入队顺序 | 按 priority 排序 |
| 典型场景 | 日志、缓存清理 | 发券、通知、上报 | 价格计算、数据聚合 |
事件投递
业务模块写完数据库后抛出事件,Listener 接收后决定是同步处理还是投递队列。
// 1. 定义事件类(可选,按需定义)
// api/app/event/order/OrderPaid.php
namespace app\event\order;
class OrderPaid
{
public int $orderId;
public array $orderData;
}
// 2. 在 Core Service 中触发事件
use app\event\order\OrderCompleted;
class CoreOrderService
{
public function complete(int $orderId): void
{
// 1. 写库:更新订单状态
$this->repository->markCompleted($orderId);
// 2. 抛事件:通知关注方(不阻塞主流程的后续逻辑在此投递队列)
$order = $this->repository->getFull($orderId);
app()->event->trigger(new OrderCompleted($orderId, $order));
}
}
队列消费
Listener 接收事件后通过 QueueDispatcher 投递队列,队列 handler 异步执行耗时逻辑。框架支持一个 topic 对应多个 handler(multi-cast),并通过声明式 queue.php 自动装载。
// 1. 定义 Listener
// api/app/listener/order/OrderCompletedListener.php
namespace app\listener\order;
use app\event\order\OrderCompleted;
use app\Core\service\queue\QueueDispatcher;
class OrderCompletedListener
{
public function handle(OrderCompleted $event): void
{
// 投递到队列,异步处理
app()->make(QueueDispatcher::class)->dispatch(
'order.completed.followup',
['order_id' => $event->orderId, 'data' => $event->orderData],
['biz_key' => 'order.completed:' . $event->orderId]
);
}
}
// 2. 插件中声明 queue.php
// api/applications/mall/plugins/coupon/queue.php
use Applications\mall\plugins\coupon\queue\CouponOrderCompletedHandler;
return [
'order.completed.followup' => [
CouponOrderCompletedHandler::class,
],
];
// 3. 插件中定义队列 handler
// api/applications/mall/plugins/coupon/queue/CouponOrderCompletedHandler.php
namespace Applications\mall\plugins\coupon\queue;
use app\Core\service\queue\contract\QueueHandlerInterface;
class CouponOrderCompletedHandler implements QueueHandlerInterface
{
public function topic(): string
{
return 'order.completed.followup';
}
public function handle(array $payload): void
{
$orderId = (int)($payload['order_id'] ?? 0);
// 异步发券逻辑
$this->grantCouponAfterCompleted($orderId);
}
}
扩展点管道
扩展点用于需要同步串联执行并收集返回值的场景。所有注册的处理器按 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/core/pay/ |
| CoreRefundOrchestrator | 退款编排(退款提交、退款回调路由) | ||
| 网关层 | PaymentGatewayInterface | 统一契约(7个方法) | app/service/core/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/core/security/CoreSecurityFilterService.php | 对接微信 API、开关控制、降级兜底 |
| 行业应用 | api/applications/mall/modules/content/core/CoreContentService.php | 在内容发布流程中注入调用 |
| 平台层主动调用 | api/app/service/core/media/CoreMediaService.php | 图片上传时自动检测 |
核心接口
// api/app/service/core/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\core\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 结构 | 行业应用可直接透传给前端 |
开发示例
下面的示例全部来自项目实际做法,目的是让新增代码与现有结构和风格保持一致。
前端列表页示例
// 文件:admin/src/applications/mall/pages/goods/index.vue
<template>
<t-card :bordered="false">
<div class="table-container">
<t-form class="search-form" :data="searchForm" layout="inline" @submit="handleSearch">
<t-form-item name="keyword" label="名称/编号">
<t-input v-model="searchForm.keyword" placeholder="请输入商品名称/编号" />
</t-form-item>
<t-form-item name="status" label="状态">
<t-select v-model="searchForm.status" placeholder="请选择状态" clearable>
<t-option :value="1" label="上架" />
<t-option :value="0" label="下架" />
</t-select>
</t-form-item>
<t-form-item>
<t-button theme="primary" type="submit">
<AppIcon name="iconfont icon-action-search" class="app-icon-gap" />
查询
</t-button>
<t-button theme="default" variant="base" @click="handleReset">
<AppIcon name="iconfont icon-action-refresh" class="app-icon-gap" />
重置
</t-button>
<t-button class="search-action-btn" @click="handleAdd">
<AppIcon name="iconfont icon-action-add" class="app-icon-gap" />
新增商品
</t-button>
</t-form-item>
</t-form>
<t-table
:data="data"
:columns="columns"
:pagination="pagination"
:loading="dataLoading"
row-key="id"
hover
@page-change="handlePageChange"
>
<template #status="{ row }">
<t-tag v-if="row.status === 1" theme="success" variant="light">上架</t-tag>
<t-tag v-else theme="danger" variant="light">下架</t-tag>
</template>
<template #op="{ row }">
<t-link theme="primary" hover="color" @click="handleEdit(row)">
<AppIcon name="iconfont icon-action-edit" class="app-icon-gap" />
编辑
</t-link>
<t-popconfirm content="确认删除吗?" @confirm="handleDelete(row)">
<t-link theme="danger" hover="color" class="op-link">删除</t-link>
</t-popconfirm>
</template>
</t-table>
</div>
</t-card>
</template>
<script setup lang="ts">
import { reactive, ref } from 'vue';
import AppIcon from '@/components/AppIcon.vue';
const data = ref([]);
const dataLoading = ref(false);
const pagination = reactive({ current: 1, pageSize: 10, total: 0 });
const searchForm = reactive({ keyword: '', status: undefined as number | undefined });
const handleSearch = () => {};
const handleReset = () => {};
const handleAdd = () => {};
const handleEdit = (_row: unknown) => {};
const handleDelete = (_row: unknown) => {};
const handlePageChange = () => {};
</script>
前端配置页示例
// 文件:admin/src/core/pages/system/menu-config.vue
<template>
<div class="page-admin-system-menu">
<t-card class="search-form" :bordered="false" title="菜单配置">
<t-alert
theme="info"
message="左侧主栏优先显示简称,页面标题、面包屑和权限分配继续使用完整名称。保存后刷新页面即可生效。"
/>
</t-card>
<t-card class="table-container" :bordered="false">
<template #actions>
<t-space>
<t-button theme="primary" @click="handleSave">保存配置</t-button>
<t-button theme="default" variant="outline" @click="handleRefreshCache">更新缓存</t-button>
</t-space>
</template>
<t-table row-key="code" :data="tableData" :columns="tableColumns" :loading="loading">
<template #editableTitles="{ row }">
<div class="edit-cell">
<t-input v-model="row.title_short" size="small" maxlength="12" placeholder="主栏简称" />
<t-input v-model="row.title_full" size="small" maxlength="30" placeholder="完整名称" />
</div>
</template>
<template #status="{ row }">
<t-switch :model-value="row.status === 1" @change="(value) => handleToggle(row, 'status', value)" />
</template>
</t-table>
</t-card>
</div>
</template>
前端 API 示例
// 文件:admin/src/applications/mall/api/goods.ts
import { request } from '@/utils/request';
export interface GoodsRow {
id: number;
goods_no?: string;
name?: string;
status?: number;
}
export function getGoodsList(params: Recordable<unknown>) {
return request.get(
{
url: '/goods/list',
params,
},
{
isTransformResponse: false,
},
);
}
export function deleteGoods(id: number) {
return request.post(
{
url: '/goods/delete',
data: { id },
},
{
isTransformResponse: false,
},
);
}
后端路由与控制器示例
以商品保存接口为例,展示从路由到数据层的完整调用链路。
1. 路由定义
// api/applications/mall/modules/goods/route/admin.php
Route::post('goods/save', 'Applications\\mall\\adminapi\\controller\\goods\\Goods@save')
->middleware(\app\middleware\AdminPermission::class, 'goods.save');
2. 控制器 — 入参校验 + 业务校验 + 调服务
// api/applications/mall/adminapi/controller/goods/Goods.php
public function save(Request $request)
{
$params = $request->post();
$this->validate($params, GoodsValidator::class . '.save');
$this->validateDomain(
$this->app->make(GoodsDomainValidator::class), $params, 'save'
);
return json($this->app->make(GoodsService::class)->save($request));
}
3. 应用服务 — 收口参数,调核心服务
// api/applications/mall/modules/goods/service/admin/GoodsService.php
public function save(Request $request): array
{
// 可在此做幂等性防重、图片路径标准化等应用层收口
$result = $this->core()->save([
'id' => (int)$request->post('id', 0),
'name' => $request->post('name', ''),
'status' => $request->post('status', 1),
'skus' => $request->post('skus/a', []),
'banners' => $this->normalizeImageArray(
$request->post('banners/a', [])
),
// ... 其余参数收口
]);
return $result;
}
private function core(): CoreGoodsService
{
return $this->app->make(CoreGoodsService::class);
}
4. 核心服务 — 写流程、事务、状态流转
// api/applications/mall/modules/goods/core/CoreGoodsService.php
public function save(array $payload): array
{
$id = (int)($payload['id'] ?? 0);
$name = trim((string)($payload['name'] ?? ''));
if ($name === '') {
return ['code' => 1, 'msg' => '商品名称不能为空'];
}
$savePayload = [
'name' => $name,
'status' => (int)($payload['status'] ?? 1) ? 1 : 0,
];
if ($id > 0) {
$goods = $this->repository()->findById($id);
if (!$goods) {
return ['code' => 1, 'msg' => '商品不存在'];
}
$this->repository()->save($goods, $savePayload);
} else {
$savePayload['goods_no'] = $this->generateGoodsNo();
$goods = $this->repository()->create($savePayload);
}
// 重建子表数据,收口最终销售价
$this->saveSkus((int)$goods->id, $payload);
$this->syncGoodsPriceFromSkus((int)$goods->id, $goods);
return ['code' => 0, 'msg' => '保存成功', 'data' => ['id' => (int)$goods->id]];
}
5. 模型 — 映射数据表
// api/applications/mall/modules/goods/model/Goods.php
class Goods extends Model
{
protected $name = 'mall_goods';
protected $autoWriteTimestamp = true;
protected $createTime = 'create_time';
protected $updateTime = 'update_time';
}
6. 仓储 — 数据库访问封装
// api/applications/mall/modules/goods/repository/GoodsRepository.php
public function findById(int $id): ?Goods
{
$goods = Goods::find($id);
return $goods instanceof Goods ? $goods : null;
}
public function create(array $payload): Goods
{
return Goods::create($payload);
}
public function save(Goods $goods, array $payload = []): void
{
if ($payload) {
$goods->save($payload);
return;
}
$goods->save();
}
整条链路自上而下:路由(URL → 中间件鉴权)→ 控制器(入参校验 + 业务校验)→ 应用服务(参数收口、幂等防重)→ 核心服务(写流程 + 事务 + 状态流转)→ 仓储(数据库访问封装)→ 模型(映射数据表)→ 返回。只做简单读接口时,可省略应用服务或核心服务,由控制器直接调仓储。
业务模块目录示例
最小必要目录: api/applications/mall/modules/goods/ ├─ manifest.json # 业务模块清单,声明编码、依赖、路由桥接 ├─ route/admin.php # 后台接口路由 ├─ route/app.php # 前台接口路由 ├─ service/ │ ├─ admin/GoodsService.php # 后台应用服务 │ └─ api/GoodsService.php # 前台应用服务 ├─ core/CoreGoodsService.php # 核心写流程、事务、状态流转 ├─ repository/GoodsRepository.php ├─ model/Goods.php └─ validate/ ├─ admin/GoodsValidator.php ├─ api/GoodsValidator.php └─ domain/GoodsDomainValidator.php 说明: 1. 上面是“最小必要结构”,不是要求一次把所有扩展文件全部建满。 2. `manifest.json` 的作用是声明业务模块编码、依赖关系、路由桥接与聚合元数据,便于行业应用统一注册、统一识别、统一维护。 3. `manifest.json` 更适合以下场景: - 一个行业应用内存在多个业务模块,需要统一注册与聚合; - 后续可能做模块级启停、备份、迁移、清单扫描; - 需要把模块边界、依赖和入口描述清楚,增强规范能力。 4. `manifest.json` 不是所有小功能都必须强制补齐;如果只是行业应用内偶尔出现的简单能力,且不涉及独立解耦、独立治理、独立交付,可以先按普通业务目录实现。 5. 只有当你希望这个能力具备更清晰的边界、更好的可维护性、或者后续适合做解耦治理时,再补业务模块清单会更有价值。 6. 只做读接口时,可保留 manifest、route、service、repository、model、validate;涉及写流程、事务、状态流转时,再补 core/CoreGoodsService.php。
交付要求
代码交付不是改完文件就结束,而是必须通过实际运行质量门槛。
只要本轮涉及后台前端,必须通过 npm run lint、npm run stylelint、npm run build:type、npm run build。
只要本轮涉及接口,必须在 PHP 8.0以上环境下实际测试请求验证,不能只做静态分析。
至少覆盖一个成功场景;写接口还应覆盖一个失败或校验场景,确认不是 404/500,且权限、校验、事务行为符合预期。
目录变更、接口变更、安装方式变更、AI skill 变更后,文档中心与相关 README,必须同步维护更新。
交付检查清单
1. 改动文件重新阅读关键片段 2. 清理调试输出和占位代码 3. 跑前端 lint / stylelint / build:type / build 4. 跑后端真实接口验证 5. 检查文档、接口中心、skills 口径一致 6. 确认最终打包成功后再正式交付
安装与导库
数据目录承担安装与升级职责;运行时菜单、权限、消息、设置定义统一由代码与清单文件维护。
| 文件 | 用途 | 适用场景 |
|---|---|---|
data/install/sys.sql |
框架基础表 | 所有新环境必导。 |
data/install/mall/schema.sql |
mall 行业正式安装基线 | 新环境标准导入方案。 |
data/install/mall/full.sql |
mall 完整表 | 需要完整基线但不带 H5 测试数据时使用。 |
data/install/mall/full_with_h5_seed.sql |
mall 完整表 + H5 测试数据 | 需要一份带测试数据的完整导入包时使用。 |
data/seeds/test_data_h5.sql |
附加测试数据 | 在已有基线库上追加导入时使用。 |
标准导入顺序
方案 A:标准新环境 1. data/install/sys.sql 2. data/install/mall/schema.sql 方案 B:完整表 + H5 测试数据 1. data/install/sys.sql 2. data/install/mall/full_with_h5_seed.sql
宝塔部署 ThinkPHP 8
1. 宝塔创建站点 - 项目目录:/www/wwwroot/onezan.test/api - 运行目录:/www/wwwroot/onezan.test/api/public - PHP 版本:PHP 8.0 2. PHP 扩展建议 - 必装:pdo_mysql、mbstring、curl、fileinfo、openssl、gd、zip - 使用 Redis 队列时额外安装:redis 3. 安装服务端依赖 cd /www/wwwroot/onezan.test/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 5. 导库 - 标准环境:data/install/sys.sql + data/install/mall/schema.sql - 测试环境:data/install/sys.sql + data/install/mall/full_with_h5_seed.sql 6. 首次检查 - 登录后台检查系统设置 - 菜单、权限、manifest 改动后执行“更新缓存”
伪静态配置
Nginx:
location / {
try_files $uri $uri/ /index.php?$query_string;
}
Apache /.htaccess:
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ index.php?s=/$1 [QSA,PT,L]
说明:
- 网站运行目录必须指向 api/public
- 站点根目录下的 /admin/、/docs/、/apidoc/ 都从 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 5. 宝塔准备项 - 安装 Redis 服务 - PHP 8.0 安装 redis 扩展 - 保存系统设置后再启动 queue worker
Supervisor 长驻示例
[program:onezan_queue_default] command=/usr/bin/php /www/wwwroot/onezan.test/api/think queue:work --queue=default --sleep=3 directory=/www/wwwroot/onezan.test/api autostart=true autorestart=true startsecs=5 startretries=3 user=www redirect_stderr=true stdout_logfile=/www/wwwlogs/onezan_queue_default.log stdout_logfile_maxbytes=20MB stdout_logfile_backups=10 stopwaitsecs=60 说明: - 在宝塔 Supervisor 或守护进程中新增这份配置 - 驱动为 sync 或队列未启用时,不需要启动 worker - 多队列场景可按 queue 名复制多份 program 配置
前端开发环境
目录:admin/ 1. 安装依赖 npm install 2. 开发配置 文件:admin/.env.development VITE_BASE_URL = / VITE_IS_REQUEST_PROXY = true VITE_API_URL = http://onezan.test VITE_API_URL_PREFIX = /adminapi 3. 本地开发 npm run dev 4. 说明 - vite.config.ts 默认把 /adminapi 代理到 http://127.0.0.1:3000/ - 如本地宝塔站点不是该地址,按实际环境调整代理目标
前端生产打包与发布
1. 生产配置 文件:admin/.env.release VITE_BASE_URL=/admin/ VITE_IS_REQUEST_PROXY=false VITE_API_URL= VITE_API_URL_PREFIX=/adminapi 2. 打包 cd admin npm run build 3. 发布产物 - 打包目录:admin/dist/ - 发布目标:api/public/admin/ 4. 交付前检查 npm run lint npm run stylelint npm run build:type npm run build
升级说明
框架升级:data/upgrade/framework/YYYYMMDD_xxx.sql 行业升级:data/upgrade/mall/YYYYMMDD_xxx.sql 平台基础能力升级(安装时执行):api/install/xxx.sql(如 api/install/points.sql 用户积分系统) 发生表结构变更时: 1. 同步安装基线 2. 补升级 SQL 3. 如涉及菜单/权限/manifest,再更新缓存
开发资源
/docs/:目录结构、开发规范、开发示例、安装方式、交付要求。
/apidoc/:前台与后台接口说明、请求示例、响应示例、在线调试入口。
/assets/iconfont/:菜单图标和业务图标名称查询入口,前端图标以这里和本地 iconfont 为准。
data/README.md:安装 SQL、完整表、种子数据、升级 SQL 的说明入口。
技能包使用
项目内置技能包规则,帮助 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 风格和交付要求协同开发,不偏离规范目录。