feat: CRUD definitions, build script, DDL generation

- 3 CRUD JSON files: customer_balance, accounting_records, sync_state
- Build script with model validation, CRUD validation, DDL generation
- DDL: db/schema.sql (72 lines, 7 tables)
- Scripts: validate_models.py, validate_crud.py, generate_ddl.py
This commit is contained in:
Hermes Agent 2026-05-20 18:28:59 +08:00
parent 5936a2f328
commit acb9674375
8 changed files with 410 additions and 1 deletions

View File

@ -1,3 +1,22 @@
#!/bin/bash
# SageAPI build script
set -e
xls2ui -m ../models -o ../wwwroot sageapi *.json
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
cd "$SCRIPT_DIR"
echo "=== SageAPI Build ==="
# 1. Validate JSON models
echo "[1/3] Validating model definitions..."
python3 scripts/validate_models.py
# 2. Validate CRUD JSON
echo "[2/3] Validating CRUD definitions..."
python3 scripts/validate_crud.py
# 3. Generate DDL
echo "[3/3] Generating DDL..."
python3 scripts/generate_ddl.py
echo "=== Build Complete ==="

152
db/schema.sql Normal file
View File

@ -0,0 +1,152 @@
-- SageAPI DDL (auto-generated)
CREATE TABLE IF NOT EXISTS `accounting_records` (
`id` VARCHAR(32) NOT NULL DEFAULT COMMENT '主键',
`customer_id` VARCHAR(32) NOT NULL DEFAULT COMMENT '客户ID',
`llmid` VARCHAR(32) NULL DEFAULT COMMENT '模型ID',
`model_name` VARCHAR(128) NULL DEFAULT COMMENT '模型名称',
`pricing_id` VARCHAR(32) NULL DEFAULT COMMENT '定价ID',
`input_tokens` BIGINT NULL COMMENT '输入token数',
`output_tokens` BIGINT NULL COMMENT '输出token数',
`total_tokens` BIGINT NULL COMMENT '总token数',
`quantity` DECIMAL(15,4) NULL COMMENT '用量(图片数/分钟数等)',
`amount` DECIMAL(15,6) NOT NULL DEFAULT 0.0 COMMENT '金额',
`currency` VARCHAR(8) NOT NULL DEFAULT 'CNY' COMMENT '货币单位',
`request_id` VARCHAR(64) NULL DEFAULT COMMENT '请求ID幂等键',
`transno` VARCHAR(64) NULL DEFAULT COMMENT '事务号',
`status` VARCHAR(16) NOT NULL DEFAULT 'pending' COMMENT '状态: pending/accounted/failed',
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
ALTER TABLE `accounting_records` ADD INDEX `idx_customer_id` (`customer_id`);
ALTER TABLE `accounting_records` ADD INDEX `idx_llmid` (`llmid`);
ALTER TABLE `accounting_records` ADD UNIQUE `idx_request_id` (`request_id`);
ALTER TABLE `accounting_records` ADD INDEX `idx_status` (`status`);
ALTER TABLE `accounting_records` ADD INDEX `idx_created_at` (`created_at`);
CREATE TABLE IF NOT EXISTS `customer_balance` (
`id` VARCHAR(32) NOT NULL DEFAULT COMMENT '主键,即 customer_id',
`balance` DECIMAL(15,4) NOT NULL DEFAULT 0.0 COMMENT '当前余额',
`currency` VARCHAR(8) NOT NULL DEFAULT 'CNY' COMMENT '货币单位',
`credit_limit` DECIMAL(15,4) NULL COMMENT '信用额度',
`last_recharge` DATETIME NULL COMMENT '最后充值时间',
`last_consumption` DATETIME NULL COMMENT '最后消费时间',
`status` VARCHAR(16) NOT NULL DEFAULT 'active' COMMENT '状态: active/suspended/arrears',
`sync_version` VARCHAR(32) NULL DEFAULT COMMENT '同步版本号',
`cached_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '缓存更新时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
ALTER TABLE `customer_balance` ADD INDEX `idx_status` (`status`);
ALTER TABLE `customer_balance` ADD INDEX `idx_balance` (`balance`);
CREATE TABLE IF NOT EXISTS `llmage_cache` (
`id` VARCHAR(32) NOT NULL DEFAULT COMMENT '主键',
`llmid` VARCHAR(32) NOT NULL DEFAULT COMMENT '关联模型ID',
`model_name` VARCHAR(128) NULL DEFAULT COMMENT '模型名称',
`upappid` VARCHAR(32) NOT NULL DEFAULT COMMENT '上游应用ID',
`apiname` VARCHAR(128) NOT NULL DEFAULT COMMENT 'API名称',
`api_url` VARCHAR(512) NULL DEFAULT COMMENT 'API端点URL',
`api_params` TEXT NULL COMMENT 'API参数配置JSON',
`model_params` TEXT NULL COMMENT '模型参数配置JSONmax_tokens, temperature等',
`status` VARCHAR(16) NOT NULL DEFAULT 'active' COMMENT '状态: active/inactive',
`sync_version` VARCHAR(32) NULL DEFAULT COMMENT '同步版本号',
`cached_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '缓存写入时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
ALTER TABLE `llmage_cache` ADD INDEX `idx_llmid` (`llmid`);
ALTER TABLE `llmage_cache` ADD INDEX `idx_upappid` (`upappid`);
ALTER TABLE `llmage_cache` ADD INDEX `idx_apiname` (`apiname`);
CREATE TABLE IF NOT EXISTS `pricing_cache` (
`id` VARCHAR(32) NOT NULL DEFAULT COMMENT '主键,对应 pricing_program id (ppid)',
`llmid` VARCHAR(32) NOT NULL DEFAULT COMMENT '关联模型ID',
`model_name` VARCHAR(128) NULL DEFAULT COMMENT '模型名称',
`pricing_type` VARCHAR(32) NOT NULL DEFAULT COMMENT '计费类型: token/image/video/audio',
`input_price` DECIMAL(10,6) NULL COMMENT '输入单价每千token',
`output_price` DECIMAL(10,6) NULL COMMENT '输出单价每千token',
`unit_price` DECIMAL(10,6) NULL COMMENT '统一单价(按次/按图/按分钟等)',
`currency` VARCHAR(8) NOT NULL DEFAULT 'CNY' COMMENT '货币单位',
`status` VARCHAR(16) NOT NULL DEFAULT 'active' COMMENT '状态: active/inactive/deprecated',
`effective_from` DATETIME NULL COMMENT '生效时间',
`effective_to` DATETIME NULL COMMENT '失效时间',
`sync_version` VARCHAR(32) NULL DEFAULT COMMENT '同步版本号',
`cached_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '缓存写入时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
ALTER TABLE `pricing_cache` ADD INDEX `idx_llmid` (`llmid`);
ALTER TABLE `pricing_cache` ADD INDEX `idx_pricing_type` (`pricing_type`);
ALTER TABLE `pricing_cache` ADD INDEX `idx_status` (`status`);
CREATE TABLE IF NOT EXISTS `sync_state` (
`id` VARCHAR(32) NOT NULL DEFAULT COMMENT '主键',
`entity_type` VARCHAR(32) NOT NULL DEFAULT COMMENT '实体类型: users/pricing/llmage/uapi',
`entity_id` VARCHAR(64) NULL DEFAULT COMMENT '实体标识(全量同步时为空)',
`last_sync_time` DATETIME NULL COMMENT '最后同步时间',
`sync_version` VARCHAR(32) NULL DEFAULT COMMENT 'Sage返回的版本标识',
`sync_status` VARCHAR(16) NOT NULL DEFAULT 'success' COMMENT '同步状态: success/pending/failed',
`error_msg` TEXT NULL COMMENT '失败原因',
`retry_count` INT NOT NULL DEFAULT 0 COMMENT '重试次数',
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
ALTER TABLE `sync_state` ADD INDEX `idx_entity_type` (`entity_type`);
ALTER TABLE `sync_state` ADD UNIQUE `idx_entity_type_id` (`entity_type`,`entity_id`);
ALTER TABLE `sync_state` ADD INDEX `idx_sync_status` (`sync_status`);
CREATE TABLE IF NOT EXISTS `uapi_cache` (
`id` VARCHAR(32) NOT NULL DEFAULT COMMENT '主键',
`upappid` VARCHAR(32) NOT NULL DEFAULT COMMENT '上游应用ID',
`apiname` VARCHAR(128) NOT NULL DEFAULT COMMENT 'API名称',
`method` VARCHAR(16) NULL DEFAULT 'POST' COMMENT 'HTTP方法',
`endpoint` VARCHAR(512) NULL DEFAULT COMMENT 'API端点',
`auth_type` VARCHAR(32) NULL DEFAULT 'bearer' COMMENT '认证类型',
`rate_limit` INT NULL COMMENT '速率限制(次/分钟)',
`description` TEXT NULL COMMENT 'API描述',
`status` VARCHAR(16) NOT NULL DEFAULT 'active' COMMENT '状态',
`sync_version` VARCHAR(32) NULL DEFAULT COMMENT '同步版本号',
`cached_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '缓存写入时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
ALTER TABLE `uapi_cache` ADD UNIQUE `idx_upappid_apiname` (`upappid`,`apiname`);
ALTER TABLE `uapi_cache` ADD INDEX `idx_status` (`status`);
CREATE TABLE IF NOT EXISTS `users_cache` (
`id` VARCHAR(32) NOT NULL DEFAULT COMMENT '主键,对应 users 表 id',
`username` VARCHAR(128) NOT NULL DEFAULT COMMENT '用户名',
`orgid` VARCHAR(32) NULL DEFAULT COMMENT '组织ID',
`orgname` VARCHAR(255) NULL DEFAULT COMMENT '组织名称',
`email` VARCHAR(128) NULL DEFAULT COMMENT '邮箱',
`phone` VARCHAR(32) NULL DEFAULT COMMENT '手机号',
`status` VARCHAR(16) NOT NULL DEFAULT 'active' COMMENT '状态: active/inactive/suspended',
`created_at` DATETIME NULL COMMENT '创建时间',
`updated_at` DATETIME NULL COMMENT '更新时间',
`sync_version` VARCHAR(32) NULL DEFAULT COMMENT '同步版本号',
`cached_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '缓存写入时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
ALTER TABLE `users_cache` ADD INDEX `idx_username` (`username`);
ALTER TABLE `users_cache` ADD INDEX `idx_orgid` (`orgid`);
ALTER TABLE `users_cache` ADD INDEX `idx_sync_version` (`sync_version`);

View File

@ -0,0 +1,41 @@
{
"accounting_records": {
"params": {
"title": "记账记录管理",
"dbname": "sageapi",
"page_size": 20
},
"fields": [
{"name": "id", "title": "记录ID", "type": "string", "readonly": true, "width": 120},
{"name": "customer_id", "title": "客户ID", "type": "string", "width": 120},
{"name": "llmid", "title": "模型ID", "type": "string", "width": 120},
{"name": "model_name", "title": "模型名称", "type": "string", "width": 140},
{"name": "pricing_id", "title": "定价ID", "type": "string", "width": 100},
{"name": "input_tokens", "title": "输入Token", "type": "int", "width": 90, "align": "right"},
{"name": "output_tokens", "title": "输出Token", "type": "int", "width": 90, "align": "right"},
{"name": "total_tokens", "title": "总Token", "type": "int", "width": 90, "align": "right"},
{"name": "quantity", "title": "用量", "type": "float", "width": 80, "align": "right"},
{"name": "amount", "title": "金额", "type": "float", "width": 90, "align": "right"},
{"name": "currency", "title": "货币", "type": "string", "width": 60},
{"name": "request_id", "title": "请求ID", "type": "string", "width": 120},
{"name": "transno", "title": "事务号", "type": "string", "width": 120},
{"name": "status", "title": "状态", "type": "select", "width": 80, "options": [{"value": "pending", "label": "待处理"}, {"value": "accounted", "label": "已记账"}, {"value": "failed", "label": "失败"}]},
{"name": "created_at", "title": "创建时间", "type": "datetime", "width": 160, "readonly": true},
{"name": "updated_at", "title": "更新时间", "type": "datetime", "width": 160, "readonly": true}
],
"list": {
"columns": ["id", "customer_id", "model_name", "amount", "total_tokens", "status", "created_at"],
"filters": [
{"field": "customer_id", "type": "text", "label": "客户ID"},
{"field": "status", "type": "select", "label": "状态", "options": [{"value": "", "label": "全部"}, {"value": "pending", "label": "待处理"}, {"value": "accounted", "label": "已记账"}, {"value": "failed", "label": "失败"}]},
{"field": "date_from", "type": "date", "label": "起始日期"},
{"field": "date_to", "type": "date", "label": "截止日期"}
],
"sort": {"field": "created_at", "order": "desc"}
},
"view": {"fields": ["id", "customer_id", "llmid", "model_name", "pricing_id", "input_tokens", "output_tokens", "total_tokens", "quantity", "amount", "currency", "request_id", "transno", "status", "created_at", "updated_at"]},
"create": {"enabled": false},
"update": {"enabled": false},
"delete": {"enabled": false}
}
}

View File

@ -0,0 +1,33 @@
{
"customer_balance": {
"params": {
"title": "客户余额管理",
"dbname": "sageapi",
"page_size": 20
},
"fields": [
{"name": "id", "title": "客户ID", "type": "string", "readonly": true, "width": 120},
{"name": "balance", "title": "余额", "type": "float", "width": 100, "align": "right"},
{"name": "currency", "title": "货币", "type": "string", "width": 60},
{"name": "credit_limit", "title": "信用额度", "type": "float", "width": 100, "align": "right"},
{"name": "last_recharge", "title": "最后充值", "type": "datetime", "width": 160},
{"name": "last_consumption", "title": "最后消费", "type": "datetime", "width": 160},
{"name": "status", "title": "状态", "type": "select", "width": 80, "options": [{"value": "active", "label": "正常"}, {"value": "suspended", "label": "暂停"}, {"value": "arrears", "label": "欠费"}]},
{"name": "cached_at", "title": "缓存时间", "type": "datetime", "width": 160, "readonly": true}
],
"list": {
"columns": ["id", "balance", "currency", "status", "last_recharge", "cached_at"],
"filters": [
{"field": "id", "type": "text", "label": "客户ID"},
{"field": "status", "type": "select", "label": "状态", "options": [{"value": "", "label": "全部"}, {"value": "active", "label": "正常"}, {"value": "suspended", "label": "暂停"}, {"value": "arrears", "label": "欠费"}]}
]
},
"view": {"fields": ["id", "balance", "currency", "credit_limit", "last_recharge", "last_consumption", "status", "cached_at"]},
"create": {"enabled": false},
"update": {
"enabled": true,
"fields": ["balance", "credit_limit", "status"]
},
"delete": {"enabled": false}
}
}

36
json/sync_state.json Normal file
View File

@ -0,0 +1,36 @@
{
"sync_state": {
"params": {
"title": "同步状态管理",
"dbname": "sageapi",
"page_size": 20
},
"fields": [
{"name": "id", "title": "ID", "type": "string", "readonly": true, "width": 100},
{"name": "entity_type", "title": "实体类型", "type": "select", "width": 100, "options": [{"value": "users", "label": "用户"}, {"value": "pricing", "label": "定价"}, {"value": "uapi", "label": "UAPI"}, {"value": "llmage", "label": "LLMage"}]},
{"name": "entity_id", "title": "实体ID", "type": "string", "width": 120},
{"name": "last_sync_time", "title": "最后同步时间", "type": "datetime", "width": 160},
{"name": "sync_version", "title": "同步版本", "type": "string", "width": 100},
{"name": "sync_status", "title": "同步状态", "type": "select", "width": 80, "options": [{"value": "success", "label": "成功"}, {"value": "pending", "label": "待同步"}, {"value": "failed", "label": "失败"}]},
{"name": "error_msg", "title": "错误信息", "type": "text", "width": 200},
{"name": "retry_count", "title": "重试次数", "type": "int", "width": 70, "align": "right"},
{"name": "created_at", "title": "创建时间", "type": "datetime", "width": 160, "readonly": true},
{"name": "updated_at", "title": "更新时间", "type": "datetime", "width": 160, "readonly": true}
],
"list": {
"columns": ["entity_type", "entity_id", "sync_status", "last_sync_time", "retry_count", "updated_at"],
"filters": [
{"field": "entity_type", "type": "select", "label": "实体类型", "options": [{"value": "", "label": "全部"}, {"value": "users", "label": "用户"}, {"value": "pricing", "label": "定价"}, {"value": "uapi", "label": "UAPI"}, {"value": "llmage", "label": "LLMage"}]},
{"field": "sync_status", "type": "select", "label": "状态", "options": [{"value": "", "label": "全部"}, {"value": "success", "label": "成功"}, {"value": "pending", "label": "待同步"}, {"value": "failed", "label": "失败"}]}
],
"sort": {"field": "updated_at", "order": "desc"}
},
"view": {"fields": ["id", "entity_type", "entity_id", "last_sync_time", "sync_version", "sync_status", "error_msg", "retry_count", "created_at", "updated_at"]},
"create": {"enabled": false},
"update": {
"enabled": true,
"fields": ["sync_status", "retry_count"]
},
"delete": {"enabled": false}
}
}

55
scripts/generate_ddl.py Executable file
View File

@ -0,0 +1,55 @@
#!/usr/bin/env python3
"""Generate DDL from SageAPI model JSON definitions."""
import json
import os
def generate_ddl(models_dir='models', output_path='db/schema.sql'):
os.makedirs(os.path.dirname(output_path), exist_ok=True)
ddl_lines = ['-- SageAPI DDL (auto-generated)', '']
for f in sorted(os.listdir(models_dir)):
if not f.endswith('.json'):
continue
with open(os.path.join(models_dir, f)) as fh:
data = json.load(fh)
summary = data['summary'][0]
tblname = summary['name']
primary = summary['primary']
btick = '`'
ddl_lines.append(f'CREATE TABLE IF NOT EXISTS {btick}{tblname}{btick} (')
col_defs = []
for field in data['fields']:
nullable = 'NULL' if field.get('nullable', True) else 'NOT NULL'
default = ''
if field.get('default') is not None:
if isinstance(field['default'], str) and field['default'] not in ('CURRENT_TIMESTAMP', 'NULL', ''):
default = f"DEFAULT '{field['default']}'"
else:
default = f"DEFAULT {field['default']}"
comment = f"COMMENT '{field.get('comment', '')}'"
fname = f'{btick}{field["name"]}{btick}'
col_defs.append(f' {fname} {field["type"]} {nullable} {default} {comment}'.strip())
pk = f'{btick}{primary}{btick}'
col_defs.append(f' PRIMARY KEY ({pk})')
ddl_lines.append(',\n'.join(col_defs))
ddl_lines.append(') ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;')
ddl_lines.append('')
tbl = f'{btick}{tblname}{btick}'
for idx in data.get('idxfields', []):
unique = 'UNIQUE' if idx.get('unique') else 'INDEX'
cols = ','.join(f'{btick}{c}{btick}' for c in idx['fields'])
idx_name = f'{btick}{idx["name"]}{btick}'
ddl_lines.append(f'ALTER TABLE {tbl} ADD {unique} {idx_name} ({cols});')
ddl_lines.append('')
with open(output_path, 'w') as fh:
fh.write('\n'.join(ddl_lines))
print(f' Generated: {output_path} ({len(ddl_lines)} lines)')
if __name__ == '__main__':
generate_ddl()

32
scripts/validate_crud.py Executable file
View File

@ -0,0 +1,32 @@
#!/usr/bin/env python3
"""Validate SageAPI CRUD JSON definitions."""
import json
import os
import sys
def validate_crud(json_dir='json'):
errors = 0
for f in sorted(os.listdir(json_dir)):
if not f.endswith('.json'):
continue
filepath = os.path.join(json_dir, f)
with open(filepath) as fh:
data = json.load(fh)
for tblname, config in data.items():
if 'params' not in config:
print(f'ERROR: {f} root key "{tblname}" missing "params"')
errors += 1
if 'fields' not in config:
print(f'ERROR: {f} root key "{tblname}" missing "fields"')
errors += 1
if not errors:
print(f' OK: {f} ({tblname})')
if errors:
print(f'\n{errors} error(s) found')
sys.exit(1)
print('All CRUD definitions valid')
if __name__ == '__main__':
validate_crud()

41
scripts/validate_models.py Executable file
View File

@ -0,0 +1,41 @@
#!/usr/bin/env python3
"""Validate SageAPI model JSON definitions."""
import json
import os
import sys
def validate_models(models_dir='models'):
errors = 0
for f in sorted(os.listdir(models_dir)):
if not f.endswith('.json'):
continue
filepath = os.path.join(models_dir, f)
with open(filepath) as fh:
data = json.load(fh)
for key in ['summary', 'fields', 'idxfields']:
if key not in data:
print(f'ERROR: {f} missing "{key}"')
errors += 1
if not data.get('summary') or not isinstance(data['summary'], list):
print(f'ERROR: {f} summary must be a non-empty list')
errors += 1
continue
summary = data['summary'][0]
if 'primary' not in summary:
print(f'ERROR: {f} summary must have "primary" field')
errors += 1
table_name = summary.get('name', '?')
primary = summary.get('primary', '?')
print(f' OK: {f} ({table_name}, primary={primary})')
if errors:
print(f'\n{errors} error(s) found')
sys.exit(1)
print('All model definitions valid')
if __name__ == '__main__':
validate_models()