From 71576381bd1eda006d8467a61e006881791d324a Mon Sep 17 00:00:00 2001 From: ymq Date: Wed, 26 Aug 2026 14:51:10 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20account=5Fresource=20=E8=B5=84=E6=BA=90?= =?UTF-8?q?=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 参照 llmage 范式(llm主表无ppid + llm_api_map挂ppid + llmusage计费六件套): - 资源主表不带 ppid,定价挂在「资源×用法」映射表上 - 对外统一 resolve_ppid(resource_ref_id, ctx),load 时注册到产品层解析器注册表 - 含 models 四段式/json CRUD/i18n四语言/load_path/init 种子数据 --- account_resource/__init__.py | 11 ++ account_resource/init.py | 325 ++++++++++++++++++++++++++++++++ i18n/en/msg.txt | 7 + i18n/jp/msg.txt | 7 + i18n/ko/msg.txt | 7 + i18n/zh/msg.txt | 7 + init/data.json | 68 +++++++ json/acctres_pricing_map.json | 70 +++++++ json/acctres_spec.json | 94 +++++++++ json/acctres_usage.json | 130 +++++++++++++ models/acctres_pricing_map.json | 120 ++++++++++++ models/acctres_spec.json | 149 +++++++++++++++ models/acctres_usage.json | 229 ++++++++++++++++++++++ pyproject.toml | 17 ++ scripts/load_path.py | 33 ++++ 15 files changed, 1274 insertions(+) create mode 100644 account_resource/__init__.py create mode 100644 account_resource/init.py create mode 100644 i18n/en/msg.txt create mode 100644 i18n/jp/msg.txt create mode 100644 i18n/ko/msg.txt create mode 100644 i18n/zh/msg.txt create mode 100644 init/data.json create mode 100644 json/acctres_pricing_map.json create mode 100644 json/acctres_spec.json create mode 100644 json/acctres_usage.json create mode 100644 models/acctres_pricing_map.json create mode 100644 models/acctres_spec.json create mode 100644 models/acctres_usage.json create mode 100644 pyproject.toml create mode 100644 scripts/load_path.py diff --git a/account_resource/__init__.py b/account_resource/__init__.py new file mode 100644 index 0000000..af94081 --- /dev/null +++ b/account_resource/__init__.py @@ -0,0 +1,11 @@ +"""账号资源模块 - 账号规格/权益配额/定价映射(spec×charge_mode→ppid)""" +from .init import ( + load_account_resource, + get_account_spec, + get_account_privileges, + get_account_ppid, + account_charging, + resolve_ppid, +) + +__version__ = '1.0.0' diff --git a/account_resource/init.py b/account_resource/init.py new file mode 100644 index 0000000..9b97979 --- /dev/null +++ b/account_resource/init.py @@ -0,0 +1,325 @@ +"""account_resource — 账号资源模块 + +参照 llmage 范式(资源模块的标准形态): + llmage: llm(主表,无ppid) → llm_api_map(llmid×apiname→ppid) → llmusage(计费六件套) + account_resource: acctres_spec → acctres_pricing_map(spec×charge_mode→ppid) → acctres_usage + +**ppid 挂法各资源不同,但对外作用一致**: + 账号按「计费方式」挂价(试用/月付/年付),存储按「计量方式」挂价(按量/包月/峰值)。 + 两者都通过 resolve_ppid(resource_ref_id, ctx) 对外暴露,产品层不必知道内部差异。 + +宿主集成: + from account_resource.init import load_account_resource + load_account_resource() + +对外注册到 ServerEnv 的能力见 load_account_resource()。 +""" +import json +import logging +import datetime + +from ahserver.serverenv import ServerEnv +from appPublic.uniqueID import getID +from sqlor.dbpools import DBPools + +MODULE_NAME = 'account_resource' +MODULE_VERSION = '1.0.0' +RESOURCE_TYPE = 'account_spec' # product_resource.resource_type 用这个值 + +logger = logging.getLogger(__name__) + + +def _get_dbname(): + """动态取库名,禁硬编码(宿主通过 get_module_dbname 映射)。""" + env = ServerEnv() + return env.get_module_dbname(MODULE_NAME) + + +def _now(): + return datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') + + +def _val(rec, key, default=''): + """sqlor 行对象只支持属性访问,dict 也兼容。""" + if isinstance(rec, dict): + return rec.get(key, default) + return getattr(rec, key, default) + + +# ═══════════════════════════════════════════════════════════════════ +# 规格(资源主表) +# ═══════════════════════════════════════════════════════════════════ + +async def acctres_spec_list(ns=None): + """规格列表。ns 可含 status / spec_type 过滤。""" + ns = ns or {} + sql = "SELECT * FROM acctres_spec WHERE 1=1" + params = {} + if ns.get('status'): + sql += " AND status=${status}$" + params['status'] = ns['status'] + if ns.get('spec_type'): + sql += " AND spec_type=${spec_type}$" + params['spec_type'] = ns['spec_type'] + sql += " ORDER BY sort_order, spec_code" + async with DBPools().sqlorContext(_get_dbname()) as sor: + return await sor.sqlExe(sql, params) + + +async def get_account_spec(spec_id_or_code): + """按 id 或 spec_code 取规格(产品层拿到 resource_ref_id 后调这个)。""" + async with DBPools().sqlorContext(_get_dbname()) as sor: + recs = await sor.sqlExe( + "SELECT * FROM acctres_spec WHERE id=${k}$ OR spec_code=${k}$", + {"k": spec_id_or_code}) + return recs[0] if recs else None + + +async def get_account_privileges(spec_id_or_code): + """取规格权益(开通账号时用它设置配额限制)。""" + spec = await get_account_spec(spec_id_or_code) + if not spec: + return None + priv = { + 'workspace_max': int(_val(spec, 'workspace_max', 1) or 1), + 'workspace_gb': float(_val(spec, 'workspace_gb', 0) or 0), + 'concurrent_task': int(_val(spec, 'concurrent_task', 1) or 1), + 'gpu_allowed': str(_val(spec, 'gpu_allowed', '0')) == '1', + 'member_max': int(_val(spec, 'member_max', 1) or 1), + } + extra = _val(spec, 'privilege_json', '') + if extra: + try: + priv.update(json.loads(extra)) + except (ValueError, TypeError): + logger.warning('privilege_json 解析失败: spec=%s', spec_id_or_code) + return priv + + +async def acctres_spec_create(data): + data = dict(data or {}) + data.setdefault('id', getID()) + data.setdefault('created_at', _now()) + async with DBPools().sqlorContext(_get_dbname()) as sor: + await sor.C('acctres_spec', data) + return data['id'] + + +async def acctres_spec_update(data): + data = dict(data or {}) + data['updated_at'] = _now() + async with DBPools().sqlorContext(_get_dbname()) as sor: + await sor.U('acctres_spec', data) # id 放在 data 里,sor.U 只接受 2 参数 + return True + + +async def acctres_spec_delete(data): + async with DBPools().sqlorContext(_get_dbname()) as sor: + await sor.D('acctres_spec', {'id': (data or {}).get('id', '')}) + return True + + +# ═══════════════════════════════════════════════════════════════════ +# 定价映射(spec × charge_mode → ppid)—— 本模块的 ppid 挂法 +# ═══════════════════════════════════════════════════════════════════ + +async def acctres_pricing_map_list(ns=None): + ns = ns or {} + sql = ("SELECT m.*, s.spec_code, s.spec_name FROM acctres_pricing_map m " + "LEFT JOIN acctres_spec s ON m.spec_id=s.id WHERE 1=1") + params = {} + if ns.get('spec_id'): + sql += " AND m.spec_id=${spec_id}$" + params['spec_id'] = ns['spec_id'] + sql += " ORDER BY s.sort_order, m.charge_mode" + async with DBPools().sqlorContext(_get_dbname()) as sor: + return await sor.sqlExe(sql, params) + + +async def acctres_pricing_map_create(data): + data = dict(data or {}) + data.setdefault('id', getID()) + data.setdefault('created_at', _now()) + async with DBPools().sqlorContext(_get_dbname()) as sor: + await sor.C('acctres_pricing_map', data) + return data['id'] + + +async def acctres_pricing_map_update(data): + data = dict(data or {}) + data['updated_at'] = _now() + async with DBPools().sqlorContext(_get_dbname()) as sor: + await sor.U('acctres_pricing_map', data) + return True + + +async def acctres_pricing_map_delete(data): + async with DBPools().sqlorContext(_get_dbname()) as sor: + await sor.D('acctres_pricing_map', {'id': (data or {}).get('id', '')}) + return True + + +async def get_account_ppid(spec_id_or_code, charge_mode=None): + """**统一解析器契约**:给定资源 + 用法上下文 → ppid。 + + charge_mode 为空时取 is_default='1' 的那条。 + 产品层不直接调这个,而是调 resolve_resource_ppid(resource_type, ...)(见 init 注册)。 + """ + spec = await get_account_spec(spec_id_or_code) + if not spec: + logger.warning('规格不存在: %s', spec_id_or_code) + return None + spec_id = _val(spec, 'id') + + sql = ("SELECT ppid, charge_mode, charge_unit, valid_days FROM acctres_pricing_map " + "WHERE spec_id=${spec_id}$ AND status='active'") + params = {'spec_id': spec_id} + if charge_mode: + sql += " AND charge_mode=${charge_mode}$" + params['charge_mode'] = charge_mode + else: + sql += " AND is_default='1'" + async with DBPools().sqlorContext(_get_dbname()) as sor: + recs = await sor.sqlExe(sql, params) + if not recs: + logger.warning('无定价映射: spec=%s charge_mode=%s', spec_id, charge_mode) + return None + return _val(recs[0], 'ppid') or None + + +async def resolve_ppid(resource_ref_id, ctx=None): + """资源定价解析器(注册给产品层统一调用)。 + + ctx: {'charge_mode': 'month'|'year'|'trial', ...} + """ + ctx = ctx or {} + return await get_account_ppid(resource_ref_id, ctx.get('charge_mode')) + + +# ═══════════════════════════════════════════════════════════════════ +# 用量与计费 +# ═══════════════════════════════════════════════════════════════════ + +async def account_charging(spec_id_or_code, charge_mode, userid, userorgid, + subscription_id='', quantity=1, usages=None, + period_start=None, period_end=None, discount_rate=None): + """账号资源计费:解析 ppid → 调 pricing 引擎 → 落用量记录。 + + 折扣由产品层传入(discount_rate),本模块只按资源真实定价算原价再乘折扣。 + 返回 usage 记录 dict(含 amount)。 + """ + env = ServerEnv() + ppid = await get_account_ppid(spec_id_or_code, charge_mode) + + usages = dict(usages or {}) + usages.setdefault('duration', quantity) + usages.setdefault('flat', quantity) + usages.setdefault('charge_mode', charge_mode) + + amount = 0.0 + if ppid: + charging = getattr(env, 'pricing_program_charging', None) + if charging is None: + logger.warning('pricing 模块未加载,账号计费金额记 0(ppid=%s)', ppid) + else: + try: + r = await charging(ppid, usages) + if isinstance(r, dict): + amount = float(r.get('amount', 0) or 0) + elif isinstance(r, (int, float)): + amount = float(r) + except Exception as e: + logger.error('账号计费失败 ppid=%s: %s', ppid, e) + else: + logger.warning('无 ppid,账号计费金额记 0: spec=%s mode=%s', + spec_id_or_code, charge_mode) + + rate = float(discount_rate) if discount_rate not in (None, '') else 1.0 + final_amount = round(amount * rate, 4) + + spec = await get_account_spec(spec_id_or_code) + rec = { + 'id': getID(), + 'spec_id': _val(spec, 'id') if spec else '', + 'charge_mode': charge_mode, + 'subscription_id': subscription_id, + 'userid': userid, + 'userorgid': userorgid, + 'use_date': datetime.date.today().strftime('%Y-%m-%d'), + 'use_time': _now(), + 'period_start': period_start, + 'period_end': period_end, + 'quantity': quantity, + 'usages': json.dumps(usages, ensure_ascii=False), + 'transno': getID(), + 'amount': final_amount, + 'discount_rate': rate, + 'accounting_status': 'pending', + 'status': 'active', + 'org_id': userorgid, + 'created_at': _now(), + } + async with DBPools().sqlorContext(_get_dbname()) as sor: + await sor.C('acctres_usage', rec) + logger.info('账号计费: spec=%s mode=%s 原价=%s 折扣=%s 应收=%s', + spec_id_or_code, charge_mode, amount, rate, final_amount) + return rec + + +async def get_accounting_acctres_usages(limit=200): + """待记账用量(供 accounting 模块出账扫描)。""" + async with DBPools().sqlorContext(_get_dbname()) as sor: + return await sor.sqlExe( + "SELECT * FROM acctres_usage WHERE accounting_status='pending' " + "ORDER BY use_time LIMIT " + str(int(limit)), {}) + + +async def mark_acctres_accounted(usage_id, status='accounted'): + async with DBPools().sqlorContext(_get_dbname()) as sor: + await sor.U('acctres_usage', {'id': usage_id, 'accounting_status': status, + 'updated_at': _now()}) + return True + + +# ═══════════════════════════════════════════════════════════════════ +# Module Loader +# ═══════════════════════════════════════════════════════════════════ + +def load_account_resource(): + """注册账号资源能力到 ServerEnv。""" + env = ServerEnv() + + # 规格 CRUD + env.acctres_spec_list = acctres_spec_list + env.acctres_spec_create = acctres_spec_create + env.acctres_spec_update = acctres_spec_update + env.acctres_spec_delete = acctres_spec_delete + env.get_account_spec = get_account_spec + env.get_account_privileges = get_account_privileges + + # 定价映射 CRUD + env.acctres_pricing_map_list = acctres_pricing_map_list + env.acctres_pricing_map_create = acctres_pricing_map_create + env.acctres_pricing_map_update = acctres_pricing_map_update + env.acctres_pricing_map_delete = acctres_pricing_map_delete + env.get_account_ppid = get_account_ppid + + # 计费 + env.account_charging = account_charging + env.get_accounting_acctres_usages = get_accounting_acctres_usages + env.mark_acctres_accounted = mark_acctres_accounted + + # 向产品层注册「资源定价解析器」——各资源模块挂法不同,接口统一 + reg = getattr(env, 'register_resource_pricing_resolver', None) + if reg: + reg(RESOURCE_TYPE, resolve_ppid) + else: + # 产品层未加载时兜底:直接挂到 env,产品层加载后可自行拾取 + existing = getattr(env, '_resource_pricing_resolvers', None) or {} + existing[RESOURCE_TYPE] = resolve_ppid + env._resource_pricing_resolvers = existing + logger.info('产品层解析器注册表未就绪,已暂存 resolver: %s', RESOURCE_TYPE) + + logger.info('account_resource module loaded (v%s, resource_type=%s)', + MODULE_VERSION, RESOURCE_TYPE) + return True diff --git a/i18n/en/msg.txt b/i18n/en/msg.txt new file mode 100644 index 0000000..36e320b --- /dev/null +++ b/i18n/en/msg.txt @@ -0,0 +1,7 @@ +账号规格: Account Spec +账号资源: Account Resource +权益配额: Privilege Quota +计费方式: Charge Mode +试用: Trial +月租: Monthly +年租: Yearly diff --git a/i18n/jp/msg.txt b/i18n/jp/msg.txt new file mode 100644 index 0000000..d7c2f71 --- /dev/null +++ b/i18n/jp/msg.txt @@ -0,0 +1,7 @@ +账号规格: アカウント仕様 +账号资源: アカウントリソース +权益配额: 権益クォータ +计费方式: 課金方式 +试用: トライアル +月租: 月額 +年租: 年額 diff --git a/i18n/ko/msg.txt b/i18n/ko/msg.txt new file mode 100644 index 0000000..946f5bb --- /dev/null +++ b/i18n/ko/msg.txt @@ -0,0 +1,7 @@ +账号规格: 계정 사양 +账号资源: 계정 리소스 +权益配额: 권익 할당량 +计费方式: 과금 방식 +试用: 체험 +月租: 월정액 +年租: 연정액 diff --git a/i18n/zh/msg.txt b/i18n/zh/msg.txt new file mode 100644 index 0000000..81444bf --- /dev/null +++ b/i18n/zh/msg.txt @@ -0,0 +1,7 @@ +账号规格: 账号规格 +账号资源: 账号资源 +权益配额: 权益配额 +计费方式: 计费方式 +试用: 试用 +月租: 月租 +年租: 年租 diff --git a/init/data.json b/init/data.json new file mode 100644 index 0000000..64443ac --- /dev/null +++ b/init/data.json @@ -0,0 +1,68 @@ +{ + "acctres_spec": [ + { + "spec_code": "ACC-TRIAL-7D", + "spec_name": "试用账号(7天)", + "spec_type": "trial", + "workspace_max": 1, + "workspace_gb": 5, + "concurrent_task": 1, + "gpu_allowed": "0", + "member_max": 1, + "sort_order": 10, + "status": "active", + "description": "新用户试用,每机构限领一次" + }, + { + "spec_code": "ACC-MONTHLY", + "spec_name": "月租账号", + "spec_type": "standard", + "workspace_max": 5, + "workspace_gb": 50, + "concurrent_task": 3, + "gpu_allowed": "1", + "member_max": 5, + "sort_order": 20, + "status": "active" + }, + { + "spec_code": "ACC-YEARLY", + "spec_name": "年租账号", + "spec_type": "standard", + "workspace_max": 20, + "workspace_gb": 200, + "concurrent_task": 10, + "gpu_allowed": "1", + "member_max": 20, + "sort_order": 30, + "status": "active" + } + ], + "acctres_pricing_map": [ + { + "spec_code": "ACC-TRIAL-7D", + "charge_mode": "trial", + "charge_unit": "次", + "valid_days": 7, + "is_default": "1", + "ppid": "" + }, + { + "spec_code": "ACC-MONTHLY", + "charge_mode": "month", + "charge_unit": "月", + "valid_days": 30, + "is_default": "1", + "ppid": "" + }, + { + "spec_code": "ACC-YEARLY", + "charge_mode": "year", + "charge_unit": "年", + "valid_days": 365, + "is_default": "1", + "ppid": "" + } + ], + "_note": "ppid 待建定价项目后回填;charge_mode 是本模块的 ppid 挂载维度" +} \ No newline at end of file diff --git a/json/acctres_pricing_map.json b/json/acctres_pricing_map.json new file mode 100644 index 0000000..e051126 --- /dev/null +++ b/json/acctres_pricing_map.json @@ -0,0 +1,70 @@ +{ + "tblname": "acctres_pricing_map", + "params": { + "title": "账号定价映射", + "browserfields": [ + { + "field": "spec_id", + "title": "规格ID" + }, + { + "field": "charge_mode", + "title": "计费方式" + }, + { + "field": "charge_unit", + "title": "计费单位" + }, + { + "field": "valid_days", + "title": "有效天数" + }, + { + "field": "ppid", + "title": "定价项目ID" + }, + { + "field": "is_default", + "title": "缺省方式" + }, + { + "field": "status", + "title": "状态" + } + ], + "editfields": [ + { + "field": "spec_id", + "uitype": "Text" + }, + { + "field": "charge_mode", + "uitype": "Text" + }, + { + "field": "charge_unit", + "uitype": "Text" + }, + { + "field": "valid_days", + "uitype": "Text" + }, + { + "field": "ppid", + "uitype": "Text" + }, + { + "field": "is_default", + "uitype": "Text" + }, + { + "field": "status", + "uitype": "Text" + } + ], + "searchfields": [ + "spec_id", + "charge_mode" + ] + } +} diff --git a/json/acctres_spec.json b/json/acctres_spec.json new file mode 100644 index 0000000..db6cd4d --- /dev/null +++ b/json/acctres_spec.json @@ -0,0 +1,94 @@ +{ + "tblname": "acctres_spec", + "params": { + "title": "账号规格", + "browserfields": [ + { + "field": "spec_code", + "title": "规格编码" + }, + { + "field": "spec_name", + "title": "规格名称" + }, + { + "field": "spec_type", + "title": "规格类型" + }, + { + "field": "workspace_max", + "title": "可建工作空间数" + }, + { + "field": "workspace_gb", + "title": "赠送容量GB" + }, + { + "field": "concurrent_task", + "title": "并发任务数" + }, + { + "field": "gpu_allowed", + "title": "允许GPU" + }, + { + "field": "status", + "title": "状态" + } + ], + "editfields": [ + { + "field": "spec_code", + "uitype": "Text" + }, + { + "field": "spec_name", + "uitype": "Text" + }, + { + "field": "spec_type", + "uitype": "Text" + }, + { + "field": "description", + "uitype": "Text" + }, + { + "field": "workspace_max", + "uitype": "Text" + }, + { + "field": "workspace_gb", + "uitype": "Text" + }, + { + "field": "concurrent_task", + "uitype": "Text" + }, + { + "field": "gpu_allowed", + "uitype": "Text" + }, + { + "field": "member_max", + "uitype": "Text" + }, + { + "field": "privilege_json", + "uitype": "Text" + }, + { + "field": "sort_order", + "uitype": "Text" + }, + { + "field": "status", + "uitype": "Text" + } + ], + "searchfields": [ + "spec_code", + "spec_name" + ] + } +} diff --git a/json/acctres_usage.json b/json/acctres_usage.json new file mode 100644 index 0000000..97f5e9b --- /dev/null +++ b/json/acctres_usage.json @@ -0,0 +1,130 @@ +{ + "tblname": "acctres_usage", + "params": { + "title": "账号用量", + "browserfields": [ + { + "field": "spec_id", + "title": "规格ID" + }, + { + "field": "charge_mode", + "title": "计费方式" + }, + { + "field": "userorgid", + "title": "用户机构" + }, + { + "field": "use_date", + "title": "使用日期" + }, + { + "field": "quantity", + "title": "数量" + }, + { + "field": "amount", + "title": "交易金额" + }, + { + "field": "discount_rate", + "title": "折扣率" + }, + { + "field": "accounting_status", + "title": "记账状态" + } + ], + "editfields": [ + { + "field": "spec_id", + "uitype": "Text" + }, + { + "field": "charge_mode", + "uitype": "Text" + }, + { + "field": "subscription_id", + "uitype": "Text" + }, + { + "field": "userid", + "uitype": "Text" + }, + { + "field": "userorgid", + "uitype": "Text" + }, + { + "field": "use_date", + "uitype": "Text" + }, + { + "field": "use_time", + "uitype": "Text" + }, + { + "field": "period_start", + "uitype": "Text" + }, + { + "field": "period_end", + "uitype": "Text" + }, + { + "field": "quantity", + "uitype": "Text" + }, + { + "field": "usages", + "uitype": "Text" + }, + { + "field": "transno", + "uitype": "Text" + }, + { + "field": "amount", + "uitype": "Text" + }, + { + "field": "amount_currency", + "uitype": "Text" + }, + { + "field": "amount_base", + "uitype": "Text" + }, + { + "field": "cost", + "uitype": "Text" + }, + { + "field": "cost_currency", + "uitype": "Text" + }, + { + "field": "cost_base", + "uitype": "Text" + }, + { + "field": "discount_rate", + "uitype": "Text" + }, + { + "field": "accounting_status", + "uitype": "Text" + }, + { + "field": "status", + "uitype": "Text" + } + ], + "searchfields": [ + "spec_id", + "charge_mode" + ] + } +} diff --git a/models/acctres_pricing_map.json b/models/acctres_pricing_map.json new file mode 100644 index 0000000..cb127ff --- /dev/null +++ b/models/acctres_pricing_map.json @@ -0,0 +1,120 @@ +{ + "summary": [ + { + "name": "acctres_pricing_map", + "title": "账号规格定价映射", + "primary": [ + "id" + ] + } + ], + "fields": [ + { + "name": "id", + "title": "ID", + "type": "str", + "length": 32, + "nullable": "no" + }, + { + "name": "spec_id", + "title": "规格ID", + "type": "str", + "length": 32, + "nullable": "no" + }, + { + "name": "charge_mode", + "title": "计费方式", + "type": "str", + "length": 32, + "nullable": "no" + }, + { + "name": "charge_unit", + "title": "计费单位", + "type": "str", + "length": 16, + "nullable": "yes" + }, + { + "name": "valid_days", + "title": "有效天数", + "type": "int", + "nullable": "yes" + }, + { + "name": "ppid", + "title": "定价项目ID", + "type": "str", + "length": 32, + "nullable": "yes" + }, + { + "name": "is_default", + "title": "缺省方式", + "type": "str", + "length": 1, + "nullable": "yes", + "default": "0" + }, + { + "name": "status", + "title": "状态", + "type": "str", + "length": 16, + "nullable": "yes", + "default": "active" + }, + { + "name": "org_id", + "title": "所属机构", + "type": "str", + "length": 32, + "nullable": "yes" + }, + { + "name": "created_by", + "title": "创建人", + "type": "str", + "length": 32, + "nullable": "yes" + }, + { + "name": "created_at", + "title": "创建时间", + "type": "timestamp", + "nullable": "yes" + }, + { + "name": "updated_at", + "title": "更新时间", + "type": "timestamp", + "nullable": "yes" + } + ], + "indexes": [ + { + "name": "idx_apm_spec", + "idxtype": "index", + "idxfields": [ + "spec_id" + ] + }, + { + "name": "idx_apm_mode", + "idxtype": "unique", + "idxfields": [ + "spec_id", + "charge_mode" + ] + }, + { + "name": "idx_apm_ppid", + "idxtype": "index", + "idxfields": [ + "ppid" + ] + } + ] +} \ No newline at end of file diff --git a/models/acctres_spec.json b/models/acctres_spec.json new file mode 100644 index 0000000..e9d56c5 --- /dev/null +++ b/models/acctres_spec.json @@ -0,0 +1,149 @@ +{ + "summary": [ + { + "name": "acctres_spec", + "title": "账号规格", + "primary": [ + "id" + ] + } + ], + "fields": [ + { + "name": "id", + "title": "ID", + "type": "str", + "length": 32, + "nullable": "no" + }, + { + "name": "spec_code", + "title": "规格编码", + "type": "str", + "length": 64, + "nullable": "no" + }, + { + "name": "spec_name", + "title": "规格名称", + "type": "str", + "length": 255, + "nullable": "yes" + }, + { + "name": "spec_type", + "title": "规格类型", + "type": "str", + "length": 32, + "nullable": "yes", + "default": "standard" + }, + { + "name": "description", + "title": "说明", + "type": "text", + "nullable": "yes" + }, + { + "name": "workspace_max", + "title": "可建工作空间数", + "type": "int", + "nullable": "yes", + "default": "1" + }, + { + "name": "workspace_gb", + "title": "赠送容量GB", + "type": "double", + "length": 18, + "dec": 2, + "nullable": "yes", + "default": "0" + }, + { + "name": "concurrent_task", + "title": "并发任务数", + "type": "int", + "nullable": "yes", + "default": "1" + }, + { + "name": "gpu_allowed", + "title": "允许GPU", + "type": "str", + "length": 1, + "nullable": "yes", + "default": "0" + }, + { + "name": "member_max", + "title": "成员数上限", + "type": "int", + "nullable": "yes", + "default": "1" + }, + { + "name": "privilege_json", + "title": "其他权益JSON", + "type": "text", + "nullable": "yes" + }, + { + "name": "sort_order", + "title": "排序", + "type": "int", + "nullable": "yes", + "default": "0" + }, + { + "name": "status", + "title": "状态", + "type": "str", + "length": 16, + "nullable": "yes", + "default": "active" + }, + { + "name": "org_id", + "title": "所属机构", + "type": "str", + "length": 32, + "nullable": "yes" + }, + { + "name": "created_by", + "title": "创建人", + "type": "str", + "length": 32, + "nullable": "yes" + }, + { + "name": "created_at", + "title": "创建时间", + "type": "timestamp", + "nullable": "yes" + }, + { + "name": "updated_at", + "title": "更新时间", + "type": "timestamp", + "nullable": "yes" + } + ], + "indexes": [ + { + "name": "idx_spec_code", + "idxtype": "unique", + "idxfields": [ + "spec_code" + ] + }, + { + "name": "idx_spec_status", + "idxtype": "index", + "idxfields": [ + "status" + ] + } + ] +} \ No newline at end of file diff --git a/models/acctres_usage.json b/models/acctres_usage.json new file mode 100644 index 0000000..9051a0f --- /dev/null +++ b/models/acctres_usage.json @@ -0,0 +1,229 @@ +{ + "summary": [ + { + "name": "acctres_usage", + "title": "账号资源用量", + "primary": [ + "id" + ] + } + ], + "fields": [ + { + "name": "id", + "title": "ID", + "type": "str", + "length": 32, + "nullable": "no" + }, + { + "name": "spec_id", + "title": "规格ID", + "type": "str", + "length": 32, + "nullable": "yes" + }, + { + "name": "charge_mode", + "title": "计费方式", + "type": "str", + "length": 32, + "nullable": "yes" + }, + { + "name": "subscription_id", + "title": "订购ID", + "type": "str", + "length": 32, + "nullable": "yes" + }, + { + "name": "userid", + "title": "用户ID", + "type": "str", + "length": 32, + "nullable": "yes" + }, + { + "name": "userorgid", + "title": "用户机构", + "type": "str", + "length": 32, + "nullable": "yes" + }, + { + "name": "use_date", + "title": "使用日期", + "type": "date", + "nullable": "yes" + }, + { + "name": "use_time", + "title": "使用时间", + "type": "timestamp", + "nullable": "yes" + }, + { + "name": "period_start", + "title": "计费周期起", + "type": "date", + "nullable": "yes" + }, + { + "name": "period_end", + "title": "计费周期止", + "type": "date", + "nullable": "yes" + }, + { + "name": "quantity", + "title": "数量", + "type": "double", + "length": 18, + "dec": 4, + "nullable": "yes", + "default": "1" + }, + { + "name": "usages", + "title": "用量信息JSON", + "type": "text", + "nullable": "yes" + }, + { + "name": "transno", + "title": "交易号", + "type": "str", + "length": 64, + "nullable": "yes" + }, + { + "name": "amount", + "title": "交易金额", + "type": "double", + "length": 18, + "dec": 4, + "nullable": "yes" + }, + { + "name": "amount_currency", + "title": "计费币种", + "type": "str", + "length": 8, + "nullable": "yes" + }, + { + "name": "amount_base", + "title": "折本位币金额", + "type": "double", + "length": 18, + "dec": 4, + "nullable": "yes" + }, + { + "name": "cost", + "title": "成本", + "type": "double", + "length": 18, + "dec": 4, + "nullable": "yes" + }, + { + "name": "cost_currency", + "title": "成本币种", + "type": "str", + "length": 8, + "nullable": "yes" + }, + { + "name": "cost_base", + "title": "折本位币成本", + "type": "double", + "length": 18, + "dec": 4, + "nullable": "yes" + }, + { + "name": "discount_rate", + "title": "折扣率", + "type": "double", + "length": 8, + "dec": 4, + "nullable": "yes" + }, + { + "name": "accounting_status", + "title": "记账状态", + "type": "str", + "length": 16, + "nullable": "yes", + "default": "pending" + }, + { + "name": "status", + "title": "状态", + "type": "str", + "length": 16, + "nullable": "yes", + "default": "active" + }, + { + "name": "org_id", + "title": "所属机构", + "type": "str", + "length": 32, + "nullable": "yes" + }, + { + "name": "created_by", + "title": "创建人", + "type": "str", + "length": 32, + "nullable": "yes" + }, + { + "name": "created_at", + "title": "创建时间", + "type": "timestamp", + "nullable": "yes" + }, + { + "name": "updated_at", + "title": "更新时间", + "type": "timestamp", + "nullable": "yes" + } + ], + "indexes": [ + { + "name": "idx_au_user", + "idxtype": "index", + "idxfields": [ + "userid", + "use_date" + ] + }, + { + "name": "idx_au_org", + "idxtype": "index", + "idxfields": [ + "userorgid", + "use_date" + ] + }, + { + "name": "idx_au_acct", + "idxtype": "index", + "idxfields": [ + "accounting_status" + ] + }, + { + "name": "idx_au_sub", + "idxtype": "index", + "idxfields": [ + "subscription_id" + ] + } + ] +} \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..99fcf5c --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,17 @@ +[build-system] +requires = ["setuptools>=45", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "account-resource" +version = "1.0.0" +description = "账号资源模块 - 账号规格/权益配额/定价映射(spec×charge_mode→ppid)" +requires-python = ">=3.8" +dependencies = [ + "sqlor", + "bricks_for_python", +] + +[tool.setuptools.packages.find] +where = ["."] +include = ["account_resource*"] diff --git a/scripts/load_path.py b/scripts/load_path.py new file mode 100644 index 0000000..8a59f00 --- /dev/null +++ b/scripts/load_path.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 +"""RBAC path registration for account_resource module. + +宿主根目录执行:py3/bin/python pkgs/account_resource/scripts/load_path.py +(pipeline-app 的 load_path.sh 自动扫描 pkgs/*/scripts/load_path.py) +""" +import subprocess + +MOD = "account_resource" + +PATHS_ANY = [] + +PATHS_LOGINED = [ + f"/{MOD}", + f"/{MOD}/acctres_spec/index.ui", + f"/{MOD}/acctres_pricing_map/index.ui", + f"/{MOD}/acctres_usage/index.ui", +] + + +def register_paths(): + for path in PATHS_ANY: + subprocess.run(["py3/bin/python", "set_role_perm.py", "any", path]) + print(f" any: {path}") + for path in PATHS_LOGINED: + subprocess.run(["py3/bin/python", "set_role_perm.py", "logined", path]) + print(f" logined: {path}") + + +if __name__ == "__main__": + print(f"=== {MOD} RBAC registration ===") + register_paths() + print(f"Done. any={len(PATHS_ANY)} logined={len(PATHS_LOGINED)}")