refactor: vendor config and org-group mapping

This commit is contained in:
yumoqing 2026-05-28 16:44:00 +08:00
parent 4280ad6894
commit 2cd0974909
17 changed files with 539 additions and 111 deletions

View File

@ -0,0 +1,22 @@
{
"tblname": "rl_org_group",
"alias": "rl_org_group_list",
"title": "机构素材组合映射",
"params": {
"logined_userorgid": "org_id",
"browserfields": {
"id": {"title": "ID", "widgettype": "Text"},
"org_id": {"title": "机构ID", "widgettype": "Text"},
"vendor": {"title": "供应商", "widgettype": "Text"},
"vendor_group_id": {"title": "供应商组合ID", "widgettype": "Text"},
"local_group_id": {"title": "本地组合ID", "widgettype": "Text"},
"status": {"title": "状态", "widgettype": "Text"},
"create_time": {"title": "创建时间", "widgettype": "Text"}
},
"editable": {
"new_data_url": "{{entire_url('../api/rl_org_group_create.dspy')}}",
"update_data_url": "{{entire_url('../api/rl_org_group_update.dspy')}}",
"delete_data_url": "{{entire_url('../api/rl_org_group_delete.dspy')}}"
}
}
}

View File

@ -0,0 +1,21 @@
{
"tblname": "rl_vendor_config",
"alias": "rl_vendor_config_list",
"title": "供应商配置管理",
"params": {
"logined_userorgid": "org_id",
"browserfields": {
"id": {"title": "ID", "widgettype": "Text"},
"vendor": {"title": "供应商", "widgettype": "Text"},
"status": {"title": "状态", "widgettype": "Text"},
"callback_url": {"title": "回调URL", "widgettype": "Text"},
"create_time": {"title": "创建时间", "widgettype": "Text"},
"update_time": {"title": "更新时间", "widgettype": "Text"}
},
"editable": {
"new_data_url": "{{entire_url('../api/rl_vendor_config_create.dspy')}}",
"update_data_url": "{{entire_url('../api/rl_vendor_config_update.dspy')}}",
"delete_data_url": "{{entire_url('../api/rl_vendor_config_delete.dspy')}}"
}
}
}

77
models/rl_org_group.json Normal file
View File

@ -0,0 +1,77 @@
{
"summary": [
{
"name": "rl_org_group",
"title": "机构素材组合映射",
"primary": ["id"]
}
],
"fields": [
{
"name": "id",
"title": "主键",
"type": "str",
"length": 32,
"nullable": "no"
},
{
"name": "org_id",
"title": "机构ID",
"type": "str",
"length": 32,
"nullable": "no"
},
{
"name": "vendor",
"title": "供应商",
"type": "str",
"length": 50,
"nullable": "no"
},
{
"name": "vendor_group_id",
"title": "供应商端组合ID",
"type": "str",
"length": 200
},
{
"name": "local_group_id",
"title": "本地组合ID",
"type": "str",
"length": 32
},
{
"name": "status",
"title": "状态",
"type": "str",
"length": 20,
"default": "active"
},
{
"name": "create_time",
"title": "创建时间",
"type": "datetime"
}
],
"indexes": [
{
"name": "idx_rl_org_group_org",
"idxtype": "index",
"idxfields": ["org_id"]
},
{
"name": "idx_rl_org_group_vgid",
"idxtype": "index",
"idxfields": ["vendor_group_id"]
}
],
"codes": [
{
"field": "vendor",
"table": "appcodes_kv",
"valuefield": "k",
"textfield": "v",
"cond": "parentid='rl_vendor'"
}
]
}

View File

