feat: storage_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:11 +08:00
parent b174a1a169
commit 04b1b19b94
17 changed files with 1589 additions and 0 deletions

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

@ -0,0 +1,6 @@
存储规格: Storage Spec
存储资源: Storage Resource
容量采样: Capacity Sample
计量方式: Meter Mode
计费容量: Billable Capacity
峰值容量: Peak Capacity

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

@ -0,0 +1,6 @@
存储规格: ストレージ仕様
存储资源: ストレージリソース
容量采样: 容量サンプリング
计量方式: 計量方式
计费容量: 課金容量
峰值容量: ピーク容量

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

@ -0,0 +1,6 @@
存储规格: 스토리지 사양
存储资源: 스토리지 리소스
容量采样: 용량 샘플링
计量方式: 계량 방식
计费容量: 과금 용량
峰值容量: 피크 용량

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

@ -0,0 +1,6 @@
存储规格: 存储规格
存储资源: 存储资源
容量采样: 容量采样
计量方式: 计量方式
计费容量: 计费容量
峰值容量: 峰值容量

36
init/data.json Normal file
View File

@ -0,0 +1,36 @@
{
"storres_spec": [
{
"spec_code": "WS-STORAGE-STD",
"spec_name": "工作空间标准存储",
"storage_class": "standard",
"min_gb": 1,
"step_gb": 1,
"capacity_total": -1,
"capacity_used": 0,
"backup_enabled": "0",
"sort_order": 10,
"status": "active",
"description": "工作空间容量,按 GB月 计费"
}
],
"storres_pricing_map": [
{
"spec_code": "WS-STORAGE-STD",
"meter_mode": "ondemand",
"meter_unit": "GB月",
"settle_period": "month",
"is_default": "1",
"ppid": ""
},
{
"spec_code": "WS-STORAGE-STD",
"meter_mode": "package",
"meter_unit": "GB月",
"settle_period": "month",
"is_default": "0",
"ppid": ""
}
],
"_note": "ppid 待建定价项目后回填meter_mode 是本模块的 ppid 挂载维度(与账号按 charge_mode 不同)"
}

View File

@ -0,0 +1,70 @@
{
"tblname": "storres_pricing_map",
"params": {
"title": "存储定价映射",
"browserfields": [
{
"field": "spec_id",
"title": "规格ID"
},
{
"field": "meter_mode",
"title": "计量方式"
},
{
"field": "meter_unit",
"title": "计量单位"
},
{
"field": "settle_period",
"title": "结算周期"
},
{
"field": "ppid",
"title": "定价项目ID"
},
{
"field": "is_default",
"title": "缺省方式"
},
{
"field": "status",
"title": "状态"
}
],
"editfields": [
{
"field": "spec_id",
"uitype": "Text"
},
{
"field": "meter_mode",
"uitype": "Text"
},
{
"field": "meter_unit",
"uitype": "Text"
},
{
"field": "settle_period",
"uitype": "Text"
},
{
"field": "ppid",
"uitype": "Text"
},
{
"field": "is_default",
"uitype": "Text"
},
{
"field": "status",
"uitype": "Text"
}
],
"searchfields": [
"spec_id",
"meter_mode"
]
}
}

98
json/storres_sample.json Normal file
View File

@ -0,0 +1,98 @@
{
"tblname": "storres_sample",
"params": {
"title": "容量采样",
"browserfields": [
{
"field": "spec_id",
"title": "规格ID"
},
{
"field": "target_type",
"title": "计量对象类型"
},
{
"field": "target_id",
"title": "计量对象ID"
},
{
"field": "sample_date",
"title": "采样日期"
},
{
"field": "used_gb",
"title": "实测容量GB"
},
{
"field": "quota_gb",
"title": "配额容量GB"
},
{
"field": "over_gb",
"title": "超额GB"
},
{
"field": "source",
"title": "采样来源"
}
],
"editfields": [
{
"field": "spec_id",
"uitype": "Text"
},
{
"field": "subscription_id",
"uitype": "Text"
},
{
"field": "target_type",
"uitype": "Text"
},
{
"field": "target_id",
"uitype": "Text"
},
{
"field": "userid",
"uitype": "Text"
},
{
"field": "userorgid",
"uitype": "Text"
},
{
"field": "sample_date",
"uitype": "Text"
},
{
"field": "sample_time",
"uitype": "Text"
},
{
"field": "used_gb",
"uitype": "Text"
},
{
"field": "quota_gb",
"uitype": "Text"
},
{
"field": "over_gb",
"uitype": "Text"
},
{
"field": "source",
"uitype": "Text"
},
{
"field": "status",
"uitype": "Text"
}
],
"searchfields": [
"spec_id",
"target_type"
]
}
}

