Compare commits

..

No commits in common. "main" and "feat/modern-ui-pricing" have entirely different histories.

34 changed files with 490 additions and 2171 deletions

1
.gitignore vendored
View File

@ -1 +0,0 @@
__pycache__/

252
README.md
View File

@ -331,258 +331,6 @@ llmage (模型管理) ──→ llm.ppid ──→ pricing_program.id
--- ---
## HTTP API 接口
### 获取定价展示数据
```
GET /pricing/api/get_pricing_display.dspy?ppid={ppid}
Authorization: Bearer {token}
```
**说明:** 返回指定定价项目的可读定价数据,供前端展示。自动解析 pricing_data 字段,转换为人类可读的价格表。
**返回示例Token 定价):**
```json
{
"status": "ok",
"data": {
"ppid": "5i1JIpqERgCWqKQ4DCegD",
"name": "通义千问 qwen3.7-max",
"pricing_type": "per_use",
"items": [
{
"filters": {"model": "qwen3.7-max"},
"filter_labels": {"模型": "qwen3.7-max"},
"price_factors": [
{
"factor": "uncache_tokens",
"label": "非缓存tokens",
"unit_price": 6.0,
"unit": "百万",
"unit_label": "元/百万"
}
]
},
{
"filters": {"model": "qwen3.7-max"},
"filter_labels": {"模型": "qwen3.7-max"},
"price_factors": [
{
"factor": "cached_tokens",
"label": "缓存tokens",
"unit_price": 1.2,
"unit": "百万",
"unit_label": "元/百万"
}
]
}
],
"display_text": "【通义千问 qwen3.7-max】定价:\n - 非缓存tokens: 6.0 元/百万\n - 缓存tokens: 1.2 元/百万"
}
}
```
**返回示例(视频生成定价):**
```json
{
"status": "ok",
"data": {
"ppid": "vidu_video_pricing",
"name": "viduq3视频定价",
"pricing_type": "per_use",
"items": [
{
"filters": {"model": "viduq3-turbo", "resolution": "1080p", "off_peak": "0"},
"filter_labels": {"模型": "viduq3-turbo", "分辨率": "1080p", "错峰": "0"},
"price_factors": [
{
"factor": "duration",
"label": "时长",
"unit_price": 0.56,
"unit": "秒",
"unit_label": "元/秒"
}
]
}
],
"display_text": "【viduq3视频定价】定价:\n - 时长: 0.56 元/秒 [model=viduq3-turbo, resolution=1080p, off_peak=0]"
}
}
```
**返回字段说明:**
- `ppid`: 定价项目 ID
- `name`: 定价项目名称
- `pricing_type`: 定价类型(`per_use` 按次计费)
- `items`: 定价项列表
- `filters`: 过滤条件(如适用模型、分辨率等)
- `filter_labels`: 过滤条件的中文标签
- `price_factors`: 计价因子列表
- `factor`: 因子字段名
- `label`: 因子中文名称
- `unit_price`: 单位价格(已转换为展示价,无需再乘)
- `unit`: 单位(百万、秒、次等)
- `unit_label`: 单位标签(元/百万、元/秒等)
- `tiered`: 阶梯定价(仅当价格不同时展示)
- `display_text`: 人类可读的价格表文本
---
## pricing_data 使用指南
### 两种格式
pricing_data 支持两种格式:
#### 1. 新格式推荐price_factors + unit_prices + unit
适用于简单定价场景,直接指定计价因子和单位价格。
**示例 1Token 定价(文本生成)**
```yaml
unit_values:
百万: 1000000
fields:
model:
type: str
role: filter
label: 模型
uncache_tokens:
type: float
role: factor
label: 非缓存tokens
cached_tokens:
type: float
role: factor
label: 缓存tokens
completion_tokens:
type: float
role: factor
label: 输出tokens
pricings:
# 非缓存输入定价
- price_factors: uncache_tokens
unit_prices: 6.0
unit: 百万
filters:
- model: qwen3.7-max
# 缓存输入定价
- price_factors: cached_tokens
unit_prices: 1.2
unit: 百万
filters:
- model: qwen3.7-max
# 输出定价
- price_factors: completion_tokens
unit_prices: 18.0
unit: 百万
filters:
- model: qwen3.7-max
```
**示例 2视频生成定价多维度过滤**
```yaml
unit_values:
次: 1
fields:
model:
type: str
role: filter
label: 模型
resolution:
type: str
role: filter
label: 分辨率
duration:
type: int
role: filter
label: 时长(秒)
value_mode: between
off_peak:
type: str
role: filter
label: 错峰执行
flat:
type: float
role: factor
label: 固定费用
pricings:
- price_factors: flat
unit_prices: 85.0
unit: 次
filters:
- model: viduq2-pro
- resolution: 1080p
- duration: '1'
- off_peak: false
- price_factors: flat
unit_prices: 43.0
unit: 次
filters:
- model: viduq2-pro
- resolution: 1080p
- duration: '1'
- off_peak: true
- price_factors: duration
unit_prices: 0.56
unit: 秒
filters:
- model: viduq3-turbo
- resolution: 1080p
- off_peak: 0
```
#### 2. 旧格式formula公式计算
适用于复杂定价场景,使用 Python 表达式计算金额。
**示例:按 Token 数量计算**
```yaml
fields:
model:
type: str
role: filter
label: 模型
prompt_tokens:
type: float
role: factor
label: 输入tokens
completion_tokens:
type: float
role: factor
label: 输出tokens
pricings:
- model: gpt-4
formula: (3.2 * prompt_tokens + 16 * completion_tokens) / 1000000.0
- model: gpt-3.5
formula: (0.5 * prompt_tokens + 1.5 * completion_tokens) / 1000000.0
```
### 关键规则
1. **unit_values 必须定义**:在 pricing_data 顶部定义 `unit_values`,如 `百万: 1000000`,用于单位换算
2. **unit_prices 存储展示价**:新格式中 `unit_prices` 直接存储展示价格(如 `6.0` 表示 6.0 元/百万),不需要再乘以单位值
3. **filters 支持两种格式**
- 列表格式:`filters: [{model: xxx}, {resolution: yyy}]`
- 字典格式:直接作为字段写在 pricing 条目中
4. **value_mode 默认精确匹配**:不指定时做 `=` 精确匹配,可指定 `between``in``>``<`
5. **formula 可引用任意字段**:旧格式中 formula 可引用 config_data 中的任意字段名
6. **多条件匹配**:如果多条定价规则都匹配,全部返回(不是只返回第一条)
---
## 关键设计要点 ## 关键设计要点
1. **YAML 驱动定价规则**:所有定价逻辑用 YAML 描述,支持字段定义、选项、匹配模式、计算公式 1. **YAML 驱动定价规则**:所有定价逻辑用 YAML 描述,支持字段定义、选项、匹配模式、计算公式

View File

@ -1,49 +0,0 @@
Add Error: Add Error
Add Success: Add Success
Authorization Error: Authorization Error
Cancel: Cancel
Conform: Confirm
Delete Error: Delete Error
Delete Success: Delete Success
Discard: Discard
Please login: Please login
Record no exist or with wrong ownership: Record no exist or with wrong ownership
Reset: Reset
Submit: Submit
Update Error: Update Error
Update Success: Update Success
Validation Failed: Validation Failed
failed: failed
id: id
ok: ok
ppid parameter required: ppid parameter required
上传定价数据: Upload Pricing Data
下载定价数据: Download Pricing Data
价格: Price
供应商: Supplier
供应商折扣: Supplier Discount
名称: Name
启用日期: Effective Date
失效日期: Expiry Date
定价属于: Pricing Belongs To
定价数据: Pricing Data
定价文件: Pricing File
定价时序数据没找到: Pricing time-series data not found
定价模版: Pricing Template
定价测试: Pricing Test
定价管理: Pricing Management
定价细项: Pricing Line Items
定价项目: Pricing Item
定价项目id: Pricing Item ID
定价项目时序: Pricing Item Time Series
所属机构: Organization
折扣不能为空: Discount cannot be empty
折扣不能大于1: Discount cannot be greater than 1
折扣不能小于0: Discount cannot be less than 0
折扣必须是数字: Discount must be a number
描述: Description
测试: Test
规格明细: Specification Details
计费数据(json): Billing Data (JSON)
项目名称: Project Name
验证定价: Validate Pricing

View File

@ -1,49 +0,0 @@
Add Error: 追加エラー
Add Success: 追加成功
Authorization Error: 認証エラー
Cancel: キャンセル
Conform: 確認
Delete Error: 削除エラー
Delete Success: 削除成功
Discard: 破棄
Please login: ログインしてください
Record no exist or with wrong ownership: レコードが存在しないか、所有権が不正です
Reset: リセット
Submit: 送信
Update Error: 更新エラー
Update Success: 更新成功
Validation Failed: バリデーション失敗
failed: 失敗
id: id
ok: ok
ppid parameter required: ppidパラメーターが必要です
上传定价数据: 価格データアップロード
下载定价数据: 価格データダウンロード
价格: 価格
供应商: サプライヤー
供应商折扣: サプライヤー割引
名称: 名称
启用日期: 有効開始日
失效日期: 有効期限
定价属于: 価格所属
定价数据: 価格データ
定价文件: 価格ファイル
定价时序数据没找到: 価格時系列データが見つかりません
定价模版: 価格テンプレート
定价测试: 価格テスト
定价管理: 価格管理
定价细项: 価格明細
定价项目: 価格項目
定价项目id: 価格項目ID
定价项目时序: 価格項目時系列
所属机构: 所属組織
折扣不能为空: 割引は必須です
折扣不能大于1: 割引は1より大きくできません
折扣不能小于0: 割引は0より小さくできません
折扣必须是数字: 割引は数値である必要があります
描述: 説明
测试: テスト
规格明细: 仕様明細
计费数据(json): 課金データ(JSON)
项目名称: プロジェクト名
验证定价: 価格検証

View File

