扫码到手机上查看
或分享到朋友圈和好友

SiteOS开放平台开发文档
DEVELOPER GUIDE

构建可安装、可更新的 SiteOS 应用

从目录约定、接口实现到打包发布,按应用类型查阅完整开发规范。

开发校验发布安装与更新

功能模块概览

功能模块承载后端业务能力,可同时提供后台管理页、PC 前台控制器和 Uni API。源码位于 app/{name}/,目录名使用全小写字母数字,应用类型固定为 module

边界原则模块应独立安装、升级和卸载。不要直接引用其他业务模块内部的 Model 或类;跨模块能力应通过平台公共层或约定接口交互。

推荐开发顺序

  1. 确定模块标识与数据边界,创建 module.json
  2. 建立 Model、API 与后台控制器菜单。
  3. 实现后台页面及需要的前台/Uni 调用端。
  4. 补齐迁移、语言模板、安全校验与发布说明。

从零创建功能模块

1. 在开发者中心建档

进入开发者中心的本地开发功能,选择“功能模块”,填写应用标识、名称和说明。标识一旦在开放平台建档便是长期身份,不要使用公司名、环境名或版本号作为标识。系统根据 app/devcenter/stub/module/ 生成基础目录。

2. 建立最小可运行页面

以应用标识 book 为例:在 controller/Admin.php 增加 books() 并返回 vue();建立 view/admin/books/books.vue、同名 JS 和 LESS;建立 api/admin/book_books.php。启用应用后刷新后台导航即可进入页面。

3. 建表并创建 Model

开发库中新建 {prefix}book_books,同时创建 model/BookBooks.php。模型不是可选装饰:安装包打表、卸载识别和数据库快照都依赖模型归属。开发者中心的表管理可辅助创建表和 Model。

4. 完成接口闭环

先实现列表、详情、保存、删除四个接口,再接后台页面。每个写操作都要校验 ID、字段范围和权限;删除不存在的记录应返回明确错误,不能对空对象直接调用方法。

5. 本地验证后发布

以普通管理员而非仅超级管理员验证菜单权限;再测试空库首装、带旧数据更新和失败重试。发布时开发者中心会生成 ZIP、结构快照和必要的自动迁移,并向开放平台提交审核。

目录与 Manifest

app/example/
├── module.json
├── controller/Admin.php
├── model/ExampleItem.php
├── api/admin/example_item.php
├── api/uni/example_item.php
├── view/admin/item/{item.vue,item.js,item.less}
├── lang/tpl/{zh-cn.php,en.php}
└── migrations/1.1.0_add_status.sql

module.json 声明 nametypeversiontitledescription,功能模块可选声明 iconname 必须等于目录名,version 使用三段 SemVer,例如 1.2.0

字段要求
typemodule
name与模块目录名一致
version三段 SemVer
表归属每张业务表必须有对应 Model

完整 CRUD 示例

下面展示一个最小“图书”资源的关键代码。示例重点是文件命名、返回结构、校验和资源存在性,实际项目应再加入业务权限与验证器。

Model:model/BookBooks.php

<?php
namespace app\book\model;
use app\BaseModel;

class BookBooks extends BaseModel
{
    // 默认由类名解析为 book_books 表
}

后台 API:api/admin/book_books.php

<?php
namespace app\book\api\admin;

class book_books
{
    public function book_books($params)
    {
        $query = (new \app\book\model\BookBooks)->order('id desc');
        if (!empty($params['keyword'])) {
            $query->whereLike('title', '%' . trim($params['keyword']) . '%');
        }
        return show($query->paginate(max(1, (int)($params['limit'] ?? 20))));
    }

    /** @title 新增或编辑 */
    public function book_books_save($params)
    {
        $title = trim((string)($params['title'] ?? ''));
        if ($title === '' || mb_strlen($title) > 100) {
            return show([], 0, '标题不能为空且不能超过100字');
        }
        $id = (int)($params['id'] ?? 0);
        $model = $id ? \app\book\model\BookBooks::find($id) : new \app\book\model\BookBooks;
        if (!$model) return show([], 0, '记录不存在');
        $model->save(['title' => $title, 'status' => (int)($params['status'] ?? 1)]);
        return show(['id' => $model->id], 1, '保存成功');
    }