94
json/storres_spec.json Normal file
View File

@ -0,0 +1,94 @@
{
"tblname": "storres_spec",
"params": {
"title": "存储规格",
"browserfields": [
{
"field": "spec_code",
"title": "规格编码"
},
{
"field": "spec_name",
"title": "规格名称"
},
{
"field": "storage_class",
"title": "存储类型"
},
{
"field": "min_gb",
"title": "起售容量GB"
},
{
"field": "capacity_total",
"title": "可售总量GB"
},
{
"field": "capacity_used",
"title": "已分配GB"
},
{
"field": "status",
"title": "状态"
}
],
"editfields": [
{
"field": "spec_code",
"uitype": "Text"
},
{
"field": "spec_name",
"uitype": "Text"
},
{
"field": "storage_class",
"uitype": "Text"
},
{
"field": "description",
"uitype": "Text"
},
{
"field": "min_gb",
"uitype": "Text"
},
{
"field": "max_gb",
"uitype": "Text"
},
{
"field": "step_gb",
"uitype": "Text"
},
{
"field": "capacity_total",
"uitype": "Text"
},
{
"field": "capacity_used",
"uitype": "Text"
},
{
"field": "backup_enabled",
"uitype": "Text"
},
{
"field": "attr_json",
"uitype": "Text"
},
{
"field": "sort_order",
"uitype": "Text"
},
{
"field": "status",
"uitype": "Text"
}
],
"searchfields": [
"spec_code",
"spec_name"
]
}
}

142
json/storres_usage.json Normal file
View File

