Compare commits
56 Commits
feat/moder
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c7b364926d | ||
|
|
38f0c10dfe | ||
|
|
d89961c1e4 | ||
|
|
851ea8b51b | ||
|
|
9a0f82b28d | ||
|
|
74d9848674 | ||
|
|
a6150a4309 | ||
|
|
7d8fae01ca | ||
|
|
0d841f078e | ||
|
|
9241edfb6c | ||
|
|
edee247a11 | ||
|
|
d22dbc57ae | ||
|
|
bcdb1e02cd | ||
|
|
972400e382 | ||
|
|
759cd14a56 | ||
|
|
616c3a1926 | ||
| c5eb5b9399 | |||
| c875a8dc2b | |||
| 7df8e530a4 | |||
| ac3d7c6d91 | |||
| 00abc0caa4 | |||
| c8b5bba342 | |||
| 8d1808fe96 | |||
| 9094e07465 | |||
| 22a3a96833 | |||
| fffcd03c87 | |||
| c2a7dbb98f | |||
| c1bfaca049 | |||
| 502468e0cb | |||
| 3ad50beda8 | |||
| 66b4744def | |||
| 38ff69ca55 | |||
| 3eefe1a67f | |||
| 2706815dee | |||
| 022bab8314 | |||
| df1fa2cfe0 | |||
| e2d46f4074 | |||
| da07250049 | |||
| 7200454c46 | |||
| 392f281758 | |||
| 977be0d39c | |||
| 5d4e008ec8 | |||
| ba69fb84d1 | |||
| b7ca795127 | |||
| 52dd91c6ee | |||
| 7b9d3d2ba0 | |||
| 71329af722 | |||
| 12472b792f | |||
| fc1f8bb182 | |||
| 8159c79d55 | |||
| c041c76c9f | |||
| b7e69b48cd | |||
| 5813749a98 | |||
| c1bdc467a6 | |||
| 6379c1a0e3 | |||
| ce5062215e |
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
__pycache__/
|
||||
252
README.md
252
README.md
@ -331,6 +331,258 @@ 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
|
||||
|
||||
适用于简单定价场景,直接指定计价因子和单位价格。
|
||||
|
||||
**示例 1:Token 定价(文本生成)**
|
||||
|
||||
```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 描述,支持字段定义、选项、匹配模式、计算公式
|
||||
|
||||
49
i18n/en/msg.txt
Normal file
49
i18n/en/msg.txt
Normal file
@ -0,0 +1,49 @@
|
||||
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
|
||||
49
i18n/jp/msg.txt
Normal file
49
i18n/jp/msg.txt
Normal file
@ -0,0 +1,49 @@
|
||||
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)
|
||||
项目名称: プロジェクト名
|
||||
验证定价: 価格検証
|
||||
49
i18n/ko/msg.txt
Normal file
49
i18n/ko/msg.txt
Normal file
@ -0,0 +1,49 @@
|
||||
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)
|
||||
项目名称: 프로젝트 이름
|
||||
验证定价: 가격 검증
|
||||
49
i18n/zh/msg.txt
Normal file
49
i18n/zh/msg.txt
Normal file
@ -0,0 +1,49 @@
|
||||
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)
|
||||
项目名称: 项目名称
|
||||
验证定价: 验证定价
|
||||
@ -7,13 +7,22 @@
|
||||
"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"}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"data_filter": {
|
||||
"fields": [
|
||||
{"field": "name", "title": "项目名称", "uitype": "str"},
|
||||
{"field": "providerid", "title": "供应商", "uitype": "code"}
|
||||
]
|
||||
},
|
||||
"editexclouded": [
|
||||
"id", "ownerid"
|
||||
],
|
||||
@ -45,7 +54,8 @@
|
||||
"width": "70%",
|
||||
"height": "70%",
|
||||
"auto_open": true,
|
||||
"archor": "cc"
|
||||
"archor": "cc",
|
||||
"title": "定价测试"
|
||||
},
|
||||
"options": {
|
||||
"url": "{{entire_url('../test_pricing_program.ui')}}",
|
||||
|
||||
@ -2,13 +2,12 @@
|
||||
"tblname": "pricing_program_timing",
|
||||
"title": "定价项目时序",
|
||||
"params": {
|
||||
"logined_userorgid": "ownerid",
|
||||
"browserfields": {
|
||||
"exclouded": ["id", "ownerid", "ppid" ],
|
||||
"exclouded": ["id", "ppid" ],
|
||||
"alters": {}
|
||||
},
|
||||
"editexclouded": [
|
||||
"id", "ownerid", "ppid", "name"
|
||||
"id", "ppid", "name"
|
||||
],
|
||||
"subtables":[
|
||||
{
|
||||
@ -29,13 +28,16 @@
|
||||
{
|
||||
"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')}}"
|
||||
}
|
||||
@ -59,7 +61,10 @@
|
||||
"wid": "self",
|
||||
"event": "upload_pricing_data",
|
||||
"actiontype": "urlwidget",
|
||||
"target": "self",
|
||||
"target": "PopupWindow",
|
||||
"popup_options": {
|
||||
"title": "上传定价数据"
|
||||
},
|
||||
"options": {
|
||||
"params": {
|
||||
"ppid": "{{params_kw.ppid}}"
|
||||
@ -87,6 +92,7 @@
|
||||
"actiontype": "urlwidget",
|
||||
"target": "PopupWindow",
|
||||
"popup_options": {
|
||||
"title": "验证定价"
|
||||
},
|
||||
"options":{
|
||||
"params": {
|
||||
|
||||
@ -44,7 +44,8 @@
|
||||
"name": "discount",
|
||||
"title": "供应商折扣",
|
||||
"type": "float",
|
||||
"length": 18
|
||||
"length": 18,
|
||||
"dec": 2
|
||||
},
|
||||
{
|
||||
"name": "description",
|
||||
|
||||
@ -0,0 +1,6 @@
|
||||
from pricing.pricing import (
|
||||
PricingProgram,
|
||||
test_pricing,
|
||||
generate_formula_from_factors,
|
||||
get_pricing_display,
|
||||
)
|
||||
@ -3,7 +3,8 @@ from sqlor.dbpools import DBPools
|
||||
from pricing.pricing import (
|
||||
PricingProgram,
|
||||
test_pricing,
|
||||
get_pricing_program
|
||||
generate_formula_from_factors,
|
||||
get_pricing_display,
|
||||
)
|
||||
from ahserver.serverenv import ServerEnv
|
||||
|
||||
@ -27,7 +28,6 @@ def _bind_pricing_events(dbpools, dbname):
|
||||
|
||||
def load_pricing():
|
||||
env = ServerEnv()
|
||||
env.get_pricing_program = get_pricing_program
|
||||
env.write_pricing_patten = PricingProgram.write_pricing_patten
|
||||
env.write_pricing_data = PricingProgram.write_pricing_data
|
||||
env.pricing_program_charging = PricingProgram.charging
|
||||
@ -35,6 +35,11 @@ def load_pricing():
|
||||
env.load_pricing_data = PricingProgram.load_pricing_data
|
||||
env.get_pricing_program = PricingProgram.get_pricing_program
|
||||
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()
|
||||
dbname = env.get_module_dbname('pricing')
|
||||
if dbname:
|
||||
@ -42,4 +47,3 @@ def load_pricing():
|
||||
debug(f'Pricing event listeners bound for database: {dbname}')
|
||||
else:
|
||||
debug('Pricing event listeners skipped: no database configured for pricing module')
|
||||
|
||||
|
||||
@ -6,9 +6,22 @@ from sqlor.dbpools import DBPools, get_sor_context
|
||||
from appPublic.log import debug, exception, info, MyLogger
|
||||
from appPublic.timeUtils import curDateString
|
||||
from appPublic.dictObject import DictObject
|
||||
from appPublic.jsonConfig import getConfig
|
||||
from .write_pattern import write_pattern_xlsx, load_xlsx_pricing
|
||||
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描述定价策略,
|
||||
在pricing_program的pricing_spec表中定义定价的数据字段
|
||||
@ -115,8 +128,10 @@ def typevalue(v, t):
|
||||
return v
|
||||
return f(v)
|
||||
|
||||
def check_value(field, spec_value, data_value):
|
||||
if field.value_mode == 'between':
|
||||
def check_value(field, spec_value, data_value, value_mode=None):
|
||||
if value_mode is None:
|
||||
value_mode = field.value_mode
|
||||
if value_mode == 'between':
|
||||
arr = spec_value.strip().split()
|
||||
if len(arr) < 2 or len(arr) > 3:
|
||||
e = f'{spec_value=} error'
|
||||
@ -142,17 +157,19 @@ def check_value(field, spec_value, data_value):
|
||||
return arr[0] < fvalue and fvalue < arr[-1]
|
||||
if arr[1] == '~=':
|
||||
return arr[0] < fvalue and fvalue <= arr[-1]
|
||||
e = f'{arr[1]}不认识的期间逻辑,只支持:~ =~ ~='
|
||||
if arr[1] == '=~=':
|
||||
return arr[0] <= fvalue and fvalue <= arr[-1]
|
||||
e = f'{arr[1]}不认识的期间逻辑,只支持:~ =~ ~= =~='
|
||||
exception(e)
|
||||
raise Exception(e)
|
||||
|
||||
if field.value_mode == 'in':
|
||||
if value_mode == 'in':
|
||||
arr = spec_value.strip().split()
|
||||
arr = [ typevalue(a, field.type) for a in arr ]
|
||||
# debug(f'{arr=}, {data_value=}')
|
||||
return data_value in arr
|
||||
|
||||
mode = field.value_mode
|
||||
mode = value_mode
|
||||
if not mode or mode == '=':
|
||||
mode = '=='
|
||||
ns = {
|
||||
@ -173,6 +190,13 @@ def data_mapping(ns, name, v):
|
||||
class PricingProgram:
|
||||
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
|
||||
async def get_pricing_program(ppid):
|
||||
env = ServerEnv()
|
||||
@ -424,9 +448,10 @@ class PricingProgram:
|
||||
async def get_ppid_pricing(ppid):
|
||||
dat = curDateString()
|
||||
k = f'{ppid}.{dat}'
|
||||
d = PricingProgram.pricing_data.get(k)
|
||||
if d:
|
||||
return d
|
||||
if _cache_enabled():
|
||||
d = PricingProgram.pricing_data.get(k)
|
||||
if d:
|
||||
return d
|
||||
env = ServerEnv()
|
||||
async with get_sor_context(env, 'pricing') as sor:
|
||||
sql = """select a.name, a.ownerid, a.providerid,
|
||||
@ -445,7 +470,8 @@ class PricingProgram:
|
||||
e = Exception(f'{ppid=},{dat=} data not found')
|
||||
exception(f'{e}')
|
||||
raise e
|
||||
d = recs[0]
|
||||
d = recs[0]
|
||||
if _cache_enabled():
|
||||
PricingProgram.pricing_data[k] = d
|
||||
dates = PricingProgram.pricing_data.get(ppid, [])
|
||||
dates.append(dat)
|
||||
@ -456,15 +482,16 @@ class PricingProgram:
|
||||
PricingProgram.pricing_data[dk]
|
||||
dates = dates[-2:]
|
||||
PricingProgram.pricing_data[ppid] = dates
|
||||
return d
|
||||
return d
|
||||
|
||||
async def buffered_charging(ppid, data):
|
||||
r = await PricingProgram.get_ppid_pricing(ppid)
|
||||
prices = PricingProgram.get_pricing_from_ymalstr(data,
|
||||
r.pricing_data)
|
||||
r.pricing_data)
|
||||
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:
|
||||
p.cost = p.amount * r.discount
|
||||
p.cost = p.amount * discount
|
||||
return prices
|
||||
|
||||
async def charging(sor, ppid, data):
|
||||
@ -496,20 +523,28 @@ order by b.enabled_date desc"""
|
||||
r.pricing_data)
|
||||
debug(f'{r.prices=}')
|
||||
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:
|
||||
p.cost = p.amount * r.discount
|
||||
p.cost = p.amount * discount
|
||||
return r.prices
|
||||
|
||||
@staticmethod
|
||||
def get_pricing_from_ymalstr(config_data, yamlstr):
|
||||
"""
|
||||
yamlstr是从
|
||||
解析定价YAML并计算费用。
|
||||
支持两种格式:
|
||||
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:
|
||||
e = Exception(f'config_data is None, {yamlstr=}')
|
||||
exception(f'{e=}')
|
||||
raise e
|
||||
config_data = config_data.copy()
|
||||
# 用 DictObject 包装,支持 dot notation 属性访问(derived 表达式依赖此特性)
|
||||
config_data = DictObject(**config_data)
|
||||
d = None
|
||||
try:
|
||||
d = yaml.safe_load(yamlstr)
|
||||
@ -523,19 +558,62 @@ order by b.enabled_date desc"""
|
||||
if not d.pricings:
|
||||
exception(f'{d} has not "pricings"')
|
||||
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 = []
|
||||
for i, p in enumerate(d.pricings):
|
||||
if not p.formula:
|
||||
debug(f'无公式:{p=}')
|
||||
# 跳过需要人工审核的记录
|
||||
if p.get('_NEEDS_MANUAL_REVIEW'):
|
||||
debug(f'跳过需要人工审核的定价项: {i}')
|
||||
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
|
||||
times = 1
|
||||
unit = 1
|
||||
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:
|
||||
continue
|
||||
if k == 'formula':
|
||||
if k in skip_keys:
|
||||
continue
|
||||
f = d.fields.get(k)
|
||||
if not f:
|
||||
@ -543,8 +621,6 @@ order by b.enabled_date desc"""
|
||||
exception(f'{e}')
|
||||
raise Exception(e)
|
||||
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)
|
||||
if data_value is None:
|
||||
if 'default' in f.keys():
|
||||
@ -556,48 +632,338 @@ order by b.enabled_date desc"""
|
||||
try:
|
||||
flg = check_value(f, spec_value, data_value)
|
||||
if not flg:
|
||||
# 条件不满足
|
||||
# debug(f'条件不满足:{p=},{spec_value=}, {data_value=}, {k=}')
|
||||
p_ok = False
|
||||
break
|
||||
except Exception as e:
|
||||
msg = f'{p=},{f}: {spec_value=}, {data_value=}'
|
||||
exception(f'{e}:{msg}')
|
||||
break
|
||||
if p_ok and p.formula:
|
||||
np = p.copy()
|
||||
formula = p.formula
|
||||
if not formula:
|
||||
e = f'{p} not formula found'
|
||||
exception(e)
|
||||
raise Exception(e)
|
||||
debug(f'{formula=}, {ns=}, {p=}, {d.fields=}')
|
||||
np.data = config_data
|
||||
np.amount = eval(formula, config_data.copy())
|
||||
|
||||
# 检查 filters 区间条件(新格式,AND逻辑:所有filter_item都要匹配)
|
||||
# 带 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)
|
||||
else:
|
||||
info(f'{config_data=}, {p=}, {d.model_mappings=}, mismatched')
|
||||
|
||||
elif is_old_format:
|
||||
# 旧格式:formula
|
||||
formula = p.formula
|
||||
debug(f'{formula=}, {ns=}, {p=}, {d.fields=}')
|
||||
env_data = DictObject(config_data)
|
||||
np.amount = eval(formula, env_data)
|
||||
ret_items.append(np)
|
||||
|
||||
if len(ret_items) == 0:
|
||||
e = f'{config_data=}{yamlstr=}没有找到合适的定价'
|
||||
exception(e)
|
||||
raise Exception(e)
|
||||
return ret_items
|
||||
|
||||
async def get_pricing_program(ppid):
|
||||
env = ServerEnv()
|
||||
async with get_sor_context(env, 'pricing') as sor:
|
||||
recs = await sor.R('pricing_program', {'id': ppid})
|
||||
if len(recs) == 0:
|
||||
exception(f'{ppid} not found in pricing_program_timing')
|
||||
return None
|
||||
pp = recs[0]
|
||||
if ppt.pricing_data is None:
|
||||
exception(f'{ppid} pricing_data is None in pricing_program_timing')
|
||||
return None
|
||||
try:
|
||||
PricingProgram.pp_db2app(ppt)
|
||||
except Exception as e:
|
||||
return None
|
||||
def generate_formula_from_factors(price_factors):
|
||||
"""从 price_factors 数组自动生成 formula 字符串。
|
||||
|
||||
price_factors 格式:
|
||||
[
|
||||
{"factor": "prompt_tokens", "unit_price": 3.2, "unit_label": "元/百万Token"},
|
||||
{"factor": "completion_tokens", "unit_price": 16.0, "unit_label": "元/百万Token"}
|
||||
]
|
||||
返回: "(3.2 * float(prompt_tokens) + 16.0 * float(completion_tokens)) / 1000000.0"
|
||||
"""
|
||||
if not price_factors:
|
||||
return None
|
||||
parts = []
|
||||
has_million = False
|
||||
for f in price_factors:
|
||||
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):
|
||||
env = ServerEnv()
|
||||
@ -617,8 +983,10 @@ async def get_pricing_program_timeing(pptid):
|
||||
return ppt
|
||||
|
||||
async def test_pricing(pptid, data):
|
||||
ppt = get_pricing_program_timeing(pptid)
|
||||
prices = PricingProgram.get_pricing_from_ymalstr(data, ppt.pricing_data)
|
||||
ppt = await get_pricing_program_timeing(pptid)
|
||||
# ppt.pricing_data 已被 ppt_db2app 解析为 dict,需要转回 YAML 字符串
|
||||
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:
|
||||
return None
|
||||
amount = 0
|
||||
|
||||
BIN
scripts/__pycache__/load_path.cpython-310.pyc
Normal file
BIN
scripts/__pycache__/load_path.cpython-310.pyc
Normal file
Binary file not shown.
115
scripts/load_path.py
Normal file
115
scripts/load_path.py
Normal file
@ -0,0 +1,115 @@
|
||||
#!/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()
|
||||
10
wwwroot/api/get_pricing_display.dspy
Normal file
10
wwwroot/api/get_pricing_display.dspy
Normal file
@ -0,0 +1,10 @@
|
||||
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)
|
||||
114
wwwroot/index.ui
114
wwwroot/index.ui
@ -3,8 +3,7 @@
|
||||
"options": {
|
||||
"width": "100%",
|
||||
"height": "100%",
|
||||
"padding": "0",
|
||||
"bgcolor": "#0B1120"
|
||||
"padding": "0"
|
||||
},
|
||||
"subwidgets": [
|
||||
{
|
||||
@ -18,9 +17,7 @@
|
||||
{
|
||||
"widgettype": "Title2",
|
||||
"options": {
|
||||
"text": "定价管理",
|
||||
"color": "#F1F5F9",
|
||||
"fontWeight": "700"
|
||||
"text": "定价管理"
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -30,70 +27,77 @@
|
||||
"widgettype": "Text",
|
||||
"options": {
|
||||
"text": "模型定价项目与计费规则配置",
|
||||
"fontSize": "14px",
|
||||
"color": "#64748B"
|
||||
"cfontsize": 1.2
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"widgettype": "VBox",
|
||||
"widgettype": "VScrollPanel",
|
||||
"options": {
|
||||
"bgcolor": "#1E293B",
|
||||
"padding": "24px",
|
||||
"borderRadius": "12px",
|
||||
"border": "1px solid #334155",
|
||||
"cursor": "pointer"
|
||||
"css": "filler"
|
||||
},
|
||||
"binds": [
|
||||
{
|
||||
"wid": "self",
|
||||
"event": "click",
|
||||
"actiontype": "urlwidget",
|
||||
"target": "app.pricing_content",
|
||||
"options": {
|
||||
"url": "{{entire_url('/pricing/pricing_program')}}"
|
||||
},
|
||||
"mode": "replace"
|
||||
}
|
||||
],
|
||||
"subwidgets": [
|
||||
{
|
||||
"widgettype": "Svg",
|
||||
"widgettype": "VBox",
|
||||
"options": {
|
||||
"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",
|
||||
"height": "36px",
|
||||
"marginBottom": "16px"
|
||||
}
|
||||
"spacing": 24
|
||||
},
|
||||
"subwidgets": [
|
||||
{
|
||||
"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": "Title4",
|
||||
"options": {
|
||||
"text": "定价项目管理",
|
||||
"color": "#F1F5F9",
|
||||
"fontWeight": "600",
|
||||
"marginBottom": "8px"
|
||||
}
|
||||
},
|
||||
{
|
||||
"widgettype": "Text",
|
||||
"options": {
|
||||
"text": "管理模型定价规则、计费项目和定时任务",
|
||||
"fontSize": "14px",
|
||||
"color": "#94A3B8"
|
||||
}
|
||||
"widgettype": "VBox",
|
||||
"id": "pricing_content"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"widgettype": "VBox",
|
||||
"id": "pricing_content",
|
||||
"css": "filler",
|
||||
"options": {
|
||||
"width": "100%",
|
||||
"overflowY": "auto"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
"options": {
|
||||
"title": "{{pp.id}}",
|
||||
"description": {{json.dumps(pp.description)}},
|
||||
"height": "200%",
|
||||
"fields":[
|
||||
{
|
||||
"name": "xlsx_file",
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
{
|
||||
"name": "pricing",
|
||||
"label": "定价管理",
|
||||
"url": "{{entire_url('/pricing/index.ui')}}"
|
||||
"url": "{{entire_url('/pricing/pricing_program')}}"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -1 +0,0 @@
|
||||
return await get_pricing_specs_by_pptid(params_kw.pptid)
|
||||
@ -1,118 +0,0 @@
|
||||
|
||||
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":[]
|
||||
}
|
||||
@ -1,6 +0,0 @@
|
||||
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 []
|
||||
@ -1,189 +0,0 @@
|
||||
{
|
||||
"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);"
|
||||
}
|
||||
]
|
||||
|
||||
}
|
||||
99
wwwroot/pricing_program/add_pricing_program.dspy
Normal file
99
wwwroot/pricing_program/add_pricing_program.dspy
Normal file
@ -0,0 +1,99 @@
|
||||
|
||||
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"
|
||||
}
|
||||
}
|
||||
47
wwwroot/pricing_program/delete_pricing_program.dspy
Normal file
47
wwwroot/pricing_program/delete_pricing_program.dspy
Normal file
@ -0,0 +1,47 @@
|
||||
|
||||
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"
|
||||
}
|
||||
}
|
||||
135
wwwroot/pricing_program/get_pricing_program.dspy
Normal file
135
wwwroot/pricing_program/get_pricing_program.dspy
Normal file
@ -0,0 +1,135 @@
|
||||
|
||||
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":[]
|
||||
}
|
||||
289
wwwroot/pricing_program/index.ui
Normal file
289
wwwroot/pricing_program/index.ui
Normal file
@ -0,0 +1,289 @@
|
||||
|
||||
{
|
||||
"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')}}"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
}]
|
||||
}
|
||||
118
wwwroot/pricing_program/update_pricing_program.dspy
Normal file
118
wwwroot/pricing_program/update_pricing_program.dspy
Normal file
@ -0,0 +1,118 @@
|
||||
|
||||
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"
|
||||
}
|
||||
}
|
||||
@ -8,18 +8,16 @@ if not id or len(id) > 32:
|
||||
id = uuid()
|
||||
ns['id'] = id
|
||||
|
||||
|
||||
|
||||
|
||||
db = DBPools()
|
||||
dbname = get_module_dbname('pricing')
|
||||
async with db.sqlorContext(dbname) as sor:
|
||||
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())
|
||||
r = await sor.C('pricing_program_timing', ns.copy())
|
||||
return {
|
||||
"widgettype":"Message",
|
||||
"options":{
|
||||
"user_data":ns,
|
||||
"cwidth":16,
|
||||
"cheight":9,
|
||||
"title":"Add Success",
|
||||
@ -37,4 +35,4 @@ return {
|
||||
"timeout":3,
|
||||
"message":"failed"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -7,7 +7,7 @@ ns = {
|
||||
db = DBPools()
|
||||
dbname = get_module_dbname('pricing')
|
||||
async with db.sqlorContext(dbname) as sor:
|
||||
r = await sor.D('pricing_item', ns)
|
||||
r = await sor.D('pricing_program_timing', ns)
|
||||
debug('delete success');
|
||||
return {
|
||||
"widgettype":"Message",
|
||||
@ -0,0 +1,93 @@
|
||||
|
||||
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":[]
|
||||
}
|
||||
260
wwwroot/pricing_program_timing/index.ui
Normal file
260
wwwroot/pricing_program_timing/index.ui
Normal file
@ -0,0 +1,260 @@
|
||||
|
||||
{
|
||||
"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')}}"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
}]
|
||||
}
|
||||
@ -7,21 +7,13 @@ for k,v in ns.items():
|
||||
|
||||
|
||||
|
||||
|
||||
db = DBPools()
|
||||
dbname = get_module_dbname('pricing')
|
||||
async with db.sqlorContext(dbname) as sor:
|
||||
ori = await sor.R('pricing_item', {'id': ns.id})
|
||||
ori_sv = {}
|
||||
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=}');
|
||||
|
||||
r = await sor.U('pricing_program_timing', ns)
|
||||
debug('update success');
|
||||
return {
|
||||
"widgettype":"Message",
|
||||
"options":{
|
||||
@ -42,4 +34,4 @@ return {
|
||||
"timeout":3,
|
||||
"message":"failed"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -4,19 +4,26 @@ try:
|
||||
if isinstance(data, str):
|
||||
data = json.loads(data)
|
||||
x = await buffered_charging(ppid, data)
|
||||
return {
|
||||
"status": "ok",
|
||||
"data": {
|
||||
"ppid": ppid,
|
||||
"data": data,
|
||||
"result": x
|
||||
}
|
||||
}
|
||||
subwidgets = []
|
||||
if isinstance(x, list):
|
||||
for r in x:
|
||||
subwidgets.append({
|
||||
"widgettype": "Text",
|
||||
"options": {"text": json.dumps(r, ensure_ascii=False), "width": "100%"}
|
||||
})
|
||||
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:
|
||||
exception(f'{ppid=}, {data=}, {e}{format_exc()}')
|
||||
return {
|
||||
"status": "error",
|
||||
"data": {
|
||||
"message": f'{ppid=}, {data=}, {e}'
|
||||
}
|
||||
}
|
||||
return json.dumps({
|
||||
"widgettype": "Text",
|
||||
"options": {"width": "100%", "color": "red", "text": f'{ppid=}, {data=}, {e}'}
|
||||
}, ensure_ascii=False)
|
||||
|
||||
@ -29,7 +29,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"widgettype": "VBox",
|
||||
"widgettype": "VScrollPanel",
|
||||
"id": "result",
|
||||
"options": {
|
||||
"height": "50%",
|
||||
@ -41,28 +41,11 @@
|
||||
{
|
||||
"wid": "input_form",
|
||||
"event": "submit",
|
||||
"actiontype": "urldata",
|
||||
"actiontype": "urlwidget",
|
||||
"target": "result",
|
||||
"options": {
|
||||
"url": "{{entire_url('./test_pricing_program.dspy')}}",
|
||||
"params":{}
|
||||
},
|
||||
"status_of": {
|
||||
"ok": {
|
||||
"widgettype": "Text",
|
||||
"options":{
|
||||
"width": "100%",
|
||||
"text": "${result}"
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"widgettype": "Text",
|
||||
"options":{
|
||||
"width": "100%",
|
||||
"color": "red",
|
||||
"text": "${message}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user