    /** @title 删除 */
    public function book_books_delete($params)
    {
        $model = \app\book\model\BookBooks::find((int)($params['id'] ?? 0));
        if (!$model) return show([], 0, '记录不存在');
        $model->delete();
        return show([], 1, '删除成功');
    }
}

页面请求:view/admin/books/books.js

export default {
  data() { return { loading: false, list: [], form: { title: '', status: 1 } } },
  methods: {
    load() {
      this.loading = true
      return this.$tdy.request({ url: '/api/admin?api=book_books', data: { limit: 20 } })
        .then(res => { if (res.code) this.list = res.data.data || [] })
        .finally(() => { this.loading = false })
    },
    save() {
      return this.$tdy.request({ url: '/api/admin?api=book_books_save', data: this.form })
        .then(res => { res.code ? this.load() : this.$message.error(res.message) })
    }
  },
  mounted() { this.load() }
}

模型、接口与权限

模型与表

业务 Model 继承 app\BaseModel。表归属以模块 model/ 为权威;种子或固定配置表设置 protected $isFixed = true。SQL 同时兼容 MySQL 与达梦数据库。

API 分发

后台接口放在 api/admin/,Uni/H5 接口放在 api/uni/。文件、类和方法保持同名 snake_case,例如 example_item.php → class example_item → example_item($params)

this.$tdy.request({
  url: '/api/admin?api=example_item',
  data: { page: 1 }
})

业务响应统一使用 show($data, $code, $message, $time)。后台菜单由 controller/Admin.php 的 PHPDoc 反射生成;使用 @title@subpage@authnode@public 表达菜单与权限。

后台与前台页面

后台页面位于 view/admin/{page}/,保持 Vue Options API 风格,优先复用 tdy-tabletdy-filtrate、上传组件和 Element Plus 图标。

模块自带的 PC 前台页面会与站点网站模板共同加载。所有前台 CSS 类必须以 tdy-app-{name} 开头,避免主题全局样式相互污染。接口统一走 $tdy.request

不要修改public/@/ 是自动生成的静态镜像;开发时只修改模块源码。

语言、文件与安全

语言包

