From 530f33770459bb0d6b4c9a2dea10cc5846383990 Mon Sep 17 00:00:00 2001 From: yumoqing Date: Thu, 28 May 2026 16:25:02 +0800 Subject: [PATCH] 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 --- json/rl_app_user_list.json | 43 ++++++++++ models/rl_app_user.json | 109 ++++++++++++++++++++++++ reallife_asset/init.py | 123 +++++++++++++++++++++++++++ scripts/load_path.py | 11 +++ wwwroot/api/rl_app_user_create.dspy | 32 +++++++ wwwroot/api/rl_app_user_delete.dspy | 9 ++ wwwroot/api/rl_app_user_update.dspy | 31 +++++++ wwwroot/api/rl_apply.dspy | 11 +++ wwwroot/api/rl_check_app_status.dspy | 16 ++++ wwwroot/api/rl_status.dspy | 8 ++ wwwroot/api/rl_upload.dspy | 11 +++ wwwroot/api/rl_verify.dspy | 11 +++ 12 files changed, 415 insertions(+) create mode 100644 json/rl_app_user_list.json create mode 100644 models/rl_app_user.json create mode 100644 wwwroot/api/rl_app_user_create.dspy create mode 100644 wwwroot/api/rl_app_user_delete.dspy create mode 100644 wwwroot/api/rl_app_user_update.dspy create mode 100644 wwwroot/api/rl_apply.dspy create mode 100644 wwwroot/api/rl_check_app_status.dspy create mode 100644 wwwroot/api/rl_status.dspy create mode 100644 wwwroot/api/rl_upload.dspy create mode 100644 wwwroot/api/rl_verify.dspy diff --git a/json/rl_app_user_list.json b/json/rl_app_user_list.json new file mode 100644 index 0000000..dbac03f --- /dev/null +++ b/json/rl_app_user_list.json @@ -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')}}" + } + } +} \ No newline at end of file diff --git a/models/rl_app_user.json b/models/rl_app_user.json new file mode 100644 index 0000000..48e9d19 --- /dev/null +++ b/models/rl_app_user.json @@ -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'" + } + ] +} \ No newline at end of file diff --git a/reallife_asset/init.py b/reallife_asset/init.py index f880ffd..bd5eb20 100644 --- a/reallife_asset/init.py +++ b/reallife_asset/init.py @@ -387,6 +387,122 @@ async def rl_sync_assets_from_vendor(org_id, local_group_id, 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 # ============================================================ @@ -402,4 +518,11 @@ def load_reallife_asset(): g.rl_delete_group = rl_delete_group g.rl_sync_group_from_vendor = rl_sync_group_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 diff --git a/scripts/load_path.py b/scripts/load_path.py index 69269b8..2b173be 100644 --- a/scripts/load_path.py +++ b/scripts/load_path.py @@ -46,6 +46,17 @@ paths_logined = [ "/reallife_asset/api/sync_assets.dspy", "/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", + # 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): diff --git a/wwwroot/api/rl_app_user_create.dspy b/wwwroot/api/rl_app_user_create.dspy new file mode 100644 index 0000000..cefd4bc --- /dev/null +++ b/wwwroot/api/rl_app_user_create.dspy @@ -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} diff --git a/wwwroot/api/rl_app_user_delete.dspy b/wwwroot/api/rl_app_user_delete.dspy new file mode 100644 index 0000000..bcdf815 --- /dev/null +++ b/wwwroot/api/rl_app_user_delete.dspy @@ -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} diff --git a/wwwroot/api/rl_app_user_update.dspy b/wwwroot/api/rl_app_user_update.dspy new file mode 100644 index 0000000..ab12bd9 --- /dev/null +++ b/wwwroot/api/rl_app_user_update.dspy @@ -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} diff --git a/wwwroot/api/rl_apply.dspy b/wwwroot/api/rl_apply.dspy new file mode 100644 index 0000000..a1b212c --- /dev/null +++ b/wwwroot/api/rl_apply.dspy @@ -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 diff --git a/wwwroot/api/rl_check_app_status.dspy b/wwwroot/api/rl_check_app_status.dspy new file mode 100644 index 0000000..2e8ccd6 --- /dev/null +++ b/wwwroot/api/rl_check_app_status.dspy @@ -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 diff --git a/wwwroot/api/rl_status.dspy b/wwwroot/api/rl_status.dspy new file mode 100644 index 0000000..72dbf36 --- /dev/null +++ b/wwwroot/api/rl_status.dspy @@ -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 diff --git a/wwwroot/api/rl_upload.dspy b/wwwroot/api/rl_upload.dspy new file mode 100644 index 0000000..7ec56ea --- /dev/null +++ b/wwwroot/api/rl_upload.dspy @@ -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 diff --git a/wwwroot/api/rl_verify.dspy b/wwwroot/api/rl_verify.dspy new file mode 100644 index 0000000..fa345d0 --- /dev/null +++ b/wwwroot/api/rl_verify.dspy @@ -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