onezan-admin 文档中心 项目整体架构、目录结构、开发规范、开发示例、安装方式与交付要求的统一入口
项目开发文档 接口中心 图标文档 后台
文档中心 总体说明 项目概览

onezan-admin 快速开发框架

壹站 onezan-admin 适用于核心接入行业应用实现多样化可定制、快速交付的场景。项目采用前后端分离结构,内置系统核心功能:用户、渠道多端、权限控制和基础配置能力,可作为新项目的统一底座继续扩展。

开发文档
docs /docs/
服务端:ThinkPHP 8 后台端:Vue 3 + TypeScript + TDesign 统一入口:/docs /apidoc /admin
整体架构

后端采用平台核心 + 平台基础能力 + 行业应用 + 业务模块 / 扩展插件结构,前端按平台基础能力、业务模块、扩展插件三类后台页面组织。

内置行业

框架默认内置行业应用为 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/controllerapi/app/api/controllerapi/app/serviceapi/app/repositoryapi/app/modelapi/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/pagesadmin/src/core/api 框架核心页面与平台聚合接口,如管理员、用户、渠道、系统配置、菜单配置。
业务模块后台 admin/src/applications/{industry}/pagesadmin/src/applications/{industry}/api 业务模块页面与接口,如商品、内容、订单、财务。
扩展插件后台 admin/src/applications/{industry}/plugins/{plugin}/pagesadmin/src/applications/{industry}/plugins/{plugin}/api 扩展插件页面与接口,如优惠券、充值、会员。
路由层 admin/src/router/core/entry.tsadmin/src/router/registry.tsadmin/src/applications/{industry}/router/*.tsadmin/src/applications/{industry}/plugins/router.ts 框架路由组聚合;行业路由文件自动发现;插件路由由各行业自己的 plugins/router.ts 自动收集。
数据层 data/installdata/seedsdata/upgrade 安装 SQL、测试数据、升级 SQL;运行时菜单、权限、消息、设置定义统一由代码与清单文件维护。
注意 项目目录创建规范使用 admin/src/coreadmin/src/applications/{industry}api/appapi/applications/{industry}data/installdata/upgrade

前端规范

前端的首要目标是让新增页面与框架内置页面保持同一套 UI、布局、按钮、弹窗、提示和交互风格。

页面放置

平台基础能力页面放 admin/src/core/pages;业务模块页面放 admin/src/applications/{industry}/pages/{biz}(如 applications/mall/pages/goods);扩展插件页面放 admin/src/applications/{industry}/plugins/{plugin}/pages

API 放置

平台基础能力接口放 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/ 提供平台级通用组件:AppIconUserTwoLineRegionPicker(省市区三级地区选择,基于 element-china-area-data + t-cascader,行业应用可直接复用)。

命名惯例

用途 建议命名 说明
查询表单 searchForm 所有列表页统一。
列表数据 tableData / data / listData 可以三选一,但单文件内必须保持一致。
列表请求 fetchList() 统一入口,页面列表请求统一使用这个命名。
查询/重置 handleSearch() / handleReset() 查询通常重置到第一页后重新拉取。
分页 pagination 统一使用 currentpageSizetotal
保存 handleSave() 弹窗、配置页、编辑页都统一。

页面范式

1. 简单 CRUD:列表页 + 弹窗
2. 字段较多:列表页 + edit.vue 独立编辑页
3. 在线配置:顶部说明 + 可编辑表格 + 统一保存按钮
4. 详情展示:抽屉或描述列表,不额外引入陌生交互

行业设置页

行业应用可通过 menus.json 将设置页菜单自动注入到"系统设置"二级菜单下,无需修改核心框架代码。

步骤文件说明
1api/applications/{industry}/menus.json新增菜单项,parent_code 设为 "system"(自动挂载到系统设置下)
2admin/src/applications/{industry}/router/settings.ts创建路由文件,用 Layout 包裹,path 必须以 / 开头。文件存在即生效,无需单独 entry
3admin/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 指标、趋势图表与快捷入口。

步骤文件说明
1api/applications/{industry}/menus.json新增菜单项,parent_code 设为 "dashboard"(自动挂载到概况下)
2admin/src/applications/{industry}/router/stats.ts创建路由文件,与设置页同一模板结构,无需单独 entry
3admin/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/**

底部导航动态菜单

框架提供一套可后台管理的 C 端底部导航能力,适用于需要由后台统一下发导航配置的用户端页面。是否使用动态菜单由前端页面自行决定;如果某些页面需要保持固定样式、固定入口或独立交互,也可以继续使用静态菜单,不接后台动态。

后台配置入口

管理员在 渠道管理 → 底部导航 中维护导航方案,在 渠道管理 → 链接管理 中维护可复用链接。导航方案支持按渠道生效,也支持不选渠道作为通用配置。

前端接入原则

动态菜单只负责下发 图标 / 文案 / 跳转 / 颜色 / 展示类型。页面布局、占位高度、浮动样式、胶囊形态等仍由前端组件控制。需要固定样式的页面可保留静态菜单。

相关接口

接口 用途 说明
GET /api/sys/channel/bottom-nav 前台获取底部导航方案 channel 返回当前终端可用的导航方案列表;可传 id 只取指定方案。
GET /adminapi/sys/channel/bottom-nav 后台读取底部导航中心配置 返回 linksmenuslink_types,供后台列表页和编辑页使用。
POST /adminapi/sys/channel/save/bottom-nav 后台保存底部导航中心配置 统一提交导航方案和链接引用关系。

方案一:直接使用后台动态菜单

适用于需要由后台统一控制导航图标、文案、链接和展示类型的页面。推荐做法是页面进入时先请求动态菜单,拿到数据后再渲染底部导航,避免先显示静态菜单再切成动态菜单的闪动问题。

// composables/use-channel-bottom-nav.ts
import { ref } from 'vue';
import { sysApi } from '@/api';
import type { BottomNavItemData, BottomNavStyleData } from '@/api';

export function useChannelBottomNav() {
  const navItems = ref<BottomNavItemData[]>([]);
  const navStyle = ref<BottomNavStyleData | undefined>(undefined);
  const navReady = ref(false);

  const resolveChannel = () => {
    // #ifdef MP-WEIXIN
    return 'weapp';
    // #endif
    // #ifdef H5
    return /MicroMessenger/i.test(window.navigator.userAgent) ? 'wechat' : 'h5';
    // #endif
    return 'h5';
  };

  const loadBottomNav = async (menuId = 0) => {
    try {
      const res = await sysApi.getBottomNavMenus(resolveChannel(), menuId || undefined);
      if (res.code === 0) {
        const menus = Array.isArray(res.data?.menus) ? res.data.menus : [];
        const current = menus[0];
        navItems.value = Array.isArray(current?.items) ? current.items : [];
        navStyle.value = current?.style;
      }
    } finally {
      navReady.value = true;
    }
  };

  return { navItems, navStyle, navReady, loadBottomNav };
}
<script setup lang="ts">
import { onShow } from '@dcloudio/uni-app';
import AppBottomNav from '@/components/app/AppBottomNav.vue';
import { useChannelBottomNav } from '@/composables/use-channel-bottom-nav';

const { navItems, navStyle, navReady, loadBottomNav } = useChannelBottomNav();

onShow(() => {
  loadBottomNav();
});
</script>

<template>
  <AppBottomNav
    v-if="navReady && navItems.length"
    :items="navItems"
    :style-config="navStyle"
    active-key="index"
    theme="green"
  />
</template>
说明 AppBottomNav 只消费后台下发的数据,不负责请求接口。推荐由页面层决定是否启用动态菜单,并在数据准备完成后再渲染组件。

方案二:不使用动态菜单,但直接调用接口自行渲染

适用于自定义 C 端项目。你可以完全不用框架提供的 AppBottomNav,只调用接口拿纯数据,自己按项目视觉体系渲染。

import { sysApi } from '@/api';

const loadMenus = async () => {
  const res = await sysApi.getBottomNavMenus('h5');
  if (res.code !== 0) return [];
  return res.data?.menus || [];
};

const menus = await loadMenus();
const currentMenu = menus[0];
// currentMenu.style.display_type: text / icon / icon_text
// currentMenu.items[n].label
// currentMenu.items[n].default_icon
// currentMenu.items[n].active_icon
// currentMenu.items[n].link
<template>
  <view class="custom-nav">
    <view
      v-for="item in menu.items"
      :key="item.link?.id || item.label"
      class="custom-nav__item"
      @tap="handleNavTap(item)"
    >
      <image :src="isActive(item) ? item.active_icon : item.default_icon" mode="aspectFit" />
      <text>{{ item.label }}</text>
    </view>
  </view>
</template>

方案三:保留静态菜单

适用于需要固定入口、固定样式或独立交互逻辑的页面。这类页面可以完全不接动态菜单,继续手写静态菜单项。

<template>
  <AppBottomNav
    v-if="!isPublicMode"
    :items="bottomTabs"
    active-key="clan-home"
    theme="clan"
    floating
  />
</template>

<script setup lang="ts">
const bottomTabs = [
  { key: 'nav-home', label: '首页', icon: 'jiazu', activeIcon: 'jiazu', url: '/pages/index/index', mode: 'switchTab' },
  { key: 'nav-center', label: '中心', icon: 'zupu', activeIcon: 'zupu', url: '/pages/custom/center', mode: 'navigateTo' },
  { key: 'nav-list', label: '列表', icon: 'dangan1', activeIcon: 'dangan1', url: '/pages/custom/list', mode: 'navigateTo' },
  { key: 'nav-message', label: '消息', icon: 'zaixiankefu', activeIcon: 'zaixiankefu', url: '/pages/custom/message', mode: 'navigateTo' },
];
</script>
边界 是否动态管理由页面自行决定。框架只提供后台配置能力和公开接口,不强制所有页面都切成动态菜单。需要固定业务入口的页面,保留静态菜单是合理做法。

后端规范

后端开发围绕平台核心放 api/app/Core,平台基础能力放 api/app,行业应用、业务模块与扩展插件放 api/applications

固定分层

Controller 应用 Service Core Service Repository Model DB。复杂查询下沉 Repository,复杂写入下沉 Core Service。

写操作校验

写操作必须执行格式校验 + 领域校验;读操作至少做格式校验。不要在控制器和 Service 里堆零散 if。

路由组织

平台基础能力路由走 api/route/admin/*.phpapi/route/app/*.php;业务模块与扩展插件使用各自目录下的 route/admin.phproute/app.php

更新缓存

菜单、权限、消息、设置、manifest 改动后,通过 POST /adminapi/sys/platform/sync 或后台更新缓存刷新聚合结果,并清理 api/runtime/cacheapi/runtime/log 运行时文件。

安装 SQL

框架基础表在 data/install/sys.sql;行业安装基线在 data/install/{industry}/schema.sql;升级 SQL 在 data/upgrade

规范目录

使用 api/appapi/app/Coreapi/applications/{industry}data/installdata/upgrade

校验结构

目录 职责
格式校验 api/app/validate/adminapi/app/validate/api 以及业务模块 / 扩展插件自身 validate/adminvalidate/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           ← 插件队列声明(可选)

快速开始:一行命令创建行业应用

推荐方式 使用 CLI 脚手架一键生成完整骨架,包含后端控制器/Service/Repository/Model/Validator + 前端页面/API/路由,省去手动创建目录和文件的步骤。
// === 创建行业应用(后端 + 前端 全部骨架) ===
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.phproute/app.php 是本行业的总入口
3添加业务模块或插件modules/plugins/ 下按规范创建
4同步菜单权限修改 menus.jsonpermissions.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 接口 插件自身的配置、列表、管理功能
核心原则 业务模块不 import 插件代码。插件通过扩展点、事件、Provider 接口与业务模块协作,确保可插拔。

通信处理

框架提供三种通信机制:事件(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. 插件实现处理器(见上方"插件开发"章节的完整示例)
选择建议 需要返回值且串联执行 → ExtensionPoint(如价格计算);
不关心返回值、允许延迟 → Event + Queue(如发券、通知);
不关心返回值、需同步完成 → Event + Listener(如日志记录)。

支付模块:Gateway Driver 模式

设计动机

支付模块需要对接多个第三方支付服务商(微信、支付宝、余额、线下转账),且不同行业应用的支付逻辑各有不同。为避免在每个行业应用中重复编写 SDK 对接代码,框架采用 Gateway Driver 模式 实现平台层复用。

核心收益:新增支付方式只需新增一个 Gateway 类并注册到编排器,无需修改任何已有代码(开闭原则)。

分层架构

层级组件职责目录
行业层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;
}

支付方式配置控制

GatewayisAllocatable支持终端说明
WechatGatewaytruepc, h5, wechat, weapp, appDB 配置
AlipayGatewaytruepc, h5, app, aliappDB 配置
BalanceGatewayfalse全部终端始终可用
OfflineGatewayfalse全部终端始终可用

开关联动逻辑

  1. channel_profiles.status = 0 → 该终端所有支付方式不可用
  2. channel_payment_configs.status = 0 → 该支付方式在该终端不可用
  3. Gateway::supportsTerminal() = false → 该支付方式不支持该终端
  4. 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_enabled switch
  • 配置存储在 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 lintnpm run stylelintnpm run build:typenpm 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 风格和交付要求协同开发,不偏离规范目录。