From 577f1f15615788bbf2e906cf89368d20a1858ead Mon Sep 17 00:00:00 2001 From: ymq Date: Thu, 27 Aug 2026 10:40:53 +0800 Subject: [PATCH] =?UTF-8?q?feat(purchase):=20=E4=B8=80=E6=AC=A1=E6=80=A7?= =?UTF-8?q?=E7=9B=B4=E8=B4=AD=E8=AE=A1=E8=B4=B9=20storage=5Fdirect=5Fcharg?= =?UTF-8?q?ing(=E9=80=89A=E8=B4=AD=E4=B9=B0=E5=8D=B3=E8=AE=B0=E8=B4=A6)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 存储买断模式:不依赖采样,按购买容量×月数直接计费出账。 - storage_direct_charging(spec, storage_gb, valid_months):package定价×月数 - calculate_product_cost 支持 direct 模式复用 package 定价 + 月数乘法 - 注册 env.storage_direct_charging + __init__ 导出 --- storage_resource/__init__.py | 1 + .../__pycache__/init.cpython-310.pyc | Bin 11491 -> 13840 bytes .../product_interface.cpython-310.pyc | Bin 5301 -> 5426 bytes storage_resource/init.py | 90 ++++++++++++++++++ storage_resource/product_interface.py | 8 +- 5 files changed, 98 insertions(+), 1 deletion(-) diff --git a/storage_resource/__init__.py b/storage_resource/__init__.py index 9b786f6..bc8edc9 100644 --- a/storage_resource/__init__.py +++ b/storage_resource/__init__.py @@ -5,6 +5,7 @@ from .init import ( get_storage_ppid, record_storage_sample, storage_charging, + storage_direct_charging, resolve_ppid, ) diff --git a/storage_resource/__pycache__/init.cpython-310.pyc b/storage_resource/__pycache__/init.cpython-310.pyc index 1c5a6c6b11e93a243d8a6aa69d21255bdf8837fb..6a8fc8aa07884c6a2d22fbc19bf9f93ea21505d1 100644 GIT binary patch delta 4349 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 R#f`x+PwrT*+v6=7_#3qP6aN4J diff --git a/storage_resource/__pycache__/product_interface.cpython-310.pyc b/storage_resource/__pycache__/product_interface.cpython-310.pyc index 590965385d7507818d59b31c8c6098b6103048e7..f8108b0a53da55ca0bba8d92bc7a442e27d879f2 100644 GIT binary patch delta 511 zcmXw#L2J}N6vyA2WGBfa+07>DLOtn0a0}8y(N+)Yp@M?9JroZjOW)eot-H%4Yi*g> zLZru1m}`;lK|Cmef)}seJxe};-$1-rC(;hg|GoErzc(|?*Y1~v<~R%mn05Wf(evN) zpPSdBR%puF`)+o&fck#4vacG%hfH1}XN0Oz@SNqAvdAr&;w5_sgb$!_l#|^cMjlxw ztOvPWLgDVsDfhIY9EGzB&Ql)Y6yzhrike-69<0G=-iDm-K0_!3!d_^)z7icU#J)i| zJT})9;{qz_F0o%Lid23 Y#a7B9;|beEj{L98bdYXhrl;w>e{B1C8vp7gP)!21y&^CL%J0C2+RM)+lqucrHL;L6p z)V|lbN?BTxCE_`g<(wS>;UQ#P^3f(SGO1Ls2vQ@5%$zPs^Pw!c#K#3^NsDj{NmsMh zob5pbJs7SSkn)RDgiIhzU9Ac!n($9>=<6$)xD*l4?q4y%38W_3*)I9zj@Fqx!o#XgoQE{wF!sVH&D-&{PfIR~>&12deMypis)+7N6xL x9uFyW)XZPK51LwR1zTQQZ?WqJH_%3lRZyJ-E!#FM 1: + amount = amount * valid_months + except Exception as e: + logger.error('直购计费失败 ppid=%s: %s', ppid, e) + raise Exception('计费失败: %s' % e) + else: + raise Exception('存储无定价方案,无法购买(缺 storres_pricing_map 映射)') + + rate = float(discount_rate) if discount_rate not in (None, '') else 1.0 + final_amount = round(amount * rate, 4) + + today = datetime.date.today() + end = today + datetime.timedelta(days=30 * valid_months) + rec = { + 'id': getID(), + 'spec_id': spec_id, + 'meter_mode': 'direct', + 'subscription_id': subscription_id, + 'userid': userid, + 'userorgid': userorgid, + 'use_date': _today(), + 'use_time': _now(), + 'period_start': today.strftime('%Y-%m-%d'), + 'period_end': end.strftime('%Y-%m-%d'), + 'sample_count': 0, + 'avg_gb': storage_gb, + 'max_gb': storage_gb, + 'billable_gb': storage_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 容量=%sGB 有效=%s月 原价=%s 折扣=%s 应收=%s', + spec_id_or_code, storage_gb, valid_months, 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( @@ -349,6 +438,7 @@ def load_storage_resource(): env.record_storage_sample = record_storage_sample env.summarize_samples = summarize_samples env.storage_charging = storage_charging + env.storage_direct_charging = storage_direct_charging env.get_accounting_storres_usages = get_accounting_storres_usages env.mark_storres_accounted = mark_storres_accounted diff --git a/storage_resource/product_interface.py b/storage_resource/product_interface.py index 1bee681..a8e538e 100644 --- a/storage_resource/product_interface.py +++ b/storage_resource/product_interface.py @@ -141,7 +141,9 @@ async def calculate_product_cost(resource_ref_id, usage_data, user_org_id=None): usage_data = usage_data or {} meter_mode = usage_data.get('meter_mode', 'ondemand') - ppid = await get_storage_ppid(getattr(spec, 'id'), meter_mode) + # direct(直购) 复用 package 定价(买断=包月价×月数) + ppid_mode = 'package' if meter_mode == 'direct' else meter_mode + ppid = await get_storage_ppid(getattr(spec, 'id'), ppid_mode) if not ppid: return {'success': False, 'message': '无定价方案(ppid为空)'} @@ -154,6 +156,10 @@ async def calculate_product_cost(resource_ref_id, usage_data, user_org_id=None): return {'success': False, 'message': '定价计算失败: %s' % e} amount = sum(getattr(p, 'amount', 0) for p in prices) + # 直购多个月:单价按 GB月,购买 N 个月按 N 倍计(与 storage_direct_charging 一致) + valid_months = int(usage_data.get('valid_months', 1) or 1) + if meter_mode == 'direct' and valid_months > 1: + amount = amount * valid_months # 资源层只算真实定价(原价)。产品级折扣(客户/分销商/供应商)由产品层 # calculate_product_cost 按 product_id 精确计算,见 product_management.core。