开发者维护的语言源仅放在 app/{name}/lang/tpl/*.php。同级上方的 lang/zh-cn.phplang/en.php 等属于站点按模板生成并允许用户修改的文件,发布包会主动排除,不能直接编辑。新增 lang() 键时同步补齐所支持语言的模板源。

上传与本地文件

上传应复用平台上传能力。服务端必须使用允许的扩展名和 MIME 白名单、限制大小、生成服务端文件名,并把最终路径约束在预期目录。不得信任客户端文件名,也不得将用户输入直接拼接为磁盘路径。

接口安全基线

  • 查询、修改与删除均校验当前用户对目标资源的归属或业务权限。
  • 金额、状态和权限字段由服务端决定,不能直接接受客户端结果。
  • 删除、重置、退款等危险操作需要明确确认、防重复提交和可理解的失败反馈。
  • 数据库使用 ORM/参数绑定;禁止把输入拼入 SQL、Shell 或动态类名。
  • 日志只记录排障所需标识,不记录密码、Token、密钥、支付原文或完整个人敏感信息。

迁移、打包与发布

增量数据库变更写入 migrations/{semver}_{desc}.sql,表前缀使用 tableprefix_,已发布迁移不可改名或覆盖。更新只运行待执行迁移。

开发者中心打包时会按 Model 归属从当前开发数据库重新导出首装 install/sql 基线;固定表同时导出数据。首次安装执行基线后调用 Migration::baseline(),更新会跳过含 DROP 的基线,仅运行 Migration::runPending()。因此每次升级的结构和数据变化都必须有新迁移。

包内路径必须保持 app/{name}/...。打包自动排除 .svn.gitnode_modules、站点生成语言包等内容。应用级版本只看 module.json.version;不要创建或依赖应用级 .version

  • 校验所有写接口的登录态、权限与输入格式。
  • 检查上传类型、大小、文件名与路径穿越风险。
  • 确认响应、日志和页面不包含密码、Token 或密钥。
  • 验证 MySQL/达梦兼容与失败反馈。
  • 提升 module.json.version,填写变更说明后由开发者中心提交。

验收与排错

现象优先检查
后台不显示应用应用是否启用;Admin.php 类级 @title;Manifest 是否为合法 JSON
菜单不显示方法是否 public、带方法级 @title;当前角色是否拥有节点
API 找不到类或方法api 参数、文件、命名空间、类名、方法名是否完全一致
安装后缺表每张业务表是否存在对应 Model,首装 SQL 是否完整
更新后字段未变化是否新增更高版本迁移;不要只修改首装 SQL
页面改动未生效只改源码,随后后台清缓存/触发静态构建;不要改 public/@/
仅超级管理员可用使用普通角色验证 PHPDoc 权限节点与菜单授权
达梦执行失败检查反引号、MySQL 专有函数、字段类型和多表更新语法,按项目适配方式改写

网站模板概览

网站模板是 PC 端 ThinkPHP think-view 服务端渲染主题,源码位于 view/template/{name}/,不是 Vue 项目。页面通过继承默认基座、覆写公共件和业务页面形成完整网站。

核心目标模板应只负责展示与运营配置。访客可见的标题、正文、图片、按钮、列表、视频与表单均应进入 DIY 数据源。

从零创建网站模板

  1. 在开发者中心选择网站模板并生成脚手架,得到 module.jsonsetting.jsoncommon/index/
  2. 先完成 common/header.htmlfooter.htmlstyle.less,确认桌面和移动导航。
  3. index/index.html 划分 hero、服务、案例、资讯、联系等区块;每个运营区块建立对应 DIY 设置。
  4. 按站点需求覆写 Portal 列表页和详情页;未覆写页面会回退默认模板。
  5. 在后台切换到该模板,使用 DIY 模式检查每项内容是否可编辑,再测试普通访问、登录态与表单。

开发过程中只编辑 view/template/{name}/。LESS、JS 和模板会由系统按内容指纹生成静态文件。

标识与 Manifest

模板标识最长 48 个字符,必须以小写字母开头,只能包含小写字母、数字和连字符;defaultcus 是保留标识。应用身份是 (type, name) 复合键,因此可与功能模块同名,但同类型内不能重复。

{
  "name": "onemary-demo-1",
  "type": "web-template",
  "version": "1.0.0",
  "title": "演示网站模板",
  "description": "适用行业与页面范围说明"
}

JSON 必须无 BOM、无注释、无尾逗号;name 等于目录名,版本为三段 SemVer。网站模板不拥有数据库,不声明表或迁移。

目录、继承与资源

view/template/onemary-demo-1/
├── module.json          # type: web-template
├── setting.json         # DIY 数据源
├── common/{header.html,footer.html,style.less,script.js}
├── index/{index.html,index.less,index.js}
└── portal/article/{column,detail}/...

页面使用 {{extend name="$__TEMPLATE_BASE__" /}} 继承基座,并在 content 区块输出内容。不要复制 default/global/;缺失的公共件会自动回退 default。

资源路径使用 /@/openplatform/static/@/__template__/siteos/static/@/__template__/default/static,不要写死 /@/

首页区块完整示例

一个区块要同时完成模板标记、字段定义和样式。以下 hero 示例中的字段名仅作演示,实际模板应使用稳定且全局唯一的短键。

index/index.html

{{extend name="$__TEMPLATE_BASE__" /}}
{{block name="content"}}
{{diy name="home_hero" title="首页首屏"}}
<section class="site-demo-hero" style="background-image:url('{{$diy.heroBg}}')">
  <div class="container">
    <h1>{{$diy.heroTitle}}</h1>
    <div class="site-demo-hero-text">{{$diy.heroText|raw}}</div>
    <a href="{{$diy.heroLink}}">{{$diy.heroButton}}</a>
  </div>
</section>
{{/diy}}
{{/block}}

setting.json 对应区块

{
  "name": "home_hero",
  "title": "首页首屏",
  "items": [
    { "name": "heroBg", "title": "背景图", "type": "pic", "value": "/@/openplatform/static/images/hero.jpg" },
    { "name": "heroTitle", "title": "主标题", "type": "text", "value": "让每个站点独立生长" },
    { "name": "heroText", "title": "说明", "type": "textarea", "value": "完整的业务与内容能力" },
    { "name": "heroButton", "title": "按钮文字", "type": "text", "value": "了解更多" },
    { "name": "heroLink", "title": "按钮链接", "type": "link", "value": "/portal/article/column" }
  ]
}

该对象应放在 pages[name=__diy__].setting 数组内。图片默认值使用静态占位符;富文本只有在字段确实允许可信 HTML 时才使用 |raw

DIY 数据与组件

setting.json 的区块 name 必须与页面中的 {{diy name="hero"}}…{{/diy}} 对应。字段键在模板内保持唯一。

类型用途
text / textarea单行文案、富文本
pic / video / link图片、视频、链接
loop可编辑重复列表
dynamic文章等真实业务数据
form留资、咨询与订阅表单

系统导航 {{nav}}、登录区 {{login /}} 与 Portal 正文属于系统数据,不需要重复 DIY 化。

页面、标签与交互

门户列表覆写放在 portal/article/column/list_*.html,详情覆写放在 portal/article/detail/view_*.html。可使用 think-view 的条件、循环、函数和 SiteOS 自定义标签。

基础库已由默认模板加载,不要重复引入。业务请求通过全局 $tdy.request,登录、确认、复制等交互优先使用 $tdy 对应能力。

输出安全普通变量默认转义;仅对可信且确需 HTML 的富文本使用 |raw,不要把用户可控内容直接作为 HTML 输出。

SEO、性能与安全

页面标题、关键词和描述由系统的 $__PAGE_TITLE__$__PAGE_KEYWORDS__$__PAGE_DESCRIPTION__ 注入。语义化使用标题层级、导航、主内容和替代文本;重要信息不要只存在于背景图或动画中。

  • 图片选择合适尺寸和格式,非首屏资源延迟加载,避免在模板重复加载全局库。
  • 交互需支持键盘焦点、清晰的 hover/focus 状态与移动端触控。
  • 外链、表单和动态内容使用 HTTPS;用户输入不拼接进脚本、样式或 HTML 属性。
  • 不要在模板中写 API 密钥、后台地址、真实账号或环境专属域名。
  • 公共 LESS 避免无范围的标签重置,以免污染模块前台页面。

适配、检查与发布

  • 桌面端与移动端分别检查导航、图片、表单和长文本。
  • 逐项确认所有运营内容均可在 DIY 中修改。
  • 检查链接、图片占位符及模板不存在的回退效果。
  • 确认没有修改或打包 public/@/ 生成物。
  • 提升 SemVer,通过开发者中心以 web-template 类型提交。

微信小程序模板概览

微信小程序模板是独立的 uni-app 项目,主要面向微信小程序,也可按项目配置构建 H5。源码位于 view/uniapp/{name}/,运行 Vue 3 createSSRApp,页面保持 Options API。

项目隔离每个子目录都是独立 HBuilderX 工程。各项目内的 tdy/ 是复制体,不是共享 npm 包,修改公共能力时必须明确同步范围。

从零创建微信小程序模板

  1. 在开发者中心选择微信小程序模板生成骨架,然后用 HBuilderX 打开该子目录。
  2. manifest.json 配置应用信息与微信 AppID;API 根地址由发布时生成的微信扩展配置 ext.host 注入。
  3. 需要 SiteOS 登录、请求、store 或多语言时,从同版本代表性模板复制完整 tdy/ 并按其 main.js 接入,不能只复制单个文件。
  4. pages.json 注册页面和分包;先跑通首页及一个真实 API,再扩展业务页面。
  5. 使用微信开发者工具和真机联调授权、上传、分享、支付等平台能力。
  6. 发布应用源码时由开发者中心打包;向微信发行则使用 HBuilderX,两个发布过程相互独立。

工程结构与 Manifest

view/uniapp/onemary-demo-1/
├── module.json           # type: uni-template
├── manifest.json
├── pages.json
├── App.vue / main.js
├── ext.json              # 微信扩展配置与本地调试 API 主机
├── setting.json          # 业务/DIY 配置
├── pages/
├── components/
└── tdy/                  # 当前项目的全局插件复制体

module.json.name 必须与工程目录一致,类型固定为 uni-template。不要把环境密钥写入 manifest、页面、分享链接或前端日志。

列表页完整示例

示例包含首次加载、分页、空状态、防重复请求和错误提示。服务端接口仍须自行校验分页上限及访问权限。

<template>
  <view class="book-page">
    <view v-for="item in list" :key="item.id" class="book-item" @click="openDetail(item.id)">
      <image :src="item.cover" mode="aspectFill" />
      <text>{{ item.title }}</text>
    </view>
    <view v-if="!loading && !list.length" class="book-empty">暂无内容</view>
  </view>
</template>

<script>
export default {
  data() { return { list: [], page: 1, finished: false, loading: false } },
  onLoad() { this.load(true) },
  onReachBottom() { this.load(false) },
  methods: {
    load(reset) {
      if (this.loading || (!reset && this.finished)) return
      if (reset) { this.page = 1; this.finished = false }
      this.loading = true
      this.$tdy.request({ url: '/api/uni?api=book_books', data: { page: this.page, limit: 20 } })
        .then(res => {
          if (!res.code) return uni.showToast({ title: res.message || '加载失败', icon: 'none' })
          const rows = res.data.data || []
          this.list = reset ? rows : this.list.concat(rows)
          this.finished = rows.length < 20
          this.page += 1
        })
        .finally(() => { this.loading = false })
    },
    openDetail(id) { uni.navigateTo({ url: `/pages/book/detail?id=${encodeURIComponent(id)}` }) }
  }
}
</script>

详情页接收参数后应转换并校验类型;不能因为入口来自自己的列表页就信任 ID。页面重复进入时按业务需要在 onShow 刷新,避免无条件重复请求。

页面、组件与状态

页面使用 data()computedmethodsonLoad() 等 Options API。新页面需在 pages.json 注册;分包页面同时控制主包体积与依赖边界。

通用展示拆为组件,业务状态按现有项目的 store 约定维护。不要把项目改造成 Composition API 或新增 npm 构建流,除非应用自身已有明确规范。

请求、登录与平台能力

接口统一通过 this.$tdy.request() 调用 /api/uni?api=模块_功能/api/uni 不全局强制登录,需要会员身份的服务端方法必须主动调用 uid() 并校验资源归属。

this.$tdy.request({
  url: '/api/uni?api=example_item',
  data: { id: this.id }
}).then(res => { this.detail = res.data })

登录、上传、支付、分享、定位等能力优先沿用当前子项目的 $tdy 封装和条件编译写法,分别处理授权拒绝、网络失败与重复提交。

配置、多端与安全

API 域名由微信扩展配置 ext.host 注入,业务与 DIY 配置放在 setting.json。使用 #ifdef / #ifndef 隔离微信小程序与 H5 差异,并在每个目标端真实验证。

  • 客户端输入不能替代服务端校验和权限判断。
  • 敏感凭据只保存在服务端;前端不展示内部错误栈。
  • 分享参数只传公开标识,不传 Token、密码或密钥。
  • 上传前后均校验文件类型、大小和业务归属。

体验、性能与审核

  • 列表使用分页和防重复加载,页面卸载时清理定时器、监听器与未完成状态。
  • 图片按展示尺寸压缩,长列表避免一次渲染全部数据;首屏只请求必要接口。
  • 统一设计加载、空数据、错误、无权限和离线状态,按钮提交期间禁用。
  • 申请相机、相册、定位、手机号等权限前说明用途,并处理用户拒绝后的替代路径。
  • 微信小程序隐私协议、类目、接口域名、业务域名与所用能力保持一致,发布前完成体验版验证。
  • H5 同时检查安全区、浏览器返回、分享落地、刷新恢复与登录跳转。

调试、构建与发布

使用 HBuilderX 运行与发行,不使用 npm run buildunpackage/dist/ 是构建产物,不直接修改,也不作为应用源码维护。

  • 验证未登录、登录过期、无权限和空数据状态。
  • 检查微信开发者工具与真机的授权、支付、分享流程。
  • 校验 H5 安全区、返回导航和跨端样式。
  • 确认发布生成的 ext.host 指向目标站点且扩展配置不含秘密信息。
  • 提升 SemVer,通过开发者中心以 uni-template 类型提交。