feat: add downapp user API interfaces and ops management table

- New table rl_app_user for application tracking
- APIs: rl_apply, rl_verify, rl_upload, rl_status, rl_check_app_status
- Ops CRUD for managing applications and keys
- Multi-vendor support reserved via vendor field
This commit is contained in:
yumoqing 2026-05-28 16:25:02 +08:00
parent 78036b9640
commit 530f337704
12 changed files with 415 additions and 0 deletions

View File

@ -0,0 +1,43 @@
{
"tblname": "rl_app_user",
"alias": "rl_app_user_list",
"title": "真人素材申请管理",
"params": {
"logined_userorgid": "org_id",
"browserfields": {
"id": {
"title": "ID",
"widgettype": "Text"
},
"downapp_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_app_user_create.dspy')}}",
"update_data_url": "{{entire_url('../api/rl_app_user_update.dspy')}}",
"delete_data_url": "{{entire_url('../api/rl_app_user_delete.dspy')}}"
}
}
}

109
models/rl_app_user.json Normal file
View File

@ -0,0 +1,109 @@
{
"summary": [
{
"name": "rl_app_user",
"title": "用户真人素材申请",
"primary": [
"id"
]
}
],
"fields": [
{
"name": "id",
"title": "主键",
"type": "str",
"length": 32,
"nullable": "no"
},
{
"name": "org_id",
"title": "机构",
"type": "str",
"length": 32,
"default": "0"
},
{
"name": "downapp_id",
"title": "申请用户ID",
"type": "str",
"length": 32
},
{
"name": "vendor",
"title": "供应商",
"type": "str",
"length": 50
},
{
"name": "status",
"title": "状态",
"type": "str",
"length": 20,
"default": "pending"
},
{
"name": "ak",
"title": "AccessKey(加密)",
"type": "text"
},
{
"name": "sk",
"title": "SecretKey(加密)",
"type": "text"
},
{
"name": "callback_url",
"title": "回调URL",
"type": "str",
"length": 500
},
{
"name": "remark",
"title": "备注",
"type": "text"
},
{
"name": "create_time",
"title": "创建时间",
"type": "datetime"
},
{
"name": "update_time",
"title": "更新时间",
"type": "datetime"
}
],
"indexes": [
{
"name": "idx_rl_app_user_org",
"idxtype": "index",
"idxfields": [
"org_id"
]
},
{
"name": "idx_rl_app_user_user",
"idxtype": "index",
"idxfields": [
"downapp_id"
]
}
],
"codes": [
{
"field": "vendor",
"table": "appcodes_kv",
"valuefield": "k",
"textfield": "v",
"cond": "parentid='rl_vendor'"
},
{
"field": "status",
"table": "appcodes_kv",
"valuefield": "k",
"textfield": "v",
"cond": "parentid='rl_app_status'"
}
]
}

View File

@ -387,6 +387,122 @@ async def rl_sync_assets_from_vendor(org_id, local_group_id,
return {"success": True, "synced": synced} return {"success": True, "synced": synced}
# ============================================================
# Downapp User API Proxies
# ============================================================
async def _get_user_keys(downapp_id, vendor="volcengine"):
"""Helper: Check application status and return decrypted keys."""
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})
if not recs:
return {"success": False, "message": "未申请或供应商不支持"}
rec = recs[0]
if rec.status != "active":
return {"success": False, "message": f"申请状态: {rec.status},未通过审批"}
env = ServerEnv()
ak = env.password_decode(rec.ak)
sk = env.password_decode(rec.sk)
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()
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,
"org_id": org_id,
"downapp_id": downapp_id,
"vendor": vendor,
"status": "pending",
"callback_url": callback_url,
"create_time": now,
"update_time": now,
})
return {"success": True, "app_id": aid, "status": "pending"}
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)
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
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
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
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
)
return result
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"
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
result = await rl_sync_asset_status(
asset_id, vendor=vendor,
apikey=keys["ak"], secretkey=keys["sk"]
)
return result
# ============================================================ # ============================================================
# Module loader # Module loader
# ============================================================ # ============================================================
@ -402,4 +518,11 @@ def load_reallife_asset():
g.rl_delete_group = rl_delete_group g.rl_delete_group = rl_delete_group
g.rl_sync_group_from_vendor = rl_sync_group_from_vendor g.rl_sync_group_from_vendor = rl_sync_group_from_vendor
g.rl_sync_assets_from_vendor = rl_sync_assets_from_vendor 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_upload_user = rl_upload_user
g.rl_sync_asset_status_user = rl_sync_asset_status_user
return True return True

View File

@ -46,6 +46,17 @@ paths_logined = [
"/reallife_asset/api/sync_assets.dspy", "/reallife_asset/api/sync_assets.dspy",
"/reallife_asset/api/get_rl_asset_group_list.dspy", "/reallife_asset/api/get_rl_asset_group_list.dspy",
"/reallife_asset/api/get_rl_asset_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",
# Ops management CRUD
"/reallife_asset/api/rl_app_user_create.dspy",
"/reallife_asset/api/rl_app_user_update.dspy",
"/reallife_asset/api/rl_app_user_delete.dspy",
"/reallife_asset/rl_app_user_list",
"/reallife_asset/rl_app_user_list/index.ui",
] ]
def run_set_perm(role, path): def run_set_perm(role, path):

View File

@ -0,0 +1,32 @@
# Create app record
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")
# Decrypt keys if provided by form? No, usually Ops enters plain text, we encrypt.
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,
"downapp_id": params_kw.get('downapp_id', ''),
"vendor": params_kw.get('vendor', 'volcengine'),
"status": params_kw.get('status', 'pending'),
"ak": ak,
"sk": sk,
"callback_url": params_kw.get('callback_url', ''),
"remark": params_kw.get('remark', ''),
"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_app_user", 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_app_user", {"id": id})
return {"success": True}

View File

@ -0,0 +1,31 @@
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_app_user", {"id": id})
if not recs: return {"success": False, "message": "Not found"}
# Prepare update data
upd = {}
for k in ['status', 'remark', 'callback_url', 'vendor']:
if params_kw.get(k):
upd[k] = params_kw.get(k)
# Handle keys encryption
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_app_user", upd, {"id": id})
return {"success": True}

11
wwwroot/api/rl_apply.dspy Normal file
View File

@ -0,0 +1,11 @@
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

@ -0,0 +1,16 @@
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,8 @@
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": "参数缺失"}
result = await rl_sync_asset_status_user((await get_userorgid()) or '0', asset_id, downapp_id)
return result

View File

@ -0,0 +1,11 @@
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:
return {"success": False, "message": "参数缺失"}
result = await rl_upload_user((await get_userorgid()) or '0', group_id, source_url, asset_type, name, downapp_id)
return result

View File

@ -0,0 +1,11 @@
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)
return result