commit 9a588233d030288b457fc198233a1c71bf61303c Author: longpeng Date: Sun Aug 30 08:08:25 2026 +0800 feat: implement config center phases 1-5 diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..90d28cb --- /dev/null +++ b/.dockerignore @@ -0,0 +1,7 @@ +.git +.env +bin +coverage.out +web/dist +web/node_modules +*.log diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..e6e3792 --- /dev/null +++ b/.env.example @@ -0,0 +1,14 @@ +HTTP_ADDR=:8080 +DATABASE_URL=postgres://configcenter:configcenter@localhost:5432/configcenter?sslmode=disable +ETCD_ENDPOINTS=http://localhost:2379 +ETCD_DIAL_TIMEOUT=5s +OUTBOX_INTERVAL=500ms +OUTBOX_BATCH_SIZE=50 +OUTBOX_MAX_RETRY=12 +CORS_ALLOWED_ORIGINS=http://localhost:5173 +AUTH_ENABLED=false +JWT_SECRET=replace-with-at-least-32-random-characters +JWT_TTL=8h +BOOTSTRAP_ADMIN_USERNAME=admin +BOOTSTRAP_ADMIN_PASSWORD=replace-with-at-least-12-characters +BOOTSTRAP_ADMIN_DISPLAY_NAME=Administrator diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7f31b2b --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +.env +.idea/ +.vscode/ +bin/ +coverage.out +web/dist/ +web/node_modules/ +*.log +__pycache__/ +*.pyc diff --git a/ConfigCenter.jsx b/ConfigCenter.jsx new file mode 100644 index 0000000..2956548 --- /dev/null +++ b/ConfigCenter.jsx @@ -0,0 +1,1371 @@ +import React, { useState, useEffect } from 'react'; +import { + Package, FolderTree, Globe, SlidersHorizontal, Plus, Pencil, Trash2, + Search, X, UploadCloud, History, RotateCcw, AlertTriangle, Check, + LockKeyhole, GitBranch, Users, LogOut, ShieldCheck, +} from 'lucide-react'; +import { api } from './web/src/api.js'; + +/* ========================================================================= + * 工具函数 + * ========================================================================= */ +let idCounter = 1000; +function genId(prefix) { + idCounter += 1; + return `${prefix}-${idCounter}`; +} +function nowStr() { + const d = new Date(); + const p = (n) => String(n).padStart(2, '0'); + return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`; +} +function todayStr() { + const d = new Date(); + const p = (n) => String(n).padStart(2, '0'); + return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`; +} + +const ENV_STYLES = { + DEV: { badge: 'bg-emerald-50 text-emerald-700 border-emerald-200', dot: 'bg-emerald-500' }, + TEST: { badge: 'bg-amber-50 text-amber-700 border-amber-200', dot: 'bg-amber-500' }, + STAGING: { badge: 'bg-violet-50 text-violet-700 border-violet-200', dot: 'bg-violet-500' }, + PROD: { badge: 'bg-rose-50 text-rose-700 border-rose-200', dot: 'bg-rose-500' }, +}; +function envStyle(code) { + return ENV_STYLES[code] || { badge: 'bg-slate-100 text-slate-700 border-slate-200', dot: 'bg-slate-400' }; +} +const TYPE_BADGE = { + properties: 'bg-slate-100 text-slate-600', + yaml: 'bg-indigo-50 text-indigo-600', + json: 'bg-amber-50 text-amber-600', + xml: 'bg-violet-50 text-violet-600', +}; + +/* ========================================================================= + * 种子数据 + * ========================================================================= */ +const initialEnvironments = [ + { id: 'env-dev', code: 'DEV', name: '开发环境', desc: '开发联调使用', order: 1 }, + { id: 'env-test', code: 'TEST', name: '测试环境', desc: 'QA 测试验证', order: 2 }, + { id: 'env-staging', code: 'STAGING', name: '预发环境', desc: '上线前灰度验证', order: 3 }, + { id: 'env-prod', code: 'PROD', name: '生产环境', desc: '线上正式环境', order: 4 }, +]; + +const initialApps = [ + { id: 'app-user', code: 'user-service', name: '用户服务', department: '平台架构部', owner: '张伟', desc: '用户注册、登录与账户中心', createdAt: '2026-01-10' }, + { id: 'app-order', code: 'order-service', name: '订单服务', department: '交易平台部', owner: '李娜', desc: '订单创建、支付与售后流程', createdAt: '2026-02-15' }, + { id: 'app-gateway', code: 'api-gateway', name: 'API 网关', department: '平台架构部', owner: '王强', desc: '统一流量入口与路由转发', createdAt: '2026-01-22' }, +]; + +const initialNamespaces = [ + { id: 'ns-user-app', appId: 'app-user', name: 'application', type: 'properties', desc: '默认应用配置' }, + { id: 'ns-user-db', appId: 'app-user', name: 'database', type: 'yaml', desc: '数据库连接配置' }, + { id: 'ns-order-app', appId: 'app-order', name: 'application', type: 'properties', desc: '默认应用配置' }, + { id: 'ns-gateway-app', appId: 'app-gateway', name: 'application', type: 'properties', desc: '默认应用配置' }, + { id: 'ns-gateway-routes', appId: 'app-gateway', name: 'routes', type: 'json', desc: '路由与限流规则' }, +]; + +const rawConfigs = [ + { appId: 'app-user', nsId: 'ns-user-app', envId: 'env-dev', key: 'server.port', value: '8080', comment: '服务监听端口', released: true }, + { appId: 'app-user', nsId: 'ns-user-app', envId: 'env-dev', key: 'feature.newLogin.enabled', value: 'true', comment: '新登录页灰度开关', released: true }, + { appId: 'app-user', nsId: 'ns-user-app', envId: 'env-dev', key: 'log.level', value: 'INFO', releasedValueOverride: 'DEBUG', comment: '日志级别' }, + { appId: 'app-user', nsId: 'ns-user-app', envId: 'env-test', key: 'server.port', value: '8080', released: true }, + { appId: 'app-user', nsId: 'ns-user-app', envId: 'env-test', key: 'feature.newLogin.enabled', value: 'false', released: true }, + { appId: 'app-user', nsId: 'ns-user-app', envId: 'env-prod', key: 'server.port', value: '8080', released: true }, + { appId: 'app-user', nsId: 'ns-user-app', envId: 'env-prod', key: 'feature.newLogin.enabled', value: 'false', released: true }, + { appId: 'app-user', nsId: 'ns-user-app', envId: 'env-prod', key: 'rate.limit.qps', value: '500', comment: '接口限流阈值', released: true, pendingDelete: true }, + { appId: 'app-user', nsId: 'ns-user-app', envId: 'env-prod', key: 'cache.ttl.seconds', value: '300', comment: '缓存过期时间' }, + { appId: 'app-user', nsId: 'ns-user-db', envId: 'env-dev', key: 'db.url', value: 'jdbc:mysql://dev-db:3306/user', comment: '数据库连接地址', released: true }, + { appId: 'app-user', nsId: 'ns-user-db', envId: 'env-dev', key: 'db.pool.size', value: '10', released: true }, + { appId: 'app-user', nsId: 'ns-user-db', envId: 'env-prod', key: 'db.url', value: 'jdbc:mysql://prod-db-vip:3306/user', released: true }, + { appId: 'app-user', nsId: 'ns-user-db', envId: 'env-prod', key: 'db.pool.size', value: '50', released: true }, + { appId: 'app-order', nsId: 'ns-order-app', envId: 'env-dev', key: 'pay.channel.default', value: 'alipay', comment: '默认支付渠道', released: true }, + { appId: 'app-order', nsId: 'ns-order-app', envId: 'env-dev', key: 'order.timeout.minutes', value: '30', released: true }, + { appId: 'app-order', nsId: 'ns-order-app', envId: 'env-test', key: 'pay.channel.default', value: 'alipay', released: true }, + { appId: 'app-order', nsId: 'ns-order-app', envId: 'env-prod', key: 'pay.channel.default', value: 'wechat', released: true }, + { appId: 'app-order', nsId: 'ns-order-app', envId: 'env-prod', key: 'order.timeout.minutes', value: '15', released: true }, + { appId: 'app-gateway', nsId: 'ns-gateway-app', envId: 'env-dev', key: 'gateway.timeout.ms', value: '3000', released: true }, + { appId: 'app-gateway', nsId: 'ns-gateway-app', envId: 'env-prod', key: 'gateway.timeout.ms', value: '1500', released: true }, + { appId: 'app-gateway', nsId: 'ns-gateway-routes', envId: 'env-prod', key: 'routes.user-service', value: '/api/user/** -> user-service', comment: '用户服务路由规则', released: true }, +]; +const initialConfigs = rawConfigs.map((r) => ({ + id: genId('cfg'), + appId: r.appId, + nsId: r.nsId, + envId: r.envId, + key: r.key, + value: r.value, + releasedValue: r.releasedValueOverride !== undefined ? r.releasedValueOverride : (r.released ? r.value : null), + pendingDelete: !!r.pendingDelete, + comment: r.comment || '', + updatedAt: '2026-08-25 14:30', +})); + +const initialReleases = [ + { id: genId('rel'), appId: 'app-user', nsId: 'ns-user-app', envId: 'env-prod', version: 1, time: '2026-07-01 10:00', operator: 'admin', comment: '初始化生产环境配置', added: 2, modified: 0, removed: 0, snapshot: { 'server.port': '8080', 'feature.newLogin.enabled': 'false' } }, + { id: genId('rel'), appId: 'app-user', nsId: 'ns-user-app', envId: 'env-prod', version: 2, time: '2026-08-10 16:20', operator: 'admin', comment: '新增限流配置', added: 1, modified: 0, removed: 0, snapshot: { 'server.port': '8080', 'feature.newLogin.enabled': 'false', 'rate.limit.qps': '500' } }, +]; + +/* ========================================================================= + * 通用小组件 + * ========================================================================= */ +function Field({ label, required, children }) { + return ( + + ); +} +function inputCls(disabled) { + return `w-full rounded-lg border px-3 py-1.5 text-sm outline-none transition ${ + disabled ? 'bg-slate-50 text-slate-400 border-slate-200 cursor-not-allowed' : 'border-slate-200 focus:border-indigo-400 focus:ring-2 focus:ring-indigo-100' + }`; +} +function EmptyHint({ text }) { + return
{text}
; +} +function Toast({ toast }) { + const tones = { success: 'bg-slate-900 text-white', danger: 'bg-rose-600 text-white' }; + return ( +
+ {toast.msg} +
+ ); +} + +function LoginPage({ onLogin }) { + const [username, setUsername] = useState(''); + const [password, setPassword] = useState(''); + const [error, setError] = useState(''); + const [submitting, setSubmitting] = useState(false); + + async function submit(e) { + e.preventDefault(); + setSubmitting(true); + setError(''); + try { + await onLogin(username, password); + } catch (err) { + setError(err.message); + } finally { + setSubmitting(false); + } + } + + return ( +
+
+
+
+

登录配置中心

使用管理员或应用成员账号

+
+ setUsername(e.target.value)} className={inputCls()} autoComplete="username" /> + setPassword(e.target.value)} className={inputCls()} autoComplete="current-password" /> + {error &&

{error}

} + +
+
+ ); +} +function ConfirmDialog({ confirm, onCancel }) { + return ( +
+
e.stopPropagation()} className="bg-white rounded-xl shadow-xl w-full max-w-sm p-5"> +
+
+ +
+
+

{confirm.title || '确认操作'}

+

{confirm.message}

+
+
+
+ + +
+
+
+ ); +} + +/* ========================================================================= + * 新建 / 编辑 弹窗(应用、命名空间、环境、配置项 共用) + * ========================================================================= */ +function EntityModal({ modal, apps, onClose, onSubmit }) { + const { entityType, mode, data } = modal; + const [form, setForm] = useState(() => data || {}); + const set = (k, v) => setForm((f) => ({ ...f, [k]: v })); + + const titles = { + app: mode === 'add' ? '新建应用' : '编辑应用', + namespace: mode === 'add' ? '新建命名空间' : '编辑命名空间', + environment: mode === 'add' ? '新建环境' : '编辑环境', + config: mode === 'add' ? '添加配置项' : '编辑配置项', + }; + + function handleSubmit(e) { + e.preventDefault(); + onSubmit(form); + } + + return ( +
+
e.stopPropagation()} onSubmit={handleSubmit} className="bg-white rounded-xl shadow-xl w-full max-w-md overflow-hidden"> +
+

{titles[entityType]}

+ +
+ +
+ {entityType === 'app' && ( + <> + + set('code', e.target.value)} placeholder="如 user-service" className={inputCls(mode === 'edit') + ' font-mono'} /> + + + set('name', e.target.value)} className={inputCls()} /> + + + set('department', e.target.value)} className={inputCls()} /> + + + set('owner', e.target.value)} className={inputCls()} /> + + +