@ -1,49 +0,0 @@
Add Error: 추가 오류
Add Success: 추가 성공
Authorization Error: 인증 오류
Cancel: 취소
Conform: 확인
Delete Error: 삭제 오류
Delete Success: 삭제 성공
Discard: 폐기
Please login: 로그인해주세요
Record no exist or with wrong ownership: 레코드가 존재하지 않거나 소유권이 잘못되었습니다
Reset: 초기화
Submit: 제출
Update Error: 업데이트 오류
Update Success: 업데이트 성공
Validation Failed: 유효성 검사 실패
failed: 실패
id: id
ok: ok
ppid parameter required: ppid 파라미터가 필요합니다
上传定价数据: 가격 데이터 업로드
下载定价数据: 가격 데이터 다운로드
价格: 가격
供应商: 공급업체
供应商折扣: 공급업체 할인
名称: 이름
启用日期: 유효 시작일
失效日期: 만료일
定价属于: 가격 소속
定价数据: 가격 데이터
定价文件: 가격 파일
定价时序数据没找到: 가격 시계열 데이터를 찾을 수 없습니다
定价模版: 가격 템플릿
定价测试: 가격 테스트
定价管理: 가격 관리
定价细项: 가격 세부 항목
定价项目: 가격 항목
定价项目id: 가격 항목 ID
定价项目时序: 가격 항목 시계열
所属机构: 소속 조직
折扣不能为空: 할인은 비워둘 수 없습니다
折扣不能大于1: 할인은 1보다 클 수 없습니다
折扣不能小于0: 할인은 0보다 작을 수 없습니다
折扣必须是数字: 할인은 숫자여야 합니다
描述: 설명
测试: 테스트
规格明细: 규격 상세
计费数据(json): 과금 데이터(JSON)
项目名称: 프로젝트 이름
验证定价: 가격 검증

View File

@ -1,49 +0,0 @@
Add Error: Add Error
Add Success: Add Success
Authorization Error: Authorization Error
Cancel: Cancel
Conform: Conform
Delete Error: Delete Error
Delete Success: Delete Success
Discard: Discard
Please login: Please login
Record no exist or with wrong ownership: Record no exist or with wrong ownership
Reset: Reset
Submit: Submit
Update Error: Update Error
Update Success: Update Success
Validation Failed: Validation Failed
failed: failed
id: id
ok: ok
ppid parameter required: ppid parameter required
上传定价数据: 上传定价数据
下载定价数据: 下载定价数据
价格: 价格
供应商: 供应商
供应商折扣: 供应商折扣
名称: 名称
启用日期: 启用日期
失效日期: 失效日期
定价属于: 定价属于
定价数据: 定价数据
定价文件: 定价文件
定价时序数据没找到: 定价时序数据没找到
定价模版: 定价模版
定价测试: 定价测试
定价管理: 定价管理
定价细项: 定价细项
定价项目: 定价项目
定价项目id: 定价项目id
定价项目时序: 定价项目时序
所属机构: 所属机构
折扣不能为空: 折扣不能为空
折扣不能大于1: 折扣不能大于1
折扣不能小于0: 折扣不能小于0
折扣必须是数字: 折扣必须是数字
描述: 描述
测试: 测试
规格明细: 规格明细
计费数据(json): 计费数据(json)
项目名称: 项目名称
验证定价: 验证定价

View File

