fix: spec compliance pass - ServerEnv removal in .dspy, core.py DBPools singleton, scripts/load_path.py

- All .dspy files: replace ServerEnv() org_id access with await get_userorgid()
  (get_userorgid is registered as global in ahserver processorResource)
- core.py: simplify _get_db to use DBPools() singleton directly (DBPools is @SingletonDecorator)
  remove unnecessary db.databases = config.databases assignment
- core.py: add MODULE_NAME constant, use env.get_module_dbname(MODULE_NAME) pattern
- Create scripts/load_path.py with all RBAC paths per module-development-spec
  Covers: entry pages, feature .ui files, CRUD directories, all API .dspy endpoints
- .gitignore: add __pycache__/ exclusion
- All models/*.json and json/*.json pass spec validation checks
This commit is contained in:
yumoqing 2026-05-25 17:02:39 +08:00
parent 4fd136bf53
commit 43787a63a4
17 changed files with 159 additions and 66 deletions

1
.gitignore vendored
View File

@ -1 +1,2 @@
product_management.egg-info/
__pycache__/

View File

@ -8,6 +8,9 @@ from sqlor.dbpools import DBPools
from ahserver.serverenv import ServerEnv
MODULE_NAME = "product_management"
class ProductManager:
"""Core manager for product catalog, category tree, and operator configs.
@ -15,15 +18,10 @@ class ProductManager:
Different resellers have completely independent category trees and products.
"""
def _get_db(self):
"""Get database context following Sage singleton fork-safe pattern."""
def _get_dbname(self):
"""Get module database name from ServerEnv."""
env = ServerEnv()
dbname = env.get_module_dbname('product_management')
from appPublic.jsonConfig import getConfig
config = getConfig()
db = DBPools()
db.databases = config.databases
return db, dbname
return env.get_module_dbname(MODULE_NAME)
def _get_current_org_id(self):
"""Get current user's organization ID from ServerEnv."""
@ -35,8 +33,8 @@ class ProductManager:
if not org_id:
org_id = self._get_current_org_id()
db, dbname = self._get_db()
async with db.sqlorContext(dbname) as sor:
dbname = self._get_dbname()
async with DBPools().sqlorContext(dbname) as sor:
sql = """SELECT * FROM product_category
WHERE org_id = ${org_id}$ AND status = '1'
ORDER BY sort_order ASC, name ASC"""
@ -65,10 +63,10 @@ class ProductManager:
if not org_id:
org_id = self._get_current_org_id()
db, dbname = self._get_db()
dbname = self._get_dbname()
# Get all sub-category IDs recursively within same org
async with db.sqlorContext(dbname) as sor:
async with DBPools().sqlorContext(dbname) as sor:
all_ids = [category_id]
queue = [category_id]
while queue:
@ -132,7 +130,7 @@ class ProductManager:
if not org_id:
org_id = self._get_current_org_id()
db, dbname = self._get_db()
dbname = self._get_dbname()
conditions = ["p.status = '1'", "p.org_id = ${org_id}$"]
params = {'org_id': org_id}
@ -150,7 +148,7 @@ class ProductManager:
where_clause = " AND ".join(conditions)
async with db.sqlorContext(dbname) as sor:
async with DBPools().sqlorContext(dbname) as sor:
sql = f"""SELECT p.id, p.product_code, p.product_name, p.category_id,
pc.name as category_name, p.brief_intro,
p.price, p.currency, p.enabled_date, p.expired_date,
@ -191,7 +189,7 @@ class ProductManager:
except:
user_id = 'anonymous'
db, dbname = self._get_db()
dbname = self._get_dbname()
conditions = ["p.org_id = ${org_id}$"]
params = {'org_id': org_id}
@ -207,7 +205,7 @@ class ProductManager:
where_clause = " AND ".join(conditions)
async with db.sqlorContext(dbname) as sor:
async with DBPools().sqlorContext(dbname) as sor:
sql = f"""SELECT p.*, pc.name as category_name, pc.description as category_description
FROM product p
LEFT JOIN product_category pc ON p.category_id = pc.id AND p.org_id = pc.org_id
@ -277,11 +275,11 @@ class ProductManager:
if not product_id:
return {'success': False, 'message': 'Missing product_id'}
db, dbname = self._get_db()
dbname = self._get_dbname()
now = time.strftime('%Y-%m-%d %H:%M:%S')
quantity = int(quantity) if quantity else 1
async with db.sqlorContext(dbname) as sor:
async with DBPools().sqlorContext(dbname) as sor:
sql = """SELECT * FROM product WHERE id = ${product_id}$ AND status = '1' AND org_id = ${org_id}$"""
rows = await sor.sqlExe(sql, {'product_id': product_id, 'org_id': org_id})
if not rows:
@ -340,9 +338,9 @@ class ProductManager:
if not product_id:
return {'success': False, 'message': 'Missing product_id'}
db, dbname = self._get_db()
dbname = self._get_dbname()
async with db.sqlorContext(dbname) as sor:
async with DBPools().sqlorContext(dbname) as sor:
sql = """SELECT * FROM product WHERE id = ${product_id}$ AND status = '1' AND org_id = ${org_id}$"""
rows = await sor.sqlExe(sql, {'product_id': product_id, 'org_id': org_id})
if not rows:
@ -401,9 +399,9 @@ class ProductManager:
except:
user_id = 'anonymous'
db, dbname = self._get_db()
dbname = self._get_dbname()
async with db.sqlorContext(dbname) as sor:
async with DBPools().sqlorContext(dbname) as sor:
sql = """SELECT * FROM product_type_config
WHERE category_id = ${category_id}$
AND org_id = ${org_id}$
@ -443,7 +441,7 @@ class ProductManager:
if not config_name:
return {'success': False, 'message': 'Missing config_name'}
db, dbname = self._get_db()
dbname = self._get_dbname()
now = time.strftime('%Y-%m-%d %H:%M:%S')
try:
@ -451,7 +449,7 @@ class ProductManager:
except:
return {'success': False, 'message': 'Invalid config_json format'}
async with db.sqlorContext(dbname) as sor:
async with DBPools().sqlorContext(dbname) as sor:
# Verify category belongs to org
cat_check = await sor.sqlExe(
"SELECT id FROM product_category WHERE id = ${category_id}$ AND org_id = ${org_id}$",

122
scripts/load_path.py Normal file
View File

@ -0,0 +1,122 @@
#!/usr/bin/env python3
"""
product_management 模块 RBAC 权限管理脚本
使用方法:
cd ~/repos/sage
./py3/bin/python ~/repos/product_management/scripts/load_path.py
每次代码变更如有新 path 出现需同步更新此脚本
"""
import subprocess
import os
import sys
def find_sage_root():
candidates = [
os.path.expanduser("~/repos/sage"),
os.path.expanduser("~/sage"),
os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))),
]
for c in candidates:
if os.path.isdir(os.path.join(c, "py3")) and os.path.isdir(os.path.join(c, "wwwroot")):
return c
return None
SAGE_ROOT = find_sage_root()
if not SAGE_ROOT:
print("ERROR: Cannot find Sage root directory")
sys.exit(1)
PYTHON = os.path.join(SAGE_ROOT, "py3", "bin", "python")
SET_PERM_SCRIPT = os.path.join(SAGE_ROOT, "set_role_perm.py")
MOD = "product_management"
# ============================================================
# 权限路径定义 — 每次新增页面或API时同步更新
# ============================================================
# any — 无需登录(仅静态资源和菜单)
PATHS_ANY = [
f"/{MOD}/menu.ui",
]
# logined — 需要认证的页面和 API
PATHS_LOGINED = [
# Module entry
f"/{MOD}",
f"/{MOD}/index.ui",
# Feature pages
f"/{MOD}/category_manage.ui",
f"/{MOD}/product_manage.ui",
f"/{MOD}/product_type_config_manage.ui",
# CRUD alias directories (directory path + index.ui path)
f"/{MOD}/product_category_tree",
f"/{MOD}/product_category_tree/index.ui",
f"/{MOD}/product_list",
f"/{MOD}/product_list/index.ui",
f"/{MOD}/product_type_config_list",
f"/{MOD}/product_type_config_list/index.ui",
# API endpoints (.dspy)
f"/{MOD}/api/category_options.dspy",
f"/{MOD}/api/product_brief.dspy",
f"/{MOD}/api/product_category_create.dspy",
f"/{MOD}/api/product_category_update.dspy",
f"/{MOD}/api/product_category_delete.dspy",
f"/{MOD}/api/product_create.dspy",
f"/{MOD}/api/product_update.dspy",
f"/{MOD}/api/product_delete.dspy",
f"/{MOD}/api/product_detail.dspy",
f"/{MOD}/api/product_purchase.dspy",
f"/{MOD}/api/product_use.dspy",
f"/{MOD}/api/product_type_config_create.dspy",
f"/{MOD}/api/product_type_config_update.dspy",
f"/{MOD}/api/product_type_config_delete.dspy",
# CRUD auto-generated .dspy
f"/{MOD}/product_category_tree/get_product_category.dspy",
f"/{MOD}/product_category_tree/new_product_category.dspy",
f"/{MOD}/product_category_tree/update_product_category.dspy",
f"/{MOD}/product_category_tree/delete_product_category.dspy",
f"/{MOD}/product_list/get_product.dspy",
f"/{MOD}/product_list/add_product.dspy",
f"/{MOD}/product_list/update_product.dspy",
f"/{MOD}/product_list/delete_product.dspy",
f"/{MOD}/product_type_config_list/get_product_type_config.dspy",
f"/{MOD}/product_type_config_list/add_product_type_config.dspy",
f"/{MOD}/product_type_config_list/update_product_type_config.dspy",
f"/{MOD}/product_type_config_list/delete_product_type_config.dspy",
]
# ============================================================
# 执行注册
# ============================================================
def run_set_perm(role, path):
cmd = [PYTHON, SET_PERM_SCRIPT, role, path]
result = subprocess.run(cmd, capture_output=True, text=True)
return result.returncode == 0
def register_role_paths(role, paths):
count = 0
for p in paths:
if run_set_perm(role, p):
count += 1
print(f" {role}: {count}/{len(paths)} paths registered")
return count
def main():
print(f"Sage root: {SAGE_ROOT}")
total = 0
total += register_role_paths("any", PATHS_ANY)
total += register_role_paths("logined", PATHS_LOGINED)
print(f"\nDone. Total {total} permission entries registered.")
print("NOTE: Restart Sage after permission changes to reload RBAC cache.")
if __name__ == "__main__":
main()

View File

@ -5,9 +5,7 @@ result = {'success': False, 'data': []}
try:
user_id = await get_user()
from ahserver.serverenv import ServerEnv
env = ServerEnv()
org_id = getattr(env, 'orgid', None) or getattr(env, 'org_id', '0')
org_id = (await get_userorgid()) or '0'
dbname = get_module_dbname('product_management')
sql = """SELECT id, name FROM product_category

View File

@ -15,9 +15,7 @@ result = {'success': False, 'data': [], 'total': 0}
try:
user_id = await get_user()
from ahserver.serverenv import ServerEnv
env = ServerEnv()
org_id = params_kw.get('org_id', None) or getattr(env, 'orgid', None) or getattr(env, 'org_id', '0')
org_id = params_kw.get('org_id', None) or (await get_userorgid()) or '0'
product_id = params_kw.get('product_id', '')
product_code = params_kw.get('product_code', '')

View File

@ -6,9 +6,7 @@ result = {'widgettype': 'Message', 'options': {'title': 'Error', 'message': 'Inv
try:
user_id = await get_user()
from ahserver.serverenv import ServerEnv
env = ServerEnv()
org_id = getattr(env, 'orgid', None) or getattr(env, 'org_id', '0')
org_id = (await get_userorgid()) or '0'
now = time.strftime('%Y-%m-%d %H:%M:%S')
dbname = get_module_dbname('product_management')

View File

@ -5,9 +5,7 @@ result = {'widgettype': 'Message', 'options': {'title': 'Error', 'message': 'Inv
try:
user_id = await get_user()
from ahserver.serverenv import ServerEnv
env = ServerEnv()
org_id = getattr(env, 'orgid', None) or getattr(env, 'org_id', '0')
org_id = (await get_userorgid()) or '0'
dbname = get_module_dbname('product_management')
data = dict(params_kw)

View File

@ -5,9 +5,7 @@ result = {'widgettype': 'Message', 'options': {'title': 'Error', 'message': 'Inv
try:
user_id = await get_user()
from ahserver.serverenv import ServerEnv
env = ServerEnv()
org_id = getattr(env, 'orgid', None) or getattr(env, 'org_id', '0')
org_id = (await get_userorgid()) or '0'
now = time.strftime('%Y-%m-%d %H:%M:%S')
dbname = get_module_dbname('product_management')

View File

@ -6,9 +6,7 @@ result = {'widgettype': 'Message', 'options': {'title': 'Error', 'message': 'Inv
try:
user_id = await get_user()
from ahserver.serverenv import ServerEnv
env = ServerEnv()
org_id = getattr(env, 'orgid', None) or getattr(env, 'org_id', '0')
org_id = (await get_userorgid()) or '0'
now = time.strftime('%Y-%m-%d %H:%M:%S')
dbname = get_module_dbname('product_management')

View File

@ -5,9 +5,7 @@ result = {'widgettype': 'Message', 'options': {'title': 'Error', 'message': 'Inv
try:
user_id = await get_user()
from ahserver.serverenv import ServerEnv
env = ServerEnv()
org_id = getattr(env, 'orgid', None) or getattr(env, 'org_id', '0')
org_id = (await get_userorgid()) or '0'
dbname = get_module_dbname('product_management')
data = dict(params_kw)

View File

@ -18,9 +18,7 @@ result = {'success': False, 'data': {}}
try:
user_id = await get_user()
from ahserver.serverenv import ServerEnv
env = ServerEnv()
org_id = params_kw.get('org_id', None) or getattr(env, 'orgid', None) or getattr(env, 'org_id', '0')
org_id = params_kw.get('org_id', None) or (await get_userorgid()) or '0'
product_id = params_kw.get('product_id', '')
product_code = params_kw.get('product_code', '')

View File

@ -16,9 +16,7 @@ result = {'success': False, 'order_id': '', 'message': ''}
try:
user_id = await get_user()
from ahserver.serverenv import ServerEnv
env = ServerEnv()
org_id = params_kw.get('org_id', None) or getattr(env, 'orgid', None) or getattr(env, 'org_id', '0')
org_id = params_kw.get('org_id', None) or (await get_userorgid()) or '0'
now = time.strftime('%Y-%m-%d %H:%M:%S')
product_id = params_kw.get('product_id', '')

View File

@ -6,9 +6,7 @@ result = {'widgettype': 'Message', 'options': {'title': 'Error', 'message': 'Inv
try:
user_id = await get_user()
from ahserver.serverenv import ServerEnv
env = ServerEnv()
org_id = getattr(env, 'orgid', None) or getattr(env, 'org_id', '0')
org_id = (await get_userorgid()) or '0'
now = time.strftime('%Y-%m-%d %H:%M:%S')
dbname = get_module_dbname('product_management')

View File

@ -5,9 +5,7 @@ result = {'widgettype': 'Message', 'options': {'title': 'Error', 'message': 'Inv
try:
user_id = await get_user()
from ahserver.serverenv import ServerEnv
env = ServerEnv()
org_id = getattr(env, 'orgid', None) or getattr(env, 'org_id', '0')
org_id = (await get_userorgid()) or '0'
dbname = get_module_dbname('product_management')
data = dict(params_kw)

View File

@ -5,9 +5,7 @@ result = {'widgettype': 'Message', 'options': {'title': 'Error', 'message': 'Inv
try:
user_id = await get_user()
from ahserver.serverenv import ServerEnv
env = ServerEnv()
org_id = getattr(env, 'orgid', None) or getattr(env, 'org_id', '0')
org_id = (await get_userorgid()) or '0'
now = time.strftime('%Y-%m-%d %H:%M:%S')
dbname = get_module_dbname('product_management')

View File

@ -5,9 +5,7 @@ result = {'widgettype': 'Message', 'options': {'title': 'Error', 'message': 'Inv
try:
user_id = await get_user()
from ahserver.serverenv import ServerEnv
env = ServerEnv()
org_id = getattr(env, 'orgid', None) or getattr(env, 'org_id', '0')
org_id = (await get_userorgid()) or '0'
now = time.strftime('%Y-%m-%d %H:%M:%S')
dbname = get_module_dbname('product_management')

View File

@ -16,9 +16,7 @@ result = {'success': False, 'use_record_id': '', 'message': '', 'data': {}}
try:
user_id = await get_user()
from ahserver.serverenv import ServerEnv
env = ServerEnv()
org_id = params_kw.get('org_id', None) or getattr(env, 'orgid', None) or getattr(env, 'org_id', '0')
org_id = params_kw.get('org_id', None) or (await get_userorgid()) or '0'
now = time.strftime('%Y-%m-%d %H:%M:%S')
product_id = params_kw.get('product_id', '')