cyi-box: Memphis-style toolbox admin platform

Go(fun)+SQLite backend, Vue 3 frontend. Modules: dashboard, pay gateway
(Epay-compatible + CDK stock), license/card system (per-app appid, domain
binding), AI memory bank with built-in MCP server (/mcp, FTS5 search,
signed read links), server SSH monitor + terminal + SFTP, proxy pool with
protocol-level alive checks, DB manager (mysql/pg/sqlite + SQL
autocomplete + import/export), disk share, vault (AES-GCM + TOTP),
bookmarks, dev tools.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
2026-10-01 01:58:47 +08:00
co-authored by Devin
commit 834ecacdb9
326 changed files with 27813 additions and 0 deletions
+242
View File
@@ -0,0 +1,242 @@
package service
import (
"context"
"database/sql"
"encoding/json"
"errors"
"fmt"
"strings"
"time"
"github.com/cyi-cc/fun"
"github.com/cyi-cc/cyi-box/backend/internal/config"
"github.com/cyi-cc/cyi-box/backend/internal/dbx"
"github.com/cyi-cc/cyi-box/backend/internal/dto"
"github.com/cyi-cc/cyi-box/backend/internal/store"
"github.com/cyi-cc/cyi-box/backend/internal/vault"
)
// DbMgrSvc 通用数据库管理器:连接密码 AES-GCM 落库,全部端点仅管理员;
// 查询固定 20s 超时 + 行数封顶,防止一条烂 SQL 拖死进程
type DbMgrSvc struct {
fun.Ctx
Store *store.Store
Cfg *config.Config `fun:"auto"`
}
func (s *DbMgrSvc) List() ([]dto.DbconnView, error) {
items, err := s.Store.ListDbconns()
if err != nil {
return nil, fmt.Errorf("dbm: list failed: %w", err)
}
views := make([]dto.DbconnView, 0, len(items))
for _, e := range items {
views = append(views, dbconnView(e))
}
return views, nil
}
func (s *DbMgrSvc) Save(d dto.SaveDbconnDto) (dto.DbconnView, error) {
name := strings.TrimSpace(d.Name)
engine := strings.TrimSpace(d.Engine)
switch engine {
case "mysql", "postgres", "sqlite":
default:
return dto.DbconnView{}, fun.Error(4001, "引擎只支持 mysql / postgres / sqlite")
}
if name == "" || len(name) > 64 {
return dto.DbconnView{}, fun.Error(4001, "名称必填且不超过 64 字符")
}
e := store.Dbconn{Name: name, Engine: engine}
if d.Id != nil {
old, err := s.Store.GetDbconn(*d.Id)
if errors.Is(err, store.ErrNotFound) {
return dto.DbconnView{}, fun.Error(4001, "连接不存在")
}
if err != nil {
return dto.DbconnView{}, err
}
e = old
e.Name, e.Engine = name, engine
}
if d.Host != nil {
e.Host = strings.TrimSpace(*d.Host)
}
if d.Port != nil {
e.Port = *d.Port
}
if d.Username != nil {
e.Username = strings.TrimSpace(*d.Username)
}
if d.Database != nil {
e.Database = strings.TrimSpace(*d.Database)
}
if d.Params != nil {
e.Params = strings.TrimSpace(*d.Params)
}
if d.Password != nil {
pw := *d.Password
if pw == "" {
e.PasswordEnc = ""
} else {
enc, err := vault.Encrypt(s.Cfg.VaultKey, pw)
if err != nil {
return dto.DbconnView{}, err
}
e.PasswordEnc = enc
}
}
if e.Engine != "sqlite" && e.Host == "" {
return dto.DbconnView{}, fun.Error(4001, "主机地址必填")
}
if e.Engine == "sqlite" && e.Database == "" {
return dto.DbconnView{}, fun.Error(4001, "sqlite 需在「数据库」填文件路径")
}
if err := s.Store.SaveDbconn(&e); err != nil {
return dto.DbconnView{}, fmt.Errorf("dbm: save failed: %w", err)
}
return dbconnView(e), nil
}
func (s *DbMgrSvc) Delete(d dto.DbconnIdDto) (dto.OkView, error) {
if err := s.Store.DeleteDbconn(d.Id); err != nil {
if errors.Is(err, store.ErrNotFound) {
return dto.OkView{}, fun.Error(4001, "连接不存在")
}
return dto.OkView{}, fmt.Errorf("dbm: delete failed: %w", err)
}
return dto.OkView{Message: "已删除"}, nil
}
// openConn 解密码 + 建连 + ping
func openConn(st *store.Store, key []byte, id int64, database string) (*sql.DB, store.Dbconn, error) {
e, err := st.GetDbconn(id)
if errors.Is(err, store.ErrNotFound) {
return nil, e, fun.Error(4001, "连接不存在")
}
if err != nil {
return nil, e, err
}
pw := ""
if e.PasswordEnc != "" {
pw, err = vault.Decrypt(key, e.PasswordEnc)
if err != nil {
return nil, e, fmt.Errorf("dbm: decrypt password failed: %w", err)
}
}
d, err := dbx.Open(dbx.Conn{
Engine: e.Engine, Host: e.Host, Port: e.Port, Username: e.Username,
Password: pw, Database: e.Database, Params: e.Params,
}, database)
return d, e, err
}
func (s *DbMgrSvc) open(id int64, database string) (*sql.DB, store.Dbconn, error) {
return openConn(s.Store, s.Cfg.VaultKey, id, database)
}
// Test 连通性测试
func (s *DbMgrSvc) Test(d dto.DbconnIdDto) (dto.OkView, error) {
c, _, err := s.open(d.Id, "")
if err != nil {
return dto.OkView{}, fun.Error(4001, err.Error())
}
defer c.Close()
return dto.OkView{Message: "连接成功"}, nil
}
func (s *DbMgrSvc) Databases(d dto.DbScopeDto) (dto.DbDatabasesView, error) {
c, e, err := s.open(d.Id, "")
if err != nil {
return dto.DbDatabasesView{}, err
}
defer c.Close()
names, err := dbx.Databases(c, e.Engine)
if err != nil {
return dto.DbDatabasesView{}, fmt.Errorf("dbm: list databases failed: %w", err)
}
return dto.DbDatabasesView{Names: names}, nil
}
func (s *DbMgrSvc) Tables(d dto.DbScopeDto) (dto.DbTablesView, error) {
db := ""
if d.Database != nil {
db = *d.Database
}
c, e, err := s.open(d.Id, db)
if err != nil {
return dto.DbTablesView{}, err
}
defer c.Close()
tables, cols, err := dbx.TablesAndColumns(c, e.Engine, db)
if err != nil {
return dto.DbTablesView{}, fmt.Errorf("dbm: list tables failed: %w", err)
}
v := dto.DbTablesView{Tables: []dto.DbTableView{}, Columns: []dto.DbColumnView{}}
for _, t := range tables {
v.Tables = append(v.Tables, dto.DbTableView{Name: t.Name, Type: t.Type})
}
for _, cl := range cols {
v.Columns = append(v.Columns, dto.DbColumnView{
Table: cl.Table, Name: cl.Name, Type: cl.Type, Pk: cl.Pk,
})
}
return v, nil
}
const dbQueryTimeout = 20 * time.Second
// Query 执行单条 SQL:SELECT 回结果集(JSON 行串),其余回 affected rows
func (s *DbMgrSvc) Query(d dto.DbQueryDto) (dto.DbQueryView, error) {
sqlText := strings.TrimSpace(d.Sql)
if sqlText == "" {
return dto.DbQueryView{}, fun.Error(4001, "SQL 不能为空")
}
db := ""
if d.Database != nil {
db = *d.Database
}
c, _, err := s.open(d.Id, db)
if err != nil {
return dto.DbQueryView{}, err
}
defer c.Close()
maxRows := int64(500)
if d.MaxRows != nil && *d.MaxRows > 0 {
maxRows = *d.MaxRows
}
ctx, cancel := context.WithTimeout(context.Background(), dbQueryTimeout)
defer cancel()
res, err := dbx.Query(ctx, c, sqlText, maxRows)
if err != nil {
return dto.DbQueryView{}, fun.Error(4002, "执行失败:"+err.Error())
}
rowsJson, _ := json.Marshal(res.Rows)
return dto.DbQueryView{
IsSelect: res.IsSelect,
Columns: res.Columns,
RowsJson: string(rowsJson),
RowCount: res.RowCount,
AffectedRows: res.AffectedRows,
Truncated: res.Truncated,
ElapsedMs: res.ElapsedMs,
}, nil
}
func dbconnView(e store.Dbconn) dto.DbconnView {
return dto.DbconnView{
Id: e.Id,
Name: e.Name,
Engine: e.Engine,
Host: e.Host,
Port: e.Port,
Username: e.Username,
Database: e.Database,
HasPassword: e.PasswordEnc != "",
CreatedAt: e.CreatedAt,
}
}