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:
parent
b174a1a169
commit
04b1b19b94
6
i18n/en/msg.txt
Normal file
6
i18n/en/msg.txt
Normal file
@ -0,0 +1,6 @@
|
||||
存储规格: Storage Spec
|
||||
存储资源: Storage Resource
|
||||
容量采样: Capacity Sample
|
||||
计量方式: Meter Mode
|
||||
计费容量: Billable Capacity
|
||||
峰值容量: Peak Capacity
|
||||
6
i18n/jp/msg.txt
Normal file
6
i18n/jp/msg.txt
Normal file
@ -0,0 +1,6 @@
|
||||
存储规格: ストレージ仕様
|
||||
存储资源: ストレージリソース
|
||||
容量采样: 容量サンプリング
|
||||
计量方式: 計量方式
|
||||
计费容量: 課金容量
|
||||
峰值容量: ピーク容量
|
||||
6
i18n/ko/msg.txt
Normal file
6
i18n/ko/msg.txt
Normal file
@ -0,0 +1,6 @@
|
||||
存储规格: 스토리지 사양
|
||||
存储资源: 스토리지 리소스
|
||||
容量采样: 용량 샘플링
|
||||
计量方式: 계량 방식
|
||||
计费容量: 과금 용량
|
||||
峰值容量: 피크 용량
|
||||
6
i18n/zh/msg.txt
Normal file
6
i18n/zh/msg.txt
Normal file
@ -0,0 +1,6 @@
|
||||
存储规格: 存储规格
|
||||
存储资源: 存储资源
|
||||
容量采样: 容量采样
|
||||
计量方式: 计量方式
|
||||
计费容量: 计费容量
|
||||
峰值容量: 峰值容量
|
||||
36
init/data.json
Normal file
36
init/data.json
Normal 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 不同)"
|
||||
}
|
||||
70
json/storres_pricing_map.json
Normal file
70
json/storres_pricing_map.json
Normal 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
98
json/storres_sample.json
Normal 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
94
json/storres_spec.json
Normal 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
142
json/storres_usage.json
Normal 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"
|
||||
]
|
||||
}
|
||||
}
|
||||
122
models/storres_pricing_map.json
Normal file
122
models/storres_pricing_map.json
Normal 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
168
models/storres_sample.json
Normal 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
164
models/storres_spec.json
Normal 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
243
models/storres_usage.json
Normal 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
17
pyproject.toml
Normal 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
34
scripts/load_path.py
Normal 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)}")
|
||||
11
storage_resource/__init__.py
Normal file
11
storage_resource/__init__.py
Normal 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
366
storage_resource/init.py
Normal 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 模块未加载,存储计费金额记 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, 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
|
||||
Loading…
x
Reference in New Issue
Block a user