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 描述,支持字段定义、选项、匹配模式、计算公式
|
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": {
|
"browserfields": {
|
||||||
"exclouded": ["id", "ownerid", "pricing_spec" ],
|
"exclouded": ["id", "ownerid", "pricing_spec" ],
|
||||||
"alters": {
|
"alters": {
|
||||||
"providerid":{
|
"discount": {
|
||||||
"valueField": "id",
|
"rules": [
|
||||||
"textField": "orgname",
|
{"type": "required", "message": "折扣不能为空"},
|
||||||
"dataurl":"{{entire_url('/rbac/get_provider.dspy')}}"
|
{"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": [
|
"editexclouded": [
|
||||||
"id", "ownerid"
|
"id", "ownerid"
|
||||||
],
|
],
|
||||||
@ -45,7 +54,8 @@
|
|||||||
"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')}}",
|
||||||
|
|||||||
@ -2,13 +2,12 @@
|
|||||||
"tblname": "pricing_program_timing",
|
"tblname": "pricing_program_timing",
|
||||||
"title": "定价项目时序",
|
"title": "定价项目时序",
|
||||||
"params": {
|
"params": {
|
||||||
"logined_userorgid": "ownerid",
|
|
||||||
"browserfields": {
|
"browserfields": {
|
||||||
"exclouded": ["id", "ownerid", "ppid" ],
|
"exclouded": ["id", "ppid" ],
|
||||||
"alters": {}
|
"alters": {}
|
||||||
},
|
},
|
||||||
"editexclouded": [
|
"editexclouded": [
|
||||||
"id", "ownerid", "ppid", "name"
|
"id", "ppid", "name"
|
||||||
],
|
],
|
||||||
"subtables":[
|
"subtables":[
|
||||||
{
|
{
|
||||||
@ -29,13 +28,16 @@
|
|||||||
{
|
{
|
||||||
"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')}}"
|
||||||
}
|
}
|
||||||
@ -59,7 +61,10 @@
|
|||||||
"wid": "self",
|
"wid": "self",
|
||||||
"event": "upload_pricing_data",
|
"event": "upload_pricing_data",
|
||||||
"actiontype": "urlwidget",
|
"actiontype": "urlwidget",
|
||||||
"target": "self",
|
"target": "PopupWindow",
|
||||||
|
"popup_options": {
|
||||||
|
"title": "上传定价数据"
|
||||||
|
},
|
||||||
"options": {
|
"options": {
|
||||||
"params": {
|
"params": {
|
||||||
"ppid": "{{params_kw.ppid}}"
|
"ppid": "{{params_kw.ppid}}"
|
||||||
@ -87,6 +92,7 @@
|
|||||||
"actiontype": "urlwidget",
|
"actiontype": "urlwidget",
|
||||||
"target": "PopupWindow",
|
"target": "PopupWindow",
|
||||||
"popup_options": {
|
"popup_options": {
|
||||||
|
"title": "验证定价"
|
||||||
},
|
},
|
||||||
"options":{
|
"options":{
|
||||||
"params": {
|
"params": {
|
||||||
|
|||||||
@ -44,7 +44,8 @@
|
|||||||
"name": "discount",
|
"name": "discount",
|
||||||
"title": "供应商折扣",
|
"title": "供应商折扣",
|
||||||
"type": "float",
|
"type": "float",
|
||||||
"length": 18
|
"length": 18,
|
||||||
|
"dec": 2
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "description",
|
"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 (
|
from pricing.pricing import (
|
||||||
PricingProgram,
|
PricingProgram,
|
||||||
test_pricing,
|
test_pricing,
|
||||||
get_pricing_program
|
generate_formula_from_factors,
|
||||||
|
get_pricing_display,
|
||||||
)
|
)
|
||||||
from ahserver.serverenv import ServerEnv
|
from ahserver.serverenv import ServerEnv
|
||||||
|
|
||||||
@ -27,7 +28,6 @@ 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,6 +35,11 @@ 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:
|
||||||
@ -42,4 +47,3 @@ 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')
|
||||||
|
|
||||||
|
|||||||
@ -6,9 +6,22 @@ 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表中定义定价的数据字段
|
||||||
@ -115,8 +128,10 @@ def typevalue(v, t):
|
|||||||
return v
|
return v
|
||||||
return f(v)
|
return f(v)
|
||||||
|
|
||||||
def check_value(field, spec_value, data_value):
|
def check_value(field, spec_value, data_value, value_mode=None):
|
||||||
if field.value_mode == 'between':
|
if value_mode is None:
|
||||||
|
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'
|
||||||
@ -142,17 +157,19 @@ def check_value(field, spec_value, data_value):
|
|||||||
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]
|
||||||
e = f'{arr[1]}不认识的期间逻辑,只支持:~ =~ ~='
|
if 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 field.value_mode == 'in':
|
if 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 = field.value_mode
|
mode = value_mode
|
||||||
if not mode or mode == '=':
|
if not mode or mode == '=':
|
||||||
mode = '=='
|
mode = '=='
|
||||||
ns = {
|
ns = {
|
||||||
@ -173,6 +190,13 @@ 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()
|
||||||
@ -424,9 +448,10 @@ 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}'
|
||||||
d = PricingProgram.pricing_data.get(k)
|
if _cache_enabled():
|
||||||
if d:
|
d = PricingProgram.pricing_data.get(k)
|
||||||
return d
|
if 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,
|
||||||
@ -445,7 +470,8 @@ 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)
|
||||||
@ -456,15 +482,16 @@ 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 * r.discount
|
p.cost = p.amount * discount
|
||||||
return prices
|
return prices
|
||||||
|
|
||||||
async def charging(sor, ppid, data):
|
async def charging(sor, ppid, data):
|
||||||
@ -496,20 +523,28 @@ 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 * r.discount
|
p.cost = p.amount * 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):
|
||||||
"""
|
"""
|
||||||
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:
|
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
|
||||||
config_data = config_data.copy()
|
# 用 DictObject 包装,支持 dot notation 属性访问(derived 表达式依赖此特性)
|
||||||
|
config_data = DictObject(**config_data)
|
||||||
d = None
|
d = None
|
||||||
try:
|
try:
|
||||||
d = yaml.safe_load(yamlstr)
|
d = yaml.safe_load(yamlstr)
|
||||||
@ -523,19 +558,62 @@ 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:
|
# 跳过需要人工审核的记录
|
||||||
debug(f'无公式:{p=}')
|
if p.get('_NEEDS_MANUAL_REVIEW'):
|
||||||
|
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 == 'formula':
|
if k in skip_keys:
|
||||||
continue
|
continue
|
||||||
f = d.fields.get(k)
|
f = d.fields.get(k)
|
||||||
if not f:
|
if not f:
|
||||||
@ -543,8 +621,6 @@ 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():
|
||||||
@ -556,48 +632,338 @@ 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:
|
|
||||||
np = p.copy()
|
# 检查 filters 区间条件(新格式,AND逻辑:所有filter_item都要匹配)
|
||||||
formula = p.formula
|
# 带 unit_prices 的 filter_item 是 tiered 定价,跳过(由后面第二块处理)
|
||||||
if not formula:
|
if p_ok and is_new_format and 'filters' in p:
|
||||||
e = f'{p} not formula found'
|
for filter_item in p['filters']:
|
||||||
exception(e)
|
if 'unit_prices' in filter_item:
|
||||||
raise Exception(e)
|
continue # tiered定价项,不在此处检查
|
||||||
debug(f'{formula=}, {ns=}, {p=}, {d.fields=}')
|
item_ok = True
|
||||||
np.data = config_data
|
item_value_mode = filter_item.get('value_mode')
|
||||||
np.amount = eval(formula, config_data.copy())
|
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)
|
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:
|
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
|
||||||
|
|
||||||
async def get_pricing_program(ppid):
|
def generate_formula_from_factors(price_factors):
|
||||||
env = ServerEnv()
|
"""从 price_factors 数组自动生成 formula 字符串。
|
||||||
async with get_sor_context(env, 'pricing') as sor:
|
|
||||||
recs = await sor.R('pricing_program', {'id': ppid})
|
price_factors 格式:
|
||||||
if len(recs) == 0:
|
[
|
||||||
exception(f'{ppid} not found in pricing_program_timing')
|
{"factor": "prompt_tokens", "unit_price": 3.2, "unit_label": "元/百万Token"},
|
||||||
return None
|
{"factor": "completion_tokens", "unit_price": 16.0, "unit_label": "元/百万Token"}
|
||||||
pp = recs[0]
|
]
|
||||||
if ppt.pricing_data is None:
|
返回: "(3.2 * float(prompt_tokens) + 16.0 * float(completion_tokens)) / 1000000.0"
|
||||||
exception(f'{ppid} pricing_data is None in pricing_program_timing')
|
"""
|
||||||
return None
|
if not price_factors:
|
||||||
try:
|
return None
|
||||||
PricingProgram.pp_db2app(ppt)
|
parts = []
|
||||||
except Exception as e:
|
has_million = False
|
||||||
return None
|
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):
|
async def get_pricing_program_timeing(pptid):
|
||||||
env = ServerEnv()
|
env = ServerEnv()
|
||||||
@ -617,8 +983,10 @@ async def get_pricing_program_timeing(pptid):
|
|||||||
return ppt
|
return ppt
|
||||||
|
|
||||||
async def test_pricing(pptid, data):
|
async def test_pricing(pptid, data):
|
||||||
ppt = get_pricing_program_timeing(pptid)
|
ppt = await get_pricing_program_timeing(pptid)
|
||||||
prices = PricingProgram.get_pricing_from_ymalstr(data, ppt.pricing_data)
|
# 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:
|
if prices is None:
|
||||||
return None
|
return None
|
||||||
amount = 0
|
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": {
|
"options": {
|
||||||
"width": "100%",
|
"width": "100%",
|
||||||
"height": "100%",
|
"height": "100%",
|
||||||
"padding": "0",
|
"padding": "0"
|
||||||
"bgcolor": "#0B1120"
|
|
||||||
},
|
},
|
||||||
"subwidgets": [
|
"subwidgets": [
|
||||||
{
|
{
|
||||||
@ -18,9 +17,7 @@
|
|||||||
{
|
{
|
||||||
"widgettype": "Title2",
|
"widgettype": "Title2",
|
||||||
"options": {
|
"options": {
|
||||||
"text": "定价管理",
|
"text": "定价管理"
|
||||||
"color": "#F1F5F9",
|
|
||||||
"fontWeight": "700"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -30,70 +27,77 @@
|
|||||||
"widgettype": "Text",
|
"widgettype": "Text",
|
||||||
"options": {
|
"options": {
|
||||||
"text": "模型定价项目与计费规则配置",
|
"text": "模型定价项目与计费规则配置",
|
||||||
"fontSize": "14px",
|
"cfontsize": 1.2
|
||||||
"color": "#64748B"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"widgettype": "VBox",
|
"widgettype": "VScrollPanel",
|
||||||
"options": {
|
"options": {
|
||||||
"bgcolor": "#1E293B",
|
"css": "filler"
|
||||||
"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": "Svg",
|
"widgettype": "VBox",
|
||||||
"options": {
|
"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>",
|
"spacing": 24
|
||||||
"width": "36px",
|
},
|
||||||
"height": "36px",
|
"subwidgets": [
|
||||||
"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": "Title4",
|
"widgettype": "VBox",
|
||||||
"options": {
|
"id": "pricing_content"
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@
|
|||||||
"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",
|
||||||
|
|||||||
@ -6,7 +6,7 @@
|
|||||||
{
|
{
|
||||||
"name": "pricing",
|
"name": "pricing",
|
||||||
"label": "定价管理",
|
"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()
|
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:
|
||||||
fs = await sor_get_spec_fields(sor, ns.id)
|
r = await sor.C('pricing_program_timing', ns.copy())
|
||||||
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",
|
||||||
@ -37,4 +35,4 @@ return {
|
|||||||
"timeout":3,
|
"timeout":3,
|
||||||
"message":"failed"
|
"message":"failed"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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_item', ns)
|
r = await sor.D('pricing_program_timing', ns)
|
||||||
debug('delete success');
|
debug('delete success');
|
||||||
return {
|
return {
|
||||||
"widgettype":"Message",
|
"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()
|
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})
|
|
||||||
ori_sv = {}
|
r = await sor.U('pricing_program_timing', ns)
|
||||||
if ori.spec_value:
|
debug('update success');
|
||||||
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":{
|
||||||
@ -42,4 +34,4 @@ return {
|
|||||||
"timeout":3,
|
"timeout":3,
|
||||||
"message":"failed"
|
"message":"failed"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -4,19 +4,26 @@ 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)
|
||||||
return {
|
subwidgets = []
|
||||||
"status": "ok",
|
if isinstance(x, list):
|
||||||
"data": {
|
for r in x:
|
||||||
"ppid": ppid,
|
subwidgets.append({
|
||||||
"data": data,
|
"widgettype": "Text",
|
||||||
"result": x
|
"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:
|
except Exception as e:
|
||||||
exception(f'{ppid=}, {data=}, {e}{format_exc()}')
|
exception(f'{ppid=}, {data=}, {e}{format_exc()}')
|
||||||
return {
|
return json.dumps({
|
||||||
"status": "error",
|
"widgettype": "Text",
|
||||||
"data": {
|
"options": {"width": "100%", "color": "red", "text": f'{ppid=}, {data=}, {e}'}
|
||||||
"message": f'{ppid=}, {data=}, {e}'
|
}, ensure_ascii=False)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -29,7 +29,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"widgettype": "VBox",
|
"widgettype": "VScrollPanel",
|
||||||
"id": "result",
|
"id": "result",
|
||||||
"options": {
|
"options": {
|
||||||
"height": "50%",
|
"height": "50%",
|
||||||
@ -41,28 +41,11 @@
|
|||||||
{
|
{
|
||||||
"wid": "input_form",
|
"wid": "input_form",
|
||||||
"event": "submit",
|
"event": "submit",
|
||||||
"actiontype": "urldata",
|
"actiontype": "urlwidget",
|
||||||
"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}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user