@ -0,0 +1,142 @@
{
"tblname": "storres_usage",
"params": {
"title": "存储用量",
"browserfields": [
{
"field": "spec_id",
"title": "规格ID"
},
{
"field": "meter_mode",
"title": "计量方式"
},
{
"field": "userorgid",
"title": "用户机构"
},
{
"field": "period_start",
"title": "计费周期起"
},
{
"field": "period_end",
"title": "计费周期止"
},
{
"field": "billable_gb",
"title": "计费容量GB"
},
{
"field": "amount",
"title": "交易金额"
},
{
"field": "accounting_status",
"title": "记账状态"
}
],
"editfields": [
{
"field": "spec_id",
"uitype": "Text"
},
{
"field": "meter_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": "sample_count",
"uitype": "Text"
},
{
"field": "avg_gb",
"uitype": "Text"
},
{
"field": "max_gb",
"uitype": "Text"
},
{
"field": "billable_gb",
"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",
"meter_mode"
]
}
}

View File

@ -0,0 +1,122 @@
{
"summary": [
{
"name": "storres_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": "meter_mode",
"title": "计量方式",
"type": "str",
"length": 32,
"nullable": "no"
},
{
"name": "meter_unit",
"title": "计量单位",
"type": "str",
"length": 16,
"nullable": "yes"
},
{
"name": "settle_period",
"title": "结算周期",
"type": "str",
"length": 16,
"nullable": "yes",
"default": "month"
},
{
"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_spm_spec",
"idxtype": "index",
"idxfields": [
"spec_id"
]
},
{
"name": "idx_spm_mode",
"idxtype": "unique",
"idxfields": [
"spec_id",
"meter_mode"
]
},
{
"name": "idx_spm_ppid",
"idxtype": "index",
"idxfields": [
"ppid"
]
}
]
}

168
models/storres_sample.json Normal file
View File

@ -0,0 +1,168 @@
{
"summary": [
{
"name": "storres_sample",
"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": "subscription_id",
"title": "订购ID",
"type": "str",
"length": 32,
"nullable": "yes"
},
{
"name": "target_type",
"title": "计量对象类型",
"type": "str",
"length": 32,
"nullable": "yes"
},
{
"name": "target_id",
"title": "计量对象ID",
"type": "str",
"length": 64,
"nullable": "yes"
},
{
"name": "userid",
"title": "用户ID",
"type": "str",
"length": 32,
"nullable": "yes"
},
{
"name": "userorgid",
"title": "用户机构",
"type": "str",
"length": 32,
"nullable": "yes"
},
{
"name": "sample_date",
"title": "采样日期",
"type": "date",
"nullable": "yes"
},
{
"name": "sample_time",
"title": "采样时间",
"type": "timestamp",
"nullable": "yes"
},
{
"name": "used_gb",
"title": "实测容量GB",
"type": "double",
"length": 18,
"dec": 4,
"nullable": "yes"
},
{
"name": "quota_gb",
"title": "配额容量GB",
"type": "double",
"length": 18,
"dec": 4,
"nullable": "yes"
},
{
"name": "over_gb",
"title": "超额GB",
"type": "double",
"length": 18,
"dec": 4,
"nullable": "yes",
"default": "0"
},
{
"name": "source",
"title": "采样来源",
"type": "str",
"length": 32,
"nullable": "yes",
"default": "scan"
},
{
"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_ss_target",
"idxtype": "index",
"idxfields": [
"target_type",
"target_id",
"sample_date"
]
},
{
"name": "idx_ss_org",
"idxtype": "index",
"idxfields": [
"userorgid",
"sample_date"
]
},
{
"name": "idx_ss_sub",
"idxtype": "index",
"idxfields": [
"subscription_id",
"sample_date"
]
}
]
}

164
models/storres_spec.json Normal file
View File

@ -0,0 +1,164 @@
{
"summary": [
{
"name": "storres_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": "storage_class",
"title": "存储类型",
"type": "str",
"length": 32,
"nullable": "yes",
"default": "standard"
},
{
"name": "description",
"title": "说明",
"type": "text",
"nullable": "yes"
},
{
"name": "min_gb",
"title": "起售容量GB",
"type": "double",
"length": 18,
"dec": 2,
"nullable": "yes",
"default": "1"
},
{
"name": "max_gb",
"title": "单次上限GB",
"type": "double",
"length": 18,
"dec": 2,
"nullable": "yes"
},
{
"name": "step_gb",
"title": "购买步长GB",
"type": "double",
"length": 18,
"dec": 2,
"nullable": "yes",
"default": "1"
},
{
"name": "capacity_total",
"title": "可售总量GB",
"type": "double",
"length": 18,
"dec": 2,
"nullable": "yes",
"default": "-1"
},
{
"name": "capacity_used",
"title": "已分配GB",
"type": "double",
"length": 18,
"dec": 2,
"nullable": "yes",
"default": "0"
},
{
"name": "backup_enabled",
"title": "含备份",
"type": "str",
"length": 1,
"nullable": "yes",
"default": "0"
},
{
"name": "attr_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_sspec_code",
"idxtype": "unique",
"idxfields": [
"spec_code"
]
},
{
"name": "idx_sspec_class",
"idxtype": "index",
"idxfields": [
"storage_class",
"status"
]
}
]
}

243
models/storres_usage.json Normal file
View File

@ -0,0 +1,243 @@
{
"summary": [
{
"name": "storres_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": "meter_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": "sample_count",
"title": "采样点数",
"type": "int",
"nullable": "yes"
},
{
"name": "avg_gb",
"title": "平均容量GB",
"type": "double",
"length": 18,
"dec": 4,
"nullable": "yes"
},
{
"name": "max_gb",
"title": "峰值容量GB",
"type": "double",
"length": 18,
"dec": 4,
"nullable": "yes"
},
{
"name": "billable_gb",
"title": "计费容量GB",
"type": "double",
"length": 18,
"dec": 4,
"nullable": "yes"
},
{
"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_su_org",
"idxtype": "index",
"idxfields": [
"userorgid",
"use_date"
]
},
{
"name": "idx_su_acct",
"idxtype": "index",
"idxfields": [
"accounting_status"
]
},
{
"name": "idx_su_sub",
"idxtype": "index",
"idxfields": [
"subscription_id",
"period_start"
]
}
]
}

17
pyproject.toml Normal file
View File

@ -0,0 +1,17 @@
[build-system]
requires = ["setuptools>=45", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "storage-resource"
version = "1.0.0"
description = "存储资源模块 - 工作空间容量规格/采样/定价映射(spec×meter_mode→ppid)"
requires-python = ">=3.8"
dependencies = [
"sqlor",
"bricks_for_python",
]
[tool.setuptools.packages.find]
where = ["."]
include = ["storage_resource*"]

34
scripts/load_path.py Normal file
View File

@ -0,0 +1,34 @@
#!/usr/bin/env python3
"""RBAC path registration for storage_resource module.
宿主根目录执行py3/bin/python pkgs/storage_resource/scripts/load_path.py
pipeline-app load_path.sh 自动扫描 pkgs/*/scripts/load_path.py
"""
import subprocess
MOD = "storage_resource"
PATHS_ANY = []
PATHS_LOGINED = [
f"/{MOD}",
f"/{MOD}/storres_spec/index.ui",
f"/{MOD}/storres_pricing_map/index.ui",
f"/{MOD}/storres_sample/index.ui",
f"/{MOD}/storres_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)}")

View File

@ -0,0 +1,11 @@
"""存储资源模块 - 工作空间容量规格/采样/定价映射(spec×meter_mode→ppid)"""
from .init import (
load_storage_resource,
get_storage_spec,
get_storage_ppid,
record_storage_sample,
storage_charging,
resolve_ppid,
)
__version__ = '1.0.0'

366
storage_resource/init.py Normal file
View File

@ -0,0 +1,366 @@
"""storage_resource — 存储资源模块(工作空间容量 G/月)
参照 llmage 范式 **ppid 挂法与账号资源不同**
account_resource: acctres_spec × charge_mode(//试用) ppid
storage_resource: storres_spec × meter_mode(按量/包月/峰值) ppid
存储比账号多一层采样容量是连续占用量必须周期采样再按 GB月 汇总
而账号是一次性/周期性开通事件这正是每类资源和 ppid 对应方式可不同
但最终作用一样两者都通过 resolve_ppid(resource_ref_id, ctx) 对外统一
宿主集成:
from storage_resource.init import load_storage_resource
load_storage_resource()
"""
import json
import logging
import datetime
from ahserver.serverenv import ServerEnv
from appPublic.uniqueID import getID
from sqlor.dbpools import DBPools
MODULE_NAME = 'storage_resource'
MODULE_VERSION = '1.0.0'
RESOURCE_TYPE = 'workspace_storage' # product_resource.resource_type 用这个值
logger = logging.getLogger(__name__)
def _get_dbname():
env = ServerEnv()
return env.get_module_dbname(MODULE_NAME)
def _now():
return datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
def _today():
return datetime.date.today().strftime('%Y-%m-%d')
def _val(rec, key, default=''):
if isinstance(rec, dict):
return rec.get(key, default)
return getattr(rec, key, default)
# ═══════════════════════════════════════════════════════════════════
# 规格(资源主表)
# ═══════════════════════════════════════════════════════════════════
async def storres_spec_list(ns=None):
ns = ns or {}
sql = "SELECT * FROM storres_spec WHERE 1=1"
params = {}
if ns.get('status'):
sql += " AND status=${status}$"
params['status'] = ns['status']
if ns.get('storage_class'):
sql += " AND storage_class=${storage_class}$"
params['storage_class'] = ns['storage_class']
sql += " ORDER BY sort_order, spec_code"
async with DBPools().sqlorContext(_get_dbname()) as sor:
return await sor.sqlExe(sql, params)
async def get_storage_spec(spec_id_or_code):
async with DBPools().sqlorContext(_get_dbname()) as sor:
recs = await sor.sqlExe(
"SELECT * FROM storres_spec WHERE id=${k}$ OR spec_code=${k}$",
{"k": spec_id_or_code})
return recs[0] if recs else None
async def storres_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('storres_spec', data)
return data['id']
async def storres_spec_update(data):
data = dict(data or {})
data['updated_at'] = _now()
async with DBPools().sqlorContext(_get_dbname()) as sor:
await sor.U('storres_spec', data)
return True
async def storres_spec_delete(data):
async with DBPools().sqlorContext(_get_dbname()) as sor:
await sor.D('storres_spec', {'id': (data or {}).get('id', '')})
return True
async def check_capacity_available(spec_id_or_code, need_gb):
"""库存校验capacity_total=-1 表示不限。"""
spec = await get_storage_spec(spec_id_or_code)
if not spec:
return False, '规格不存在'
total = float(_val(spec, 'capacity_total', -1) or -1)
if total < 0:
return True, 'unlimited'
used = float(_val(spec, 'capacity_used', 0) or 0)
need = float(need_gb or 0)
if used + need > total:
return False, '可售容量不足(剩余 %.2fGB)' % (total - used)
return True, 'ok'
# ═══════════════════════════════════════════════════════════════════
# 定价映射spec × meter_mode → ppid
# ═══════════════════════════════════════════════════════════════════
async def storres_pricing_map_list(ns=None):
ns = ns or {}
sql = ("SELECT m.*, s.spec_code, s.spec_name FROM storres_pricing_map m "
"LEFT JOIN storres_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.meter_mode"
async with DBPools().sqlorContext(_get_dbname()) as sor:
return await sor.sqlExe(sql, params)
async def storres_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('storres_pricing_map', data)
return data['id']
async def storres_pricing_map_update(data):
data = dict(data or {})
data['updated_at'] = _now()
async with DBPools().sqlorContext(_get_dbname()) as sor:
await sor.U('storres_pricing_map', data)
return True
async def storres_pricing_map_delete(data):
async with DBPools().sqlorContext(_get_dbname()) as sor:
await sor.D('storres_pricing_map', {'id': (data or {}).get('id', '')})
return True
async def get_storage_ppid(spec_id_or_code, meter_mode=None):
"""**统一解析器契约**:资源 + 计量方式 → ppid。"""
spec = await get_storage_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, meter_mode, meter_unit, settle_period FROM storres_pricing_map "
"WHERE spec_id=${spec_id}$ AND status='active'")
params = {'spec_id': spec_id}
if meter_mode:
sql += " AND meter_mode=${meter_mode}$"
params['meter_mode'] = meter_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 meter_mode=%s', spec_id, meter_mode)
return None
return _val(recs[0], 'ppid') or None
async def resolve_ppid(resource_ref_id, ctx=None):
"""资源定价解析器(注册给产品层统一调用)。
ctx: {'meter_mode': 'ondemand'|'package'|'peak', ...}
"""
ctx = ctx or {}
return await get_storage_ppid(resource_ref_id, ctx.get('meter_mode'))
# ═══════════════════════════════════════════════════════════════════
# 容量采样(存储特有)
# ═══════════════════════════════════════════════════════════════════
async def record_storage_sample(spec_id, target_type, target_id, used_gb,
userid='', userorgid='', subscription_id='',
quota_gb=None, source='scan'):
"""记一次容量采样。定时任务按机构/工作空间扫盘后调用。"""
over = 0.0
if quota_gb not in (None, ''):
over = max(0.0, float(used_gb or 0) - float(quota_gb))
rec = {
'id': getID(),
'spec_id': spec_id,
'subscription_id': subscription_id,
'target_type': target_type,
'target_id': str(target_id),
'userid': userid,
'userorgid': userorgid,
'sample_date': _today(),
'sample_time': _now(),
'used_gb': float(used_gb or 0),
'quota_gb': float(quota_gb) if quota_gb not in (None, '') else None,
'over_gb': over,
'source': source,
'status': 'active',
'org_id': userorgid,
'created_at': _now(),
}
async with DBPools().sqlorContext(_get_dbname()) as sor:
await sor.C('storres_sample', rec)
return rec['id']
async def summarize_samples(subscription_id, period_start, period_end):
"""汇总某订购在周期内的采样:返回 (采样点数, 均值GB, 峰值GB)。
GB月 计费用均值更公平峰值供 meter_mode='peak'
"""
async with DBPools().sqlorContext(_get_dbname()) as sor:
recs = await sor.sqlExe(
"SELECT COUNT(*) AS cnt, AVG(used_gb) AS avg_gb, MAX(used_gb) AS max_gb "
"FROM storres_sample WHERE subscription_id=${sid}$ "
"AND sample_date >= ${ps}$ AND sample_date <= ${pe}$",
{'sid': subscription_id, 'ps': period_start, 'pe': period_end})
if not recs:
return 0, 0.0, 0.0
r = recs[0]
cnt = int(_val(r, 'cnt', 0) or 0)
# SUM/AVG 返回 Decimal必须 float() 否则 json 序列化炸
avg_gb = float(_val(r, 'avg_gb', 0) or 0)
max_gb = float(_val(r, 'max_gb', 0) or 0)
return cnt, round(avg_gb, 4), round(max_gb, 4)
# ═══════════════════════════════════════════════════════════════════
# 出账
# ═══════════════════════════════════════════════════════════════════
async def storage_charging(spec_id_or_code, meter_mode, userid, userorgid,
subscription_id, period_start, period_end,
discount_rate=None):
"""按周期出账:汇总采样 → 解析 ppid → pricing 计费 → 落用量记录。"""
env = ServerEnv()
cnt, avg_gb, max_gb = await summarize_samples(subscription_id, period_start, period_end)
billable_gb = max_gb if meter_mode == 'peak' else avg_gb
ppid = await get_storage_ppid(spec_id_or_code, meter_mode)
usages = {
'storage_gb': billable_gb,
'avg_gb': avg_gb,
'max_gb': max_gb,
'sample_count': cnt,
'meter_mode': meter_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, meter_mode)
rate = float(discount_rate) if discount_rate not in (None, '') else 1.0
final_amount = round(amount * rate, 4)
spec = await get_storage_spec(spec_id_or_code)
rec = {
'id': getID(),
'spec_id': _val(spec, 'id') if spec else '',
'meter_mode': meter_mode,
'subscription_id': subscription_id,
'userid': userid,
'userorgid': userorgid,
'use_date': _today(),
'use_time': _now(),
'period_start': period_start,
'period_end': period_end,
'sample_count': cnt,
'avg_gb': avg_gb,
'max_gb': max_gb,
'billable_gb': billable_gb,
'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('storres_usage', rec)
logger.info('存储出账: spec=%s mode=%s 计费=%sGB(采样%d点) 原价=%s 折扣=%s 应收=%s',
spec_id_or_code, meter_mode, billable_gb, cnt, amount, rate, final_amount)
return rec
async def get_accounting_storres_usages(limit=200):
async with DBPools().sqlorContext(_get_dbname()) as sor:
return await sor.sqlExe(
"SELECT * FROM storres_usage WHERE accounting_status='pending' "
"ORDER BY use_time LIMIT " + str(int(limit)), {})
async def mark_storres_accounted(usage_id, status='accounted'):
async with DBPools().sqlorContext(_get_dbname()) as sor:
await sor.U('storres_usage', {'id': usage_id, 'accounting_status': status,
'updated_at': _now()})
return True
# ═══════════════════════════════════════════════════════════════════
# Module Loader
# ═══════════════════════════════════════════════════════════════════
def load_storage_resource():
env = ServerEnv()
env.storres_spec_list = storres_spec_list
env.storres_spec_create = storres_spec_create
env.storres_spec_update = storres_spec_update
env.storres_spec_delete = storres_spec_delete
env.get_storage_spec = get_storage_spec
env.check_capacity_available = check_capacity_available
env.storres_pricing_map_list = storres_pricing_map_list
env.storres_pricing_map_create = storres_pricing_map_create
env.storres_pricing_map_update = storres_pricing_map_update
env.storres_pricing_map_delete = storres_pricing_map_delete
env.get_storage_ppid = get_storage_ppid
env.record_storage_sample = record_storage_sample
env.summarize_samples = summarize_samples
env.storage_charging = storage_charging
env.get_accounting_storres_usages = get_accounting_storres_usages
env.mark_storres_accounted = mark_storres_accounted
reg = getattr(env, 'register_resource_pricing_resolver', None)
if reg:
reg(RESOURCE_TYPE, resolve_ppid)
else:
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('storage_resource module loaded (v%s, resource_type=%s)',
MODULE_VERSION, RESOURCE_TYPE)
return True