88 lines
8.2 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# storage_resource — 存储资源模块
## 模块定位
工作空间容量GB/月)类资源的**规格 / 容量采样 / 定价映射**模块,宿主为 pipeline-app也可装 sage。参照 llmage 范式,但 ppid 挂法与账号资源不同:
```
account_resource: acctres_spec × charge_mode(月/年/试用) → ppid
storage_resource: storres_spec × meter_mode(按量/包月/峰值) → ppid
```
存储比账号多一层「**采样**」:容量是连续占用量,必须周期采样再按 GB月 汇总;账号是一次性/周期性开通事件。两者都通过 `resolve_ppid(resource_ref_id, ctx)` 对外统一,产品层不必知道内部差异。`RESOURCE_TYPE = 'workspace_storage'`。业务定价口径 1 元/GB/月(示例定价 `unit_prices: 2.8` 见链路技能,实际以 pricing_program 配置为准)。
宿主集成:
```python
from storage_resource.init import load_storage_resource
load_storage_resource()
```
## 表清单models/*.json共 4 张)
| 表 | 用途 | 关键字段 |
|---|---|---|
| `storres_spec` | 存储规格(资源主表) | `id`(pk32), `spec_code`(唯一64), `spec_name`, `storage_class`(standard…), `min_gb`(起售), `max_gb`(单次上限), `step_gb`(购买步长), `capacity_total`(可售总量,-1=不限), `capacity_used`(已分配), `backup_enabled`('0'/'1'), `attr_json`, `sort_order`, `status`(active) |
| `storres_pricing_map` | 规格×计量方式 → 定价项目 | `spec_id`, `meter_mode`(唯一键 spec_id+meter_mode: ondemand/package/peak), `meter_unit`(GB月), `settle_period`(month), `ppid`(→pricing_program), `is_default`('1'=缺省), `status` |
| `storres_sample` | 容量采样(存储特有) | `spec_id`, `subscription_id`, `target_type`(计量对象类型), `target_id`, `userid`/`userorgid`, `sample_date`/`sample_time`, `used_gb`(实测), `quota_gb`(配额), `over_gb`(超额=used-quota,≥0), `source`(scan…) |
| `storres_usage` | 存储资源用量(计费落账凭证) | `spec_id`, `meter_mode`, `subscription_id`, `userid`/`userorgid`, `use_date`/`use_time`(出账), `period_start/end`, `sample_count`(采样点数), `avg_gb`/`max_gb`/`billable_gb`(计费容量), `usages`(JSON), `transno`, `amount`+`amount_currency`+`amount_base`, `cost`+`cost_currency`+`cost_base`, `discount_rate`, `accounting_status`(pending→accounted) |
种子数据 `init/data.json`1 条规格WS-STORAGE-STDmin_gb=1capacity_total=-1 不限)+ 2 条定价映射ondemand is_default='1'、package。**种子 ppid 为空串**——建好 pricing_program 后必须回填,否则计费金额记 0meter_mode='peak' 若业务要用峰值计费也必须补映射条目,否则记 0。
## 对外 APIload_storage_resource() 注册到 ServerEnv
```python
# 规格 CRUD
env.storres_spec_list(ns) # ns: {status, storage_class}
env.storres_spec_create/update/delete(data)
env.get_storage_spec(spec_id_or_code)
env.check_capacity_available(spec_id_or_code, need_gb) # → (bool, msg)capacity_total<0=不限
# 定价映射 CRUD
env.storres_pricing_map_list(ns) # ns: {spec_id}LEFT JOIN 带出 spec_code/spec_name
env.storres_pricing_map_create/update/delete(data)
env.get_storage_ppid(spec_id_or_code, meter_mode=None) # meter_mode空→取is_default='1'
# 采样
env.record_storage_sample(spec_id, target_type, target_id, used_gb,
userid='', userorgid='', subscription_id='',
quota_gb=None, source='scan') # 定时扫盘后调over_gb自动算
env.summarize_samples(subscription_id, period_start, period_end) # → (采样点数, 均值GB, 峰值GB)
# 出账
env.storage_charging(spec_id_or_code, meter_mode, userid, userorgid,
subscription_id, period_start, period_end, discount_rate=None)
# 汇总采样 → billable_gb = max_gb if meter_mode=='peak' else avg_gb
# → 解析ppid → env.buffered_charging → 原价×discount_rate → 写 storres_usage(pending)
env.storage_direct_charging(spec_id_or_code, storage_gb, userid, userorgid,
valid_months=1, subscription_id='', discount_rate=None)
# 一次性直购(购买即记账,选A模式): 不依赖采样; 按 package 定价(无则缺省);
# 金额 = 单价(GB月) × storage_gb × valid_months; sample_count=0 标记直购; meter_mode='direct'
# 记账对接
env.get_accounting_storres_usages(limit=200) # pending 用量,供 accounting 出账扫描
env.mark_storres_accounted(usage_id, status='accounted')
```
`resolve_ppid(resource_ref_id, ctx)`ctx: {'meter_mode': 'ondemand'|'package'|'peak'})作为**统一定价解析器**注册:优先 `env.register_resource_pricing_resolver('workspace_storage', resolve_ppid)`,产品层未就绪时兜底暂存 `env._resource_pricing_resolvers`
## 页面/CRUDjson/*.json
4 个 xls2ui 格式 Bricks CRUD 定义:`storres_spec``storres_pricing_map``storres_sample``storres_usage`editable 指向 `./add_*.dspy` 等——**这些 dspy 端点不在本仓库**,由宿主 wwwroot 提供)。`scripts/load_path.py` 注册 RBAC 路径(全部 logined`/storage_resource``/storage_resource/{storres_spec,storres_pricing_map,storres_sample,storres_usage}/index.ui`
## 与产品/计费链路的集成关系
- **product_management**`env.product_interface` 指向本模块 `product_interface.py`module_name / get_product_display / check_product_availability / check_product_consumable / execute_product_service(_stream) / calculate_product_cost / load_product_category_product`resource_ref_id = storres_spec.id`
- `load_product_category_product`:导出「存储服务」类别 + 每规格一条 product_type='workspace_storage' 产品。
- `execute_product_service`request_data 取 `meter_mode`(默认ondemand)/`storage_gb`(默认 min_gb),返回 entitlement容量配额实际配额分配后续接入。
- `calculate_product_cost(resource_ref_id, usage_data, user_org_id=None)`**只算原价**discount 恒 1.0meter_mode='direct' 复用 package 定价valid_months>1 时按 N 倍计;客户/分销商折扣由产品层 product_management.core 精确计算。
- **accounting计费落账链**`storage_charging`/`storage_direct_charging` 写 storres_usage(pending) → 产品层 `storage_resource_accounting(usage_record)` 按 spec_id→product_id → product_accounting_generic → biz_order/biz_orderdetail → consume_accounting 复式记账acc_balance/acc_detail/bill_detail`mark_storres_accounted` 置 accounted。前置客户已开户 + 余额充足,否则 AccountIdNone/AccountOverDraw。
- **定价引擎pricing**:铁律——用 `env.buffered_charging(ppid, data)` **两参**、返回价格对象列表,金额 `sum(getattr(p,'amount',0) for p in prices)`;用三参 `pricing_program_charging` 报缺参且静默记 0commit 3e6f448 已修)。存储定价 YAML 示例:`pricings: [{price_factors: storage_gb, unit_prices: 2.8}]`
## 部署注意事项
1. **库名**:一律 `env.get_module_dbname('storage_resource')`pipeline-app 返回 'pipeline'),禁写死。
2. **建表**models/*.json 交宿主建表流程;生产库勿跑完整 create_tables.pyproduct_management 会 DROP 重建丢数据)。
3. **种子导入**init/data.json 由 import_init 导入;导完**回填 ppid**(先建 pricing_program + pricing_program_timing.pricing_data
4. **采样定时任务**:需宿主配周期扫盘任务调 `record_storage_sample`(容量事实可来自 filemgr 的 file.filesize 汇总 / 工作空间实际占用),再按月 `summarize_samples``storage_charging`
5. **RBAC**:宿主根目录执行 `py3/bin/python pkgs/storage_resource/scripts/load_path.py`
6. **meter_mode 语义**ondemand(按量,均值)/package(包月)/peak(峰值)三种都是设计值;`summarize_samples` 里 AVG/MAX 返回 Decimal必须 float() 否则 JSON 序列化炸(已在代码处理)。
7. **依赖**sqlor、bricks_for_python运行时依赖宿主提供 ahserver/appPublic/pricing/product_management。i18n/{zh,en,jp,ko}/msg.txt 已备词条(存储规格/容量采样/计量方式/计费容量/峰值容量等)。
8. `init.py` 尾部延迟 import `load_product_category_product`(避免与 product_interface 循环导入),勿上移。