Плагин — папка с Go-файлами. Магазин читает её интерпретатором на лету: положили папку, нажали «Перечитать» в админке — раздел появился. Компилировать, собирать образ и перезапускать ничего не нужно. Контракт между магазином и плагином — открытый пакет github.com/orgflycms/sdk.
Что такое плагин
Папка называется полным именем плагина: пространство.имя, например
vitams.promo. Внутри — файлы одного Go-пакета, названного коротким
именем (promo), карточка plugin.json и README.md.
Подпапок нет.
vitams.promo/ ├── promo.go package promo, func Init(api sdk.API) error ├── plugin.json карточка: name, title, description, version └── README.md для людей: что делает, как настроить
Точка входа одна — Init. Магазин зовёт её один раз при загрузке; в ней плагин
регистрирует всё, что умеет: пункты меню, адреса страниц, перехватчик корзины, свои таблицы.
Дальше магазин сам вызывает то, что зарегистрировано.
Плагину доступна стандартная библиотека Go и пакет sdk. Сторонних модулей нет
и не будет: их некому скачать и собрать. Закрыты os/exec, syscall,
unsafe, plugin и всё из net/*, кроме net/http
и net/url. Плагин с таким импортом не загрузится, ошибка будет видна в списке плагинов.
Пространство имён
При регистрации в кабинете партнёра вы выбираете пространство
имён: строчные латинские буквы, цифры и _, от двух до 21 знака. Занятость
проверяется прямо в форме. Пространство становится префиксом всех ваших плагинов:
партнёр vitams публикует vitams.promo, vitams.delivery
и так далее. Так имена не пересекаются между партнёрами, а покупатель видит, чей плагин ставит.
В форме заявки вы вводите только короткое имя — promo; префикс подставится сам.
Короткое имя — это имя Go-пакета в архиве. После первой публикации имя не меняется: на нём
папки в магазинах.
Минимальный плагин
Пункт в меню админки и страница. С этого начинается любой плагин.
// vitams.hello/hello.go
package hello
import (
"html/template"
"net/http"
"github.com/orgflycms/sdk"
)
func Init(api sdk.API) error {
api.Menu("Привет", "/")
api.Handle("GET /{$}", func(w http.ResponseWriter, r *http.Request) {
body := `<div class="card p-5">Плагин «` + template.HTMLEscapeString(api.Name()) + `» работает.</div>`
api.Page(w, r, "Привет", template.HTML(body))
})
return nil
}
// vitams.hello/plugin.json
{
"name": "vitams.hello",
"title": "Привет",
"description": "Пункт в меню и страница в админке.",
"version": "1.0.0"
}
Положите папку vitams.hello в папку плагинов магазина и нажмите «Перечитать» —
в меню админки появится «Привет».
Что даёт sdk.API
Всё, что магазин даёт плагину, — один интерфейс. Он только расширяется: метод нельзя убрать или переименовать, поэтому плагин, написанный сегодня, будет работать и после обновления магазина.
| Метод | Что делает |
|---|---|
| Name() string | Полное имя плагина, оно же имя папки: vitams.promo. |
| Menu(label, href) | Пункт в меню админки. href — адрес внутри плагина от «/». |
| Handle(pattern, h) | Обработчик по шаблону http.ServeMux: «GET /{$}», «POST /new», «POST /{id}/delete». Адреса живут под /admin/x/<имя>/ и открыты только вошедшему в админку. |
| Path(href) string | Полный адрес маршрута для ссылок и форм: Path("/new") → «/admin/x/vitams.promo/new». |
| Page(w, r, title, body) | Страница в обёртке админки: меню, шапка, подвал. body — готовая разметка. |
| DB() DB | База магазина: Exec и Query, строки приходят картами map[string]any. Плейсхолдеры $1, $2. |
| OnCart(fn) | Перехватчик корзины: зовётся при каждом расчёте — на странице корзины и при оформлении заказа. |
| Money(kopecks) string | Копейки в строку для покупателя: 123456 → «1 234,56 ₽». |
Комментарий к каждому методу — в sdk.go.
Разметку страниц плагин собирает сам, удобнее всего html/template из стандартной
библиотеки. Классы card, btn, input, field,
label, data-table, notice — те же, что в админке, они не меняются.
Своя таблица и форма
Таблицы плагин заводит сам в Init через create table if not exists.
Имя таблицы начинается с имени плагина: promo_codes, а не codes.
Чужие таблицы плагин не трогает — их схема меняется без предупреждения.
package notes
import (
"bytes"
"context"
"fmt"
"html/template"
"net/http"
"github.com/orgflycms/sdk"
)
var page = template.Must(template.New("").Parse(`
<form method="post" action="{{.New}}" class="card p-5 flex gap-3">
<input name="text" class="input" placeholder="Заметка" required>
<button class="btn">Добавить</button>
</form>
<div class="card mt-5">
{{range .Rows}}<div class="panel-row">{{.text}}</div>{{end}}
</div>`))
func Init(api sdk.API) error {
if err := api.DB().Exec(context.Background(), `create table if not exists notes_items (
id bigserial primary key,
text text not null
)`); err != nil {
return fmt.Errorf("таблица заметок: %w", err)
}
api.Menu("Заметки", "/")
api.Handle("GET /{$}", func(w http.ResponseWriter, r *http.Request) {
rows, err := api.DB().Query(r.Context(), "select text from notes_items order by id desc")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
var buf bytes.Buffer
page.Execute(&buf, map[string]any{"New": api.Path("/new"), "Rows": rows})
api.Page(w, r, "Заметки", template.HTML(buf.String()))
})
api.Handle("POST /new", func(w http.ResponseWriter, r *http.Request) {
if err := api.DB().Exec(r.Context(), "insert into notes_items (text) values ($1)", r.FormValue("text")); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
http.Redirect(w, r, api.Path("/"), http.StatusSeeOther)
})
return nil
}
Ошибка в Init — плагин не загружается, текст ошибки виден в списке плагинов.
Паника в обработчике — 500 на этот запрос, магазин продолжает работать.
Скидка в корзине
Скидки в магазине дают только плагины. Ядро считает сумму товаров и зовёт перехватчики;
перехватчик правит Discount и Notes, остальное только читает.
Суммы в копейках. Итог ниже нуля не бывает, даже если два плагина дадут больше, чем стоит корзина.
api.OnCart(func(ctx context.Context, c *sdk.Cart) {
// Промокод ввёл покупатель; пусто — не вводил.
if c.Code == "" {
return
}
rows, err := api.DB().Query(ctx,
"select discount, min_total from promo_codes where code = $1 and active", c.Code)
if err != nil || len(rows) == 0 {
return
}
discount, minTotal := rows[0]["discount"].(int64), rows[0]["min_total"].(int64)
if c.Subtotal < minTotal {
c.Notes = append(c.Notes, "Промокод "+c.Code+" действует от "+api.Money(minTotal))
return
}
c.Discount += discount
c.Notes = append(c.Notes, "Промокод "+c.Code+": −"+api.Money(discount))
})
Что лежит в корзине: Items с товаром, ценой и количеством, Code,
Subtotal, Discount, Notes; Total() — к оплате.
Полный пример с разделом в админке — плагин «Промокоды», он же образец для модерации.
plugin.json
Карточка плагина — четыре поля. name — полное имя с пространством,
version — semver, три числа через точку.
{
"name": "vitams.promo",
"title": "Промокоды",
"description": "Скидка фиксированной суммой при корзине от порога.",
"version": "1.2.0"
}
Проверить у себя
- Склонируйте sdk — в нём
go.modдля подсказок редактора. Положите папку плагина рядом сexample/и выполнитеgo build ./...: компилируется — значит, и в магазине прочитается. - Положите папку в папку плагинов своего магазина —
plugins/рядом сdocker-compose.yml— и нажмите «Перечитать» в разделе «Плагины». - Не загрузился — текст ошибки там же, в списке. Поправили файл — снова «Перечитать».
Опубликовать
В кабинете партнёра — «Новый плагин»: короткое имя, название, описание, цена (0 — бесплатный), до пяти картинок и архив с кодом. Что проверяется до модерации:
- архив
.tar.gzили.zipдо 1 МБ, в корне только*.goбез_test.go,README.mdиplugin.json; подпапок нет; - имя пакета равно короткому имени плагина, есть
func Init(; - версия — semver и больше предыдущей опубликованной;
- картинки — jpeg, png или webp до 2 МБ.
Модератор читает код и одобряет или возвращает с замечанием — оно видно в кабинете. Любая правка опубликованного плагина — та же форма и новая заявка: карточка в каталоге показывает последнее одобренное, у плагина не больше одной заявки на модерации. Правка без архива оставляет версию и код прежними.
Магазины ставят плагин кнопкой из своей админки: платный сначала покупается, потом
скачивается архив и папка vitams.promo появляется у них без перезапуска.
Правила и ограничения
- Деньги — целые копейки:
int64, вывод черезapi.Money. - Адреса плагина закрыты как вся админка: покупатель их не видит. Витрину плагин пока не меняет — только корзину.
- Сеть: только
net/httpиnet/url. Магазин сам ничего наружу не отправляет, плагин — по своему усмотрению и с ведома владельца в описании. - Тексты интерфейса — по-русски.
- Один плагин — одна папка и один пакет. Файлов сколько угодно, лишь бы в корне.