@ -0,0 +1,74 @@
{
"summary": [
{
"name": "rl_vendor_config",
"title": "供应商配置",
"primary": ["id"]
}
],
"fields": [
{
"name": "id",
"title": "主键",
"type": "str",
"length": 32,
"nullable": "no"
},
{
"name": "vendor",
"title": "供应商",
"type": "str",
"length": 50,
"nullable": "no"
},
{
"name": "ak",
"title": "AccessKey(加密)",
"type": "text"
},
{
"name": "sk",
"title": "SecretKey(加密)",
"type": "text"
},
{
"name": "status",
"title": "状态",
"type": "str",
"length": 20,
"default": "active"
},
{
"name": "callback_url",
"title": "全局回调URL",
"type": "str",
"length": 500
},
{
"name": "create_time",
"title": "创建时间",
"type": "datetime"
},
{
"name": "update_time",
"title": "更新时间",
"type": "datetime"
}
],
"indexes": [
{
"name": "idx_rl_vendor_config_vendor",
"idxtype": "unique",
"idxfields": ["vendor"]
}
],
"codes": [
{
"field": "vendor",
"table": "appcodes_kv",
"valuefield": "k",
"textfield": "v",
"cond": "parentid='rl_vendor'"
}
]
}

View File

@ -391,17 +391,17 @@ async def rl_sync_assets_from_vendor(org_id, local_group_id,
# Downapp User API Proxies
# ============================================================
async def _get_user_keys(downapp_id, vendor="volcengine"):
"""Helper: Check application status and return decrypted keys."""
async def _get_vendor_keys(vendor="volcengine"):
"""Helper: Get vendor AK/SK from config table."""
dbname = _get_dbname()
db = DBPools()
async with db.sqlorContext(dbname) as sor:
recs = await sor.R("rl_app_user", {"downapp_id": downapp_id, "vendor": vendor})
recs = await sor.R("rl_vendor_config", {"vendor": vendor})
if not recs:
return {"success": False, "message": "未申请或供应商不支持"}
return {"success": False, "message": "供应商配置不存在"}
rec = recs[0]
if rec.status != "active":
return {"success": False, "message": f"申请状态: {rec.status},未通过审批"}
return {"success": False, "message": f"供应商服务已停用"}
env = ServerEnv()
ak = env.password_decode(rec.ak)
@ -409,98 +409,238 @@ async def _get_user_keys(downapp_id, vendor="volcengine"):
return {"success": True, "ak": ak, "sk": sk, "callback_url": rec.callback_url}
async def rl_apply(org_id, downapp_id, vendor, callback_url):
"""User applies for real person asset service."""
dbname = _get_dbname()
async def rl_verify_user(org_id, downapp_id, project_name="default"):
"""User proxy: Verify vendor config -> Call vendor -> Save org-group mapping."""
keys = await _get_vendor_keys()
if not keys.get("success"):
return keys
# Call Vendor API
client = _get_client("volcengine", keys["ak"], keys["sk"])
callback_url = keys.get("callback_url", "")
# Note: downapp_id might be used to track who created it,
# but the prompt focuses on org_id mapping.
# We create a session for the user.
# We need a unique callback URL if possible, or use the global one.
# The global one usually has status in URL, but here we just use the global one.
result = client.create_visual_validate_session(callback_url, project_name)
if "error" in result:
return {"success": False, "message": result.get("error", "API调用失败")}
byted_token = result.get("BytedToken", "")
h5_link = result.get("H5Link", "")
# Save local record for checking status later
# We save to rl_asset_group as before for internal tracking,
# but link it to org via rl_org_group later?
# Or just save to rl_asset_group with org_id.
# The prompt says "登记用户机构可用的orgid...登记表".
# Let's save to rl_asset_group first, then rl_org_group upon success.
# For simplicity, we return the H5 link.
# We need to store the `byted_token` somewhere to check result later.
# We'll use rl_asset_group for this temporary state.
gid = getID()
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
db = DBPools()
async with db.sqlorContext(dbname) as sor:
# Check if already exists
existing = await sor.R("rl_app_user", {"downapp_id": downapp_id, "vendor": vendor})
if existing:
return {"success": False, "message": "已提交申请,请等待审批"}
aid = getID()
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
await sor.I("rl_app_user", {
"id": aid,
async with db.sqlorContext(_get_dbname()) as sor:
await sor.I("rl_asset_group", {
"id": gid,
"org_id": org_id,
"downapp_id": downapp_id,
"vendor": vendor,
"vendor": "volcengine",
"name": f"待认证-{downapp_id}",
"title": f"待认证",
"group_type": "LivenessFace",
"project_name": project_name,
"status": "pending",
"byted_token": byted_token,
"h5_link": h5_link,
"callback_url": callback_url,
"created_by": downapp_id,
"create_time": now,
"update_time": now,
})
return {"success": True, "app_id": aid, "status": "pending"}
return {
"success": True,
"id": gid,
"byted_token": byted_token,
"h5_link": h5_link,
}
async def rl_verify_user(org_id, group_id, downapp_id, project_name="default"):
"""User proxy: Check app -> Get keys -> Call vendor."""
keys = await _get_user_keys(downapp_id)
async def rl_check_validate_and_map(local_group_id, project_name="default"):
"""Internal: Check validation result and update rl_org_group mapping."""
keys = await _get_vendor_keys()
if not keys.get("success"):
return keys
# Check if group exists locally (optional, or just call vendor)
# For user proxy, we might not have local group_id yet if this is the first step.
# The user provides group_id? No, user gets group_id after verification.
# Wait, the prompt says "按照groupid上传素材" for step 3.
# Step 2 is "真人认证". The user calls this to get the H5 link.
# So we call rl_create_validate_session with the user's keys.
result = await rl_create_validate_session(
org_id, keys.get("vendor", "volcengine"),
keys["callback_url"], project_name,
apikey=keys["ak"], secretkey=keys["sk"],
user_id=downapp_id
)
return result
dbname = _get_dbname()
db = DBPools()
async with db.sqlorContext(dbname) as sor:
recs = await sor.R("rl_asset_group", {"id": local_group_id})
if not recs:
return {"success": False, "message": "本地记录不存在"}
rec = recs[0]
byted_token = rec.byted_token
org_id = rec.org_id
# Call Vendor
client = _get_client("volcengine", keys["ak"], keys["sk"])
result = client.get_visual_validate_result(byted_token, project_name)
if "error" in result:
return {"success": False, "message": result.get("error", "查询失败")}
vendor_group_id = result.get("GroupId", "")
if not vendor_group_id:
return {"success": False, "message": "尚未完成认证或认证失败"}
# Update mapping table rl_org_group
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
mapping_id = getID()
async with db.sqlorContext(dbname) as sor:
# Check if mapping exists
existing = await sor.R("rl_org_group", {"org_id": org_id, "vendor": "volcengine"})
if existing:
await sor.U("rl_org_group", {
"vendor_group_id": vendor_group_id,
"local_group_id": local_group_id,
"update_time": now # add update_time field if needed, or just update
}, {"id": existing[0].id})
else:
await sor.I("rl_org_group", {
"id": mapping_id,
"org_id": org_id,
"vendor": "volcengine",
"vendor_group_id": vendor_group_id,
"local_group_id": local_group_id,
"status": "active",
"create_time": now
})
# Update rl_asset_group status
async with db.sqlorContext(dbname) as sor:
await sor.U("rl_asset_group", {
"vendor_group_id": vendor_group_id,
"status": "active",
"update_time": now,
}, {"id": local_group_id})
return {"success": True, "vendor_group_id": vendor_group_id}
async def rl_upload_user(org_id, group_id, source_url, asset_type, name, downapp_id):
"""User proxy: Check app -> Get keys -> Upload asset."""
keys = await _get_user_keys(downapp_id)
if not keys.get("success"):
return keys
# We need the vendor from the keys or the record
# _get_user_keys returns the record, but I only extracted ak/sk.
# Let's fetch vendor too.
vendor = "volcengine" # Default or fetch from record
# Refetch to get vendor
"""User proxy: Validate Org-Group mapping -> Get Keys -> Upload."""
# 1. Validate Group Ownership
dbname = _get_dbname()
db = DBPools()
async with db.sqlorContext(dbname) as sor:
recs = await sor.R("rl_app_user", {"downapp_id": downapp_id})
if recs:
vendor = recs[0].vendor
# Check rl_org_group
recs = await sor.R("rl_org_group", {"org_id": org_id}) # Get all groups for org
valid = False
vendor_group_id = ""
for r in recs:
if r.local_group_id == group_id:
valid = True
vendor_group_id = r.vendor_group_id
break
if not valid:
return {"success": False, "message": "无效的素材组合ID或无权访问"}
result = await rl_create_asset(
org_id, group_id, source_url, asset_type, name,
vendor=vendor, apikey=keys["ak"], secretkey=keys["sk"],
user_id=downapp_id
# 2. Get Keys
keys = await _get_vendor_keys()
if not keys.get("success"):
return keys
# 3. Upload
client = _get_client("volcengine", keys["ak"], keys["sk"])
result = client.create_asset(
vendor_group_id, source_url, asset_type, name
)
return result
vendor_asset_id = result.get("Id", "")
if not vendor_asset_id and "error" not in result:
r = result.get("Result", {})
vendor_asset_id = r.get("Id", "")
# 4. Save Asset Record
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
asset_id = getID()
asset_uri = f"asset://{vendor_asset_id}" if vendor_asset_id else ""
async with db.sqlorContext(dbname) as sor:
await sor.I("rl_asset", {
"id": asset_id,
"org_id": org_id,
"group_id": group_id,
"vendor": "volcengine",
"vendor_asset_id": vendor_asset_id,
"asset_type": asset_type,
"name": name or source_url.split("/")[-1][:50],
"status": "Processing",
"source_url": source_url,
"asset_uri": asset_uri,
"project_name": "default",
"vendor_response": json.dumps(result, ensure_ascii=False),
"created_by": downapp_id,
"create_time": now,
"update_time": now,
})
return {
"success": "error" not in result,
"id": asset_id,
"vendor_asset_id": vendor_asset_id,
"status": "Processing",
"message": result.get("error", ""),
}
async def rl_sync_asset_status_user(org_id, asset_id, downapp_id):
"""User proxy: Check app -> Get keys -> Sync status."""
keys = await _get_user_keys(downapp_id)
if not keys.get("success"):
return keys
vendor = "volcengine"
"""User proxy: Validate ownership -> Get Keys -> Sync Status."""
dbname = _get_dbname()
db = DBPools()
# Validate asset belongs to org
async with db.sqlorContext(dbname) as sor:
recs = await sor.R("rl_app_user", {"downapp_id": downapp_id})
if recs:
vendor = recs[0].vendor
recs = await sor.R("rl_asset", {"id": asset_id, "org_id": org_id})
if not recs:
return {"success": False, "message": "素材不存在或无权访问"}
rec = recs[0]
vendor_asset_id = rec.vendor_asset_id
result = await rl_sync_asset_status(
asset_id, vendor=vendor,
apikey=keys["ak"], secretkey=keys["sk"]
)
return result
keys = await _get_vendor_keys()
if not keys.get("success"):
return keys
# Sync with vendor
client = _get_client("volcengine", keys["ak"], keys["sk"])
result = client.get_asset(vendor_asset_id)
if "error" in result:
return {"success": False, "message": result.get("error", "查询失败")}
status = result.get("Status", "")
if not status:
r = result.get("Result", {})
status = r.get("Status", result.get("status", ""))
url = result.get("URL", result.get("Result", {}).get("URL", ""))
# Update local
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
async with db.sqlorContext(dbname) as sor:
upd = {"status": status, "update_time": now, "vendor_response": json.dumps(result, ensure_ascii=False)}
if url: upd["url"] = url
await sor.U("rl_asset", upd, {"id": asset_id})
return {"success": True, "status": status, "url": url}
# ============================================================
@ -520,8 +660,8 @@ def load_reallife_asset():
g.rl_sync_assets_from_vendor = rl_sync_assets_from_vendor
# Downapp user APIs
g.rl_apply = rl_apply
g.rl_verify_user = rl_verify_user
g.rl_check_validate_and_map = rl_check_validate_and_map
g.rl_upload_user = rl_upload_user
g.rl_sync_asset_status_user = rl_sync_asset_status_user

View File

@ -47,7 +47,6 @@ paths_logined = [
"/reallife_asset/api/get_rl_asset_group_list.dspy",
"/reallife_asset/api/get_rl_asset_list.dspy",
# Downapp user APIs
"/reallife_asset/api/rl_apply.dspy",
"/reallife_asset/api/rl_verify.dspy",
"/reallife_asset/api/rl_upload.dspy",
"/reallife_asset/api/rl_status.dspy",
@ -57,6 +56,18 @@ paths_logined = [
"/reallife_asset/api/rl_app_user_delete.dspy",
"/reallife_asset/rl_app_user_list",
"/reallife_asset/rl_app_user_list/index.ui",
# Vendor Config CRUD
"/reallife_asset/api/rl_vendor_config_create.dspy",
"/reallife_asset/api/rl_vendor_config_update.dspy",
"/reallife_asset/api/rl_vendor_config_delete.dspy",
"/reallife_asset/rl_vendor_config_list",
"/reallife_asset/rl_vendor_config_list/index.ui",
# Org-Group Mapping CRUD
"/reallife_asset/api/rl_org_group_create.dspy",
"/reallife_asset/api/rl_org_group_update.dspy",
"/reallife_asset/api/rl_org_group_delete.dspy",
"/reallife_asset/rl_org_group_list",
"/reallife_asset/rl_org_group_list/index.ui",
]
def run_set_perm(role, path):

View File

@ -1,11 +0,0 @@
downapp_id = params_kw.get('downapp_id', '')
vendor = params_kw.get('vendor', 'volcengine')
callback_url = params_kw.get('callback_url', '')
if not downapp_id:
return {"success": False, "message": "downapp_id 不能为空"}
if not callback_url:
return {"success": False, "message": "callback_url 不能为空"}
result = await rl_apply((await get_userorgid()) or '0', downapp_id, vendor, callback_url)
return result

View File

@ -1,16 +0,0 @@
downapp_id = params_kw.get('downapp_id', '')
vendor = params_kw.get('vendor', 'volcengine')
if not downapp_id:
return {"success": False, "message": "downapp_id 不能为空"}
keys = await _get_user_keys(downapp_id, vendor)
if keys.get("success"):
# Get full record info
dbname = get_module_dbname('reallife_asset')
db = DBPools()
async with db.sqlorContext(dbname) as sor:
recs = await sor.R("rl_app_user", {"downapp_id": downapp_id, "vendor": vendor})
if recs:
return {"success": True, "status": recs[0].status, "id": recs[0].id, "callback_url": recs[0].callback_url}
return keys

View File

@ -0,0 +1,20 @@
org_id = params_kw.get('org_id', (await get_userorgid()) or '0')
id = params_kw.get('id', getID())
now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
data = {
"id": id,
"org_id": org_id,
"vendor": params_kw.get('vendor', 'volcengine'),
"vendor_group_id": params_kw.get('vendor_group_id', ''),
"local_group_id": params_kw.get('local_group_id', ''),
"status": params_kw.get('status', 'active'),
"create_time": now
}
db = DBPools()
dbname = get_module_dbname('reallife_asset')
async with db.sqlorContext(dbname) as sor:
await sor.I("rl_org_group", data)
return {"success": True, "id": id}

View File

@ -0,0 +1,9 @@
id = params_kw.get('id', '')
if not id: return {"success": False, "message": "id required"}
db = DBPools()
dbname = get_module_dbname('reallife_asset')
async with db.sqlorContext(dbname) as sor:
await sor.D("rl_org_group", {"id": id})
return {"success": True}

View File

@ -0,0 +1,18 @@
id = params_kw.get('id', '')
if not id: return {"success": False, "message": "id required"}
db = DBPools()
dbname = get_module_dbname('reallife_asset')
async with db.sqlorContext(dbname) as sor:
recs = await sor.R("rl_org_group", {"id": id})
if not recs: return {"success": False, "message": "Not found"}
upd = {}
for k in ['status', 'vendor_group_id', 'local_group_id', 'vendor']:
if params_kw.get(k):
upd[k] = params_kw.get(k)
async with db.sqlorContext(dbname) as sor:
await sor.U("rl_org_group", upd, {"id": id})
return {"success": True}

View File

@ -1,8 +1,7 @@
asset_id = params_kw.get('asset_id', '')
downapp_id = params_kw.get('downapp_id', '')
if not asset_id or not downapp_id:
return {"success": False, "message": "参数缺失"}
if not asset_id:
return {"success": False, "message": "asset_id 不能为空"}
result = await rl_sync_asset_status_user((await get_userorgid()) or '0', asset_id, downapp_id)
result = await rl_sync_asset_status_user((await get_userorgid()) or '0', asset_id, (await get_user()))
return result

View File

@ -2,10 +2,9 @@ group_id = params_kw.get('group_id', '')
source_url = params_kw.get('source_url', '')
asset_type = params_kw.get('asset_type', 'Image')
name = params_kw.get('name', '')
downapp_id = params_kw.get('downapp_id', '')
if not group_id or not source_url or not downapp_id:
if not group_id or not source_url:
return {"success": False, "message": "参数缺失"}
result = await rl_upload_user((await get_userorgid()) or '0', group_id, source_url, asset_type, name, downapp_id)
result = await rl_upload_user((await get_userorgid()) or '0', group_id, source_url, asset_type, name, (await get_user()))
return result

View File

@ -0,0 +1,29 @@
org_id = params_kw.get('org_id', (await get_userorgid()) or '0')
id = params_kw.get('id', getID())
now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# Encrypt keys
ak = params_kw.get('ak', '')
sk = params_kw.get('sk', '')
env = ServerEnv()
if ak: ak = env.password_encode(ak)
if sk: sk = env.password_encode(sk)
data = {
"id": id,
"org_id": org_id,
"vendor": params_kw.get('vendor', 'volcengine'),
"ak": ak,
"sk": sk,
"status": params_kw.get('status', 'active'),
"callback_url": params_kw.get('callback_url', ''),
"create_time": now,
"update_time": now
}
db = DBPools()
dbname = get_module_dbname('reallife_asset')
async with db.sqlorContext(dbname) as sor:
await sor.I("rl_vendor_config", data)
return {"success": True, "id": id}

View File

@ -0,0 +1,9 @@
id = params_kw.get('id', '')
if not id: return {"success": False, "message": "id required"}
db = DBPools()
dbname = get_module_dbname('reallife_asset')
async with db.sqlorContext(dbname) as sor:
await sor.D("rl_vendor_config", {"id": id})
return {"success": True}

View File

@ -0,0 +1,30 @@
id = params_kw.get('id', '')
if not id: return {"success": False, "message": "id required"}
db = DBPools()
dbname = get_module_dbname('reallife_asset')
async with db.sqlorContext(dbname) as sor:
recs = await sor.R("rl_vendor_config", {"id": id})
if not recs: return {"success": False, "message": "Not found"}
upd = {}
for k in ['status', 'callback_url', 'vendor']:
if params_kw.get(k):
upd[k] = params_kw.get(k)
# Encrypt keys
ak = params_kw.get('ak', None)
sk = params_kw.get('sk', None)
if ak is not None:
env = ServerEnv()
upd['ak'] = env.password_encode(ak)
if sk is not None:
env = ServerEnv()
upd['sk'] = env.password_encode(sk)
upd['update_time'] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
async with db.sqlorContext(dbname) as sor:
await sor.U("rl_vendor_config", upd, {"id": id})
return {"success": True}

View File

@ -1,11 +1,8 @@
group_id = params_kw.get('group_id', '') # Optional, if re-creating
downapp_id = params_kw.get('downapp_id', '')
project_name = params_kw.get('project_name', 'default')
if not downapp_id:
return {"success": False, "message": "downapp_id 不能为空"}
# If group_id is provided, we might be refreshing the link for an existing group?
# For simplicity, always create new session for user.
result = await rl_verify_user((await get_userorgid()) or '0', group_id, downapp_id, project_name)
result = await rl_verify_user((await get_userorgid()) or '0', downapp_id, project_name)
return result