@ -7,22 +7,13 @@
"browserfields": { "browserfields": {
"exclouded": ["id", "ownerid", "pricing_spec" ], "exclouded": ["id", "ownerid", "pricing_spec" ],
"alters": { "alters": {
"discount": { "providerid":{
"rules": [ "valueField": "id",
{"type": "required", "message": "折扣不能为空"}, "textField": "orgname",
{"type": "number", "message": "折扣必须是数字"}, "dataurl":"{{entire_url('/rbac/get_provider.dspy')}}"
{"type": "min", "value": 0, "message": "折扣不能小于0"}, }
{"type": "max", "value": 1, "message": "折扣不能大于1"}
]
}
} }
}, },
"data_filter": {
"fields": [
{"field": "name", "title": "项目名称", "uitype": "str"},
{"field": "providerid", "title": "供应商", "uitype": "code"}
]
},
"editexclouded": [ "editexclouded": [
"id", "ownerid" "id", "ownerid"
], ],
@ -54,8 +45,7 @@
"width": "70%", "width": "70%",
"height": "70%", "height": "70%",
"auto_open": true, "auto_open": true,
"archor": "cc", "archor": "cc"
"title": "定价测试"
}, },
"options": { "options": {
"url": "{{entire_url('../test_pricing_program.ui')}}", "url": "{{entire_url('../test_pricing_program.ui')}}",

View File

@ -2,12 +2,13 @@
"tblname": "pricing_program_timing", "tblname": "pricing_program_timing",
"title": "定价项目时序", "title": "定价项目时序",
"params": { "params": {
"logined_userorgid": "ownerid",
"browserfields": { "browserfields": {
"exclouded": ["id", "ppid" ], "exclouded": ["id", "ownerid", "ppid" ],
"alters": {} "alters": {}
}, },
"editexclouded": [ "editexclouded": [
"id", "ppid", "name" "id", "ownerid", "ppid", "name"
], ],
"subtables":[ "subtables":[
{ {
@ -28,16 +29,13 @@
{ {
"name": "upload_pricing_data", "name": "upload_pricing_data",
"label": "上传定价数据", "label": "上传定价数据",
"selected_row": true,
"icon": "{{entire_url('/bricks/imgs/upload.svg')}}" "icon": "{{entire_url('/bricks/imgs/upload.svg')}}"
}, { }, {
"name": "download_pricing_data", "name": "download_pricing_data",
"label": "下载定价数据", "label": "下载定价数据",
"selected_row": true,
"icon": "{{entire_url('/bricks/imgs/download.svg')}}" "icon": "{{entire_url('/bricks/imgs/download.svg')}}"
}, { }, {
"name": "test", "name": "test",
"selected_row": true,
"label": "验证定价", "label": "验证定价",
"icon": "{{entire_url('/bricks/imgs/test.svg')}}" "icon": "{{entire_url('/bricks/imgs/test.svg')}}"
} }
@ -61,10 +59,7 @@
"wid": "self", "wid": "self",
"event": "upload_pricing_data", "event": "upload_pricing_data",
"actiontype": "urlwidget", "actiontype": "urlwidget",
"target": "PopupWindow", "target": "self",
"popup_options": {
"title": "上传定价数据"
},
"options": { "options": {
"params": { "params": {
"ppid": "{{params_kw.ppid}}" "ppid": "{{params_kw.ppid}}"
@ -92,7 +87,6 @@
"actiontype": "urlwidget", "actiontype": "urlwidget",
"target": "PopupWindow", "target": "PopupWindow",
"popup_options": { "popup_options": {
"title": "验证定价"
}, },
"options":{ "options":{
"params": { "params": {

View File

@ -44,8 +44,7 @@
"name": "discount", "name": "discount",
"title": "供应商折扣", "title": "供应商折扣",
"type": "float", "type": "float",
"length": 18, "length": 18
"dec": 2
}, },
{ {
"name": "description", "name": "description",

View File

@ -1,6 +0,0 @@
from pricing.pricing import (
PricingProgram,
test_pricing,
generate_formula_from_factors,
get_pricing_display,
)

View File

@ -3,8 +3,7 @@ from sqlor.dbpools import DBPools
from pricing.pricing import ( from pricing.pricing import (
PricingProgram, PricingProgram,
test_pricing, test_pricing,
generate_formula_from_factors, get_pricing_program
get_pricing_display,
) )
from ahserver.serverenv import ServerEnv from ahserver.serverenv import ServerEnv
@ -28,6 +27,7 @@ def _bind_pricing_events(dbpools, dbname):
def load_pricing(): def load_pricing():
env = ServerEnv() env = ServerEnv()
env.get_pricing_program = get_pricing_program
env.write_pricing_patten = PricingProgram.write_pricing_patten env.write_pricing_patten = PricingProgram.write_pricing_patten
env.write_pricing_data = PricingProgram.write_pricing_data env.write_pricing_data = PricingProgram.write_pricing_data
env.pricing_program_charging = PricingProgram.charging env.pricing_program_charging = PricingProgram.charging
@ -35,11 +35,6 @@ def load_pricing():
env.load_pricing_data = PricingProgram.load_pricing_data env.load_pricing_data = PricingProgram.load_pricing_data
env.get_pricing_program = PricingProgram.get_pricing_program env.get_pricing_program = PricingProgram.get_pricing_program
env.test_pricing = test_pricing env.test_pricing = test_pricing
env.generate_formula_from_factors = generate_formula_from_factors
env.get_pricing_display = get_pricing_display
# Bind hot_reload event — only when running in ahserver (event_dispatcher available)
if getattr(env, 'event_dispatcher', None) is not None:
env.event_dispatcher.bind('hot_reload', PricingProgram.on_hot_reload)
dbpools = DBPools() dbpools = DBPools()
dbname = env.get_module_dbname('pricing') dbname = env.get_module_dbname('pricing')
if dbname: if dbname:
@ -47,3 +42,4 @@ def load_pricing():
debug(f'Pricing event listeners bound for database: {dbname}') debug(f'Pricing event listeners bound for database: {dbname}')
else: else:
debug('Pricing event listeners skipped: no database configured for pricing module') debug('Pricing event listeners skipped: no database configured for pricing module')

View File

@ -6,22 +6,9 @@ from sqlor.dbpools import DBPools, get_sor_context
from appPublic.log import debug, exception, info, MyLogger from appPublic.log import debug, exception, info, MyLogger
from appPublic.timeUtils import curDateString from appPublic.timeUtils import curDateString
from appPublic.dictObject import DictObject from appPublic.dictObject import DictObject
from appPublic.jsonConfig import getConfig
from .write_pattern import write_pattern_xlsx, load_xlsx_pricing from .write_pattern import write_pattern_xlsx, load_xlsx_pricing
import yaml import yaml
def _cache_enabled():
"""Check if cache is enabled for pricing module in config.json"""
try:
config = getConfig()
module_cache = config.module_cache
if module_cache is None:
return True
return getattr(module_cache, 'pricing', True)
except Exception:
return True
""" """
采用yaml描述定价策略 采用yaml描述定价策略
在pricing_program的pricing_spec表中定义定价的数据字段 在pricing_program的pricing_spec表中定义定价的数据字段
@ -128,10 +115,8 @@ def typevalue(v, t):
return v return v
return f(v) return f(v)
def check_value(field, spec_value, data_value, value_mode=None): def check_value(field, spec_value, data_value):
if value_mode is None: if field.value_mode == 'between':
value_mode = field.value_mode
if value_mode == 'between':
arr = spec_value.strip().split() arr = spec_value.strip().split()
if len(arr) < 2 or len(arr) > 3: if len(arr) < 2 or len(arr) > 3:
e = f'{spec_value=} error' e = f'{spec_value=} error'
@ -157,19 +142,17 @@ def check_value(field, spec_value, data_value, value_mode=None):
return arr[0] < fvalue and fvalue < arr[-1] return arr[0] < fvalue and fvalue < arr[-1]
if arr[1] == '~=': if arr[1] == '~=':
return arr[0] < fvalue and fvalue <= arr[-1] return arr[0] < fvalue and fvalue <= arr[-1]
if arr[1] == '=~=': e = f'{arr[1]}不认识的期间逻辑,只支持:~ =~ ~='
return arr[0] <= fvalue and fvalue <= arr[-1]
e = f'{arr[1]}不认识的期间逻辑,只支持:~ =~ ~= =~='
exception(e) exception(e)
raise Exception(e) raise Exception(e)
if value_mode == 'in': if field.value_mode == 'in':
arr = spec_value.strip().split() arr = spec_value.strip().split()
arr = [ typevalue(a, field.type) for a in arr ] arr = [ typevalue(a, field.type) for a in arr ]
# debug(f'{arr=}, {data_value=}') # debug(f'{arr=}, {data_value=}')
return data_value in arr return data_value in arr
mode = value_mode mode = field.value_mode
if not mode or mode == '=': if not mode or mode == '=':
mode = '==' mode = '=='
ns = { ns = {
@ -190,13 +173,6 @@ def data_mapping(ns, name, v):
class PricingProgram: class PricingProgram:
pricing_data = {} pricing_data = {}
@staticmethod
def on_hot_reload(data=None):
"""Event handler for hot_reload event. Clears pricing cache."""
from appPublic.log import debug
debug(f'[pricing] on_hot_reload called, clearing pricing_data (data={data})')
PricingProgram.pricing_data.clear()
@staticmethod @staticmethod
async def get_pricing_program(ppid): async def get_pricing_program(ppid):
env = ServerEnv() env = ServerEnv()
@ -448,10 +424,9 @@ class PricingProgram:
async def get_ppid_pricing(ppid): async def get_ppid_pricing(ppid):
dat = curDateString() dat = curDateString()
k = f'{ppid}.{dat}' k = f'{ppid}.{dat}'
if _cache_enabled(): d = PricingProgram.pricing_data.get(k)
d = PricingProgram.pricing_data.get(k) if d:
if d: return d
return d
env = ServerEnv() env = ServerEnv()
async with get_sor_context(env, 'pricing') as sor: async with get_sor_context(env, 'pricing') as sor:
sql = """select a.name, a.ownerid, a.providerid, sql = """select a.name, a.ownerid, a.providerid,
@ -470,8 +445,7 @@ class PricingProgram:
e = Exception(f'{ppid=},{dat=} data not found') e = Exception(f'{ppid=},{dat=} data not found')
exception(f'{e}') exception(f'{e}')
raise e raise e
d = recs[0] d = recs[0]
if _cache_enabled():
PricingProgram.pricing_data[k] = d PricingProgram.pricing_data[k] = d
dates = PricingProgram.pricing_data.get(ppid, []) dates = PricingProgram.pricing_data.get(ppid, [])
dates.append(dat) dates.append(dat)
@ -482,16 +456,15 @@ class PricingProgram:
PricingProgram.pricing_data[dk] PricingProgram.pricing_data[dk]
dates = dates[-2:] dates = dates[-2:]
PricingProgram.pricing_data[ppid] = dates PricingProgram.pricing_data[ppid] = dates
return d return d
async def buffered_charging(ppid, data): async def buffered_charging(ppid, data):
r = await PricingProgram.get_ppid_pricing(ppid) r = await PricingProgram.get_ppid_pricing(ppid)
prices = PricingProgram.get_pricing_from_ymalstr(data, prices = PricingProgram.get_pricing_from_ymalstr(data,
r.pricing_data) r.pricing_data)
amt = 0.0 amt = 0.0
discount = max(0.0, min(1.0, r.discount)) if r.discount is not None else 1.0
for p in prices: for p in prices:
p.cost = p.amount * discount p.cost = p.amount * r.discount
return prices return prices
async def charging(sor, ppid, data): async def charging(sor, ppid, data):
@ -523,28 +496,20 @@ order by b.enabled_date desc"""
r.pricing_data) r.pricing_data)
debug(f'{r.prices=}') debug(f'{r.prices=}')
amt = 0.0 amt = 0.0
discount = max(0.0, min(1.0, r.discount)) if r.discount is not None else 1.0
for p in r.prices: for p in r.prices:
p.cost = p.amount * discount p.cost = p.amount * r.discount
return r.prices return r.prices
@staticmethod @staticmethod
def get_pricing_from_ymalstr(config_data, yamlstr): def get_pricing_from_ymalstr(config_data, yamlstr):
""" """
解析定价YAML并计算费用 yamlstr是从
支持两种格式
1. 旧格式formula 字段eval计算
2. 新格式price_factors + unit_prices + unit自动计算
支持 derived 字段 fields 中定义 derived 表达式从原始 usage 数据计算衍生字段
例如uncached_prompt_tokens.derived = "prompt_tokens - prompt_tokens_details.cached_tokens"
""" """
if config_data is None: if config_data is None:
e = Exception(f'config_data is None, {yamlstr=}') e = Exception(f'config_data is None, {yamlstr=}')
exception(f'{e=}') exception(f'{e=}')
raise e raise e
# 用 DictObject 包装,支持 dot notation 属性访问derived 表达式依赖此特性) config_data = config_data.copy()
config_data = DictObject(**config_data)
d = None d = None
try: try:
d = yaml.safe_load(yamlstr) d = yaml.safe_load(yamlstr)
@ -558,62 +523,19 @@ order by b.enabled_date desc"""
if not d.pricings: if not d.pricings:
exception(f'{d} has not "pricings"') exception(f'{d} has not "pricings"')
raise Exception(f'定价定义中没有pricing数据') raise Exception(f'定价定义中没有pricing数据')
# 处理 derived 字段:从原始 usage 数据计算衍生字段
# DictObject 支持属性访问嵌套字段,如 prompt_tokens_details.cached_tokens
for field_name, field_def in d.fields.items():
if not isinstance(field_def, dict):
continue
derived_expr = field_def.get('derived')
if not derived_expr:
continue
# eval 环境:用 config_data 本身DictObject 支持 dot notation 属性访问)
eval_env = dict(config_data)
# 将顶层 key 也注入 eval 环境,使 eval 能找到嵌套属性
for k in list(config_data.keys()):
eval_env[k] = config_data[k]
try:
result = eval(derived_expr, {}, eval_env)
config_data[field_name] = result
debug(f'derived field {field_name} = {derived_expr} = {result}')
except Exception as e:
debug(f'derived field {field_name} evaluation failed: {derived_expr}, error: {e}')
config_data[field_name] = 0
# 单位映射表
unit_values = d.get('unit_values', {'百万': 1000000, '': 1, '': 1000, '': 1, '': 1, '毫秒': 0.001, '元/百万tokens': 1000000, '元/total_tokens': 1, '元/times': 1})
ret_items = [] ret_items = []
for i, p in enumerate(d.pricings): for i, p in enumerate(d.pricings):
# 跳过需要人工审核的记录 if not p.formula:
if p.get('_NEEDS_MANUAL_REVIEW'): debug(f'无公式:{p=}')
debug(f'跳过需要人工审核的定价项: {i}')
continue continue
# 判断是旧格式还是新格式
# 新格式要求 price_factors 是标量(str)、unit_prices 是标量(number)
# 生产数据中 price_factors 可能是 list、unit_prices 可能是 dict此时走旧格式 formula
_raw_pf = p.get('price_factors')
_raw_up = p.get('unit_prices')
is_new_format = (_raw_pf is not None and _raw_up is not None
and not isinstance(_raw_pf, list) and not isinstance(_raw_up, dict))
is_old_format = p.get('formula') is not None
if not is_new_format and not is_old_format:
debug(f'无公式也无price_factors{p=}')
continue
p_ok = True p_ok = True
times = 1
unit = 1
ns = DictObject(**config_data) ns = DictObject(**config_data)
for k,spec_value in p.items():
# 检查过滤条件(排除定价计算字段)
skip_keys = {'formula', 'price_factors', 'unit_prices', 'unit', 'min_amount', 'filters', 'pricing_type'}
for k, spec_value in p.items():
if spec_value is None: if spec_value is None:
continue continue
if k in skip_keys: if k == 'formula':
continue continue
f = d.fields.get(k) f = d.fields.get(k)
if not f: if not f:
@ -621,6 +543,8 @@ order by b.enabled_date desc"""
exception(f'{e}') exception(f'{e}')
raise Exception(e) raise Exception(e)
data_value = config_data.get(k) data_value = config_data.get(k)
# p[f'old_{k}'] = data_value
# p[f'mapping_{k}'] = data_mapping(d, k, data_value) #需要mapping的数据转换
data_value = data_mapping(d, k, data_value) data_value = data_mapping(d, k, data_value)
if data_value is None: if data_value is None:
if 'default' in f.keys(): if 'default' in f.keys():
@ -632,338 +556,48 @@ order by b.enabled_date desc"""
try: try:
flg = check_value(f, spec_value, data_value) flg = check_value(f, spec_value, data_value)
if not flg: if not flg:
# 条件不满足
# debug(f'条件不满足:{p=},{spec_value=}, {data_value=}, {k=}')
p_ok = False p_ok = False
break break
except Exception as e: except Exception as e:
msg = f'{p=},{f}: {spec_value=}, {data_value=}' msg = f'{p=},{f}: {spec_value=}, {data_value=}'
exception(f'{e}:{msg}') exception(f'{e}:{msg}')
break break
if p_ok and p.formula:
# 检查 filters 区间条件新格式AND逻辑所有filter_item都要匹配 np = p.copy()
# 带 unit_prices 的 filter_item 是 tiered 定价,跳过(由后面第二块处理)
if p_ok and is_new_format and 'filters' in p:
for filter_item in p['filters']:
if 'unit_prices' in filter_item:
continue # tiered定价项不在此处检查
item_ok = True
item_value_mode = filter_item.get('value_mode')
for fk, fv in filter_item.items():
if fk in ('unit_prices', 'value_mode'):
continue
f = d.fields.get(fk)
if not f:
continue
data_value = config_data.get(fk)
data_value = data_mapping(d, fk, data_value)
if data_value is None:
continue # 数据中没有该键,视为匹配
try:
flg = check_value(f, fv, data_value, item_value_mode)
if not flg:
item_ok = False
break
except Exception as e:
debug(f'filter check error: {e}')
item_ok = False
break
if not item_ok:
p_ok = False
break
if not p_ok:
info(f'{config_data=}, {p=}, mismatched')
continue
np = p.copy()
np.data = config_data
if is_new_format:
# 新格式price_factors + unit_prices + unit
factor_name = p['price_factors']
unit_price = p['unit_prices']
unit_str = p.get('unit', '')
# 处理 filters 中的区间定价(查找匹配的 unit_prices
if 'filters' in p:
for filter_item in p['filters']:
item_value_mode = filter_item.get('value_mode')
for fk, fv in filter_item.items():
if fk in ('unit_prices', 'value_mode'):
continue
f = d.fields.get(fk)
if not f:
continue
data_value = config_data.get(fk)
data_value = data_mapping(d, fk, data_value)
if data_value is None:
continue
try:
flg = check_value(f, fv, data_value, item_value_mode)
if flg and 'unit_prices' in filter_item:
unit_price = filter_item['unit_prices']
except:
pass
# 获取 usage 值
if factor_name == 'flat':
# 固定费用
usage_value = 1
else:
usage_value = config_data.get(factor_name)
if usage_value is None:
debug(f'新格式config_data中缺少{factor_name}')
continue
usage_value = float(usage_value)
# 获取单位值
unit_val = unit_values.get(unit_str, 1)
if isinstance(unit_val, str):
unit_val = float(unit_val)
# 计算金额
amount = unit_price * usage_value / unit_val
# 应用 min_amount
min_amount = p.get('min_amount', 0)
if min_amount and amount < min_amount:
amount = min_amount
np.amount = amount
ret_items.append(np)
elif is_old_format:
# 旧格式formula
formula = p.formula formula = p.formula
if not formula:
e = f'{p} not formula found'
exception(e)
raise Exception(e)
debug(f'{formula=}, {ns=}, {p=}, {d.fields=}') debug(f'{formula=}, {ns=}, {p=}, {d.fields=}')
env_data = DictObject(config_data) np.data = config_data
np.amount = eval(formula, env_data) np.amount = eval(formula, config_data.copy())
ret_items.append(np) ret_items.append(np)
else:
info(f'{config_data=}, {p=}, {d.model_mappings=}, mismatched')
if len(ret_items) == 0: if len(ret_items) == 0:
e = f'{config_data=}{yamlstr=}没有找到合适的定价' e = f'{config_data=}{yamlstr=}没有找到合适的定价'
exception(e) exception(e)
raise Exception(e) raise Exception(e)
return ret_items return ret_items
def generate_formula_from_factors(price_factors): async def get_pricing_program(ppid):
"""从 price_factors 数组自动生成 formula 字符串。 env = ServerEnv()
async with get_sor_context(env, 'pricing') as sor:
price_factors 格式: recs = await sor.R('pricing_program', {'id': ppid})
[ if len(recs) == 0:
{"factor": "prompt_tokens", "unit_price": 3.2, "unit_label": "元/百万Token"}, exception(f'{ppid} not found in pricing_program_timing')
{"factor": "completion_tokens", "unit_price": 16.0, "unit_label": "元/百万Token"} return None
] pp = recs[0]
返回: "(3.2 * float(prompt_tokens) + 16.0 * float(completion_tokens)) / 1000000.0" if ppt.pricing_data is None:
""" exception(f'{ppid} pricing_data is None in pricing_program_timing')
if not price_factors: return None
return None try:
parts = [] PricingProgram.pp_db2app(ppt)
has_million = False except Exception as e:
for f in price_factors: return None
factor = f.get('factor', '')
unit_price = f.get('unit_price', 0)
unit_label = f.get('unit_label', '')
# 判断单位是否是"百万"级别(元/百万Token 等)
if '百万' in unit_label:
has_million = True
parts.append(f'{unit_price} * float({factor})')
formula = ' + '.join(parts)
if has_million:
formula = f'({formula}) / 1000000.0'
return formula
async def get_pricing_display(ppid):
"""获取定价项目的可读定价数据,供客户/前端展示。
返回结构:
{
"ppid": "...",
"name": "...",
"pricing_type": "per_use",
"items": [
{
"filters": {"model": "doubao-seed-2-0-pro"},
"filter_labels": {"模型": "doubao-seed-2-0-pro"},
"price_factors": [
{
"factor": "prompt_tokens",
"label": "输入Token",
"unit_price": 0.000006,
"unit": "百万",
"unit_label": "元/百万Token"
}
],
"formula": "...",
"min_amount": 0.01
}
]
}
"""
try:
r = await PricingProgram.get_ppid_pricing(ppid)
except Exception:
return None
pricing_data_str = r.pricing_data
d = yaml.safe_load(pricing_data_str)
if not d:
raise Exception(f'{ppid} pricing_data 为空')
fields = d.get('fields', {})
pricings = d.get('pricings', [])
pricing_type = d.get('pricing_type', 'per_use')
unit_values = d.get('unit_values', {'百万': 1000000, '': 1, '': 1000, '': 1, '': 1, '毫秒': 0.001, '元/百万tokens': 1000000, '元/total_tokens': 1, '元/times': 1})
items = []
for p in pricings:
# 跳过需要人工审核的记录
if p.get('_NEEDS_MANUAL_REVIEW'):
continue
# 提取过滤条件role=filter 的字段)
filters = {}
filter_labels = {}
skip_keys = {'formula', 'price_factors', 'unit_prices', 'unit', 'filters', 'min_amount', 'pricing_type', 'name'}
# 先从 p.items() 中提取(直接字段)
for k, v in p.items():
if k in skip_keys:
continue
fdef = fields.get(k, {})
role = fdef.get('role', 'filter') if isinstance(fdef, dict) else 'filter'
if role == 'filter':
filters[k] = v
label = fdef.get('label', k) if isinstance(fdef, dict) else k
filter_labels[label] = v
# 再从 p['filters'] 列表中提取(如果存在)
raw_filters = p.get('filters')
if isinstance(raw_filters, list):
for fi in raw_filters:
if isinstance(fi, dict):
for k, v in fi.items():
fdef = fields.get(k, {})
filters[k] = v
label = fdef.get('label', k) if isinstance(fdef, dict) else k
filter_labels[label] = v
# 新格式price_factors(scalar) + unit_prices(scalar) + unit
# 注意:生产数据中 price_factors 可能是 list、unit_prices 可能是 dict混合格式
# 此时应走旧格式 formula 路径
_raw_pf = p.get('price_factors')
_raw_up = p.get('unit_prices')
is_new_format = (_raw_pf is not None and _raw_up is not None
and not isinstance(_raw_pf, list) and not isinstance(_raw_up, dict))
if is_new_format:
factor_name = _raw_pf
unit_price = _raw_up
unit_str = p.get('unit', '')
# 获取 factor label
fdef = fields.get(factor_name, {})
factor_label = fdef.get('label', factor_name) if isinstance(fdef, dict) else factor_name
# 新格式 unit_prices 已是展示价(如 6.0 元/百万),无需再乘 unit_val
display_price = unit_price
# 构建 unit_label (元/单位)
unit_label = f"元/{unit_str}" if unit_str else ""
price_factors_display = [{
'factor': factor_name,
'label': factor_label,
'unit_price': display_price,
'unit': unit_str,
'unit_label': unit_label
}]
# 处理 filters: 提取适用条件(model)到item级区间条件放tiered
# 注意filters 已在上方提取(支持 dict 和 list 两种格式)
# tiered 仅用于价格不同的阶梯定价
tiered_pricing = []
if isinstance(p.get('filters'), list):
for fi in p['filters']:
if not isinstance(fi, dict):
continue
raw_tier_price = fi.get('unit_prices')
if raw_tier_price is None or raw_tier_price == unit_price:
continue
fi_copy = {k: v for k, v in fi.items()
if k not in ('unit_prices', 'value_mode')
and not k.endswith('_tokens')}
if fi_copy:
tiered_pricing.append({
'filters': fi_copy,
'unit_prices': raw_tier_price
})
item = {'price_factors': price_factors_display}
if filters:
item['filters'] = filters
item['filter_labels'] = filter_labels
min_amount = p.get('min_amount', 0)
if min_amount:
item['min_amount'] = min_amount
if tiered_pricing:
item['price_factors'][0]['tiered'] = tiered_pricing
items.append(item)
else:
# 旧格式formula
price_factors = p.get('price_factors', None)
if not price_factors:
# fallback: 从 fields 中构建展示信息
price_factors = []
for k, fdef in fields.items():
if not isinstance(fdef, dict):
continue
role = fdef.get('role', 'filter')
if role == 'factor' or fdef.get('type') == 'float':
label = fdef.get('label', k)
price_factors.append({
'factor': k,
'label': label,
'unit_price': None,
'unit_label': ''
})
item = {'price_factors': price_factors}
if filters:
item['filters'] = filters
item['filter_labels'] = filter_labels
if p.get('formula'):
item['formula'] = p['formula']
items.append(item)
# 生成可读价格表
display_lines = [f"{getattr(r, 'name', '')}】定价:"]
for item in items:
# 构建过滤条件文本
filter_text = ''
if item.get('filter_labels'):
filter_parts = [f"{k}={v}" for k, v in item['filter_labels'].items()]
filter_text = f" [{', '.join(filter_parts)}]"
for pf in item.get('price_factors', []):
label = pf.get('label', pf.get('factor', ''))
up = pf.get('unit_price')
ul = pf.get('unit_label', '')
if up is not None:
display_lines.append(f" - {label}: {up} {ul}{filter_text}")
if pf.get('tiered'):
for t in pf['tiered']:
t_filters = ', '.join(f"{k}={v}" for k, v in t.get('filters', {}).items())
t_price = t.get('unit_prices', '')
display_lines.append(f" · {t_filters}: {t_price} {ul}")
return {
'ppid': ppid,
'name': getattr(r, 'name', ''),
'pricing_type': pricing_type,
'items': items,
'display_text': '\n'.join(display_lines)
}
async def get_pricing_program_timeing(pptid): async def get_pricing_program_timeing(pptid):
env = ServerEnv() env = ServerEnv()
@ -983,10 +617,8 @@ async def get_pricing_program_timeing(pptid):
return ppt return ppt
async def test_pricing(pptid, data): async def test_pricing(pptid, data):
ppt = await get_pricing_program_timeing(pptid) ppt = get_pricing_program_timeing(pptid)
# ppt.pricing_data 已被 ppt_db2app 解析为 dict需要转回 YAML 字符串 prices = PricingProgram.get_pricing_from_ymalstr(data, ppt.pricing_data)
yamlstr = yaml.dump(ppt.pricing_data, allow_unicode=True) if isinstance(ppt.pricing_data, dict) else ppt.pricing_data
prices = PricingProgram.get_pricing_from_ymalstr(data, yamlstr)
if prices is None: if prices is None:
return None return None
amount = 0 amount = 0

View File

@ -1,115 +0,0 @@
#!/usr/bin/env python3
"""
pricing 模块 RBAC 权限管理脚本
使用方法:
cd ~/repos/sage
./py3/bin/python ~/repos/pricing/scripts/load_path.py
"""
import subprocess
import os
import sys
def find_sage_root():
candidates = [
os.path.expanduser("~/repos/sage"),
os.path.expanduser("~/sage"),
os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))),
]
for c in candidates:
if os.path.isdir(os.path.join(c, "py3")) and os.path.isdir(os.path.join(c, "wwwroot")):
return c
return None
SAGE_ROOT = find_sage_root()
if not SAGE_ROOT:
print("ERROR: Cannot find Sage root directory")
sys.exit(1)
PYTHON = os.path.join(SAGE_ROOT, "py3", "bin", "python")
SET_PERM_SCRIPT = os.path.join(SAGE_ROOT, "set_role_perm.py")
MOD = "pricing"
# ============================================================
# 权限路径定义(逐条列举,禁止通配符)
# ============================================================
# any — 无需登录
PATHS_ANY = [
f"/{MOD}/menu.ui",
]
# logined — 所有已登录用户
PATHS_LOGINED = [
# 模块入口
f"/{MOD}",
f"/{MOD}/index.ui",
# 顶层 .ui 页面
f"/{MOD}/load_pricing_data.ui",
f"/{MOD}/pricing_test.ui",
f"/{MOD}/test_pricing_program.ui",
# 顶层 .dspy 文件
f"/{MOD}/download_pricing_data.dspy",
f"/{MOD}/download_pricing_pattern.dspy",
f"/{MOD}/get_all_pricing_programs.dspy",
f"/{MOD}/get_platform_providers.dspy",
f"/{MOD}/test_pricing_program.dspy",
f"/{MOD}/upload_pricing_data.dspy",
# api/ .dspy 文件
f"/{MOD}/api/get_pricing_display.dspy",
# CRUD: pricing_program (auto-generated by xls2ui)
f"/{MOD}/pricing_program",
f"/{MOD}/pricing_program/index.ui",
f"/{MOD}/pricing_program/get_pricing_program.dspy",
f"/{MOD}/pricing_program/add_pricing_program.dspy",
f"/{MOD}/pricing_program/update_pricing_program.dspy",
f"/{MOD}/pricing_program/delete_pricing_program.dspy",
# CRUD: pricing_program_timing (auto-generated by xls2ui)
f"/{MOD}/pricing_program_timing",
f"/{MOD}/pricing_program_timing/index.ui",
f"/{MOD}/pricing_program_timing/get_pricing_program_timing.dspy",
f"/{MOD}/pricing_program_timing/add_pricing_program_timing.dspy",
f"/{MOD}/pricing_program_timing/update_pricing_program_timing.dspy",
f"/{MOD}/pricing_program_timing/delete_pricing_program_timing.dspy",
]
# ============================================================
# 执行注册
# ============================================================
def run_set_perm(role, path):
cmd = [PYTHON, SET_PERM_SCRIPT, role, path]
result = subprocess.run(cmd, capture_output=True, text=True)
return result.returncode == 0
def register_role_paths(role, paths):
count = 0
for p in paths:
if run_set_perm(role, p):
count += 1
print(f" {role}: {count}/{len(paths)} paths registered")
return count
def main():
print(f"Sage root: {SAGE_ROOT}")
total = 0
total += register_role_paths("any", PATHS_ANY)
total += register_role_paths("logined", PATHS_LOGINED)
print(f"\nDone. Total {total} permission entries registered.")
print("NOTE: Restart Sage after permission changes to reload RBAC cache.")
if __name__ == "__main__":
main()

View File

@ -1,10 +0,0 @@
ppid = params_kw.get('ppid')
if not ppid:
return json.dumps({"status": "error", "message": "ppid parameter required"}, ensure_ascii=False)
try:
result = await get_pricing_display(ppid)
return json.dumps({"status": "ok", "data": result}, ensure_ascii=False, default=str)
except Exception as e:
exception(f'get_pricing_display({ppid}) failed: {e}\n{format_exc()}')
return json.dumps({"status": "error", "message": str(e)}, ensure_ascii=False)

View File

@ -3,7 +3,8 @@
"options": { "options": {
"width": "100%", "width": "100%",
"height": "100%", "height": "100%",
"padding": "0" "padding": "0",
"bgcolor": "#0B1120"
}, },
"subwidgets": [ "subwidgets": [
{ {
@ -17,7 +18,9 @@
{ {
"widgettype": "Title2", "widgettype": "Title2",
"options": { "options": {
"text": "定价管理" "text": "定价管理",
"color": "#F1F5F9",
"fontWeight": "700"
} }
}, },
{ {
@ -27,77 +30,70 @@
"widgettype": "Text", "widgettype": "Text",
"options": { "options": {
"text": "模型定价项目与计费规则配置", "text": "模型定价项目与计费规则配置",
"cfontsize": 1.2 "fontSize": "14px",
"color": "#64748B"
} }
} }
] ]
}, },
{ {
"widgettype": "VScrollPanel", "widgettype": "VBox",
"options": { "options": {
"css": "filler" "bgcolor": "#1E293B",
"padding": "24px",
"borderRadius": "12px",
"border": "1px solid #334155",
"cursor": "pointer"
}, },
"binds": [
{
"wid": "self",
"event": "click",
"actiontype": "urlwidget",
"target": "app.pricing_content",
"options": {
"url": "{{entire_url('/pricing/pricing_program')}}"
},
"mode": "replace"
}
],
"subwidgets": [ "subwidgets": [
{ {
"widgettype": "VBox", "widgettype": "Svg",
"options": { "options": {
"spacing": 24 "svg": "<svg width=\"36\" height=\"36\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"#F59E0B\" stroke-width=\"1.5\"><path d=\"M12 6v12m-3-2.818l.879.659c1.171.879 3.07.879 4.242 0 1.172-.879 1.172-2.303 0-3.182C13.536 12.219 12.768 12 12 12c-.725 0-1.45-.22-2.003-.659-1.106-.879-1.106-2.303 0-3.182s2.9-.879 4.006 0l.415.33M21 12a9 9 0 11-18 0 9 9 0 0118 0z\"/></svg>",
}, "width": "36px",
"subwidgets": [ "height": "36px",
{ "marginBottom": "16px"
"widgettype": "ResponsableBox", }
"options": {
"gap": "16px",
"minWidth": "300px"
},
"subwidgets": [
{
"widgettype": "VBox",
"options": {
"css": "card",
"cwidth": 25,
"padding": "16px",
"cursor": "pointer"
},
"binds": [
{
"wid": "self",
"event": "click",
"actiontype": "urlwidget",
"target": "app.pricing_content",
"options": {
"url": "{{entire_url('/pricing/pricing_program')}}"
},
"mode": "replace"
}
],
"subwidgets": [
{
"widgettype": "Title4",
"options": {
"text": "定价项目管理",
"marginBottom": "8px"
}
},
{
"widgettype": "Text",
"options": {
"text": "管理模型定价规则、计费项目和定时任务",
"cfontsize": 1.2
}
}
]
}
]
}
]
}, },
{ {
"widgettype": "VBox", "widgettype": "Title4",
"id": "pricing_content" "options": {
"text": "定价项目管理",
"color": "#F1F5F9",
"fontWeight": "600",
"marginBottom": "8px"
}
},
{
"widgettype": "Text",
"options": {
"text": "管理模型定价规则、计费项目和定时任务",
"fontSize": "14px",
"color": "#94A3B8"
}
} }
] ]
},
{
"widgettype": "VBox",
"id": "pricing_content",
"css": "filler",
"options": {
"width": "100%",
"overflowY": "auto"
}
} }
] ]
} }

View File

@ -4,7 +4,6 @@
"options": { "options": {
"title": "{{pp.id}}", "title": "{{pp.id}}",
"description": {{json.dumps(pp.description)}}, "description": {{json.dumps(pp.description)}},
"height": "200%",
"fields":[ "fields":[
{ {
"name": "xlsx_file", "name": "xlsx_file",

View File

@ -6,7 +6,7 @@
{ {
"name": "pricing", "name": "pricing",
"label": "定价管理", "label": "定价管理",
"url": "{{entire_url('/pricing/pricing_program')}}" "url": "{{entire_url('/pricing/index.ui')}}"
} }
] ]
} }

View File

@ -0,0 +1 @@
return await get_pricing_specs_by_pptid(params_kw.pptid)

View File

@ -8,16 +8,18 @@ if not id or len(id) > 32:
id = uuid() id = uuid()
ns['id'] = id ns['id'] = id
db = DBPools() db = DBPools()
dbname = get_module_dbname('pricing') dbname = get_module_dbname('pricing')
async with db.sqlorContext(dbname) as sor: async with db.sqlorContext(dbname) as sor:
r = await sor.C('pricing_program_timing', ns.copy()) fs = await sor_get_spec_fields(sor, ns.id)
if len(fs):
ns1 = {k:ns[k] for k in fs}
ns.spec_value = json.dumps(ns1)
r = await sor.C('pricing_item', ns.copy())
return { return {
"widgettype":"Message", "widgettype":"Message",
"options":{ "options":{
"user_data":ns,
"cwidth":16, "cwidth":16,
"cheight":9, "cheight":9,
"title":"Add Success", "title":"Add Success",
@ -35,4 +37,4 @@ return {
"timeout":3, "timeout":3,
"message":"failed" "message":"failed"
} }
} }

View File

@ -7,7 +7,7 @@ ns = {
db = DBPools() db = DBPools()
dbname = get_module_dbname('pricing') dbname = get_module_dbname('pricing')
async with db.sqlorContext(dbname) as sor: async with db.sqlorContext(dbname) as sor:
r = await sor.D('pricing_program_timing', ns) r = await sor.D('pricing_item', ns)
debug('delete success'); debug('delete success');
return { return {
"widgettype":"Message", "widgettype":"Message",

View File

@ -0,0 +1,118 @@
ns = params_kw.copy()
debug(f'get_pricing_item.dspy:{ns=}')
if not ns.get('page'):
ns['page'] = 1
if not ns.get('sort'):
ns['sort'] = 'name'
sql = '''select a.*, b.pptid_text, c.psid_text, d.subppid_text
from (select * from pricing_item where 1=1 [[filterstr]]) a left join (select id as pptid,
id as pptid_text from pricing_program_timing where 1 = 1) b on a.pptid = b.pptid left join (select id as psid,
name as psid_text from pricing_spec where 1 = 1) c on a.psid = c.psid left join (select id as subppid,
name as subppid_text from pricing_program where 1 = 1) d on a.subppid = d.subppid'''
filterjson = params_kw.get('data_filter')
if not filterjson:
fields = [ f['name'] for f in [
{
"name": "id",
"title": "id",
"type": "str",
"length": 32
},
{
"name": "name",
"title": "定价项名",
"type": "str",
"length": 256
},
{
"name": "description",
"title": "描述",
"type": "text"
},
{
"name": "pptid",
"title": "项目id",
"type": "str",
"length": 32
},
{
"name": "psid",
"title": "定价规格id",
"type": "str",
"length": 32
},
{
"name": "subppid",
"title": "子项目id",
"type": "str",
"length": 32,
"nullable": "yes"
},
{
"name": "spec_value",
"title": "规格值",
"type": "text"
},
{
"name": "pricing_unit",
"title": "定价单位",
"type": "float",
"length": 18,
"dec": 5
},
{
"name": "pricing_amount",
"title": "定价金额",
"type": "float",
"length": 18,
"dec": 5
},
{
"name": "cost_amount",
"title": "成本金额",
"type": "float",
"length": 18,
"dec": 5
}
] ]
filterjson = default_filterjson(fields, ns)
filterdic = ns.copy()
filterdic['filterstr'] = ''
filterdic['userorgid'] = '${userorgid}$'
filterdic['userid'] = '${userid}$'
if filterjson:
dbf = DBFilter(filterjson)
conds = dbf.gen(ns)
if conds:
ns.update(dbf.consts)
conds = f' and {conds}'
filterdic['filterstr'] = conds
ac = ArgsConvert('[[', ']]')
vars = ac.findAllVariables(sql)
NameSpace = {v:'${' + v + '}$' for v in vars if v != 'filterstr' }
filterdic.update(NameSpace)
sql = ac.convert(sql, filterdic)
debug(f'{sql=}')
db = DBPools()
dbname = get_module_dbname('pricing')
async with db.sqlorContext(dbname) as sor:
r = await sor.sqlPaging(sql, ns)
for d in r['rows']:
if d.spec_value:
ad = json.loads(d.spec_value)
d.update(ad)
return r
return {
"total":0,
"rows":[]
}

View File

@ -0,0 +1,6 @@
id = params_kw.id
async with get_sor_context(request._run_ns, 'pricing') as sor:
recs = await sor.R('pricing_spec', {'id': id})
if len(recs):
return recs.spec_names
return []

View File

@ -0,0 +1,189 @@
{
"id":"pricing_item_tbl",
"widgettype":"Tabular",
"options":{
"width":"100%",
"height":"100%",
"title":"项目定价项",
"css":"card",
"editable":{
"new_data_url":"{{entire_url('add_pricing_item.dspy')}}",
"delete_data_url":"{{entire_url('delete_pricing_item.dspy')}}",
"update_data_url":"{{entire_url('update_pricing_item.dspy')}}"
},
"data_url":"{{entire_url('./get_pricing_item.dspy')}}",
"data_method":"GET",
"data_params":{{json.dumps(params_kw, indent=4, ensure_ascii=False)}},
"row_options":{
"browserfields": {
"exclouded": [
"id",
"pptid"
],
"alters": {
"psid": {
"dataurl": "{{entire_url('../pi_get_all_specs.dspy')}}",
"textField": "name",
"valueField": "id",
"params": {
"pptid": "{{params_kw.pptid}}"
}
}
}
},
"editexclouded":[
"id",
"pptid"
],
"fields":[
{
"name": "id",
"title": "id",
"type": "str",
"length": 32,
"cwidth": 18,
"uitype": "str",
"datatype": "str",
"label": "id"
},
{
"name": "name",
"title": "定价项名",
"type": "str",
"length": 256,
"cwidth": 18,
"uitype": "str",
"datatype": "str",
"label": "定价项名"
},
{
"name": "description",
"title": "描述",
"type": "text",
"length": 0,
"uitype": "text",
"datatype": "text",
"label": "描述"
},
{
"name": "pptid",
"title": "项目id",
"type": "str",
"length": 32,
"label": "项目id",
"uitype": "code",
"valueField": "pptid",
"textField": "pptid_text",
"params": {
"dbname": "{{get_module_dbname('pricing')}}",
"table": "pricing_program_timing",
"tblvalue": "id",
"tbltext": "id",
"valueField": "pptid",
"textField": "pptid_text"
},
"dataurl": "{{entire_url('/appbase/get_code.dspy')}}"
},
{
"name": "psid",
"title": "定价规格id",
"type": "str",
"length": 32,
"label": "定价规格id",
"uitype": "code",
"valueField": "id",
"textField": "name",
"params": {
"pptid": "{{params_kw.pptid}}"
},
"dataurl": "{{entire_url('../pi_get_all_specs.dspy')}}"
},
{
"name": "subppid",
"title": "子项目id",
"type": "str",
"length": 32,
"nullable": "yes",
"label": "子项目id",
"uitype": "code",
"valueField": "subppid",
"textField": "subppid_text",
"params": {
"dbname": "{{get_module_dbname('pricing')}}",
"table": "pricing_program",
"tblvalue": "id",
"tbltext": "name",
"valueField": "subppid",
"textField": "subppid_text"
},
"dataurl": "{{entire_url('/appbase/get_code.dspy')}}"
},
{
"name": "pricing_unit",
"title": "定价单位",
"type": "float",
"length": 18,
"dec": 5,
"cwidth": 18,
"uitype": "float",
"datatype": "float",
"label": "定价单位"
},
{
"name": "pricing_amount",
"title": "定价金额",
"type": "float",
"length": 18,
"dec": 5,
"cwidth": 18,
"uitype": "float",
"datatype": "float",
"label": "定价金额"
},
{
"name": "cost_amount",
"title": "成本金额",
"type": "float",
"length": 18,
"dec": 5,
"cwidth": 18,
"uitype": "float",
"datatype": "float",
"label": "成本金额"
}
{{get_all_spec_fields_by_pptid(request, params_kw.pptid)}}
]
},
"page_rows":160,
"cache_limit":5
}
,"binds":[
{
"wid":"psid",
"event":"changed",
"actiontype":"script",
"target":"self",
"script":"this.hide_field(params.psid);"
}
]
}

View File

@ -7,13 +7,21 @@ for k,v in ns.items():
db = DBPools() db = DBPools()
dbname = get_module_dbname('pricing') dbname = get_module_dbname('pricing')
async with db.sqlorContext(dbname) as sor: async with db.sqlorContext(dbname) as sor:
ori = await sor.R('pricing_item', {'id': ns.id})
r = await sor.U('pricing_program_timing', ns) ori_sv = {}
debug('update success'); if ori.spec_value:
ori_sv = json.loads(ori.spec_value)
fs = await sor_get_spec_fields(sor, ns.id)
if len(fs):
new_sv = {k:ns[k] for k in fs}
if new_sv:
ori_sv.update(new_sv)
ns.spec_value = json.dumps(ori_sv, indent=4)
r = await sor.U('pricing_item', ns.copy())
debug('update success, {ns=}');
return { return {
"widgettype":"Message", "widgettype":"Message",
"options":{ "options":{
@ -34,4 +42,4 @@ return {
"timeout":3, "timeout":3,
"message":"failed" "message":"failed"
} }
} }

View File

@ -1,99 +0,0 @@
ns = params_kw.copy()
for k,v in ns.items():
if v == 'NaN' or v == 'null':
ns[k] = None
id = params_kw.id
if not id or len(id) > 32:
id = uuid()
ns['id'] = id
userorgid = await get_userorgid()
if not userorgid:
return {
"widgettype":"Error",
"options":{
"title":"Authorization Error",
"timeout":3,
"cwidth":16,
"cheight":9,
"message":"Please login"
}
}
ns['ownerid'] = userorgid
_validation_rules = json.loads(r'''{"discount": [{"type": "required", "message": "折扣不能为空"}, {"type": "number", "message": "折扣必须是数字"}, {"type": "min", "value": 0, "message": "折扣不能小于0"}, {"type": "max", "value": 1, "message": "折扣不能大于1"}]}''')
import re as _re
_errors = []
for _fname, _rules in _validation_rules.items():
_val = params_kw.get(_fname, '')
if _val is None: _val = ''
_val = str(_val)
for _rule in _rules:
_rt = _rule.get('type', '')
_rm = _rule.get('message', _fname)
_rv = _rule.get('value')
if _rt == 'required':
if not _val or _val.strip() == '':
_errors.append(_rm)
break
elif _rt == 'minlength':
if _val and len(_val) < int(_rv):
_errors.append(_rm)
break
elif _rt == 'maxlength':
if len(_val) > int(_rv):
_errors.append(_rm)
break
elif _rt in ('min', 'max'):
if _val:
try:
_n = float(_val)
if _rt == 'min' and _n < float(_rv): _errors.append(_rm); break
if _rt == 'max' and _n > float(_rv): _errors.append(_rm); break
except (ValueError, TypeError):
_errors.append(_rm)
break
elif _rt == 'pattern':
if _val and not _re.match(_rv, _val):
_errors.append(_rm)
break
elif _rt == 'email':
if _val and not _re.match(r'^[^\s@]+@[^\s@]+\.[^\s@]+$', _val):
_errors.append(_rm)
break
elif _rt == 'number':
if _val:
try: float(_val)
except (ValueError, TypeError): _errors.append(_rm); break
if _errors:
return {"widgettype":"Error","options":{"title":"Validation Failed","cwidth":16,"cheight":9,"timeout":3,"message":"; ".join(_errors)}}
db = DBPools()
dbname = get_module_dbname('pricing')
async with db.sqlorContext(dbname) as sor:
r = await sor.C('pricing_program', ns.copy())
return {
"widgettype":"Message",
"options":{
"cwidth":16,
"cheight":9,
"title":"Add Success",
"timeout":3,
"message":"ok"
}
}
return {
"widgettype":"Error",
"options":{
"title":"Add Error",
"cwidth":16,
"cheight":9,
"timeout":3,
"message":"failed"
}
}

View File

@ -1,47 +0,0 @@
ns = {
'id':params_kw['id'],
}
userorgid = await get_userorgid()
if not userorgid:
return {
"widgettype":"Error",
"options":{
"title":"Authorization Error",
"timeout":3,
"cwidth":16,
"cheight":9,
"message":"Please login"
}
}
ns['ownerid'] = userorgid
db = DBPools()
dbname = get_module_dbname('pricing')
async with db.sqlorContext(dbname) as sor:
r = await sor.D('pricing_program', ns)
debug('delete success');
return {
"widgettype":"Message",
"options":{
"title":"Delete Success",
"timeout":3,
"cwidth":16,
"cheight":9,
"message":"ok"
}
}
debug('Delete failed');
return {
"widgettype":"Error",
"options":{
"title":"Delete Error",
"timeout":3,
"cwidth":16,
"cheight":9,
"message":"failed"
}
}

View File

@ -1,135 +0,0 @@
ns = params_kw.copy()
userorgid = await get_userorgid()
if not userorgid:
return {
"widgettype":"Error",
"options":{
"title":"Authorization Error",
"timeout":3,
"cwidth":16,
"cheight":9,
"message":"Please login"
}
}
ns['ownerid'] = userorgid
ns['userorgid'] = userorgid
debug(f'get_pricing_program.dspy:{ns=}')
if not ns.get('page'):
ns['page'] = 1
if not ns.get('sort'):
ns['sort'] = 'name'
sql = '''select a.*, b.ownerid_text, c.providerid_text, d.pricing_belong_text
from (select * from pricing_program where 1=1 [[filterstr]]) a left join (select id as ownerid,
orgname as ownerid_text from organization where 1 = 1) b on a.ownerid = b.ownerid left join (select id as providerid,
orgname as providerid_text from organization where 1 = 1) c on a.providerid = c.providerid left join (select k as pricing_belong,
v as pricing_belong_text from appcodes_kv where parentid='pricing_belong') d on a.pricing_belong = d.pricing_belong'''
filterjson = params_kw.get('data_filter')
if filterjson and isinstance(filterjson, str):
try:
filterjson = json.loads(filterjson)
except (json.JSONDecodeError, TypeError):
filterjson = None
# data_filter可能是CRUD字段定义({"fields":[...]}),不是过滤条件,忽略
if filterjson and isinstance(filterjson, dict) and 'fields' in filterjson:
filterjson = None
fields_str=r'''[
{
"name": "id",
"title": "id",
"type": "str",
"length": 32
},
{
"name": "name",
"title": "项目名称",
"type": "str",
"length": 256
},
{
"name": "ownerid",
"title": "所属机构",
"type": "str",
"length": 32
},
{
"name": "providerid",
"title": "供应商",
"type": "str",
"length": 32
},
{
"name": "pricing_belong",
"title": "定价属于",
"type": "str",
"length": 32
},
{
"name": "discount",
"title": "供应商折扣",
"type": "float",
"length": 18,
"dec": 2
},
{
"name": "description",
"title": "描述",
"type": "text"
},
{
"name": "pricing_spec",
"title": "规格明细",
"type": "text"
}
]'''
ori_fields = json.loads(fields_str)
if not filterjson:
fields = [ f['name'] for f in ori_fields ]
filterjson = default_filterjson(fields, ns)
# 确保 logined 过滤条件始终生效
if filterjson:
if not isinstance(filterjson, dict) or 'AND' not in filterjson:
filterjson = {'AND': [filterjson] if filterjson else []}
filterjson['AND'].append({'field': 'ownerid', 'op': '=', 'var': '__logined_orgid__'})
ns['__logined_orgid__'] = userorgid
filterdic = ns.copy()
filterdic['filterstr'] = ''
filterdic['userorgid'] = '${userorgid}$'
filterdic['userid'] = '${userid}$'
if filterjson:
dbf = DBFilter(filterjson)
conds = dbf.gen(ns)
if conds:
ns.update(dbf.consts)
conds = f' and {conds}'
filterdic['filterstr'] = conds
ac = ArgsConvert('[[', ']]')
vars = ac.findAllVariables(sql)
NameSpace = {v:'${' + v + '}$' for v in vars if v != 'filterstr' }
filterdic.update(NameSpace)
sql = ac.convert(sql, filterdic)
debug(f'{sql=}')
db = DBPools()
dbname = get_module_dbname('pricing')
async with db.sqlorContext(dbname) as sor:
r = await sor.sqlPaging(sql, ns)
return r
return {
"total":0,
"rows":[]
}

View File

@ -1,289 +0,0 @@
{
"widgettype":"VBox",
"options":{"cheight":40,"width":"100%"},
"subwidgets":[{
"id":"pricing_program_tbl",
"widgettype":"Tabular",
"options":{
"width":"100%",
"height":"100%",
"title":"定价项目",
"toolbar":{
"tools": [
{
"name": "test",
"label": "测试",
"selected_row": true,
"icon": "{{entire_url('/bricks/imgs/test.svg')}}"
},
{
"selected_row": true,
"name": "pricing_program_timing",
"icon": "{{entire_url('/imgs/pricing_program_timing.svg')}}",
"label": "定价项目时序"
}
]
},
"css":"card",
"editable":{
"new_data_url":"{{entire_url('add_pricing_program.dspy')}}",
"delete_data_url":"{{entire_url('delete_pricing_program.dspy')}}",
"update_data_url":"{{entire_url('update_pricing_program.dspy')}}"
},
"data_url":"{{entire_url('./get_pricing_program.dspy')}}",
"data_method":"GET",
"data_params":{{json.dumps(params_kw, indent=4, ensure_ascii=False)}},
"row_options":{
"browserfields": {
"exclouded": [
"id",
"ownerid",
"pricing_spec"
],
"alters": {
"providerid": {
"valueField": "id",
"textField": "orgname",
"dataurl": "{{entire_url('/rbac/get_provider.dspy')}}"
},
"discount": {
"rules": [
{
"type": "required",
"message": "折扣不能为空"
},
{
"type": "number",
"message": "折扣必须是数字"
},
{
"type": "min",
"value": 0,
"message": "折扣不能小于0"
},
{
"type": "max",
"value": 1,
"message": "折扣不能大于1"
}
]
}
}
},
"editexclouded":[
"id",
"ownerid"
],
"fields":[
{
"name": "id",
"title": "id",
"type": "str",
"length": 32,
"cwidth": 18,
"uitype": "str",
"datatype": "str",
"label": "id"
},
{
"name": "name",
"title": "项目名称",
"type": "str",
"length": 256,
"cwidth": 18,
"uitype": "str",
"datatype": "str",
"label": "项目名称"
},
{
"name": "ownerid",
"title": "所属机构",
"type": "str",
"length": 32,
"label": "所属机构",
"uitype": "code",
"valueField": "ownerid",
"textField": "ownerid_text",
"params": {
"dbname": "{{get_module_dbname('pricing')}}",
"table": "organization",
"tblvalue": "id",
"tbltext": "orgname",
"valueField": "ownerid",
"textField": "ownerid_text"
},
"dataurl": "{{entire_url('/appbase/get_code.dspy')}}"
},
{
"name": "providerid",
"title": "供应商",
"type": "str",
"length": 32,
"label": "供应商",
"uitype": "code",
"valueField": "id",
"textField": "orgname",
"params": {
"dbname": "{{get_module_dbname('pricing')}}",
"table": "organization",
"tblvalue": "id",
"tbltext": "orgname",
"valueField": "providerid",
"textField": "providerid_text"
},
"dataurl": "{{entire_url('/rbac/get_provider.dspy')}}"
},
{
"name": "pricing_belong",
"title": "定价属于",
"type": "str",
"length": 32,
"label": "定价属于",
"uitype": "code",
"valueField": "pricing_belong",
"textField": "pricing_belong_text",
"params": {
"dbname": "{{get_module_dbname('pricing')}}",
"table": "appcodes_kv",
"tblvalue": "k",
"tbltext": "v",
"valueField": "pricing_belong",
"textField": "pricing_belong_text",
"cond": "parentid='pricing_belong'"
},
"dataurl": "{{entire_url('/appbase/get_code.dspy')}}"
},
{
"name": "discount",
"title": "供应商折扣",
"type": "float",
"length": 18,
"dec": 2,
"cwidth": 18,
"uitype": "float",
"datatype": "float",
"label": "供应商折扣",
"rules": [
{
"type": "required",
"message": "折扣不能为空"
},
{
"type": "number",
"message": "折扣必须是数字"
},
{
"type": "min",
"value": 0,
"message": "折扣不能小于0"
},
{
"type": "max",
"value": 1,
"message": "折扣不能大于1"
}
]
},
{
"name": "description",
"title": "描述",
"type": "text",
"length": 0,
"uitype": "text",
"datatype": "text",
"label": "描述"
},
{
"name": "pricing_spec",
"title": "规格明细",
"type": "text",
"length": 0,
"uitype": "text",
"datatype": "text",
"label": "规格明细"
}
]
},
"page_rows":160,
"cache_limit":5
}
,"binds":[
{
"wid": "self",
"event": "test",
"actiontype": "urlwidget",
"target": "PopupWindow",
"popup_options": {
"width": "70%",
"height": "70%",
"auto_open": true,
"archor": "cc",
"title": "定价测试"
},
"options": {
"url": "{{entire_url('../test_pricing_program.ui')}}",
"params": {}
}
},
{
"wid": "self",
"event": "pricing_program_timing",
"actiontype": "urlwidget",
"target": "PopupWindow",
"popup_options": {
"title": "定价项目时序",
"icon": "{{entire_url('/appbase/get_icon.dspy')}}?id=pricing_program_timing",
"resizable": true,
"height": "70%",
"width": "70%"
},
"params_mapping": {
"mapping": {
"id": "ppid",
"referer_widget": "referer_widget"
},
"need_other": false
},
"options": {
"method": "POST",
"params": {},
"url": "{{entire_url('../pricing_program_timing')}}"
}
}
]
}]
}

View File

@ -1,118 +0,0 @@
ns = params_kw.copy()
for k,v in ns.items():
if v == 'NaN' or v == 'null':
ns[k] = None
userorgid = await get_userorgid()
if not userorgid:
return {
"widgettype":"Error",
"options":{
"title":"Authorization Error",
"timeout":3,
"cwidth":16,
"cheight":9,
"message":"Please login"
}
}
ns['ownerid'] = userorgid
_validation_rules = json.loads(r'''{"discount": [{"type": "required", "message": "折扣不能为空"}, {"type": "number", "message": "折扣必须是数字"}, {"type": "min", "value": 0, "message": "折扣不能小于0"}, {"type": "max", "value": 1, "message": "折扣不能大于1"}]}''')
import re as _re
_errors = []
for _fname, _rules in _validation_rules.items():
_val = params_kw.get(_fname, '')
if _val is None: _val = ''
_val = str(_val)
for _rule in _rules:
_rt = _rule.get('type', '')
_rm = _rule.get('message', _fname)
_rv = _rule.get('value')
if _rt == 'required':
if not _val or _val.strip() == '':
_errors.append(_rm)
break
elif _rt == 'minlength':
if _val and len(_val) < int(_rv):
_errors.append(_rm)
break
elif _rt == 'maxlength':
if len(_val) > int(_rv):
_errors.append(_rm)
break
elif _rt in ('min', 'max'):
if _val:
try:
_n = float(_val)
if _rt == 'min' and _n < float(_rv): _errors.append(_rm); break
if _rt == 'max' and _n > float(_rv): _errors.append(_rm); break
except (ValueError, TypeError):
_errors.append(_rm)
break
elif _rt == 'pattern':
if _val and not _re.match(_rv, _val):
_errors.append(_rm)
break
elif _rt == 'email':
if _val and not _re.match(r'^[^\s@]+@[^\s@]+\.[^\s@]+$', _val):
_errors.append(_rm)
break
elif _rt == 'number':
if _val:
try: float(_val)
except (ValueError, TypeError): _errors.append(_rm); break
if _errors:
return {"widgettype":"Error","options":{"title":"Validation Failed","cwidth":16,"cheight":9,"timeout":3,"message":"; ".join(_errors)}}
db = DBPools()
dbname = get_module_dbname('pricing')
async with db.sqlorContext(dbname) as sor:
ns1 = {
"ownerid": userorgid,
"id": params_kw.id
}
recs = await sor.R('pricing_program', ns1)
if len(recs) < 1:
return {
"widgettype":"Error",
"options":{
"title":"Update Error",
"cwidth":16,
"cheight":9,
"timeout":3,
"message":"Record no exist or with wrong ownership"
}
}
r = await sor.U('pricing_program', ns)
debug('update success');
return {
"widgettype":"Message",
"options":{
"title":"Update Success",
"cwidth":16,
"cheight":9,
"timeout":3,
"message":"ok"
}
}
return {
"widgettype":"Error",
"options":{
"title":"Update Error",
"cwidth":16,
"cheight":9,
"timeout":3,
"message":"failed"
}
}

View File

@ -1,93 +0,0 @@
ns = params_kw.copy()
debug(f'get_pricing_program_timing.dspy:{ns=}')
if not ns.get('page'):
ns['page'] = 1
if not ns.get('sort'):
ns['sort'] = 'id'
sql = '''select a.*, b.ppid_text
from (select * from pricing_program_timing where 1=1 [[filterstr]]) a left join (select id as ppid,
name as ppid_text from pricing_program where 1 = 1) b on a.ppid = b.ppid'''
filterjson = params_kw.get('data_filter')
if filterjson and isinstance(filterjson, str):
try:
filterjson = json.loads(filterjson)
except (json.JSONDecodeError, TypeError):
filterjson = None
# data_filter可能是CRUD字段定义({"fields":[...]}),不是过滤条件,忽略
if filterjson and isinstance(filterjson, dict) and 'fields' in filterjson:
filterjson = None
fields_str=r'''[
{
"name": "id",
"title": "id",
"type": "str",
"length": 32
},
{
"name": "ppid",
"title": "定价项目id",
"type": "str",
"length": 32
},
{
"name": "name",
"title": "名称",
"type": "str",
"length": 256
},
{
"name": "pricing_data",
"title": "定价数据",
"type": "text"
},
{
"name": "enabled_date",
"title": "启用日期",
"type": "date"
},
{
"name": "expired_date",
"title": "失效日期",
"type": "date",
"default": "9999-12-31"
}
]'''
ori_fields = json.loads(fields_str)
if not filterjson:
fields = [ f['name'] for f in ori_fields ]
filterjson = default_filterjson(fields, ns)
filterdic = ns.copy()
filterdic['filterstr'] = ''
filterdic['userorgid'] = '${userorgid}$'
filterdic['userid'] = '${userid}$'
if filterjson:
dbf = DBFilter(filterjson)
conds = dbf.gen(ns)
if conds:
ns.update(dbf.consts)
conds = f' and {conds}'
filterdic['filterstr'] = conds
ac = ArgsConvert('[[', ']]')
vars = ac.findAllVariables(sql)
NameSpace = {v:'${' + v + '}$' for v in vars if v != 'filterstr' }
filterdic.update(NameSpace)
sql = ac.convert(sql, filterdic)
debug(f'{sql=}')
db = DBPools()
dbname = get_module_dbname('pricing')
async with db.sqlorContext(dbname) as sor:
r = await sor.sqlPaging(sql, ns)
return r
return {
"total":0,
"rows":[]
}

View File

@ -1,260 +0,0 @@
{
"widgettype":"VBox",
"options":{"cheight":40,"width":"100%"},
"subwidgets":[{
"id":"pricing_program_timing_tbl",
"widgettype":"Tabular",
"options":{
"width":"100%",
"height":"100%",
"title":"定价项目时序",
"toolbar":{
"tools": [
{
"name": "download_pattern",
"label": "定价模版",
"selected_row": true,
"icon": "{{entire_url('/bricks/imgs/download.svg')}}"
},
{
"name": "upload_pricing_data",
"label": "上传定价数据",
"selected_row": true,
"icon": "{{entire_url('/bricks/imgs/upload.svg')}}"
},
{
"name": "download_pricing_data",
"label": "下载定价数据",
"selected_row": true,
"icon": "{{entire_url('/bricks/imgs/download.svg')}}"
},
{
"name": "test",
"selected_row": true,
"label": "验证定价",
"icon": "{{entire_url('/bricks/imgs/test.svg')}}"
},
{
"selected_row": true,
"name": "pricing_item",
"icon": "{{entire_url('/imgs/pricing_item.svg')}}",
"label": "定价细项"
}
]
},
"css":"card",
"editable":{
"new_data_url":"{{entire_url('add_pricing_program_timing.dspy')}}",
"delete_data_url":"{{entire_url('delete_pricing_program_timing.dspy')}}",
"update_data_url":"{{entire_url('update_pricing_program_timing.dspy')}}"
},
"data_url":"{{entire_url('./get_pricing_program_timing.dspy')}}",
"data_method":"GET",
"data_params":{{json.dumps(params_kw, indent=4, ensure_ascii=False)}},
"row_options":{
"browserfields": {
"exclouded": [
"id",
"ppid"
],
"alters": {}
},
"editexclouded":[
"id",
"ppid",
"name"
],
"fields":[
{
"name": "id",
"title": "id",
"type": "str",
"length": 32,
"cwidth": 18,
"uitype": "str",
"datatype": "str",
"label": "id"
},
{
"name": "ppid",
"title": "定价项目id",
"type": "str",
"length": 32,
"label": "定价项目id",
"uitype": "code",
"valueField": "ppid",
"textField": "ppid_text",
"params": {
"dbname": "{{get_module_dbname('pricing')}}",
"table": "pricing_program",
"tblvalue": "id",
"tbltext": "name",
"valueField": "ppid",
"textField": "ppid_text"
},
"dataurl": "{{entire_url('/appbase/get_code.dspy')}}"
},
{
"name": "name",
"title": "名称",
"type": "str",
"length": 256,
"cwidth": 18,
"uitype": "str",
"datatype": "str",
"label": "名称"
},
{
"name": "pricing_data",
"title": "定价数据",
"type": "text",
"length": 0,
"uitype": "text",
"datatype": "text",
"label": "定价数据"
},
{
"name": "enabled_date",
"title": "启用日期",
"type": "date",
"length": 0,
"uitype": "date",
"datatype": "date",
"label": "启用日期"
},
{
"name": "expired_date",
"title": "失效日期",
"type": "date",
"default": "9999-12-31",
"length": 0,
"uitype": "date",
"datatype": "date",
"label": "失效日期"
}
]
},
"page_rows":160,
"cache_limit":5
}
,"binds":[
{
"wid": "self",
"event": "download_pattern",
"actiontype": "newwindow",
"target": "self",
"options": {
"params": {
"ppid": "{{params_kw.ppid}}"
},
"method": "POST",
"url": "{{entire_url('../download_pricing_pattern.dspy')}}"
}
},
{
"wid": "self",
"event": "upload_pricing_data",
"actiontype": "urlwidget",
"target": "PopupWindow",
"popup_options": {
"title": "上传定价数据"
},
"options": {
"params": {
"ppid": "{{params_kw.ppid}}"
},
"method": "POST",
"url": "{{entire_url('../load_pricing_data.ui')}}"
}
},
{
"wid": "self",
"event": "download_pricing_data",
"actiontype": "newwindow",
"target": "self",
"options": {
"params": {
"id": "{{params_kw.id}}"
},
"method": "POST",
"url": "{{entire_url('../download_pricing_data.dspy')}}"
}
},
{
"wid": "self",
"event": "test",
"actiontype": "urlwidget",
"target": "PopupWindow",
"popup_options": {
"title": "验证定价"
},
"options": {
"params": {
"id": "{{params_kw.id}}"
},
"method": "POST",
"url": "{{entire_url('../pricing_test.ui')}}"
}
},
{
"wid": "self",
"event": "pricing_item",
"actiontype": "urlwidget",
"target": "PopupWindow",
"popup_options": {
"title": "定价细项",
"icon": "{{entire_url('/appbase/get_icon.dspy')}}?id=pricing_item",
"resizable": true,
"height": "70%",
"width": "70%"
},
"params_mapping": {
"mapping": {
"id": "pptid",
"referer_widget": "referer_widget"
},
"need_other": false
},
"options": {
"method": "POST",
"params": {},
"url": "{{entire_url('../pricing_item')}}"
}
}
]
}]
}

View File

@ -4,26 +4,19 @@ try:
if isinstance(data, str): if isinstance(data, str):
data = json.loads(data) data = json.loads(data)
x = await buffered_charging(ppid, data) x = await buffered_charging(ppid, data)
subwidgets = [] return {
if isinstance(x, list): "status": "ok",
for r in x: "data": {
subwidgets.append({ "ppid": ppid,
"widgettype": "Text", "data": data,
"options": {"text": json.dumps(r, ensure_ascii=False), "width": "100%"} "result": x
}) }
else: }
subwidgets.append({
"widgettype": "Text",
"options": {"text": json.dumps(x, ensure_ascii=False), "width": "100%"}
})
return json.dumps({
"widgettype": "VScrollPanel",
"options": {"width": "100%", "height": "100%"},
"subwidgets": subwidgets
}, ensure_ascii=False)
except Exception as e: except Exception as e:
exception(f'{ppid=}, {data=}, {e}{format_exc()}') exception(f'{ppid=}, {data=}, {e}{format_exc()}')
return json.dumps({ return {
"widgettype": "Text", "status": "error",
"options": {"width": "100%", "color": "red", "text": f'{ppid=}, {data=}, {e}'} "data": {
}, ensure_ascii=False) "message": f'{ppid=}, {data=}, {e}'
}
}

View File

@ -29,7 +29,7 @@
} }
}, },
{ {
"widgettype": "VScrollPanel", "widgettype": "VBox",
"id": "result", "id": "result",
"options": { "options": {
"height": "50%", "height": "50%",
@ -41,11 +41,28 @@
{ {
"wid": "input_form", "wid": "input_form",
"event": "submit", "event": "submit",
"actiontype": "urlwidget", "actiontype": "urldata",
"target": "result", "target": "result",
"options": { "options": {
"url": "{{entire_url('./test_pricing_program.dspy')}}", "url": "{{entire_url('./test_pricing_program.dspy')}}",
"params":{} "params":{}
},
"status_of": {
"ok": {
"widgettype": "Text",
"options":{
"width": "100%",
"text": "${result}"
}
},
"error": {
"widgettype": "Text",
"options":{
"width": "100%",
"color": "red",
"text": "${message}"
}
}
} }
} }
] ]