feat: account_resource 资源模块

参照 llmage 范式(llm主表无ppid + llm_api_map挂ppid + llmusage计费六件套):
- 资源主表不带 ppid,定价挂在「资源×用法」映射表上
- 对外统一 resolve_ppid(resource_ref_id, ctx),load 时注册到产品层解析器注册表
- 含 models 四段式/json CRUD/i18n四语言/load_path/init 种子数据
This commit is contained in:
ymq 2026-08-26 14:51:10 +08:00
parent 029ce7ced6
commit 71576381bd
15 changed files with 1274 additions and 0 deletions

View File

@ -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'

325
account_resource/init.py Normal file
View File

@ -0,0 +1,325 @@
"""account_resource — 账号资源模块
参照 llmage 范式资源模块的标准形态
llmage: llm(主表,无ppid) llm_api_map(llmid×apinameppid) llmusage(计费六件套)
account_resource: acctres_spec acctres_pricing_map(spec×charge_modeppid) 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 模块未加载,账号计费金额记 0ppid=%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

7
i18n/en/msg.txt Normal file
View File

@ -0,0 +1,7 @@
账号规格: Account Spec
账号资源: Account Resource
权益配额: Privilege Quota
计费方式: Charge Mode
试用: Trial
月租: Monthly
年租: Yearly

7
i18n/jp/msg.txt Normal file
View File

@ -0,0 +1,7 @@
账号规格: アカウント仕様
账号资源: アカウントリソース
权益配额: 権益クォータ
计费方式: 課金方式
试用: トライアル
月租: 月額
年租: 年額

7
i18n/ko/msg.txt Normal file
View File

@ -0,0 +1,7 @@
账号规格: 계정 사양
账号资源: 계정 리소스
权益配额: 권익 할당량
计费方式: 과금 방식
试用: 체험
月租: 월정액
年租: 연정액

7
i18n/zh/msg.txt Normal file
View File

@ -0,0 +1,7 @@
账号规格: 账号规格
账号资源: 账号资源
权益配额: 权益配额
计费方式: 计费方式
试用: 试用
月租: 月租
年租: 年租

68
init/data.json Normal file
View File

@ -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 挂载维度"
}

View File

@ -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"
]
}
}

94
json/acctres_spec.json Normal file
View File

@ -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"
]
}
}

130
json/acctres_usage.json Normal file
View File

@ -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"
]
}
}

View File

@ -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"
]
}
]
}

149
models/acctres_spec.json Normal file
View File

@ -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"
]
}
]
}

229
models/acctres_usage.json Normal file
View File

@ -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"
]
}
]
}

17
pyproject.toml Normal file
View File

@ -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*"]

33
scripts/load_path.py Normal file
View File

@ -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)}")