阅读说明
这份文档面向下载开源版并独立部署云团的开发者和运营者。你可以把云团理解为一个可二次开发的应用运营平台:主系统负责登录、权限、上传、应用管理、订单和应用市场;每个业务应用放在 addons 目录下独立维护。
商家自研应用与平台应用使用同一套模块规范,但来源不同。商家自研应用属于本地站点资产,不需要远程平台授权;平台应用需要从平台应用市场购买或由平台授权后同步。
面向开源版独立部署的完整实施与自研应用开发指南,从环境安装、后台初始化、官方服务绑定,到 addons 应用模块、接口、权限、前端构建和发布打包。
这份文档面向下载开源版并独立部署云团的开发者和运营者。你可以把云团理解为一个可二次开发的应用运营平台:主系统负责登录、权限、上传、应用管理、订单和应用市场;每个业务应用放在 addons 目录下独立维护。
商家自研应用与平台应用使用同一套模块规范,但来源不同。商家自研应用属于本地站点资产,不需要远程平台授权;平台应用需要从平台应用市场购买或由平台授权后同步。
fileinfo、openssl、pdo_mysql、mbstring、curl、zip 扩展。vendor,首次访问安装器前需要先在项目根目录执行 composer install --no-dev --optimize-autoloader。utf8mb4。public 目录。runtime、public/uploads、addons 以及应用发布目录。解压开源版后先进入项目根目录安装 PHP 依赖。安装完成并确认 vendor/autoload.php 存在后,再配置站点和打开安装器。
composer install --no-dev --optimize-autoloader
开源版交付包会在 public/install.php 提供首次安装入口,并把可二次开发的商家后台、自定义首页应用前端源码放在 public/source 下。首次部署时先把站点运行目录指向项目的 public 目录,再访问 /install.php,按页面提示填写数据库、当前域名、站点名称和后台管理员账号。
安装器会创建数据库、导入 database/install.sql、写入开源版 .env、创建后台超级管理员,并生成本地默认运营主体。
安装成功后,public/install.php 会自动删除。若服务器权限导致删除失败,应手动确认该文件不再对外开放。
开源版的官方服务地址固定为 https://x.bot100.cn,安装器不会提供平台地址输入框;安装完成后进入 /admin,在商家绑定页面完成官方账号绑定。业务平台则在平台管理页面按需创建,购买或授权应用不会自动创建平台。
如果服务器不能使用 Web 安装器,也可以沿用手动导入方式。先创建数据库,再导入安装脚本。数据库名可以按实际环境调整,默认示例为 yuntuan。
CREATE DATABASE `yuntuan` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
导入项目中的安装脚本:
mysql -uroot -p yuntuan < database/install.sql
完整源码或开发环境仍可使用命令行安装器:
php tools/install.php --install --db-host=127.0.0.1 --db-name=yuntuan --db-user=root --db-pass=your_password
手动安装时在项目根目录维护 .env。独立部署开源版至少需要配置数据库、发行版本、授权密钥和固定官方服务地址。
APP_DEBUG = false
DB_TYPE = mysql
DB_HOST = 127.0.0.1
DB_NAME = yuntuan
DB_USER = root
DB_PASS = your_password
DB_PORT = 3306
DB_CHARSET = utf8mb4
DB_PREFIX = yt_
YUNTUAN_EDITION = merchant
YUNTUAN_PLATFORM_API_BASE = https://x.bot100.cn
YUNTUAN_LICENSE_PATH = license.json
YUNTUAN_AUTH_KEY = 请生成并妥善保存的对称加密密钥
.env 只保留官方服务接口和本地运行配置,不应包含平台后台运行配置或平台私钥配置;平台后台源码、平台后台前端构建产物、vendor 和 node_modules 都不应进入开源版交付包。
Nginx 站点根目录应指向 public,并把不存在的文件转交给 index.php,这样 MVC 页面、API 和 Vue 后台路由都能正常工作。
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass 127.0.0.1:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
商家独立部署后,第一次进入后台应先绑定远程平台账号。绑定成功后,本地会保存商家编号、商家账号、授权域名、源码授权状态和已授权平台应用列表。
源码销售按域名和商家账号共同绑定。后台登录时会检查上次授权校验时间,超过 24 小时会重新请求平台校验。
平台应用需要远程平台授权后才能同步和安装。商家自研应用不走平台授权,默认内置应用由源码随附。
authorized_addons 来尝试开通平台应用。商家版最终会以签名授权文件和远程平台校验结果为准,平台应用安装和升级也会再次校验授权。
商家后台访问路径为 /admin。后台前端使用 TypeScript、Vite、Vue3、TailwindCSS 和 Ant Design Vue 开发,构建后的文件部署在 public/admin,接口统一走 /api。
平台概览:查看系统信息、应用类型统计和运行状态。应用管理:管理已安装应用、从官方平台同步应用、安装自研应用包、进入应用独立后台。后台用户及权限:维护管理员账号、用户分组、后台菜单和分组权限。系统设置:维护上传存储方式,本地、七牛云、阿里云、腾讯云等配置都从这里管理。个人中心:维护当前账号昵称、头像和密码。每个应用都是一个独立模块,必须放在 addons 目录下。目录名就是应用标识,只允许小写字母、数字和下划线,并建议以业务名称命名。
addons/
└── demo_app/
├── manifest.json
├── route.php
├── install.sql
├── upgrade.sql
├── backend/
│ ├── controller/
│ │ └── api/
│ ├── model/
│ └── service/
├── frontend/
│ ├── src/
│ ├── package.json
│ └── vite.config.ts
└── public/
└── admin/
| 目录或文件 | 用途 |
|---|---|
manifest.json |
应用清单,声明名称、版本、价格、支持平台、权限、菜单、接口前缀和打包规则。 |
route.php |
应用独立路由,主系统启动时会自动加载 addons 下各应用的路由文件。 |
backend |
应用 PHP 后端源码,包含控制器、模型、服务类等。 |
frontend |
应用后台前端开发源码,仅开发阶段保留,发布应用包时不允许包含未构建源码。 |
public |
应用构建后的前端和静态资源,安装时会发布到主站 public 下。 |
manifest.json 是应用被平台识别、安装和管理的核心文件。应用管理、安装器、远程同步都会读取它。
{
"name": "demo_app",
"title": "演示应用",
"version": "1.0.0",
"description": "用于演示商家自研应用开发规范。",
"type": "system_plugin",
"type_label": "系统插件",
"source": "merchant_local",
"market_visible": true,
"merchant_market_visible": true,
"default_installed": false,
"icon": "/static/addons/demo_app/icon.svg",
"price": 0,
"support_platforms": ["merchant"],
"backend_url": "/addons/demo_app/admin/",
"api_prefix": "/api/addons/demo_app/admin",
"permissions": [
{ "permission": "demo_app.manage", "title": "管理演示应用" }
],
"menus": [
{
"title": "演示应用",
"path": "/addons/demo_app/admin/",
"permission": "demo_app.manage",
"icon": "SettingOutlined",
"sort": 10,
"visible": true
}
],
"install": { "sql": "install.sql" },
"upgrade": { "sql": "upgrade.sql" },
"runtime": {
"backend": true,
"frontend": true,
"php": ">=8.1",
"yuntuan": ">=0.1.0",
"frontend_source": "frontend"
},
"package": {
"source": "frontend",
"public": "public",
"exclude": ["frontend/src", "frontend/node_modules", "frontend/**/*.map"],
"include": ["manifest.json", "route.php", "install.sql", "upgrade.sql", "backend", "public"]
}
}
source:商家自研应用使用 merchant_local,平台应用统一使用 platform。远程同步只是安装渠道,不改变应用来源。market_visible:是否在平台应用市场展示。merchant_market_visible:安装到商家站点后,是否进入商家面向普通用户的本地应用市场。default_installed:是否随源码默认安装,例如自定义首页这类站点级系统插件。support_platforms:按应用实际兼容范围填写 platform、merchant,同时支持平台自营版和商家独立部署版时填写两项。system_plugin 外,其他类型应用的后台和前台都必须绑定 app_id、merch_id、platform_id 三个参数。缺少任意一个参数时,入口和接口都应拒绝访问。
应用后端建议按照控制器、服务类、模型分层。控制器只做认证、权限、参数接收和响应;业务逻辑写在服务类;数据库表操作写在模型或服务中。
<?php
use think\facade\Route;
Route::get(
'api/addons/demo_app/admin/setting',
'\\addons\\demo_app\\backend\\controller\\api\\Setting@detail'
);
Route::put(
'api/addons/demo_app/admin/setting',
'\\addons\\demo_app\\backend\\controller\\api\\Setting@save'
);
demo_app.manage。应用表建议使用 yt_应用标识_业务名,例如 yt_demo_app_setting。这样卸载、排查和迁移时都能快速识别归属。
system_plugin 是站点级系统插件,可以按站点或系统配置隔离;其他应用类型必须以 merch_id + platform_id + app_id 作为基础运行空间。
merch_id:商家编号,用于隔离不同商家。platform_id:接入平台编号,用于隔离同一商家的不同平台。app_id:应用编号,用于读取应用安装信息和授权关系。CREATE TABLE IF NOT EXISTS `yt_demo_app_setting` (
`id` int unsigned NOT NULL AUTO_INCREMENT COMMENT '配置编号',
`merch_id` int unsigned NOT NULL DEFAULT 0 COMMENT '商家编号',
`platform_id` int unsigned NOT NULL DEFAULT 0 COMMENT '接入平台编号',
`app_id` int unsigned NOT NULL DEFAULT 0 COMMENT '应用编号',
`config_json` json DEFAULT NULL COMMENT '扩展配置',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_merch_platform_app` (`merch_id`, `platform_id`, `app_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='演示应用设置表';
$row = Db::name('demo_app_order')
->where('id', $id)
->where('merch_id', $scope['merch_id'])
->where('platform_id', $scope['platform_id'])
->where('app_id', $scope['app_id'])
->where('user_id', $userId)
->find();
应用后台前端仍使用 TypeScript、Vite、Vue3、TailwindCSS 和 Ant Design Vue。开发源码放在应用自己的 frontend 目录下,构建产物输出到应用自己的 public/admin。
base 应设置为应用后台访问路径,例如 /addons/demo_app/admin/。outDir 应输出到 addons/demo_app/public/admin。api_prefix,例如 /api/addons/demo_app/admin。import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'node:path'
export default defineConfig({
base: '/addons/demo_app/admin/',
plugins: [vue()],
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'),
},
},
build: {
outDir: '../public/admin',
emptyOutDir: true,
},
})
pnpm install
pnpm build
frontend/src、node_modules、源码映射文件和 TypeScript 构建缓存。只发布 PHP 后端、manifest、SQL、路由和 build 后的前端产物。
主系统提供统一上传能力,应用可以调用公共上传组件和上传接口。数据库中建议保存资源编号,而不是保存完整 URL。
图片、视频、音频和附件都应走统一上传入口。当前后台优先使用图片选择器,后续应用需要其他文件类型时,保持相同接口协议即可。
| 接口 | 方法 | 用途 |
|---|---|---|
/api/upload/assets |
GET | 按 scene、关键词、分组和分页读取当前登录用户的资源列表。 |
/api/upload/local |
POST | 本地上传,表单字段为 file、scene、group_id。 |
/api/upload/direct-params |
POST | 云存储前端直传前,向后端申请七牛云、阿里云 OSS 或腾讯云 COS 的直传参数。 |
/api/upload/complete-direct |
POST | 云存储直传完成后回调主系统登记资源,业务表后续只保存返回的资源编号。 |
<?php
namespace addons\demo_app\backend\controller\api;
use app\controller\api\BaseApi;
use app\service\UploadService;
use think\facade\Db;
use think\Response;
/**
* 演示应用设置接口。
*/
class Setting extends BaseApi
{
/**
* 保存应用封面资源编号。
*
* @return Response
*/
public function save(): Response
{
$admin = $this->requirePermission('demo_app.manage');
$scope = [
'merch_id' => (int) ($admin['merchant_id'] ?? 0),
'platform_id' => (int) $this->request->param('platform_id', 0),
'app_id' => (int) $this->request->param('app_id', 0),
];
$coverAssetId = max(0, (int) $this->request->param('cover_asset_id', 0));
Db::name('demo_app_setting')->where($scope)->update([
'cover_asset_id' => $coverAssetId,
'update_time' => date('Y-m-d H:i:s'),
]);
return $this->success(['cover_asset_id' => $coverAssetId], '保存成功');
}
/**
* 读取设置并把资源编号转换为当前可访问地址。
*
* @param UploadService $uploadService 上传资源服务。
* @return Response
*/
public function detail(UploadService $uploadService): Response
{
$admin = $this->requirePermission('demo_app.manage');
$scope = [
'merch_id' => (int) ($admin['merchant_id'] ?? 0),
'platform_id' => (int) $this->request->param('platform_id', 0),
'app_id' => (int) $this->request->param('app_id', 0),
];
$row = Db::name('demo_app_setting')->where($scope)->find() ?: [];
$coverAssetId = (int) ($row['cover_asset_id'] ?? 0);
return $this->success([
'cover_asset_id' => $coverAssetId,
'cover_url' => $uploadService->assetUrlById($coverAssetId),
]);
}
}
应用后台接入主系统公共组件后,可以直接使用资源选择器。组件返回资源编号,业务表只保存这个编号。
<script setup lang="ts">
import { reactive, ref } from 'vue'
import AssetPicker from '@/components/AssetPicker.vue'
import type { UploadAsset } from '@/services/upload'
const form = reactive({
cover_asset_id: 0,
})
const coverUrl = ref('')
/**
* 记录选择后的图片预览地址。
*
* @param asset 上传资源。
*/
function handleCoverSelected(asset: UploadAsset) {
coverUrl.value = asset.url
}
</script>
<template>
<AssetPicker
v-model:value="form.cover_asset_id"
scene="image"
button-text="选择图片"
@selected="handleCoverSelected"
/>
<img v-if="coverUrl" :src="coverUrl" alt="封面预览">
</template>
import { http } from '@/services/http'
/**
* 上传图片并返回资源编号。
*
* @param file 浏览器选择的文件。
*/
export async function uploadCover(file: File) {
const formData = new FormData()
formData.append('scene', 'image')
formData.append('group_id', '0')
formData.append('file', file)
const response = await http.post('/upload/local', formData)
return response.data.data.id
}
平台操作台的“系统设置”中可以维护微信支付和支付宝配置。应用后端不要自己分散设置回调地址,支付网关的统一回调入口由平台服务自动注入,应用只需要实现自己的业务回调服务;return_url 只用于支付成功后的同步跳转,不要把回调参数拼进去。
/api/payments/notify。微信和支付宝共用同一个入口,notify_url 不携带查询参数。平台在创建支付单时会把 out_trade_no、app_id、merch_id 和 platform_id 写入 platform_payment_context 表,回调时按订单号恢复上下文,再分发给应用自己的回调服务。
native、jsapi、mini_program、h5、apppage、wap、app、precreate、barcode| 字段 | 含义 |
|---|---|
display_type |
前端展示方式,常见值为 qrcode、link、html、json。 |
code_url |
二维码内容,适用于扫码类场景。 |
redirect_url |
直接跳转链接,适用于 H5、网页支付等场景。 |
payment_html |
支付表单或自动跳转 HTML,适用于支付宝网页支付。 |
json_data |
无法直接渲染或需要补充参数时返回的原始响应,前端按 JSON 展示。 |
如果微信 jsapi 或 mini_program 缺少 openid,或者支付宝条码支付缺少 auth_code,接口会直接返回 display_type=json,前端按 JSON 结果展示和排查,不要把它当成错误页。
生产环境真实支付必须使用公网可访问的 HTTPS 回调地址;本地 http:// 域名只能用于模拟回调或前端联调。
应用后端发起支付时,provider 和 scene 也要一起传入,scene 由应用根据当前客户端自行判断,不要自己拼接回调地址:
<?php
$provider = 'wechat';
$scene = $provider === 'wechat' ? 'native' : 'page'; // 应用根据当前客户端自行决定具体场景。
$payment = app(\app\service\PlatformPaymentService::class);
$order = [
'subject' => '演示应用订单',
'amount' => '19.90',
'out_trade_no' => 'DEMO' . date('YmdHis'),
'app_id' => $appId,
'merch_id' => $merchId,
'platform_id' => $platformId,
'return_url' => request()->domain() . '/addons/demo_app/pay-result',
];
$result = $provider === 'wechat'
? $payment->createWechatOrder($platformId, 'demo_app', $scene, $order)
: $payment->createAlipayOrder($platformId, 'demo_app', $scene, $order);
// 平台会自动补充统一回调地址,并按 out_trade_no 记录应用上下文。
前端不要直接拿支付密钥和证书,只调用应用自己的后端接口。provider 表示支付渠道,scene 表示支付场景,应用应该根据当前客户端自行判断场景,例如 Web 端微信常用 native,支付宝常用 page;移动浏览器可选择 h5 或 wap。前端先判断 display_type,再分别处理二维码、跳转链接、HTML 表单和 JSON:
import { http } from '@/services/http'
export async function createPayment(orderId: number, provider: 'wechat' | 'alipay', scene: string) {
const response = await http.post('/api/addons/demo_app/payment/pay', {
order_id: orderId,
provider,
scene,
return_url: window.location.href,
})
const payment = response.data.data
if (payment.display_type === 'qrcode') return payment.code_url
if (payment.display_type === 'link') return payment.redirect_url
if (payment.display_type === 'html') return payment.payment_html
return payment.json_data ?? payment
}
短信使用当前平台配置中的服务商发送。阿里云、腾讯云和七牛云的参数并不相同,主系统服务会按服务商分别调用官方 SDK。
<?php
$sms = app(\app\service\PlatformSmsService::class);
$result = $sms->sendCaptcha($platformId, 'demo_app', '13800138000', [
'code' => '123456',
], ['code']);
// 腾讯云短信会按最后一个参数中的变量顺序生成 TemplateParamSet。
| 服务商 | 后台配置项 | 发送差异 |
|---|---|---|
| 阿里云短信 | AccessKeyId、AccessKeySecret、短信签名、模板 ID。 |
模板变量以 JSON 对象传入 TemplateParam,变量名必须与阿里云模板一致。 |
| 腾讯云短信 | SdkAppID、AccessKeyId、AccessKeySecret、短信签名、模板 ID。 |
模板变量必须按模板变量顺序传入 TemplateParamSet,所以应用调用时建议显式传入变量顺序。 |
| 七牛云短信 | AccessKey、SecretKey、短信签名、模板 ID。 |
七牛发送接口使用 sendMessage(template_id, mobiles, parameters),签名通常在模板侧绑定。 |
${code}、{{code}}、{code} 这类变量,让管理员填写后发送。应用后台是独立入口,但仍建议使用主系统的后台账号和权限体系。应用权限写在 manifest 的 permissions 中,应用菜单写在 menus 中。
应用标识.动作,例如 demo_app.manage。/addons/demo_app/admin/。{
"permissions": [
{ "permission": "demo_app.manage", "title": "管理演示应用" },
{ "permission": "demo_app.order.view", "title": "查看演示应用订单" }
],
"menus": [
{
"title": "演示应用",
"path": "/addons/demo_app/admin/",
"permission": "demo_app.manage",
"icon": "AppstoreOutlined",
"sort": 20,
"visible": true
}
]
}
<?php
/**
* 查询订单列表。
*
* @return \think\Response
*/
public function orders(): \think\Response
{
$admin = $this->requirePermission('demo_app.order.view');
return $this->success([
'items' => [],
'operator' => $admin['username'] ?? '',
]);
}
商家自研应用可以在本地打包后通过商家后台“安装应用”上传;如果希望把自研应用提交到云团应用市场销售或分发,则继续使用商家后台的“开发者中心”提交审核。
manifest.json 中的 name 与应用目录名完全一致。backend_url 和 api_prefix 都绑定当前应用目录。pnpm build,确保 public/admin 存在入口文件。node_modules、调试日志和本地密钥。demo_app.zip
├── manifest.json
├── route.php
├── install.sql
├── upgrade.sql
├── backend/
└── public/
商家自研应用可以只在自己的站点安装,也可以提交到云团应用市场。提交入口位于商家后台“开发者中心”,平台审核通过后会生成独立的平台发行包;开发者上传的原始包不会直接作为市场安装包发布。
首次进入开发者中心,先填写开发者或团队名称、联系人、电话、邮箱、网站和简介。开发者名称会随应用资料提交审核,并可用于应用市场展示。
提交包必须是可直接安装的 ZIP 运行包,manifest.json 的 name 必须与申请标识一致,source 必须为 merchant_local。包内可以包含 PHP 后端、SQL 和 build 后的前端,但不能包含未构建的 Vue/TypeScript 源码、node_modules、source map、环境文件、证书、密钥、日志或备份。
上传后系统会自动检查 ZIP 路径安全、符号链接、文件数量和大小、敏感文件、Manifest、一致性以及 SQL 表作用域。应用 SQL 只能操作 __PREFIX__{addon_name}_* 私有表。
source=platform 的发行包,并按审核结果上架或保留为待上架。| 状态 | 含义 | 开发者可执行操作 |
|---|---|---|
| 草稿 | 资料或安装包尚未提交审核。 | 编辑资料、上传版本、提交审核。 |
| 待审核 | 平台正在审核当前版本。 | 查看详情和审核进度。 |
| 已驳回 | 当前版本存在需要修正的问题。 | 查看驳回原因、修改后重新提交。 |
| 已通过 | 版本审核通过,但应用尚未公开上架。 | 维护资料或上传后续版本。 |
| 已上架 | 当前审核版本已经进入应用市场。 | 维护资料、上传新版本并发起新一轮审核。 |
应用上架后仍使用同一条应用提交记录。先编辑需要调整的资料,再上传更高版本号的 ZIP;上传成功后状态会回到草稿,提交审核并通过后,新版本成为平台当前发行版本,历史版本和审核记录继续保留。
source 手动改成 platform。平台来源只能由审核发布流程生成,令牌、平台密钥和审核状态也不能写入应用安装包。
云团同时支持平台自营应用市场和商家独立部署后的本地应用市场。两者展示规则不同,需要在开发应用时提前确认。
| 场景 | 展示规则 | 适用应用 |
|---|---|---|
| 平台应用市场 | 应用已安装、启用、上架、审核通过,并且 market_visible=1。 |
平台上架售卖或免费分发的应用。 |
| 商家本地应用市场 | 商家自研应用或已授权平台应用,并且 merchant_market_visible=1。 |
商家面向普通用户售卖或开放使用的应用。 |
| 默认内置应用 | 随源码默认安装,可在商家后台应用管理中使用,但可以不进入商家本地应用市场。 | 自定义首页、站点配置、系统工具等站点级应用。 |
{
"source": "merchant_local",
"market_visible": true,
"merchant_market_visible": true,
"default_installed": false,
"has_frontend": true,
"backend_url": "/addons/demo_app/admin/",
"frontend_url": "/app"
}
如果应用没有前台入口,例如站点级工具或自定义首页后台,请把 has_frontend 配置为 false,这样系统不会展示前台按钮,也不会要求绑定前台域名。
APP_DEBUG,避免暴露错误堆栈和调试入口。YUNTUAN_AUTH_KEY、数据库密码、云存储密钥等只放在服务器环境配置中,不要提交到应用包。确认 Nginx 已配置 try_files $uri $uri/ /index.php?$query_string;,并且主系统路由中存在后台前端兜底入口。
确认应用的 route.php 已放在应用目录下,并且路由前缀与 manifest.json 中的 api_prefix 一致。
确认应用前端已经 build,入口文件和静态资源在 public/admin 内,且 Vite 的 base 与访问路径一致。
业务表应保存上传资源编号,由接口根据当前上传配置生成完整 URL。如果保存了旧域名完整路径,换域名后就需要手动迁移历史数据。