feat: 代金券模块v2完成前端层+管理/客户全链路——①12个CRUD dspy重写为薄封装(request,params_kw签名对齐,Message widget返回,零import)②实例新增走发券引擎voucher_issue.dspy(原子占额/券码/有效期,禁裸插入)③作废invalidate(unused→void条件UPDATE+原因追加remark审计)④客户我的代金券页my/index.ui(机构隔离+状态过滤+过期清扫)⑤json CRUD定义修正(规则类型短名/实例状态含void/发券作废弹窗binds/流水noedit只读)⑥load_path.py改pipeline-app版(logined逐路径精确注册含CRUD四件套)⑦seed_welcome_voucher.py WELCOME400种子(400元/前100名/限pipeline/30天/幂等)⑧i18n四语key=value格式89词条⑨pyproject packages含voucher.rules⑩删sage时代遗留(sql/tables.sql,conf,menu.json,手写mysql.ddl.sql违禁)⑪update/delete守卫:模板code不可改/实例仅unused可改客户备注/used禁删保审计/删除回吐发行额度
This commit is contained in:
parent
e59c7b1f6c
commit
6cf70983c8
9
.gitignore
vendored
Normal file
9
.gitignore
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
# xls2ui 生成产物(部署时由 build.sh 从 json/ + models/ 重新生成,不入库)
|
||||
wwwroot/voucher_template_list/
|
||||
wwwroot/voucher_rule_list/
|
||||
wwwroot/voucher_instance_list/
|
||||
wwwroot/voucher_usage_log_list/
|
||||
build/
|
||||
*.egg-info/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
324
README.md
324
README.md
@ -1,269 +1,133 @@
|
||||
# voucher — 代金券模块
|
||||
# voucher — 代金券模块(pipeline-app 宿主)
|
||||
|
||||
独立代金券管理模块,支持**可配置规则引擎**,一次性使用,不找零。
|
||||
独立代金券管理模块:**可配置规则引擎**、一次性使用、不找零、并发安全。
|
||||
|
||||
- 宿主:pipeline-app(库名走 `get_module_dbname('voucher')`,pipeline 宿主下= `pipeline`)
|
||||
- 版本:2.0.0(2026-09-12 sqlor 风格全异步重写,替代 1.x sage 时代废代码)
|
||||
|
||||
## 核心架构
|
||||
|
||||
```
|
||||
模板(类别) → 定义规则集
|
||||
实例(代金券)→ 属于某个模板,继承规则
|
||||
使用流水 → 记录每次消费抵扣
|
||||
模板 voucher_template(面值/发行量/有效期/状态,org_id 隔离)
|
||||
└─ 规则集 voucher_rule(可配置,增删不改代码;按 sort_order 依次校验)
|
||||
└─ 实例 voucher_instance(券码/状态 unused→used|expired|void,一次性)
|
||||
└─ 流水 voucher_usage_log(订单/抵扣金额/产品,只读审计)
|
||||
```
|
||||
|
||||
### 表结构
|
||||
| 表 | 说明 |
|
||||
### 关键机制
|
||||
|
||||
| 机制 | 实现 |
|
||||
|---|---|
|
||||
| voucher_template | 代金券模板(面值、有效期、发行量、状态) |
|
||||
| voucher_rule | 规则定义(模板关联,可增删启用禁用) |
|
||||
| voucher_instance | 券实例(一次性使用,状态:unused/used/expired) |
|
||||
| voucher_usage_log | 使用流水(订单、抵扣金额、产品信息) |
|
||||
| 一次性使用 | 核销用条件 UPDATE(`status='unused'` 守卫)+ affected_rows 判定,并发双花只有一个成功 |
|
||||
| 发行量控制 | `issued_count<total_count` 原子占额;total_count=0 不限量;未使用券删除回吐名额 |
|
||||
| 规则可配置 | `@register_rule` 注册 validator 纯函数,模板下挂 voucher_rule 行即生效 |
|
||||
| 作废 | `invalidate_voucher_api`:unused→void(条件 UPDATE),已用/过期/已作废拒绝,原因追加 remark 审计留痕 |
|
||||
| 过期清扫 | `expire_vouchers`:查可用券/我的券前自动把过期 unused 置 expired |
|
||||
| 规则损坏 | rule_config 非法 JSON → 拒绝使用(不静默放行,防绕过限制) |
|
||||
| 注册送券 | bind `pipeline:organization:c:after` 事件:新客户机构注册自动发 WELCOME400 欢迎券;幂等(同机构不重发);「前 N 名」由 total_count 原子占额控制;异常只记日志不阻断注册 |
|
||||
|
||||
---
|
||||
## 内置规则类型(短名,appcodes_kv 组合键 ≤32 字符约束)
|
||||
|
||||
## 可配置规则引擎
|
||||
| rule_type | rule_config 示例 | 说明 |
|
||||
|-----------|------------------|------|
|
||||
| `min_amount` | `{"min_value": 100}` | 最低消费门槛 |
|
||||
| `max_amount` | `{"max_value": 1000}` | 最高消费上限 |
|
||||
| `product_type` | `{"product_types": ["pipeline"]}` | 限定产品类型 |
|
||||
| `product` | `{"products": ["PP_X"]}` | 限定特定产品 |
|
||||
| `exclude_product` | `{"products": ["PP_X"]}` | 排除特定产品 |
|
||||
| `max_usage_count` | `{"max_count": 1}` | 最大使用次数 |
|
||||
| `user_level` | `{"min_level": 2}` | 用户等级下限 |
|
||||
|
||||
规则通过 `@register_rule` 装饰器注册到 `RULE_REGISTRY`,新增规则只需写 validator 函数,无需修改引擎代码。
|
||||
新增规则:`voucher/rules/validators.py` 加一个 `@register_rule('xxx')` 纯函数 + data.json 字典补一行,无需改引擎。
|
||||
|
||||
### 内置规则类型
|
||||
## 页面与 API
|
||||
|
||||
| rule_type | 说明 | rule_config 示例 |
|
||||
|-----------|------|------------------|
|
||||
| `min_amount` | 最低消费门槛 | `{"min_value": 100}` |
|
||||
| `max_amount` | 最高消费限制 | `{"max_value": 1000}` |
|
||||
| `applicable_product_type` | 限定产品类型(llm/image/video/audio) | `{"product_types": ["llm", "image"]}` |
|
||||
| `applicable_product` | 限定特定产品(具体模型名) | `{"products": ["gpt-4", "claude-3"]}` |
|
||||
| `exclude_product` | 排除特定产品 | `{"products": ["gpt-4"]}` |
|
||||
| `max_usage_count` | 最大使用次数 | `{"max_count": 1}` |
|
||||
| `valid_period` | 有效期检查 | `{}` (由实例 valid_from/valid_to 处理) |
|
||||
| `user_level` | 用户等级限制 | `{"min_level": 2}` |
|
||||
### 管理侧(logined;菜单「代金券管理」,工单三角色可见)
|
||||
|
||||
### 规则执行流程
|
||||
1. 查询券实例状态(unused + 未过期)
|
||||
2. 加载模板关联的所有启用规则(按 sort_order 排序)
|
||||
3. 依次执行 validator,任一失败即拒绝
|
||||
4. 全部通过 → 计算抵扣金额 = min(面值, 消费金额)
|
||||
5. 记录流水 + 标记券为已使用(一次性作废)
|
||||
| 页面 | 路径 | 说明 |
|
||||
|---|---|---|
|
||||
| 入口 | `/voucher/index.ui` | 卡片导航:模板管理/券实例管理/使用流水 |
|
||||
| 模板管理 | `/voucher/voucher_template_list/index.ui` | CRUD + 工具栏「发券」弹窗 + 子表「规则配置」 |
|
||||
| 券实例 | `/voucher/voucher_instance_list/index.ui` | 新增=发券引擎(禁裸插入)+ 工具栏「作废」弹窗 + 子表「使用记录」 |
|
||||
| 使用流水 | `/voucher/voucher_usage_log_list/index.ui` | 只读(noedit) |
|
||||
|
||||
### 新增规则步骤
|
||||
```python
|
||||
# rules/validators.py
|
||||
@register_rule('new_rule_type')
|
||||
def check_new_rule(config, context):
|
||||
# config: 从 rule_config JSON 解析
|
||||
# context: 包含 request_amount, product_type, product_name, user_level 等
|
||||
if not some_condition:
|
||||
return False, "不满足条件"
|
||||
return True, None
|
||||
```
|
||||
然后在模板管理界面添加 `rule_type: "new_rule_type"` 的规则记录即可。
|
||||
管理 API(`wwwroot/api/`,全部薄封装 → init.py ServerEnv 函数):
|
||||
- template/:create/update/delete/options(options 返回纯 `[{value,text}]` 数组)
|
||||
- rule/:create/update/delete;`rule_types.dspy` 返回已注册类型清单
|
||||
- instance/:`voucher_issue.dspy`(发券)、update(仅 unused 可改客户/备注)、delete(used 禁删)、`voucher_invalidate.dspy`(作废)
|
||||
- `apply_voucher.dspy`(核销)、`get_available.dspy`(内部查可用)
|
||||
|
||||
---
|
||||
### 客户侧(logined;菜单「我的代金券」)
|
||||
|
||||
## 与其他模块交互
|
||||
| 页面/端点 | 路径 | 说明 |
|
||||
|---|---|---|
|
||||
| 我的代金券 | `/voucher/my/index.ui` | 本机构全部券(含已用/过期/作废),状态过滤 |
|
||||
| 自助查询 | `/voucher/api/v1/available.dspy` | 可用券(登录态按机构隔离,支持 product_type/product_name/request_amount 过滤) |
|
||||
| 列表 API | `/voucher/api/my_vouchers.dspy` | Tabular 契约 `{total, rows}` |
|
||||
|
||||
### llmage 模块(消费时使用代金券)
|
||||
### 其他模块调用(ServerEnv)
|
||||
|
||||
```python
|
||||
# llmage 的计费逻辑中调用 voucher 引擎
|
||||
from voucher.rules.engine import apply_voucher, get_available_vouchers
|
||||
|
||||
# 查询客户可用代金券
|
||||
vouchers = get_available_vouchers(sor, customer_id, context={
|
||||
'product_type': 'llm', # 从 llm.catelog 获取
|
||||
'product_name': 'gpt-4', # 具体模型名
|
||||
'request_amount': amount,
|
||||
'user_level': customer.level
|
||||
})
|
||||
|
||||
# 使用代金券抵扣
|
||||
ok, deducted, err = apply_voucher(sor, instance_id, customer_id, order_id, context={
|
||||
'request_amount': amount,
|
||||
'product_type': 'llm',
|
||||
'product_name': 'gpt-4'
|
||||
})
|
||||
|
||||
# 批量使用多张券
|
||||
from voucher.rules.engine import batch_apply_vouchers
|
||||
result = batch_apply_vouchers(sor, customer_id, order_id, voucher_ids, context)
|
||||
# result: {total_deducted: 150.0, remaining: 50.0, details: [...]}
|
||||
# remaining > 0 时从余额扣减
|
||||
# 结算联动(product_management/accounting 等):
|
||||
result = await env.voucher_batch_apply(sor, customer_id, order_id, voucher_ids, context)
|
||||
# → {'total_deducted': 150.0, 'remaining': 50.0, 'details': [...]}
|
||||
# remaining > 0 时从余额扣
|
||||
# context: {request_amount(必填), product_type, product_name, user_level, used_by}
|
||||
```
|
||||
|
||||
### accounting 模块(余额扣减联动)
|
||||
|
||||
```python
|
||||
# accounting 消费流程中先尝试代金券,剩余从余额扣
|
||||
total_amount = calculate_consumption(customer_id, period)
|
||||
|
||||
# Step 1: 尝试代金券抵扣
|
||||
result = batch_apply_vouchers(sor, customer_id, order_id, voucher_ids, context)
|
||||
|
||||
# Step 2: 剩余金额从余额扣减
|
||||
if result['remaining'] > 0:
|
||||
accounting.deduct_balance(customer_id, result['remaining'], order_id)
|
||||
|
||||
# Step 3: 记录完整账单
|
||||
accounting.record_order(order_id, total_amount,
|
||||
voucher_deducted=result['total_deducted'],
|
||||
balance_deducted=result['remaining'])
|
||||
```
|
||||
|
||||
### 客户自助查询 API(voucher 模块提供)
|
||||
|
||||
voucher 模块自身提供面向远端客户的查询 API,无需通过 dapi 转发。
|
||||
|
||||
**接口**: `GET /voucher/api/v1/available.dspy`
|
||||
**认证**: Bearer Token(登录后用户)
|
||||
**参数**:
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| product_type | string | 否 | 产品类型过滤(llm/image/video/audio) |
|
||||
| product_name | string | 否 | 具体产品名过滤(如 gpt-4) |
|
||||
| request_amount | float | 否 | 预期消费金额(用于规则预校验) |
|
||||
|
||||
**返回**:
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"data": [
|
||||
{
|
||||
"id": "xxx",
|
||||
"code": "VCH-A1B2C3D4E5F6",
|
||||
"face_value": 50.00,
|
||||
"valid_from": "2026-01-01 00:00:00",
|
||||
"valid_to": "2026-01-31 00:00:00",
|
||||
"template_name": "新用户满减券"
|
||||
}
|
||||
],
|
||||
"total": 1
|
||||
}
|
||||
```
|
||||
|
||||
**调用示例**:
|
||||
```bash
|
||||
# 查询所有可用券
|
||||
curl -H "Authorization: Bearer <token>" \
|
||||
https://ai.atvoe.com/voucher/api/v1/available.dspy
|
||||
|
||||
# 按产品类型过滤
|
||||
curl -H "Authorization: Bearer <token>" \
|
||||
"https://ai.atvoe.com/voucher/api/v1/available.dspy?product_type=llm&request_amount=100"
|
||||
```
|
||||
|
||||
### 集成点总结
|
||||
|
||||
| 调用方 | 场景 | 调用方式 |
|
||||
|--------|------|----------|
|
||||
| llmage | 推理/生成时抵扣 | `apply_voucher()` 函数调用 |
|
||||
| accounting | 月度账单结算 | `batch_apply_vouchers()` 函数调用 |
|
||||
| 远端客户 | 自助查询可用券 | `GET /voucher/api/v1/available.dspy` HTTP API |
|
||||
| 任意模块 | 新增规则类型 | `@register_rule` + 模板管理界面 |
|
||||
|
||||
---
|
||||
注册函数全集(`load_voucher()`):`get_available_vouchers_api` / `apply_voucher_api` / `validate_voucher_api` / `my_vouchers_api` / 模板规则实例管理函数 / `issue_voucher_api` / `invalidate_voucher_api` / `get_voucher_template_options` / `get_registered_rule_types` / 引擎级 `voucher_issue` / `voucher_apply` / `voucher_batch_apply` / `voucher_get_available`。
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
voucher/
|
||||
├── voucher/ # Python 包
|
||||
│ ├── __init__.py
|
||||
│ └── init.py # 模块初始化 + ServerEnv 注册
|
||||
├── rules/ # 规则引擎(纯 Python)
|
||||
│ ├── registry.py # @register_rule 装饰器
|
||||
│ ├── validators.py # 内置规则校验器
|
||||
│ └── engine.py # 校验/使用/批量使用
|
||||
├── wwwroot/ # 前端
|
||||
│ ├── index.ui # 入口页(卡片导航)
|
||||
│ ├── menu.json # 菜单定义
|
||||
│ └── api/ # API 端点
|
||||
│ ├── template/ # 模板 CRUD
|
||||
│ ├── rule/ # 规则 CRUD
|
||||
│ ├── instance/ # 实例 CRUD
|
||||
│ ├── usage/ # 流水 CRUD
|
||||
│ ├── v1/ # 客户自助查询 API
|
||||
│ │ └── available.dspy # 查询可用券(Bearer 认证)
|
||||
│ ├── apply_voucher.dspy # 使用代金券
|
||||
│ ├── get_available.dspy # 内部查询可用券
|
||||
│ └── rule_types.dspy # 获取规则类型列表
|
||||
├── models/ # 表定义 JSON
|
||||
├── json/ # CRUD 定义 JSON
|
||||
├── sql/
|
||||
│ └── tables.sql # 建表 + 编码初始化
|
||||
│ ├── init.py # ServerEnv 注册 + 注册送券挂钩 + load_voucher()
|
||||
│ └── rules/ # registry(@register_rule) + validators(7种) + engine(全异步)
|
||||
├── init/data.json # 字典种子(模板状态/实例状态/规则类型/来源/启用标志)
|
||||
├── models/ # 4 张表定义(建表唯一事实源,json2ddl 渲染)
|
||||
├── json/ # 4 个 CRUD 定义(xls2ui 生成 wwwroot/voucher_*_list/,生成物不入库)
|
||||
├── wwwroot/
|
||||
│ ├── index.ui # 管理入口(卡片导航)
|
||||
│ ├── my/index.ui # 客户「我的代金券」
|
||||
│ ├── issue_form.ui # 发券弹窗表单
|
||||
│ ├── invalidate_form.ui # 作废弹窗表单
|
||||
│ └── api/ # 薄封装 dspy(零 import,委托 init.py 注册函数)
|
||||
├── scripts/
|
||||
│ └── load_path.py # RBAC 权限注册
|
||||
├── pyproject.toml
|
||||
└── README.md
|
||||
│ ├── load_path.py # RBAC 注册(logined 全路径逐个精确配)
|
||||
│ └── seed_welcome_voucher.py # WELCOME400 种子(幂等)
|
||||
├── i18n/{zh,en,jp,ko}/msg.txt # key=value 格式(merge_i18n 消费)
|
||||
└── pyproject.toml # packages = ["voucher", "voucher.rules"]
|
||||
```
|
||||
|
||||
---
|
||||
## 部署清单(pipeline-app 宿主,六处登记)
|
||||
|
||||
## 部署步骤
|
||||
1. `build.sh`:clone/pip install/xls2ui/软链四个清单加 `voucher`(已登记)
|
||||
2. `app/pipeline_app.py`:import + `load_voucher()`(已登记,ticket 之后)
|
||||
3. `scripts/import_init.py`:INIT_MODULES 加 voucher 行(已登记)
|
||||
4. 建表:`create_tables.py` 动态扫描 pkgs/*/models 自动覆盖(无需改)
|
||||
5. `wwwroot/index.ui`:客户「我的代金券」+ 管理「代金券管理」菜单(已登记,is_ticket_staff 门禁)
|
||||
6. RBAC:`./load_path.sh`(自动扫 pkgs/*/scripts/load_path.py)→ `redis-cli -n 0 FLUSHDB` → 重启
|
||||
|
||||
### 1. 代码部署
|
||||
部署后一次性操作:
|
||||
```bash
|
||||
cd ~/repos/voucher && git pull
|
||||
cd ~/repos/sage/pkgs && ln -sf ~/repos/voucher voucher
|
||||
cd voucher && ~/repos/sage/py3/bin/pip install -e .
|
||||
cd <APP_ROOT>
|
||||
./py3/bin/python pkgs/voucher/scripts/seed_welcome_voucher.py # WELCOME400 种子
|
||||
```
|
||||
|
||||
### 2. 数据库建表
|
||||
```bash
|
||||
mysql -u root -p sage < ~/repos/voucher/sql/tables.sql
|
||||
```
|
||||
## Pitfalls
|
||||
|
||||
### 3. Sage 集成
|
||||
```python
|
||||
# sage/app/sage.py
|
||||
from voucher.init import load_voucher
|
||||
# ... in init():
|
||||
load_voucher()
|
||||
```
|
||||
- **实例「新增」必须走发券 API**(voucher_issue.dspy → issue_voucher 引擎):裸插入会绕过原子占额/券码生成/有效期计算。CRUD json 的 `editable.new_data_url` 已指向它。
|
||||
- **状态字段禁止界面直改**:使用走核销 API、作废走 invalidate API(都有条件 UPDATE 原子守卫);update_voucher_instance 只允许改 customer_id/remark 且仅 unused 券。
|
||||
- 一次性使用:不追踪 remaining_value/partial 状态,抵扣 = min(面值, 消费金额),不找零。
|
||||
- rule_config 数组字段用 JSON array(validators 兼容逗号分隔串,但写入必须是 JSON)。
|
||||
- rule_type 用短名(product_type 而非 applicable_product_type)——appcodes_kv.id = `{parentid}_{k}` ≤32 字符。
|
||||
- 模板删除:已发放(issued_count>0)拒绝,引导用「停用」;删除级联删规则,已发出的券保留但 validate 拒绝(模板非 active)。
|
||||
- dspy 薄封装返回 dict(框架自动序列化),**禁 json.dumps 双重序列化**(Pitfall 38)。
|
||||
- datetime 字段回前端必须 str 化(`_jsonable_instance`),框架 json.dumps 无 default=str。
|
||||
- 弹窗表单 submited 事件的 params 是 **Response 对象**,要 `await resp.json()` 后再 widgetBuild(form.js dispatch 原始 resp)。
|
||||
|
||||
```bash
|
||||
# sage/build.sh 安装循环
|
||||
for m in ... voucher
|
||||
```
|
||||
## 仓库
|
||||
|
||||
### 4. 菜单入口
|
||||
```json
|
||||
// sage/wwwroot/global_menu.ui items 数组
|
||||
,{
|
||||
"name": "voucher",
|
||||
"label": "代金券",
|
||||
"icon": "fa fa-ticket",
|
||||
"url": "{{entire_url('/voucher/index.ui')}}",
|
||||
"target": "app.sage_main_content"
|
||||
}
|
||||
```
|
||||
|
||||
### 5. RBAC 权限
|
||||
```bash
|
||||
cd ~/repos/sage && ./py3/bin/python ~/repos/voucher/scripts/load_path.py
|
||||
```
|
||||
|
||||
### 6. 重启
|
||||
```bash
|
||||
cd ~/repos/sage && ./stop.sh && ./start.sh
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 创建模板 + 配置规则
|
||||
1. 模板管理 → 新增 → 名称"新用户满减券",面值 50,有效期 30 天
|
||||
2. 点击模板 → 规则配置 → 添加规则:
|
||||
- 规则类型: `min_amount`,配置: `{"min_value": 100}`
|
||||
- 规则类型: `applicable_product_type`,配置: `{"product_types": ["llm"]}`
|
||||
|
||||
### 发放代金券
|
||||
1. 券实例管理 → 新增 → 选择模板,填写客户 ID
|
||||
2. 系统自动生成券码,设置有效期
|
||||
|
||||
### 消费时抵扣
|
||||
```python
|
||||
# llmage 推理完成后
|
||||
context = {'request_amount': 120.0, 'product_type': 'llm', 'product_name': 'gpt-4'}
|
||||
result = batch_apply_vouchers(sor, customer_id, order_id, [vid1, vid2], context)
|
||||
# → 抵扣 50(满100可用),剩余 70 从余额扣
|
||||
```
|
||||
- 远端:git@git.opencomputing.cn:yumoqing/voucher.git(main)
|
||||
- 宿主接入提交:pipeline-app(build.sh/pipeline_app.py/import_init.py/index.ui)
|
||||
|
||||
@ -1,12 +0,0 @@
|
||||
{
|
||||
"module": "voucher",
|
||||
"databases": {
|
||||
"sage": {
|
||||
"host": "${DB_HOST}",
|
||||
"port": "${DB_PORT}",
|
||||
"user": "${DB_USER}",
|
||||
"passwd": "${DB_PASSWD}",
|
||||
"db": "sage"
|
||||
}
|
||||
}
|
||||
}
|
||||
131
i18n/en/msg.txt
131
i18n/en/msg.txt
@ -1,42 +1,89 @@
|
||||
Cancel: Cancel
|
||||
Conform: Confirm
|
||||
Discard: Discard
|
||||
Reset: Reset
|
||||
Submit: Submit
|
||||
主键ID: Primary Key ID
|
||||
产品名称: Product Name
|
||||
产品类型: Product Type
|
||||
代金券使用流水: Voucher Usage Records
|
||||
代金券实例: Voucher Instance
|
||||
代金券模板: Voucher Template
|
||||
代金券规则定义: Voucher Rule Definition
|
||||
使用时间: Usage Time
|
||||
使用的订单ID: Order ID Used
|
||||
创建人: Creator
|
||||
创建时间: Created At
|
||||
券实例ID: Voucher Instance ID
|
||||
券面值: Face Value
|
||||
发放时间: Issue Time
|
||||
唯一券码: Unique Voucher Code
|
||||
备注: Remark
|
||||
实际抵扣金额: Actual Deduction Amount
|
||||
客户ID: Customer ID
|
||||
已发放量: Issued Quantity
|
||||
总发行量: Total Issued
|
||||
执行顺序: Execution Order
|
||||
操作人: Operator
|
||||
是否启用: Enabled
|
||||
更新人: Updater
|
||||
更新时间: Updated At
|
||||
有效期天数: Validity Days
|
||||
来源: Source
|
||||
模板ID: Template ID
|
||||
模板名称: Template Name
|
||||
模板编码: Template Code
|
||||
状态: Status
|
||||
生效时间: Effective Time
|
||||
规则类型: Rule Type
|
||||
规则配置(JSON): Rule Config (JSON)
|
||||
订单/消费ID: Order/Consumption ID
|
||||
过期时间: Expiry Time
|
||||
面值: Face Value
|
||||
Cancel=Cancel
|
||||
Conform=Confirm
|
||||
Discard=Discard
|
||||
Reset=Reset
|
||||
Submit=Submit
|
||||
代金券=Voucher
|
||||
代金券管理=Voucher Management
|
||||
模板管理=Template Management
|
||||
券实例管理=Voucher Instance Management
|
||||
使用流水=Usage Records
|
||||
管理代金券模板和规则配置=Manage voucher templates and rule configuration
|
||||
查看和发放代金券=View and issue vouchers
|
||||
查看代金券使用记录=View voucher usage records
|
||||
我的代金券=My Vouchers
|
||||
发放代金券=Issue Voucher
|
||||
作废=Void
|
||||
确认作废=Confirm Void
|
||||
作废代金券=Void Voucher
|
||||
已作废=Voided
|
||||
作废原因=Void Reason
|
||||
必填,将追加到券备注(审计留痕)=Required; will be appended to voucher remark (audit trail)
|
||||
按此模板发券=Issue from this template
|
||||
代金券模板=Voucher Template
|
||||
代金券实例=Voucher Instance
|
||||
代金券规则=Voucher Rule
|
||||
代金券使用流水=Voucher Usage Log
|
||||
主键ID=Primary Key ID
|
||||
模板名称=Template Name
|
||||
模板编码=Template Code
|
||||
模板ID=Template ID
|
||||
模板=Template
|
||||
面值=Face Value
|
||||
面值(元)=Face Value (CNY)
|
||||
已抵扣(元)=Deducted (CNY)
|
||||
总发行量(0不限)=Total Issue Count (0=unlimited)
|
||||
已发放量=Issued Count
|
||||
有效期天数=Valid Days
|
||||
状态=Status
|
||||
草稿=Draft
|
||||
启用=Active
|
||||
停用=Inactive
|
||||
备注=Remark
|
||||
所属机构ID=Organization ID
|
||||
创建人=Created By
|
||||
创建时间=Created At
|
||||
更新时间=Updated At
|
||||
规则类型=Rule Type
|
||||
规则配置JSON=Rule Config JSON
|
||||
是否启用=Enabled
|
||||
执行顺序=Sort Order
|
||||
客户机构ID=Customer Organization ID
|
||||
客户机构=Customer Organization
|
||||
券码=Voucher Code
|
||||
未使用=Unused
|
||||
已使用=Used
|
||||
已过期=Expired
|
||||
实际抵扣金额=Actual Deducted
|
||||
生效时间=Valid From
|
||||
过期时间=Valid To
|
||||
发放时间=Issued At
|
||||
使用时间=Used At
|
||||
使用订单ID=Order ID
|
||||
订单ID=Order ID
|
||||
发放来源=Source
|
||||
手动发放=Manual
|
||||
注册赠券=Registration Gift
|
||||
促销活动=Promotion
|
||||
发放人=Issued By
|
||||
券实例ID=Instance ID
|
||||
券实例=Voucher Instance
|
||||
券面值=Face Value
|
||||
抵扣金额=Deducted Amount
|
||||
产品类型=Product Type
|
||||
产品名称=Product Name
|
||||
操作人=Used By
|
||||
使用记录=Usage Records
|
||||
规则配置=Rule Configuration
|
||||
最低消费=Minimum Amount
|
||||
最高消费=Maximum Amount
|
||||
限定产品类型=Product Type Limit
|
||||
限定特定产品=Product Limit
|
||||
排除特定产品=Product Exclusion
|
||||
最大使用次数=Max Usage Count
|
||||
用户等级=User Level
|
||||
发券=Issue
|
||||
客户机构ID必填=Customer is required
|
||||
可选=Optional
|
||||
查询=Search
|
||||
全部=All
|
||||
|
||||
131
i18n/jp/msg.txt
131
i18n/jp/msg.txt
@ -1,42 +1,89 @@
|
||||
Cancel: キャンセル
|
||||
Conform: 確認
|
||||
Discard: 破棄
|
||||
Reset: リセット
|
||||
Submit: 送信
|
||||
主键ID: 主キーID
|
||||
产品名称: 製品名
|
||||
产品类型: 製品タイプ
|
||||
代金券使用流水: バウチャー使用履歴
|
||||
代金券实例: バウチャーインスタンス
|
||||
代金券模板: バウチャーテンプレート
|
||||
代金券规则定义: バウチャールール定義
|
||||
使用时间: 使用日時
|
||||
使用的订单ID: 使用注文ID
|
||||
创建人: 作成者
|
||||
创建时间: 作成日時
|
||||
券实例ID: バウチャーインスタンスID
|
||||
券面值: 券面額
|
||||
发放时间: 発行日時
|
||||
唯一券码: ユニークバウチャーコード
|
||||
备注: 備考
|
||||
实际抵扣金额: 実質控除額
|
||||
客户ID: 顧客ID
|
||||
已发放量: 発行済み数量
|
||||
总发行量: 総発行量
|
||||
执行顺序: 実行順序
|
||||
操作人: 操作者
|
||||
是否启用: 有効化
|
||||
更新人: 更新者
|
||||
更新时间: 更新日時
|
||||
有效期天数: 有効日数
|
||||
来源: ソース
|
||||
模板ID: テンプレートID
|
||||
模板名称: テンプレート名
|
||||
模板编码: テンプレートコード
|
||||
状态: ステータス
|
||||
生效时间: 発効日時
|
||||
规则类型: ルールタイプ
|
||||
规则配置(JSON): ルール設定(JSON)
|
||||
订单/消费ID: 注文/消費ID
|
||||
过期时间: 有効期限
|
||||
面值: 額面
|
||||
Cancel=キャンセル
|
||||
Conform=確認
|
||||
Discard=破棄
|
||||
Reset=リセット
|
||||
Submit=送信
|
||||
代金券=クーポン
|
||||
代金券管理=クーポン管理
|
||||
模板管理=テンプレート管理
|
||||
券实例管理=クーポンインスタンス管理
|
||||
使用流水=使用履歴
|
||||
管理代金券模板和规则配置=クーポンテンプレートとルール設定を管理
|
||||
查看和发放代金券=クーポンの確認と発行
|
||||
查看代金券使用记录=クーポン使用記録の確認
|
||||
我的代金券=マイクーポン
|
||||
发放代金券=クーポン発行
|
||||
作废=無効化
|
||||
确认作废=無効化の確認
|
||||
作废代金券=クーポン無効化
|
||||
已作废=無効化済み
|
||||
作废原因=無効化理由
|
||||
必填,将追加到券备注(审计留痕)=必須入力。クーポン備考に追記されます(監査証跡)
|
||||
按此模板发券=このテンプレートで発行
|
||||
代金券模板=クーポンテンプレート
|
||||
代金券实例=クーポンインスタンス
|
||||
代金券规则=クーポンルール
|
||||
代金券使用流水=クーポン使用履歴
|
||||
主键ID=主キーID
|
||||
模板名称=テンプレート名
|
||||
模板编码=テンプレートコード
|
||||
模板ID=テンプレートID
|
||||
模板=テンプレート
|
||||
面值=額面
|
||||
面值(元)=額面(元)
|
||||
已抵扣(元)=控除額(元)
|
||||
总发行量(0不限)=総発行枚数(0=無制限)
|
||||
已发放量=発行済み枚数
|
||||
有效期天数=有効日数
|
||||
状态=ステータス
|
||||
草稿=下書き
|
||||
启用=有効
|
||||
停用=無効
|
||||
备注=備考
|
||||
所属机构ID=所属組織ID
|
||||
创建人=作成者
|
||||
创建时间=作成日時
|
||||
更新时间=更新日時
|
||||
规则类型=ルールタイプ
|
||||
规则配置JSON=ルール設定JSON
|
||||
是否启用=有効フラグ
|
||||
执行顺序=実行順序
|
||||
客户机构ID=顧客組織ID
|
||||
客户机构=顧客組織
|
||||
券码=クーポンコード
|
||||
未使用=未使用
|
||||
已使用=使用済み
|
||||
已过期=期限切れ
|
||||
实际抵扣金额=実控除額
|
||||
生效时间=有効開始
|
||||
过期时间=有効期限
|
||||
发放时间=発行日時
|
||||
使用时间=使用日時
|
||||
使用订单ID=注文ID
|
||||
订单ID=注文ID
|
||||
发放来源=発行元
|
||||
手动发放=手動発行
|
||||
注册赠券=登録プレゼント
|
||||
促销活动=プロモーション
|
||||
发放人=発行者
|
||||
券实例ID=インスタンスID
|
||||
券实例=クーポンインスタンス
|
||||
券面值=額面
|
||||
抵扣金额=控除額
|
||||
产品类型=製品タイプ
|
||||
产品名称=製品名
|
||||
操作人=操作者
|
||||
使用记录=使用記録
|
||||
规则配置=ルール設定
|
||||
最低消费=最低消費額
|
||||
最高消费=最高消費額
|
||||
限定产品类型=製品タイプ制限
|
||||
限定特定产品=製品制限
|
||||
排除特定产品=製品除外
|
||||
最大使用次数=最大使用回数
|
||||
用户等级=ユーザーレベル
|
||||
发券=発行
|
||||
客户机构ID必填=顧客組織は必須です
|
||||
可选=任意
|
||||
查询=検索
|
||||
全部=すべて
|
||||
|
||||
131
i18n/ko/msg.txt
131
i18n/ko/msg.txt
@ -1,42 +1,89 @@
|
||||
Cancel: 취소
|
||||
Conform: 확인
|
||||
Discard: 폐기
|
||||
Reset: 초기화
|
||||
Submit: 제출
|
||||
主键ID: 기본키 ID
|
||||
产品名称: 제품 이름
|
||||
产品类型: 제품 유형
|
||||
代金券使用流水: 바우처 사용 내역
|
||||
代金券实例: 바우처 인스턴스
|
||||
代金券模板: 바우처 템플릿
|
||||
代金券规则定义: 바우처 규칙 정의
|
||||
使用时间: 사용 시간
|
||||
使用的订单ID: 사용 주문 ID
|
||||
创建人: 생성자
|
||||
创建时间: 생성 시간
|
||||
券实例ID: 바우처 인스턴스 ID
|
||||
券面值: 액면가
|
||||
发放时间: 발급 시간
|
||||
唯一券码: 고유 바우처 코드
|
||||
备注: 비고
|
||||
实际抵扣金额: 실제 공제 금액
|
||||
客户ID: 고객 ID
|
||||
已发放量: 발급 수량
|
||||
总发行量: 총 발행량
|
||||
执行顺序: 실행 순서
|
||||
操作人: 운영자
|
||||
是否启用: 활성화 여부
|
||||
更新人: 업데이트 담당자
|
||||
更新时间: 업데이트 시간
|
||||
有效期天数: 유효 기간(일)
|
||||
来源: 출처
|
||||
模板ID: 템플릿 ID
|
||||
模板名称: 템플릿 이름
|
||||
模板编码: 템플릿 코드
|
||||
状态: 상태
|
||||
生效时间: 효력 시작 시간
|
||||
规则类型: 규칙 유형
|
||||
规则配置(JSON): 규칙 구성(JSON)
|
||||
订单/消费ID: 주문/소비 ID
|
||||
过期时间: 만료 시간
|
||||
面值: 액면가
|
||||
Cancel=취소
|
||||
Conform=확인
|
||||
Discard=폐기
|
||||
Reset=초기화
|
||||
Submit=제출
|
||||
代金券=상품권
|
||||
代金券管理=상품권 관리
|
||||
模板管理=템플릿 관리
|
||||
券实例管理=상품권 인스턴스 관리
|
||||
使用流水=사용 내역
|
||||
管理代金券模板和规则配置=상품권 템플릿 및 규칙 구성 관리
|
||||
查看和发放代金券=상품권 조회 및 발급
|
||||
查看代金券使用记录=상품권 사용 기록 조회
|
||||
我的代金券=내 상품권
|
||||
发放代金券=상품권 발급
|
||||
作废=무효화
|
||||
确认作废=무효화 확인
|
||||
作废代金券=상품권 무효화
|
||||
已作废=무효화됨
|
||||
作废原因=무효화 사유
|
||||
必填,将追加到券备注(审计留痕)=필수 입력, 상품권 비고에 추가됩니다(감사 추적)
|
||||
按此模板发券=이 템플릿으로 발급
|
||||
代金券模板=상품권 템플릿
|
||||
代金券实例=상품권 인스턴스
|
||||
代金券规则=상품권 규칙
|
||||
代金券使用流水=상품권 사용 내역
|
||||
主键ID=기본키 ID
|
||||
模板名称=템플릿 이름
|
||||
模板编码=템플릿 코드
|
||||
模板ID=템플릿 ID
|
||||
模板=템플릿
|
||||
面值=액면가
|
||||
面值(元)=액면가(위안)
|
||||
已抵扣(元)=차감액(위안)
|
||||
总发行量(0不限)=총 발행량(0=무제한)
|
||||
已发放量=발행 수량
|
||||
有效期天数=유효 일수
|
||||
状态=상태
|
||||
草稿=초안
|
||||
启用=활성
|
||||
停用=비활성
|
||||
备注=비고
|
||||
所属机构ID=소속 기관 ID
|
||||
创建人=생성자
|
||||
创建时间=생성 시간
|
||||
更新时间=수정 시간
|
||||
规则类型=규칙 유형
|
||||
规则配置JSON=규칙 구성 JSON
|
||||
是否启用=활성 여부
|
||||
执行顺序=실행 순서
|
||||
客户机构ID=고객 기관 ID
|
||||
客户机构=고객 기관
|
||||
券码=상품권 코드
|
||||
未使用=미사용
|
||||
已使用=사용됨
|
||||
已过期=만료됨
|
||||
实际抵扣金额=실제 차감 금액
|
||||
生效时间=발효 시간
|
||||
过期时间=만료 시간
|
||||
发放时间=발급 시간
|
||||
使用时间=사용 시간
|
||||
使用订单ID=주문 ID
|
||||
订单ID=주문 ID
|
||||
发放来源=발급 출처
|
||||
手动发放=수동 발급
|
||||
注册赠券=가입 증정
|
||||
促销活动=판촉 활동
|
||||
发放人=발급자
|
||||
券实例ID=인스턴스 ID
|
||||
券实例=상품권 인스턴스
|
||||
券面值=액면가
|
||||
抵扣金额=차감 금액
|
||||
产品类型=제품 유형
|
||||
产品名称=제품 이름
|
||||
操作人=운영자
|
||||
使用记录=사용 기록
|
||||
规则配置=규칙 구성
|
||||
最低消费=최소 소비
|
||||
最高消费=최대 소비
|
||||
限定产品类型=제품 유형 제한
|
||||
限定特定产品=특정 제품 제한
|
||||
排除特定产品=특정 제품 제외
|
||||
最大使用次数=최대 사용 횟수
|
||||
用户等级=사용자 등급
|
||||
发券=발급
|
||||
客户机构ID必填=고객 기관은 필수입니다
|
||||
可选=선택
|
||||
查询=조회
|
||||
全部=전체
|
||||
|
||||
131
i18n/zh/msg.txt
131
i18n/zh/msg.txt
@ -1,42 +1,89 @@
|
||||
Cancel: Cancel
|
||||
Conform: Conform
|
||||
Discard: Discard
|
||||
Reset: Reset
|
||||
Submit: Submit
|
||||
主键ID: 主键ID
|
||||
产品名称: 产品名称
|
||||
产品类型: 产品类型
|
||||
代金券使用流水: 代金券使用流水
|
||||
代金券实例: 代金券实例
|
||||
代金券模板: 代金券模板
|
||||
代金券规则定义: 代金券规则定义
|
||||
使用时间: 使用时间
|
||||
使用的订单ID: 使用的订单ID
|
||||
创建人: 创建人
|
||||
创建时间: 创建时间
|
||||
券实例ID: 券实例ID
|
||||
券面值: 券面值
|
||||
发放时间: 发放时间
|
||||
唯一券码: 唯一券码
|
||||
备注: 备注
|
||||
实际抵扣金额: 实际抵扣金额
|
||||
客户ID: 客户ID
|
||||
已发放量: 已发放量
|
||||
总发行量: 总发行量
|
||||
执行顺序: 执行顺序
|
||||
操作人: 操作人
|
||||
是否启用: 是否启用
|
||||
更新人: 更新人
|
||||
更新时间: 更新时间
|
||||
有效期天数: 有效期天数
|
||||
来源: 来源
|
||||
模板ID: 模板ID
|
||||
模板名称: 模板名称
|
||||
模板编码: 模板编码
|
||||
状态: 状态
|
||||
生效时间: 生效时间
|
||||
规则类型: 规则类型
|
||||
规则配置(JSON): 规则配置(JSON)
|
||||
订单/消费ID: 订单/消费ID
|
||||
过期时间: 过期时间
|
||||
面值: 面值
|
||||
Cancel=Cancel
|
||||
Conform=Conform
|
||||
Discard=Discard
|
||||
Reset=Reset
|
||||
Submit=Submit
|
||||
代金券=代金券
|
||||
代金券管理=代金券管理
|
||||
模板管理=模板管理
|
||||
券实例管理=券实例管理
|
||||
使用流水=使用流水
|
||||
管理代金券模板和规则配置=管理代金券模板和规则配置
|
||||
查看和发放代金券=查看和发放代金券
|
||||
查看代金券使用记录=查看代金券使用记录
|
||||
我的代金券=我的代金券
|
||||
发放代金券=发放代金券
|
||||
作废=作废
|
||||
确认作废=确认作废
|
||||
作废代金券=作废代金券
|
||||
已作废=已作废
|
||||
作废原因=作废原因
|
||||
必填,将追加到券备注(审计留痕)=必填,将追加到券备注(审计留痕)
|
||||
按此模板发券=按此模板发券
|
||||
代金券模板=代金券模板
|
||||
代金券实例=代金券实例
|
||||
代金券规则=代金券规则
|
||||
代金券使用流水=代金券使用流水
|
||||
主键ID=主键ID
|
||||
模板名称=模板名称
|
||||
模板编码=模板编码
|
||||
模板ID=模板ID
|
||||
模板=模板
|
||||
面值=面值
|
||||
面值(元)=面值(元)
|
||||
已抵扣(元)=已抵扣(元)
|
||||
总发行量(0不限)=总发行量(0不限)
|
||||
已发放量=已发放量
|
||||
有效期天数=有效期天数
|
||||
状态=状态
|
||||
草稿=草稿
|
||||
启用=启用
|
||||
停用=停用
|
||||
备注=备注
|
||||
所属机构ID=所属机构ID
|
||||
创建人=创建人
|
||||
创建时间=创建时间
|
||||
更新时间=更新时间
|
||||
规则类型=规则类型
|
||||
规则配置JSON=规则配置JSON
|
||||
是否启用=是否启用
|
||||
执行顺序=执行顺序
|
||||
客户机构ID=客户机构ID
|
||||
客户机构=客户机构
|
||||
券码=券码
|
||||
未使用=未使用
|
||||
已使用=已使用
|
||||
已过期=已过期
|
||||
实际抵扣金额=实际抵扣金额
|
||||
生效时间=生效时间
|
||||
过期时间=过期时间
|
||||
发放时间=发放时间
|
||||
使用时间=使用时间
|
||||
使用订单ID=使用订单ID
|
||||
订单ID=订单ID
|
||||
发放来源=发放来源
|
||||
手动发放=手动发放
|
||||
注册赠券=注册赠券
|
||||
促销活动=促销活动
|
||||
发放人=发放人
|
||||
券实例ID=券实例ID
|
||||
券实例=券实例
|
||||
券面值=券面值
|
||||
抵扣金额=抵扣金额
|
||||
产品类型=产品类型
|
||||
产品名称=产品名称
|
||||
操作人=操作人
|
||||
使用记录=使用记录
|
||||
规则配置=规则配置
|
||||
最低消费=最低消费
|
||||
最高消费=最高消费
|
||||
限定产品类型=限定产品类型
|
||||
限定特定产品=限定特定产品
|
||||
排除特定产品=排除特定产品
|
||||
最大使用次数=最大使用次数
|
||||
用户等级=用户等级
|
||||
发券=发券
|
||||
客户机构ID必填=客户机构ID必填
|
||||
可选=可选
|
||||
查询=查询
|
||||
全部=全部
|
||||
|
||||
53
init/data.json
Normal file
53
init/data.json
Normal file
@ -0,0 +1,53 @@
|
||||
{
|
||||
"appcodes": [
|
||||
{
|
||||
"parentid": "voucher_template_status",
|
||||
"parentname": "代金券模板状态",
|
||||
"items": [
|
||||
{"k": "draft", "v": "草稿"},
|
||||
{"k": "active", "v": "启用"},
|
||||
{"k": "inactive", "v": "停用"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"parentid": "voucher_instance_status",
|
||||
"parentname": "代金券实例状态",
|
||||
"items": [
|
||||
{"k": "unused", "v": "未使用"},
|
||||
{"k": "used", "v": "已使用"},
|
||||
{"k": "expired", "v": "已过期"},
|
||||
{"k": "void", "v": "已作废"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"parentid": "vchr_rule_type",
|
||||
"parentname": "代金券规则类型",
|
||||
"items": [
|
||||
{"k": "min_amount", "v": "最低消费"},
|
||||
{"k": "max_amount", "v": "最高消费"},
|
||||
{"k": "product_type", "v": "限定产品类型"},
|
||||
{"k": "product", "v": "限定特定产品"},
|
||||
{"k": "exclude_product", "v": "排除特定产品"},
|
||||
{"k": "max_usage_count", "v": "最大使用次数"},
|
||||
{"k": "user_level", "v": "用户等级"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"parentid": "vchr_source",
|
||||
"parentname": "代金券发放来源",
|
||||
"items": [
|
||||
{"k": "manual", "v": "手动发放"},
|
||||
{"k": "register", "v": "注册赠券"},
|
||||
{"k": "promo", "v": "促销活动"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"parentid": "vchr_enabled",
|
||||
"parentname": "代金券规则启用标志",
|
||||
"items": [
|
||||
{"k": "1", "v": "启用"},
|
||||
{"k": "0", "v": "禁用"}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -1,47 +1,130 @@
|
||||
{
|
||||
"tblname": "voucher_instance",
|
||||
"alias": "voucher_instance_list",
|
||||
"title": "代金券实例管理",
|
||||
"title": "代金券实例",
|
||||
"params": {
|
||||
"sortby": ["created_at desc"],
|
||||
"data_filter": {
|
||||
"AND": [
|
||||
{"field": "customer_id", "op": "=", "var": "customer_id"},
|
||||
{"field": "status", "op": "=", "var": "status"},
|
||||
{"field": "template_id", "op": "=", "var": "template_id"}
|
||||
]
|
||||
},
|
||||
"sortby": [
|
||||
"issued_at desc"
|
||||
],
|
||||
"browserfields": {
|
||||
"exclouded": [],
|
||||
"exclouded": [
|
||||
"id",
|
||||
"template_id"
|
||||
],
|
||||
"alters": {
|
||||
"template_id": {
|
||||
"uitype": "code",
|
||||
"dataurl": "{{entire_url('../api/template/voucher_template_options.dspy')}}",
|
||||
"data_field": "options",
|
||||
"textField": "text",
|
||||
"valueField": "value"
|
||||
},
|
||||
"status": {
|
||||
"uitype": "code",
|
||||
"data": [
|
||||
{"value": "unused", "text": "未使用"},
|
||||
{"value": "used", "text": "已使用"},
|
||||
{"value": "expired", "text": "已过期"}
|
||||
{
|
||||
"value": "unused",
|
||||
"text": "未使用"
|
||||
},
|
||||
{
|
||||
"value": "used",
|
||||
"text": "已使用"
|
||||
},
|
||||
{
|
||||
"value": "expired",
|
||||
"text": "已过期"
|
||||
},
|
||||
{
|
||||
"value": "void",
|
||||
"text": "已作废"
|
||||
}
|
||||
]
|
||||
},
|
||||
"source": {
|
||||
"uitype": "code",
|
||||
"data": [
|
||||
{
|
||||
"value": "manual",
|
||||
"text": "手动发放"
|
||||
},
|
||||
{
|
||||
"value": "register",
|
||||
"text": "注册赠券"
|
||||
},
|
||||
{
|
||||
"value": "promo",
|
||||
"text": "促销活动"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"editexclouded": ["used_at", "order_id", "actual_deducted"],
|
||||
"editexclouded": [
|
||||
"id",
|
||||
"code",
|
||||
"status",
|
||||
"face_value",
|
||||
"actual_deducted",
|
||||
"valid_from",
|
||||
"valid_to",
|
||||
"issued_at",
|
||||
"used_at",
|
||||
"order_id",
|
||||
"created_at",
|
||||
"updated_at"
|
||||
],
|
||||
"data_filter": {
|
||||
"AND": [
|
||||
{
|
||||
"field": "customer_id",
|
||||
"op": "=",
|
||||
"var": "customer_id"
|
||||
},
|
||||
{
|
||||
"field": "status",
|
||||
"op": "=",
|
||||
"var": "status"
|
||||
}
|
||||
]
|
||||
},
|
||||
"filter_labels": {
|
||||
"customer_id": "客户机构",
|
||||
"status": "状态"
|
||||
},
|
||||
"editable": {
|
||||
"new_data_url": "{{entire_url('../api/instance/voucher_instance_create.dspy')}}",
|
||||
"new_data_url": "{{entire_url('../api/instance/voucher_issue.dspy')}}",
|
||||
"update_data_url": "{{entire_url('../api/instance/voucher_instance_update.dspy')}}",
|
||||
"delete_data_url": "{{entire_url('../api/instance/voucher_instance_delete.dspy')}}"
|
||||
},
|
||||
"toolbar": {
|
||||
"tools": [
|
||||
{
|
||||
"name": "invalidate",
|
||||
"label": "作废",
|
||||
"selected_row": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"binds": [
|
||||
{
|
||||
"wid": "self",
|
||||
"event": "invalidate",
|
||||
"actiontype": "urlwidget",
|
||||
"target": "PopupWindow",
|
||||
"popup_options": {
|
||||
"title": "作废代金券",
|
||||
"width": "40%",
|
||||
"height": "46%",
|
||||
"archor": "cc",
|
||||
"resizable": true,
|
||||
"dismiss_events": [
|
||||
"cancel",
|
||||
"submited"
|
||||
]
|
||||
},
|
||||
"options": {
|
||||
"url": "{{entire_url('/voucher/invalidate_form.ui')}}"
|
||||
}
|
||||
}
|
||||
],
|
||||
"subtables": [
|
||||
{
|
||||
"field": "id",
|
||||
"field": "instance_id",
|
||||
"title": "使用记录",
|
||||
"url": "{{entire_url('../voucher_usage_log_list/')}}",
|
||||
"url": "{{entire_url('../voucher_usage_log_list')}}",
|
||||
"subtable": "voucher_usage_log"
|
||||
}
|
||||
]
|
||||
|
||||
@ -1,47 +1,91 @@
|
||||
{
|
||||
"tblname": "voucher_rule",
|
||||
"alias": "voucher_rule_list",
|
||||
"title": "代金券规则管理",
|
||||
"title": "代金券规则",
|
||||
"params": {
|
||||
"sortby": ["sort_order"],
|
||||
"data_filter": {
|
||||
"AND": [
|
||||
{"field": "template_id", "op": "=", "var": "template_id"},
|
||||
{"field": "rule_type", "op": "=", "var": "rule_type"}
|
||||
]
|
||||
},
|
||||
"sortby": [
|
||||
"sort_order"
|
||||
],
|
||||
"browserfields": {
|
||||
"exclouded": [],
|
||||
"exclouded": [
|
||||
"id"
|
||||
],
|
||||
"alters": {
|
||||
"template_id": {
|
||||
"uitype": "code",
|
||||
"dataurl": "{{entire_url('../api/template/voucher_template_options.dspy')}}",
|
||||
"data_field": "options",
|
||||
"textField": "text",
|
||||
"valueField": "value"
|
||||
},
|
||||
"rule_type": {
|
||||
"uitype": "code",
|
||||
"data": [
|
||||
{"value": "min_amount", "text": "最低消费"},
|
||||
{"value": "max_amount", "text": "最高消费"},
|
||||
{"value": "applicable_product_type", "text": "限定产品类型"},
|
||||
{"value": "applicable_product", "text": "限定特定产品"},
|
||||
{"value": "exclude_product", "text": "排除特定产品"},
|
||||
{"value": "max_usage_count", "text": "最大使用次数"},
|
||||
{"value": "valid_period", "text": "有效期"},
|
||||
{"value": "user_level", "text": "用户等级"}
|
||||
{
|
||||
"value": "min_amount",
|
||||
"text": "最低消费"
|
||||
},
|
||||
{
|
||||
"value": "max_amount",
|
||||
"text": "最高消费"
|
||||
},
|
||||
{
|
||||
"value": "product_type",
|
||||
"text": "限定产品类型"
|
||||
},
|
||||
{
|
||||
"value": "product",
|
||||
"text": "限定特定产品"
|
||||
},
|
||||
{
|
||||
"value": "exclude_product",
|
||||
"text": "排除特定产品"
|
||||
},
|
||||
{
|
||||
"value": "max_usage_count",
|
||||
"text": "最大使用次数"
|
||||
},
|
||||
{
|
||||
"value": "user_level",
|
||||
"text": "用户等级"
|
||||
}
|
||||
]
|
||||
},
|
||||
"enabled": {
|
||||
"uitype": "code",
|
||||
"data": [
|
||||
{"value": "1", "text": "启用"},
|
||||
{"value": "0", "text": "停用"}
|
||||
{
|
||||
"value": "1",
|
||||
"text": "启用"
|
||||
},
|
||||
{
|
||||
"value": "0",
|
||||
"text": "停用"
|
||||
}
|
||||
]
|
||||
},
|
||||
"rule_config": {
|
||||
"uitype": "text",
|
||||
"placeholder": "JSON 配置,如 {\"min_value\": 100} 或 {\"product_types\": [\"pipeline\"]}"
|
||||
}
|
||||
}
|
||||
},
|
||||
"editexclouded": [
|
||||
"id",
|
||||
"created_at",
|
||||
"updated_at"
|
||||
],
|
||||
"data_filter": {
|
||||
"AND": [
|
||||
{
|
||||
"field": "template_id",
|
||||
"op": "=",
|
||||
"var": "template_id"
|
||||
},
|
||||
{
|
||||
"field": "rule_type",
|
||||
"op": "=",
|
||||
"var": "rule_type"
|
||||
}
|
||||
]
|
||||
},
|
||||
"filter_labels": {
|
||||
"template_id": "模板",
|
||||
"rule_type": "规则类型"
|
||||
},
|
||||
"editable": {
|
||||
"new_data_url": "{{entire_url('../api/rule/voucher_rule_create.dspy')}}",
|
||||
"update_data_url": "{{entire_url('../api/rule/voucher_rule_update.dspy')}}",
|
||||
|
||||
@ -1,42 +1,105 @@
|
||||
{
|
||||
"tblname": "voucher_template",
|
||||
"alias": "voucher_template_list",
|
||||
"title": "代金券模板管理",
|
||||
"title": "代金券模板",
|
||||
"params": {
|
||||
"sortby": ["created_at desc"],
|
||||
"sortby": [
|
||||
"created_at desc"
|
||||
],
|
||||
"browserfields": {
|
||||
"exclouded": ["creator", "updater"],
|
||||
"exclouded": [
|
||||
"id",
|
||||
"org_id",
|
||||
"created_by"
|
||||
],
|
||||
"alters": {
|
||||
"status": {
|
||||
"uitype": "code",
|
||||
"data": [
|
||||
{"value": "draft", "text": "草稿"},
|
||||
{"value": "active", "text": "启用"},
|
||||
{"value": "inactive", "text": "停用"}
|
||||
{
|
||||
"value": "draft",
|
||||
"text": "草稿"
|
||||
},
|
||||
{
|
||||
"value": "active",
|
||||
"text": "启用"
|
||||
},
|
||||
{
|
||||
"value": "inactive",
|
||||
"text": "停用"
|
||||
}
|
||||
]
|
||||
},
|
||||
"face_value": {
|
||||
"uitype": "code",
|
||||
"data": []
|
||||
}
|
||||
}
|
||||
},
|
||||
"editexclouded": [
|
||||
"id",
|
||||
"code",
|
||||
"issued_count",
|
||||
"org_id",
|
||||
"created_by",
|
||||
"created_at",
|
||||
"updated_at"
|
||||
],
|
||||
"data_filter": {
|
||||
"AND": [
|
||||
{"field": "name", "op": "LIKE", "var": "name"},
|
||||
{"field": "status", "op": "=", "var": "status"}
|
||||
{
|
||||
"field": "name",
|
||||
"op": "LIKE",
|
||||
"var": "name"
|
||||
},
|
||||
{
|
||||
"field": "status",
|
||||
"op": "=",
|
||||
"var": "status"
|
||||
}
|
||||
]
|
||||
},
|
||||
"filter_labels": {
|
||||
"name": "模板名称",
|
||||
"status": "状态"
|
||||
},
|
||||
"editable": {
|
||||
"new_data_url": "{{entire_url('../api/template/voucher_template_create.dspy')}}",
|
||||
"update_data_url": "{{entire_url('../api/template/voucher_template_update.dspy')}}",
|
||||
"delete_data_url": "{{entire_url('../api/template/voucher_template_delete.dspy')}}"
|
||||
},
|
||||
"toolbar": {
|
||||
"tools": [
|
||||
{
|
||||
"name": "issue_from_tpl",
|
||||
"label": "发券",
|
||||
"selected_row": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"binds": [
|
||||
{
|
||||
"wid": "self",
|
||||
"event": "issue_from_tpl",
|
||||
"actiontype": "urlwidget",
|
||||
"target": "PopupWindow",
|
||||
"popup_options": {
|
||||
"title": "按此模板发券",
|
||||
"width": "46%",
|
||||
"height": "60%",
|
||||
"archor": "cc",
|
||||
"resizable": true,
|
||||
"dismiss_events": [
|
||||
"cancel",
|
||||
"submited"
|
||||
]
|
||||
},
|
||||
"options": {
|
||||
"url": "{{entire_url('/voucher/issue_form.ui')}}"
|
||||
}
|
||||
}
|
||||
],
|
||||
"subtables": [
|
||||
{
|
||||
"field": "id",
|
||||
"field": "template_id",
|
||||
"title": "规则配置",
|
||||
"url": "{{entire_url('../voucher_rule_list/')}}",
|
||||
"url": "{{entire_url('../voucher_rule_list')}}",
|
||||
"subtable": "voucher_rule"
|
||||
}
|
||||
]
|
||||
|
||||
@ -3,30 +3,40 @@
|
||||
"alias": "voucher_usage_log_list",
|
||||
"title": "代金券使用流水",
|
||||
"params": {
|
||||
"sortby": ["used_at desc"],
|
||||
"sortby": [
|
||||
"used_at desc"
|
||||
],
|
||||
"noedit": true,
|
||||
"browserfields": {
|
||||
"exclouded": [
|
||||
"id",
|
||||
"template_id"
|
||||
],
|
||||
"alters": {}
|
||||
},
|
||||
"data_filter": {
|
||||
"AND": [
|
||||
{"field": "instance_id", "op": "=", "var": "instance_id"},
|
||||
{"field": "customer_id", "op": "=", "var": "customer_id"},
|
||||
{"field": "order_id", "op": "=", "var": "order_id"}
|
||||
{
|
||||
"field": "instance_id",
|
||||
"op": "=",
|
||||
"var": "instance_id"
|
||||
},
|
||||
{
|
||||
"field": "customer_id",
|
||||
"op": "=",
|
||||
"var": "customer_id"
|
||||
},
|
||||
{
|
||||
"field": "order_id",
|
||||
"op": "LIKE",
|
||||
"var": "order_id"
|
||||
}
|
||||
]
|
||||
},
|
||||
"browserfields": {
|
||||
"exclouded": [],
|
||||
"alters": {
|
||||
"instance_id": {
|
||||
"uitype": "code",
|
||||
"dataurl": "{{entire_url('../api/instance/voucher_instance_options.dspy')}}",
|
||||
"data_field": "options",
|
||||
"textField": "text",
|
||||
"valueField": "value"
|
||||
}
|
||||
}
|
||||
},
|
||||
"editable": {
|
||||
"new_data_url": "{{entire_url('../api/usage/voucher_usage_log_create.dspy')}}",
|
||||
"update_data_url": "{{entire_url('../api/usage/voucher_usage_log_update.dspy')}}",
|
||||
"delete_data_url": "{{entire_url('../api/usage/voucher_usage_log_delete.dspy')}}"
|
||||
"filter_labels": {
|
||||
"instance_id": "券实例",
|
||||
"customer_id": "客户机构",
|
||||
"order_id": "订单ID"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,27 +10,27 @@
|
||||
"fields": [
|
||||
{"name": "id", "title": "主键ID", "type": "str", "length": 32, "nullable": "no"},
|
||||
{"name": "template_id", "title": "模板ID", "type": "str", "length": 32, "nullable": "no"},
|
||||
{"name": "customer_id", "title": "客户ID", "type": "str", "length": 32, "nullable": "no"},
|
||||
{"name": "code", "title": "唯一券码", "type": "str", "length": 64, "nullable": "no"},
|
||||
{"name": "customer_id", "title": "客户机构ID", "type": "str", "length": 32, "nullable": "no"},
|
||||
{"name": "code", "title": "券码", "type": "str", "length": 64, "nullable": "no"},
|
||||
{"name": "status", "title": "状态", "type": "str", "length": 16, "nullable": "no", "default": "unused"},
|
||||
{"name": "face_value", "title": "面值", "type": "double", "length": 10, "dec": 2, "nullable": "no"},
|
||||
{"name": "actual_deducted", "title": "实际抵扣金额", "type": "double", "length": 10, "dec": 2, "nullable": "yes"},
|
||||
{"name": "face_value", "title": "面值", "type": "double", "length": 18, "dec": 2, "nullable": "no"},
|
||||
{"name": "actual_deducted", "title": "实际抵扣金额", "type": "double", "length": 18, "dec": 2, "nullable": "yes"},
|
||||
{"name": "valid_from", "title": "生效时间", "type": "datetime", "nullable": "no"},
|
||||
{"name": "valid_to", "title": "过期时间", "type": "datetime", "nullable": "no"},
|
||||
{"name": "issued_at", "title": "发放时间", "type": "datetime", "nullable": "no"},
|
||||
{"name": "used_at", "title": "使用时间", "type": "datetime", "nullable": "yes"},
|
||||
{"name": "order_id", "title": "使用的订单ID", "type": "str", "length": 64, "nullable": "yes"},
|
||||
{"name": "source", "title": "来源", "type": "str", "length": 32, "nullable": "yes"},
|
||||
{"name": "remark", "title": "备注", "type": "str", "length": 255, "nullable": "yes"},
|
||||
{"name": "order_id", "title": "使用订单ID", "type": "str", "length": 64, "nullable": "yes"},
|
||||
{"name": "source", "title": "发放来源", "type": "str", "length": 32, "nullable": "no", "default": "manual"},
|
||||
{"name": "issued_by", "title": "发放人", "type": "str", "length": 32, "nullable": "yes"},
|
||||
{"name": "remark", "title": "备注", "type": "str", "length": 500, "nullable": "yes"},
|
||||
{"name": "created_at", "title": "创建时间", "type": "timestamp", "nullable": "no"},
|
||||
{"name": "updated_at", "title": "更新时间", "type": "timestamp", "nullable": "no"}
|
||||
],
|
||||
"indexes": [
|
||||
{"name": "idx_instance_code", "idxtype": "unique", "idxfields": ["code"]},
|
||||
{"name": "idx_instance_customer", "idxtype": "index", "idxfields": ["customer_id"]},
|
||||
{"name": "idx_instance_template", "idxtype": "index", "idxfields": ["template_id"]},
|
||||
{"name": "idx_instance_status", "idxtype": "index", "idxfields": ["status"]},
|
||||
{"name": "idx_instance_valid_to", "idxtype": "index", "idxfields": ["valid_to"]}
|
||||
{"name": "idx_vi_code", "idxtype": "unique", "idxfields": ["code"]},
|
||||
{"name": "idx_vi_customer", "idxtype": "index", "idxfields": ["customer_id", "status"]},
|
||||
{"name": "idx_vi_template", "idxtype": "index", "idxfields": ["template_id"]},
|
||||
{"name": "idx_vi_valid_to", "idxtype": "index", "idxfields": ["valid_to"]}
|
||||
],
|
||||
"codes": [
|
||||
{
|
||||
@ -39,12 +39,25 @@
|
||||
"valuefield": "id",
|
||||
"textfield": "name"
|
||||
},
|
||||
{
|
||||
"field": "customer_id",
|
||||
"table": "organization",
|
||||
"valuefield": "id",
|
||||
"textfield": "orgname"
|
||||
},
|
||||
{
|
||||
"field": "status",
|
||||
"table": "appcodes_kv",
|
||||
"valuefield": "k",
|
||||
"textfield": "v",
|
||||
"cond": "parentid='voucher_instance_status'"
|
||||
},
|
||||
{
|
||||
"field": "source",
|
||||
"table": "appcodes_kv",
|
||||
"valuefield": "k",
|
||||
"textfield": "v",
|
||||
"cond": "parentid='vchr_source'"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
"summary": [
|
||||
{
|
||||
"name": "voucher_rule",
|
||||
"title": "代金券规则定义",
|
||||
"title": "代金券规则",
|
||||
"primary": ["id"],
|
||||
"catelog": "entity"
|
||||
}
|
||||
@ -10,17 +10,16 @@
|
||||
"fields": [
|
||||
{"name": "id", "title": "主键ID", "type": "str", "length": 32, "nullable": "no"},
|
||||
{"name": "template_id", "title": "模板ID", "type": "str", "length": 32, "nullable": "no"},
|
||||
{"name": "rule_type", "title": "规则类型", "type": "str", "length": 32, "nullable": "no"},
|
||||
{"name": "rule_config", "title": "规则配置(JSON)", "type": "text", "nullable": "no"},
|
||||
{"name": "enabled", "title": "是否启用", "type": "short", "nullable": "no", "default": "1"},
|
||||
{"name": "rule_type", "title": "规则类型", "type": "str", "length": 64, "nullable": "no"},
|
||||
{"name": "rule_config", "title": "规则配置JSON", "type": "text", "nullable": "yes"},
|
||||
{"name": "enabled", "title": "是否启用", "type": "char", "length": 1, "nullable": "no", "default": "1"},
|
||||
{"name": "sort_order", "title": "执行顺序", "type": "int", "nullable": "no", "default": "0"},
|
||||
{"name": "remark", "title": "备注", "type": "str", "length": 255, "nullable": "yes"},
|
||||
{"name": "remark", "title": "备注", "type": "str", "length": 500, "nullable": "yes"},
|
||||
{"name": "created_at", "title": "创建时间", "type": "timestamp", "nullable": "no"},
|
||||
{"name": "updated_at", "title": "更新时间", "type": "timestamp", "nullable": "no"}
|
||||
],
|
||||
"indexes": [
|
||||
{"name": "idx_rule_template", "idxtype": "index", "idxfields": ["template_id"]},
|
||||
{"name": "idx_rule_enabled", "idxtype": "index", "idxfields": ["enabled"]}
|
||||
{"name": "idx_vr_template", "idxtype": "index", "idxfields": ["template_id"]}
|
||||
],
|
||||
"codes": [
|
||||
{
|
||||
@ -34,14 +33,14 @@
|
||||
"table": "appcodes_kv",
|
||||
"valuefield": "k",
|
||||
"textfield": "v",
|
||||
"cond": "parentid='voucher_rule_type'"
|
||||
"cond": "parentid='vchr_rule_type'"
|
||||
},
|
||||
{
|
||||
"field": "enabled",
|
||||
"table": "appcodes_kv",
|
||||
"valuefield": "k",
|
||||
"textfield": "v",
|
||||
"cond": "parentid='yes_no'"
|
||||
"cond": "parentid='vchr_enabled'"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -9,22 +9,22 @@
|
||||
],
|
||||
"fields": [
|
||||
{"name": "id", "title": "主键ID", "type": "str", "length": 32, "nullable": "no"},
|
||||
{"name": "name", "title": "模板名称", "type": "str", "length": 64, "nullable": "no"},
|
||||
{"name": "code", "title": "模板编码", "type": "str", "length": 32, "nullable": "no"},
|
||||
{"name": "face_value", "title": "面值", "type": "double", "length": 10, "dec": 2, "nullable": "no"},
|
||||
{"name": "total_count", "title": "总发行量", "type": "int", "nullable": "no", "default": "0"},
|
||||
{"name": "name", "title": "模板名称", "type": "str", "length": 128, "nullable": "no"},
|
||||
{"name": "code", "title": "模板编码", "type": "str", "length": 64, "nullable": "no"},
|
||||
{"name": "face_value", "title": "面值", "type": "double", "length": 18, "dec": 2, "nullable": "no"},
|
||||
{"name": "total_count", "title": "总发行量(0不限)", "type": "int", "nullable": "no", "default": "0"},
|
||||
{"name": "issued_count", "title": "已发放量", "type": "int", "nullable": "no", "default": "0"},
|
||||
{"name": "valid_days", "title": "有效期天数", "type": "int", "nullable": "no", "default": "30"},
|
||||
{"name": "status", "title": "状态", "type": "str", "length": 16, "nullable": "no", "default": "draft"},
|
||||
{"name": "remark", "title": "备注", "type": "str", "length": 255, "nullable": "yes"},
|
||||
{"name": "remark", "title": "备注", "type": "str", "length": 500, "nullable": "yes"},
|
||||
{"name": "org_id", "title": "所属机构ID", "type": "str", "length": 32, "nullable": "no", "default": "0"},
|
||||
{"name": "created_by", "title": "创建人", "type": "str", "length": 32, "nullable": "yes"},
|
||||
{"name": "created_at", "title": "创建时间", "type": "timestamp", "nullable": "no"},
|
||||
{"name": "updated_at", "title": "更新时间", "type": "timestamp", "nullable": "no"},
|
||||
{"name": "creator", "title": "创建人", "type": "str", "length": 32, "nullable": "yes"},
|
||||
{"name": "updater", "title": "更新人", "type": "str", "length": 32, "nullable": "yes"}
|
||||
{"name": "updated_at", "title": "更新时间", "type": "timestamp", "nullable": "no"}
|
||||
],
|
||||
"indexes": [
|
||||
{"name": "idx_template_code", "idxtype": "unique", "idxfields": ["code"]},
|
||||
{"name": "idx_template_status", "idxtype": "index", "idxfields": ["status"]}
|
||||
{"name": "idx_vt_code", "idxtype": "unique", "idxfields": ["code"]},
|
||||
{"name": "idx_vt_status", "idxtype": "index", "idxfields": ["status"]}
|
||||
],
|
||||
"codes": [
|
||||
{
|
||||
|
||||
@ -4,25 +4,27 @@
|
||||
"name": "voucher_usage_log",
|
||||
"title": "代金券使用流水",
|
||||
"primary": ["id"],
|
||||
"catelog": "entity"
|
||||
"catelog": "relation"
|
||||
}
|
||||
],
|
||||
"fields": [
|
||||
{"name": "id", "title": "主键ID", "type": "str", "length": 32, "nullable": "no"},
|
||||
{"name": "instance_id", "title": "券实例ID", "type": "str", "length": 32, "nullable": "no"},
|
||||
{"name": "customer_id", "title": "客户ID", "type": "str", "length": 32, "nullable": "no"},
|
||||
{"name": "order_id", "title": "订单/消费ID", "type": "str", "length": 64, "nullable": "no"},
|
||||
{"name": "face_value", "title": "券面值", "type": "double", "length": 10, "dec": 2, "nullable": "no"},
|
||||
{"name": "deducted_amount", "title": "实际抵扣金额", "type": "double", "length": 10, "dec": 2, "nullable": "no"},
|
||||
{"name": "template_id", "title": "模板ID", "type": "str", "length": 32, "nullable": "yes"},
|
||||
{"name": "customer_id", "title": "客户机构ID", "type": "str", "length": 32, "nullable": "no"},
|
||||
{"name": "order_id", "title": "订单ID", "type": "str", "length": 64, "nullable": "no", "default": ""},
|
||||
{"name": "face_value", "title": "券面值", "type": "double", "length": 18, "dec": 2, "nullable": "no"},
|
||||
{"name": "deducted_amount", "title": "实际抵扣金额", "type": "double", "length": 18, "dec": 2, "nullable": "no"},
|
||||
{"name": "product_type", "title": "产品类型", "type": "str", "length": 32, "nullable": "yes"},
|
||||
{"name": "product_name", "title": "产品名称", "type": "str", "length": 128, "nullable": "yes"},
|
||||
{"name": "used_at", "title": "使用时间", "type": "datetime", "nullable": "no"},
|
||||
{"name": "used_by", "title": "操作人", "type": "str", "length": 32, "nullable": "yes"},
|
||||
{"name": "product_type", "title": "产品类型", "type": "str", "length": 32, "nullable": "yes"},
|
||||
{"name": "product_name", "title": "产品名称", "type": "str", "length": 64, "nullable": "yes"}
|
||||
{"name": "created_at", "title": "创建时间", "type": "timestamp", "nullable": "no"}
|
||||
],
|
||||
"indexes": [
|
||||
{"name": "idx_log_instance", "idxtype": "index", "idxfields": ["instance_id"]},
|
||||
{"name": "idx_log_customer", "idxtype": "index", "idxfields": ["customer_id"]},
|
||||
{"name": "idx_log_order", "idxtype": "index", "idxfields": ["order_id"]}
|
||||
{"name": "idx_vul_instance", "idxtype": "index", "idxfields": ["instance_id"]},
|
||||
{"name": "idx_vul_customer", "idxtype": "index", "idxfields": ["customer_id"]},
|
||||
{"name": "idx_vul_order", "idxtype": "index", "idxfields": ["order_id"]}
|
||||
],
|
||||
"codes": [
|
||||
{
|
||||
@ -30,6 +32,12 @@
|
||||
"table": "voucher_instance",
|
||||
"valuefield": "id",
|
||||
"textfield": "code"
|
||||
},
|
||||
{
|
||||
"field": "customer_id",
|
||||
"table": "organization",
|
||||
"valuefield": "id",
|
||||
"textfield": "orgname"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -1,17 +1,17 @@
|
||||
[build-system]
|
||||
requires = ["setuptools>=45", "wheel"]
|
||||
requires = ["setuptools>=61.0"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "voucher"
|
||||
version = "1.0.0"
|
||||
description = "代金券模块:模板管理、可配置规则引擎、一次性使用代金券"
|
||||
requires-python = ">=3.8"
|
||||
version = "2.0.0"
|
||||
description = "代金券模块:模板/规则/实例/流水 + 可配置规则引擎 + 注册送券挂钩"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
dependencies = [
|
||||
"sqlor",
|
||||
"bricks_for_python",
|
||||
"bricks_for_python"
|
||||
]
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
where = ["."]
|
||||
include = ["voucher*"]
|
||||
[tool.setuptools]
|
||||
packages = ["voucher", "voucher.rules"]
|
||||
|
||||
@ -1 +0,0 @@
|
||||
# voucher rules engine
|
||||
198
rules/engine.py
198
rules/engine.py
@ -1,198 +0,0 @@
|
||||
"""代金券规则引擎:动态加载规则、校验、使用"""
|
||||
|
||||
import json
|
||||
from decimal import Decimal
|
||||
from datetime import datetime
|
||||
|
||||
from appPublic.uniqueID import getID
|
||||
from .registry import RULE_REGISTRY
|
||||
|
||||
|
||||
def validate_voucher(sor, instance_id, context):
|
||||
"""
|
||||
校验代金券是否可用
|
||||
|
||||
Args:
|
||||
sor: SQLor 上下文
|
||||
instance_id: 券实例ID
|
||||
context: dict, 包含 request_amount, product_type, product_name, user_level 等
|
||||
|
||||
Returns:
|
||||
(bool, Decimal, str): (可用, 抵扣金额, 错误信息)
|
||||
"""
|
||||
# 1. 查询券实例
|
||||
instance = sor.R('voucher_instance', {'id': instance_id}).first()
|
||||
if not instance:
|
||||
return False, Decimal('0'), "代金券不存在"
|
||||
|
||||
# 2. 状态检查(一次性使用)
|
||||
if instance.status != 'unused':
|
||||
return False, Decimal('0'), "代金券已使用或失效"
|
||||
|
||||
# 3. 有效期检查
|
||||
now = datetime.now()
|
||||
if instance.valid_from and now < instance.valid_from:
|
||||
return False, Decimal('0'), "代金券尚未生效"
|
||||
if instance.valid_to and now > instance.valid_to:
|
||||
return False, Decimal('0'), "代金券已过期"
|
||||
|
||||
# 4. 加载模板规则(按 sort_order 排序)
|
||||
rules = sor.R('voucher_rule', {
|
||||
'template_id': instance.template_id,
|
||||
'enabled': 1
|
||||
}, {'sort': 'sort_order'})
|
||||
|
||||
# 5. 构建执行上下文
|
||||
ctx = {
|
||||
'instance': instance,
|
||||
'used_count': instance.get('used_count', 0) if isinstance(instance, dict) else 0,
|
||||
'valid_from': instance.valid_from,
|
||||
'valid_to': instance.valid_to,
|
||||
**context
|
||||
}
|
||||
|
||||
# 6. 依次执行规则
|
||||
for rule in rules:
|
||||
rule_type = rule.rule_type if hasattr(rule, 'rule_type') else rule['rule_type']
|
||||
rule_config_str = rule.rule_config if hasattr(rule, 'rule_config') else rule['rule_config']
|
||||
|
||||
if rule_type not in RULE_REGISTRY:
|
||||
continue # 未知规则类型跳过
|
||||
|
||||
try:
|
||||
config = json.loads(rule_config_str) if isinstance(rule_config_str, str) else rule_config_str
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
continue
|
||||
|
||||
validator = RULE_REGISTRY[rule_type]
|
||||
ok, err_msg = validator(config, ctx)
|
||||
if not ok:
|
||||
return False, Decimal('0'), err_msg
|
||||
|
||||
# 7. 计算抵扣金额(一次性,不找零)
|
||||
face_value = Decimal(str(instance.face_value if hasattr(instance, 'face_value') else instance['face_value']))
|
||||
request_amount = Decimal(str(context.get('request_amount', 0)))
|
||||
deductible = min(face_value, request_amount)
|
||||
|
||||
return True, deductible, None
|
||||
|
||||
|
||||
def apply_voucher(sor, instance_id, customer_id, order_id, context):
|
||||
"""
|
||||
使用代金券(一次性,不找零)
|
||||
|
||||
Args:
|
||||
sor: SQLor 上下文
|
||||
instance_id: 券实例ID
|
||||
customer_id: 客户ID
|
||||
order_id: 订单ID
|
||||
context: dict, 包含 request_amount, product_type, product_name 等
|
||||
|
||||
Returns:
|
||||
(bool, Decimal, str): (成功, 抵扣金额, 错误信息)
|
||||
"""
|
||||
# 1. 校验
|
||||
ok, deductible, err = validate_voucher(sor, instance_id, context)
|
||||
if not ok:
|
||||
return False, Decimal('0'), err
|
||||
|
||||
# 2. 获取券实例信息
|
||||
instance = sor.R('voucher_instance', {'id': instance_id}).first()
|
||||
face_value = Decimal(str(instance.face_value if hasattr(instance, 'face_value') else instance['face_value']))
|
||||
|
||||
now = datetime.now()
|
||||
|
||||
# 3. 记录使用流水
|
||||
sor.I('voucher_usage_log', {
|
||||
'id': getID(),
|
||||
'instance_id': instance_id,
|
||||
'customer_id': customer_id,
|
||||
'order_id': order_id,
|
||||
'face_value': face_value,
|
||||
'deducted_amount': deductible,
|
||||
'used_at': now,
|
||||
'used_by': sor.env.get_user() if hasattr(sor, 'env') else None,
|
||||
'product_type': context.get('product_type', ''),
|
||||
'product_name': context.get('product_name', '')
|
||||
})
|
||||
|
||||
# 4. 标记券为已使用(一次性,直接作废)
|
||||
sor.U('voucher_instance', {'id': instance_id}, {
|
||||
'status': 'used',
|
||||
'actual_deducted': deductible,
|
||||
'used_at': now,
|
||||
'order_id': order_id
|
||||
})
|
||||
|
||||
return True, deductible, None
|
||||
|
||||
|
||||
def batch_apply_vouchers(sor, customer_id, order_id, voucher_ids, context):
|
||||
"""
|
||||
批量使用代金券(依次尝试,直到消费金额抵扣完)
|
||||
|
||||
Args:
|
||||
sor: SQLor 上下文
|
||||
customer_id: 客户ID
|
||||
order_id: 订单ID
|
||||
voucher_ids: 券实例ID列表
|
||||
context: dict, 包含 request_amount, product_type, product_name 等
|
||||
|
||||
Returns:
|
||||
dict: {total_deducted, remaining, details: [{voucher_id, deducted, error}]}
|
||||
"""
|
||||
total_deducted = Decimal('0')
|
||||
remaining = Decimal(str(context.get('request_amount', 0)))
|
||||
details = []
|
||||
|
||||
for vid in voucher_ids:
|
||||
if remaining <= 0:
|
||||
break
|
||||
|
||||
ctx = {**context, 'request_amount': remaining}
|
||||
ok, deducted, err = apply_voucher(sor, vid, customer_id, order_id, ctx)
|
||||
|
||||
if ok:
|
||||
total_deducted += deducted
|
||||
remaining -= deducted
|
||||
details.append({'voucher_id': vid, 'deducted': float(deducted), 'error': None})
|
||||
else:
|
||||
details.append({'voucher_id': vid, 'deducted': 0, 'error': err})
|
||||
|
||||
return {
|
||||
'total_deducted': float(total_deducted),
|
||||
'remaining': float(remaining),
|
||||
'details': details
|
||||
}
|
||||
|
||||
|
||||
def get_available_vouchers(sor, customer_id, context=None):
|
||||
"""
|
||||
查询客户可用代金券
|
||||
|
||||
Args:
|
||||
sor: SQLor 上下文
|
||||
customer_id: 客户ID
|
||||
context: 可选,用于预校验
|
||||
|
||||
Returns:
|
||||
list: 可用券实例列表
|
||||
"""
|
||||
now = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
vouchers = sor.R('voucher_instance', {
|
||||
'customer_id': customer_id,
|
||||
'status': 'unused'
|
||||
}, {'sort': 'valid_to'})
|
||||
|
||||
if not context:
|
||||
return list(vouchers)
|
||||
|
||||
# 带 context 时预校验过滤
|
||||
available = []
|
||||
for v in vouchers:
|
||||
vid = v.id if hasattr(v, 'id') else v['id']
|
||||
ok, _, _ = validate_voucher(sor, vid, context)
|
||||
if ok:
|
||||
available.append(v)
|
||||
|
||||
return available
|
||||
@ -1,21 +0,0 @@
|
||||
"""规则注册机制:新增规则只需写一个 validator 函数 + @register_rule 装饰器"""
|
||||
|
||||
RULE_REGISTRY = {}
|
||||
|
||||
|
||||
def register_rule(rule_type):
|
||||
"""装饰器:注册规则类型"""
|
||||
def decorator(func):
|
||||
RULE_REGISTRY[rule_type] = func
|
||||
return func
|
||||
return decorator
|
||||
|
||||
|
||||
def get_all_rule_types():
|
||||
"""返回所有已注册的规则类型"""
|
||||
return list(RULE_REGISTRY.keys())
|
||||
|
||||
|
||||
def get_rule_validator(rule_type):
|
||||
"""根据类型获取 validator 函数"""
|
||||
return RULE_REGISTRY.get(rule_type)
|
||||
@ -1,100 +0,0 @@
|
||||
"""内置规则校验器,每种规则一个函数,返回 (bool, error_msg)"""
|
||||
|
||||
from decimal import Decimal
|
||||
from datetime import datetime
|
||||
from .registry import register_rule
|
||||
|
||||
|
||||
@register_rule('min_amount')
|
||||
def check_min_amount(config, context):
|
||||
"""最低消费门槛"""
|
||||
min_val = Decimal(str(config.get('min_value', 0)))
|
||||
request_amount = Decimal(str(context.get('request_amount', 0)))
|
||||
if request_amount < min_val:
|
||||
return False, f"未达最低消费 {min_val} 元"
|
||||
return True, None
|
||||
|
||||
|
||||
@register_rule('max_amount')
|
||||
def check_max_amount(config, context):
|
||||
"""最高消费限制"""
|
||||
max_val = Decimal(str(config.get('max_value', 0)))
|
||||
if max_val <= 0:
|
||||
return True, None
|
||||
request_amount = Decimal(str(context.get('request_amount', 0)))
|
||||
if request_amount > max_val:
|
||||
return False, f"超过最高消费 {max_val} 元"
|
||||
return True, None
|
||||
|
||||
|
||||
@register_rule('applicable_product_type')
|
||||
def check_product_type(config, context):
|
||||
"""限定产品类型(大类:llm/image/video/audio)"""
|
||||
allowed_types = config.get('product_types', [])
|
||||
if not allowed_types:
|
||||
return True, None
|
||||
current_type = context.get('product_type', '')
|
||||
if current_type and current_type not in allowed_types:
|
||||
return False, f"仅限 {', '.join(allowed_types)} 类型使用"
|
||||
return True, None
|
||||
|
||||
|
||||
@register_rule('applicable_product')
|
||||
def check_specific_product(config, context):
|
||||
"""限定特定产品(具体模型名)"""
|
||||
allowed_products = config.get('products', [])
|
||||
if not allowed_products:
|
||||
return True, None
|
||||
current_product = context.get('product_name', '')
|
||||
if current_product and current_product not in allowed_products:
|
||||
return False, f"仅限 {', '.join(allowed_products)} 使用"
|
||||
return True, None
|
||||
|
||||
|
||||
@register_rule('exclude_product')
|
||||
def check_exclude_product(config, context):
|
||||
"""排除特定产品"""
|
||||
excluded = config.get('products', [])
|
||||
if not excluded:
|
||||
return True, None
|
||||
current_product = context.get('product_name', '')
|
||||
if current_product and current_product in excluded:
|
||||
return False, f"{current_product} 不可使用此代金券"
|
||||
return True, None
|
||||
|
||||
|
||||
@register_rule('max_usage_count')
|
||||
def check_max_usage_count(config, context):
|
||||
"""最大使用次数"""
|
||||
max_count = int(config.get('max_count', 0))
|
||||
if max_count == 0:
|
||||
return True, None
|
||||
used_count = int(context.get('used_count', 0))
|
||||
if used_count >= max_count:
|
||||
return False, f"已达最大使用次数 {max_count} 次"
|
||||
return True, None
|
||||
|
||||
|
||||
@register_rule('valid_period')
|
||||
def check_valid_period(config, context):
|
||||
"""有效期检查(通常由 instance 的 valid_from/valid_to 自动处理,此规则用于额外限制)"""
|
||||
now = datetime.now()
|
||||
valid_from = context.get('valid_from')
|
||||
valid_to = context.get('valid_to')
|
||||
if valid_from and now < valid_from:
|
||||
return False, "代金券尚未生效"
|
||||
if valid_to and now > valid_to:
|
||||
return False, "代金券已过期"
|
||||
return True, None
|
||||
|
||||
|
||||
@register_rule('user_level')
|
||||
def check_user_level(config, context):
|
||||
"""用户等级限制"""
|
||||
required_level = int(config.get('min_level', 0))
|
||||
if required_level == 0:
|
||||
return True, None
|
||||
user_level = int(context.get('user_level', 0))
|
||||
if user_level < required_level:
|
||||
return False, f"需 {required_level} 级以上用户"
|
||||
return True, None
|
||||
@ -1,76 +1,99 @@
|
||||
#!/usr/bin/env python3
|
||||
"""voucher 模块 RBAC 权限注册"""
|
||||
import os
|
||||
"""voucher 模块 RBAC 路径权限注册(pipeline-app 格式,仿 ticket/scripts/load_path.py)。
|
||||
|
||||
从应用根执行:py3/bin/python pkgs/voucher/scripts/load_path.py
|
||||
(load_path.sh 自动扫描 pkgs/*/scripts/load_path.py 并调用本脚本)
|
||||
|
||||
权限设计(逐个精确配,不用统一兜底):
|
||||
- logined:客户侧「我的代金券」页面 + 自助查询 API + 管理侧全部页面与 API。
|
||||
管理类数据面由服务端 org_id 隔离(模板带 org_id 列,CRUD logined_userorgid 机制
|
||||
不适用——平台运营代客户管券是本职,不做 org 过滤;误操作面由 helper 内校验兜底:
|
||||
模板改删校验 org、实例改/作废只动 unused 券)。
|
||||
- CRUD 生成目录(voucher_*_list/)四件套(index.ui + get/add/update/delete dspy)
|
||||
逐一注册。
|
||||
"""
|
||||
import sys
|
||||
import subprocess
|
||||
import os
|
||||
import asyncio
|
||||
|
||||
def find_sage_root():
|
||||
for candidate in [
|
||||
os.path.expanduser("~/repos/sage"),
|
||||
os.path.expanduser("~/sage"),
|
||||
]:
|
||||
if os.path.isdir(os.path.join(candidate, "wwwroot")):
|
||||
return candidate
|
||||
return None
|
||||
# 应用根:本脚本在 pkgs/voucher/scripts/ 下,上溯三级
|
||||
APP_ROOT = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), "../../.."))
|
||||
sys.path.insert(0, os.path.join(APP_ROOT, "py3", "lib", "python3.10", "site-packages"))
|
||||
sys.path.insert(0, APP_ROOT)
|
||||
|
||||
def set_perm(sage_root, role, path):
|
||||
script = os.path.join(sage_root, "set_role_perm.py")
|
||||
cmd = [os.path.join(sage_root, "py3/bin/python"), script, role, path]
|
||||
print(f" {role} {path}")
|
||||
subprocess.run(cmd, cwd=sage_root, capture_output=True)
|
||||
from sqlor.dbpools import DBPools
|
||||
from appPublic.jsonConfig import getConfig
|
||||
from appPublic.uniqueID import getID
|
||||
|
||||
def main():
|
||||
sage_root = find_sage_root()
|
||||
if not sage_root:
|
||||
print("ERROR: Sage root not found")
|
||||
sys.exit(1)
|
||||
MOD = "voucher"
|
||||
|
||||
print("Registering voucher RBAC permissions...")
|
||||
# CRUD 生成目录四件套(alias -> 表名)
|
||||
CRUD_DIRS = {
|
||||
"voucher_template_list": "voucher_template",
|
||||
"voucher_rule_list": "voucher_rule",
|
||||
"voucher_instance_list": "voucher_instance",
|
||||
"voucher_usage_log_list": "voucher_usage_log",
|
||||
}
|
||||
|
||||
# any (公开)
|
||||
any_paths = [
|
||||
"/voucher/menu.ui",
|
||||
PATHS_LOGINED = [
|
||||
# 客户侧
|
||||
f"/{MOD}/my",
|
||||
f"/{MOD}/my/index.ui",
|
||||
f"/{MOD}/api/my_vouchers.dspy",
|
||||
f"/{MOD}/api/v1/available.dspy",
|
||||
# 管理侧页面/表单
|
||||
f"/{MOD}/index.ui",
|
||||
f"/{MOD}/issue_form.ui",
|
||||
f"/{MOD}/invalidate_form.ui",
|
||||
# 管理侧 API
|
||||
f"/{MOD}/api/get_available.dspy",
|
||||
f"/{MOD}/api/apply_voucher.dspy",
|
||||
f"/{MOD}/api/rule_types.dspy",
|
||||
f"/{MOD}/api/template/voucher_template_create.dspy",
|
||||
f"/{MOD}/api/template/voucher_template_update.dspy",
|
||||
f"/{MOD}/api/template/voucher_template_delete.dspy",
|
||||
f"/{MOD}/api/template/voucher_template_options.dspy",
|
||||
f"/{MOD}/api/rule/voucher_rule_create.dspy",
|
||||
f"/{MOD}/api/rule/voucher_rule_update.dspy",
|
||||
f"/{MOD}/api/rule/voucher_rule_delete.dspy",
|
||||
f"/{MOD}/api/instance/voucher_issue.dspy",
|
||||
f"/{MOD}/api/instance/voucher_instance_update.dspy",
|
||||
f"/{MOD}/api/instance/voucher_instance_delete.dspy",
|
||||
f"/{MOD}/api/instance/voucher_invalidate.dspy",
|
||||
]
|
||||
|
||||
# CRUD 目录(框架生成,路径必须逐个注册)
|
||||
for alias, tbl in CRUD_DIRS.items():
|
||||
PATHS_LOGINED += [
|
||||
f"/{MOD}/{alias}",
|
||||
f"/{MOD}/{alias}/index.ui",
|
||||
f"/{MOD}/{alias}/get_{tbl}.dspy",
|
||||
f"/{MOD}/{alias}/add_{tbl}.dspy",
|
||||
f"/{MOD}/{alias}/update_{tbl}.dspy",
|
||||
f"/{MOD}/{alias}/delete_{tbl}.dspy",
|
||||
]
|
||||
|
||||
# logined (登录后)
|
||||
logined_paths = [
|
||||
"/voucher",
|
||||
"/voucher/index.ui",
|
||||
"/voucher/voucher_template_list",
|
||||
"/voucher/voucher_template_list/index.ui",
|
||||
"/voucher/voucher_rule_list",
|
||||
"/voucher/voucher_rule_list/index.ui",
|
||||
"/voucher/voucher_instance_list",
|
||||
"/voucher/voucher_instance_list/index.ui",
|
||||
"/voucher/voucher_usage_log_list",
|
||||
"/voucher/voucher_usage_log_list/index.ui",
|
||||
"/voucher/api/template/voucher_template_create.dspy",
|
||||
"/voucher/api/template/voucher_template_update.dspy",
|
||||
"/voucher/api/template/voucher_template_delete.dspy",
|
||||
"/voucher/api/template/voucher_template_options.dspy",
|
||||
"/voucher/api/rule/voucher_rule_create.dspy",
|
||||
"/voucher/api/rule/voucher_rule_update.dspy",
|
||||
"/voucher/api/rule/voucher_rule_delete.dspy",
|
||||
"/voucher/api/instance/voucher_instance_create.dspy",
|
||||
"/voucher/api/instance/voucher_instance_update.dspy",
|
||||
"/voucher/api/instance/voucher_instance_delete.dspy",
|
||||
"/voucher/api/instance/voucher_instance_options.dspy",
|
||||
"/voucher/api/usage/voucher_usage_log_create.dspy",
|
||||
"/voucher/api/usage/voucher_usage_log_update.dspy",
|
||||
"/voucher/api/usage/voucher_usage_log_delete.dspy",
|
||||
"/voucher/api/v1/available.dspy",
|
||||
"/voucher/api/apply_voucher.dspy",
|
||||
"/voucher/api/get_available.dspy",
|
||||
"/voucher/api/rule_types.dspy",
|
||||
]
|
||||
|
||||
for p in any_paths:
|
||||
set_perm(sage_root, "any", p)
|
||||
async def main():
|
||||
config = getConfig(APP_ROOT, NS={"workdir": APP_ROOT})
|
||||
DBPools(config.databases)
|
||||
cnt = 0
|
||||
async with DBPools().sqlorContext("pipeline") as sor:
|
||||
for path in PATHS_LOGINED:
|
||||
recs = await sor.R("permission", {"path": path})
|
||||
if recs:
|
||||
permid = recs[0].id
|
||||
else:
|
||||
permid = getID()
|
||||
await sor.C("permission", {"id": permid, "path": path})
|
||||
rp = await sor.R("rolepermission", {"roleid": "logined", "permid": permid})
|
||||
if not rp:
|
||||
await sor.C("rolepermission", {
|
||||
"id": getID(), "roleid": "logined", "permid": permid})
|
||||
cnt += 1
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
print(f"voucher load_path: {cnt} logined path-role entries ensured")
|
||||
|
||||
for p in logined_paths:
|
||||
set_perm(sage_root, "logined", p)
|
||||
|
||||
print("Done.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
asyncio.run(main())
|
||||
|
||||
65
scripts/seed_welcome_voucher.py
Normal file
65
scripts/seed_welcome_voucher.py
Normal file
@ -0,0 +1,65 @@
|
||||
#!/usr/bin/env python3
|
||||
"""voucher 种子:注册送 400 元欢迎券模板(WELCOME400)+ 规则。
|
||||
|
||||
幂等:模板按 code 判重存在即跳过(管理员后续在界面调整不受影响)。
|
||||
活动参数(2026-09-06 用户定夺):
|
||||
- 面值 400 元,前 100 名(total_count=100 原子占额控制),30 天有效
|
||||
- 规则:仅限产线订阅类产品(product_type ∈ [pipeline])
|
||||
- 状态直接 active(注册挂钩只认 active 模板)
|
||||
结束活动:管理界面把模板置 inactive 即可,无需改代码。
|
||||
|
||||
从应用根执行:py3/bin/python pkgs/voucher/scripts/seed_welcome_voucher.py
|
||||
"""
|
||||
import sys
|
||||
import os
|
||||
import asyncio
|
||||
|
||||
APP_ROOT = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), "../../.."))
|
||||
sys.path.insert(0, os.path.join(APP_ROOT, "py3", "lib", "python3.10", "site-packages"))
|
||||
sys.path.insert(0, APP_ROOT)
|
||||
|
||||
from sqlor.dbpools import DBPools
|
||||
from appPublic.jsonConfig import getConfig
|
||||
from appPublic.uniqueID import getID
|
||||
|
||||
WELCOME_CODE = "WELCOME400"
|
||||
|
||||
|
||||
async def main():
|
||||
config = getConfig(APP_ROOT, NS={"workdir": APP_ROOT})
|
||||
DBPools(config.databases)
|
||||
async with DBPools().sqlorContext("pipeline") as sor:
|
||||
recs = await sor.R("voucher_template", {"code": WELCOME_CODE})
|
||||
if recs:
|
||||
print(f"seed: {WELCOME_CODE} already exists (id={recs[0].id}), skip")
|
||||
return
|
||||
tid = getID()
|
||||
await sor.C("voucher_template", {
|
||||
"id": tid,
|
||||
"name": "新客户欢迎券",
|
||||
"code": WELCOME_CODE,
|
||||
"face_value": 400.0,
|
||||
"total_count": 100,
|
||||
"issued_count": 0,
|
||||
"valid_days": 30,
|
||||
"status": "active",
|
||||
"remark": "注册自动赠送,前100名,仅限产线订阅类产品,30天有效",
|
||||
"org_id": "0",
|
||||
"created_by": "system",
|
||||
})
|
||||
rid = getID()
|
||||
await sor.C("voucher_rule", {
|
||||
"id": rid,
|
||||
"template_id": tid,
|
||||
"rule_type": "product_type",
|
||||
"rule_config": '{"product_types": ["pipeline"]}',
|
||||
"enabled": "1",
|
||||
"sort_order": 1,
|
||||
"remark": "仅限产线订阅类产品(400券只能买产线)",
|
||||
})
|
||||
await sor.sqlExe("COMMIT", {})
|
||||
print(f"seed: {WELCOME_CODE} created (template_id={tid}, rule_id={rid})")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
107
sql/tables.sql
107
sql/tables.sql
@ -1,107 +0,0 @@
|
||||
-- ============================================
|
||||
-- 代金券模块建表脚本
|
||||
-- ============================================
|
||||
|
||||
-- 代金券模板表
|
||||
CREATE TABLE IF NOT EXISTS voucher_template (
|
||||
id VARCHAR(32) NOT NULL COMMENT '主键ID',
|
||||
name VARCHAR(64) NOT NULL COMMENT '模板名称',
|
||||
code VARCHAR(32) NOT NULL COMMENT '模板编码',
|
||||
face_value DECIMAL(10,2) NOT NULL COMMENT '面值',
|
||||
total_count INT NOT NULL DEFAULT 0 COMMENT '总发行量',
|
||||
issued_count INT NOT NULL DEFAULT 0 COMMENT '已发放量',
|
||||
valid_days INT NOT NULL DEFAULT 30 COMMENT '有效期天数',
|
||||
status VARCHAR(16) NOT NULL DEFAULT 'draft' COMMENT '状态: draft/active/inactive',
|
||||
remark VARCHAR(255) DEFAULT NULL COMMENT '备注',
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
creator VARCHAR(32) DEFAULT NULL COMMENT '创建人',
|
||||
updater VARCHAR(32) DEFAULT NULL COMMENT '更新人',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY idx_template_code (code),
|
||||
KEY idx_template_status (status)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='代金券模板';
|
||||
|
||||
-- 代金券规则表
|
||||
CREATE TABLE IF NOT EXISTS voucher_rule (
|
||||
id VARCHAR(32) NOT NULL COMMENT '主键ID',
|
||||
template_id VARCHAR(32) NOT NULL COMMENT '模板ID',
|
||||
rule_type VARCHAR(32) NOT NULL COMMENT '规则类型',
|
||||
rule_config TEXT NOT NULL COMMENT '规则配置(JSON)',
|
||||
enabled SMALLINT NOT NULL DEFAULT 1 COMMENT '是否启用',
|
||||
sort_order INT NOT NULL DEFAULT 0 COMMENT '执行顺序',
|
||||
remark VARCHAR(255) DEFAULT NULL COMMENT '备注',
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_rule_template (template_id),
|
||||
KEY idx_rule_enabled (enabled)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='代金券规则定义';
|
||||
|
||||
-- 代金券实例表
|
||||
CREATE TABLE IF NOT EXISTS voucher_instance (
|
||||
id VARCHAR(32) NOT NULL COMMENT '主键ID',
|
||||
template_id VARCHAR(32) NOT NULL COMMENT '模板ID',
|
||||
customer_id VARCHAR(32) NOT NULL COMMENT '客户ID',
|
||||
code VARCHAR(64) NOT NULL COMMENT '唯一券码',
|
||||
status VARCHAR(16) NOT NULL DEFAULT 'unused' COMMENT '状态: unused/used/expired',
|
||||
face_value DECIMAL(10,2) NOT NULL COMMENT '面值',
|
||||
actual_deducted DECIMAL(10,2) DEFAULT NULL COMMENT '实际抵扣金额',
|
||||
valid_from DATETIME NOT NULL COMMENT '生效时间',
|
||||
valid_to DATETIME NOT NULL COMMENT '过期时间',
|
||||
issued_at DATETIME NOT NULL COMMENT '发放时间',
|
||||
used_at DATETIME DEFAULT NULL COMMENT '使用时间',
|
||||
order_id VARCHAR(64) DEFAULT NULL COMMENT '使用的订单ID',
|
||||
source VARCHAR(32) DEFAULT NULL COMMENT '来源',
|
||||
remark VARCHAR(255) DEFAULT NULL COMMENT '备注',
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY idx_instance_code (code),
|
||||
KEY idx_instance_customer (customer_id),
|
||||
KEY idx_instance_template (template_id),
|
||||
KEY idx_instance_status (status),
|
||||
KEY idx_instance_valid_to (valid_to)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='代金券实例(一次性使用)';
|
||||
|
||||
-- 代金券使用流水表
|
||||
CREATE TABLE IF NOT EXISTS voucher_usage_log (
|
||||
id VARCHAR(32) NOT NULL COMMENT '主键ID',
|
||||
instance_id VARCHAR(32) NOT NULL COMMENT '券实例ID',
|
||||
customer_id VARCHAR(32) NOT NULL COMMENT '客户ID',
|
||||
order_id VARCHAR(64) NOT NULL COMMENT '订单/消费ID',
|
||||
face_value DECIMAL(10,2) NOT NULL COMMENT '券面值',
|
||||
deducted_amount DECIMAL(10,2) NOT NULL COMMENT '实际抵扣金额',
|
||||
used_at DATETIME NOT NULL COMMENT '使用时间',
|
||||
used_by VARCHAR(32) DEFAULT NULL COMMENT '操作人',
|
||||
product_type VARCHAR(32) DEFAULT NULL COMMENT '产品类型',
|
||||
product_name VARCHAR(64) DEFAULT NULL COMMENT '产品名称',
|
||||
PRIMARY KEY (id),
|
||||
KEY idx_log_instance (instance_id),
|
||||
KEY idx_log_customer (customer_id),
|
||||
KEY idx_log_order (order_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='代金券使用流水';
|
||||
|
||||
-- 初始化编码数据
|
||||
INSERT INTO appcodes (id, name, hierarchy_flg) VALUES
|
||||
('voucher_template_status', '代金券模板状态', '0'),
|
||||
('voucher_instance_status', '代金券实例状态', '0'),
|
||||
('voucher_rule_type', '代金券规则类型', '0')
|
||||
ON DUPLICATE KEY UPDATE name=VALUES(name);
|
||||
|
||||
INSERT INTO appcodes_kv (id, parentid, k, v) VALUES
|
||||
('vts_draft', 'voucher_template_status', 'draft', '草稿'),
|
||||
('vts_active', 'voucher_template_status', 'active', '启用'),
|
||||
('vts_inactive', 'voucher_template_status', 'inactive', '停用'),
|
||||
('vis_unused', 'voucher_instance_status', 'unused', '未使用'),
|
||||
('vis_used', 'voucher_instance_status', 'used', '已使用'),
|
||||
('vis_expired', 'voucher_instance_status', 'expired', '已过期'),
|
||||
('vrt_min', 'voucher_rule_type', 'min_amount', '最低消费'),
|
||||
('vrt_max', 'voucher_rule_type', 'max_amount', '最高消费'),
|
||||
('vrt_ptype', 'voucher_rule_type', 'applicable_product_type', '限定产品类型'),
|
||||
('vrt_prod', 'voucher_rule_type', 'applicable_product', '限定特定产品'),
|
||||
('vrt_exprod', 'voucher_rule_type', 'exclude_product', '排除特定产品'),
|
||||
('vrt_usage', 'voucher_rule_type', 'max_usage_count', '最大使用次数'),
|
||||
('vrt_period', 'voucher_rule_type', 'valid_period', '有效期'),
|
||||
('vrt_level', 'voucher_rule_type', 'user_level', '用户等级')
|
||||
ON DUPLICATE KEY UPDATE v=VALUES(v);
|
||||
@ -1 +1,4 @@
|
||||
# voucher module
|
||||
"""voucher 代金券模块"""
|
||||
from voucher.init import load_voucher
|
||||
|
||||
__all__ = ['load_voucher']
|
||||
|
||||
689
voucher/init.py
689
voucher/init.py
@ -1,242 +1,519 @@
|
||||
"""voucher 模块初始化"""
|
||||
"""voucher 代金券模块:对外 API(注册到 ServerEnv 供 dspy 裸名调用)。
|
||||
|
||||
架构:模板(voucher_template) → 规则集(voucher_rule) → 实例(voucher_instance) → 流水(voucher_usage_log)
|
||||
注册挂钩:bind pipeline:organization:c:after 事件——新客户机构注册即自动送欢迎券
|
||||
(前 N 名由模板 total_count 发行量原子控制,活动结束把模板置 inactive 即可)。
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from ahserver.serverenv import ServerEnv
|
||||
from appPublic.jsonConfig import getConfig
|
||||
from appPublic.log import debug, exception, info
|
||||
from appPublic.uniqueID import getID
|
||||
from sqlor.dbpools import DBPools
|
||||
|
||||
MODULE_NAME = "voucher"
|
||||
MODULE_VERSION = "1.0.0"
|
||||
DBNAME = "sage"
|
||||
from voucher.rules.engine import (
|
||||
apply_voucher,
|
||||
batch_apply_vouchers,
|
||||
expire_vouchers,
|
||||
get_available_vouchers,
|
||||
issue_voucher,
|
||||
validate_voucher,
|
||||
)
|
||||
from voucher.rules.registry import get_all_rule_types
|
||||
|
||||
MODULE_NAME = 'voucher'
|
||||
MODULE_VERSION = '2.0.0'
|
||||
|
||||
# datetime 字段(sqlor 返回 python datetime,框架 json.dumps 无 default=str 会崩)
|
||||
_DT_FIELDS = ('valid_from', 'valid_to', 'issued_at', 'used_at', 'created_at', 'updated_at')
|
||||
|
||||
|
||||
def _jsonable_instance(item):
|
||||
"""实例行 → JSON 安全(datetime 转 str,金额转 float)。"""
|
||||
for k in _DT_FIELDS:
|
||||
if item.get(k) is not None:
|
||||
item[k] = str(item[k])
|
||||
for k in ('face_value', 'actual_deducted'):
|
||||
if item.get(k) is not None:
|
||||
item[k] = float(item[k])
|
||||
return item
|
||||
|
||||
# 注册送券活动的模板编码(init/data.json 种子模板,部署后管理员可在界面调整)
|
||||
WELCOME_TEMPLATE_CODE = 'WELCOME400'
|
||||
|
||||
|
||||
def _get_dbname():
|
||||
return DBNAME
|
||||
"""库名一律由宿主决定(get_module_dbname),不硬编码。"""
|
||||
env = ServerEnv()
|
||||
fn = getattr(env, 'get_module_dbname', None)
|
||||
if callable(fn):
|
||||
name = fn(MODULE_NAME)
|
||||
if name:
|
||||
return name
|
||||
return 'pipeline'
|
||||
|
||||
|
||||
# ========== Template CRUD ==========
|
||||
|
||||
async def create_voucher_template(data):
|
||||
sor = DBPools().sqlorContext(_get_dbname())
|
||||
data['id'] = getID()
|
||||
data['issued_count'] = 0
|
||||
if 'status' not in data:
|
||||
data['status'] = 'draft'
|
||||
sor.I('voucher_template', data)
|
||||
return {'status': 'success', 'data': {'id': data['id']}}
|
||||
def _get_db():
|
||||
db = DBPools()
|
||||
if not getattr(db, 'databases', None):
|
||||
config = getConfig()
|
||||
db.databases = config.databases
|
||||
return db
|
||||
|
||||
|
||||
async def update_voucher_template(conditions, data):
|
||||
sor = DBPools().sqlorContext(_get_dbname())
|
||||
sor.U('voucher_template', conditions, data)
|
||||
return {'status': 'success'}
|
||||
|
||||
|
||||
async def delete_voucher_template(conditions):
|
||||
sor = DBPools().sqlorContext(_get_dbname())
|
||||
# 级联删除关联规则
|
||||
sor.D('voucher_rule', {'template_id': conditions.get('id', '')})
|
||||
sor.D('voucher_template', conditions)
|
||||
return {'status': 'success'}
|
||||
|
||||
|
||||
# ========== Rule CRUD ==========
|
||||
|
||||
async def create_voucher_rule(data):
|
||||
sor = DBPools().sqlorContext(_get_dbname())
|
||||
data['id'] = getID()
|
||||
if 'enabled' not in data:
|
||||
data['enabled'] = 1
|
||||
if 'sort_order' not in data:
|
||||
data['sort_order'] = 0
|
||||
sor.I('voucher_rule', data)
|
||||
return {'status': 'success', 'data': {'id': data['id']}}
|
||||
|
||||
|
||||
async def update_voucher_rule(conditions, data):
|
||||
sor = DBPools().sqlorContext(_get_dbname())
|
||||
sor.U('voucher_rule', conditions, data)
|
||||
return {'status': 'success'}
|
||||
|
||||
|
||||
async def delete_voucher_rule(conditions):
|
||||
sor = DBPools().sqlorContext(_get_dbname())
|
||||
sor.D('voucher_rule', conditions)
|
||||
return {'status': 'success'}
|
||||
|
||||
|
||||
# ========== Instance CRUD ==========
|
||||
|
||||
async def create_voucher_instance(data):
|
||||
from datetime import datetime, timedelta
|
||||
sor = DBPools().sqlorContext(_get_dbname())
|
||||
|
||||
data['id'] = getID()
|
||||
data['status'] = 'unused'
|
||||
|
||||
# 从模板获取面值
|
||||
template = sor.R('voucher_template', {'id': data.get('template_id', '')}).first()
|
||||
if template:
|
||||
data['face_value'] = template.face_value if hasattr(template, 'face_value') else template['face_value']
|
||||
valid_days = template.valid_days if hasattr(template, 'valid_days') else template.get('valid_days', 30)
|
||||
else:
|
||||
valid_days = data.get('valid_days', 30)
|
||||
|
||||
# 设置有效期
|
||||
now = datetime.now()
|
||||
data['valid_from'] = data.get('valid_from', now.strftime('%Y-%m-%d %H:%M:%S'))
|
||||
data['valid_to'] = data.get('valid_to', (now + timedelta(days=valid_days)).strftime('%Y-%m-%d %H:%M:%S'))
|
||||
data['issued_at'] = now.strftime('%Y-%m-%d %H:%M:%S')
|
||||
|
||||
# 生成唯一券码
|
||||
if 'code' not in data or not data['code']:
|
||||
import hashlib
|
||||
raw = f"{data['id']}-{data.get('customer_id', '')}-{now.timestamp()}"
|
||||
data['code'] = 'VCH-' + hashlib.md5(raw.encode()).hexdigest()[:12].upper()
|
||||
|
||||
sor.I('voucher_instance', data)
|
||||
|
||||
# 更新模板已发放量
|
||||
if template:
|
||||
tid = data.get('template_id', '')
|
||||
sor.db_execute(f"UPDATE voucher_template SET issued_count = issued_count + 1 WHERE id = '{tid}'")
|
||||
|
||||
return {'status': 'success', 'data': {'id': data['id'], 'code': data['code']}}
|
||||
|
||||
|
||||
async def update_voucher_instance(conditions, data):
|
||||
sor = DBPools().sqlorContext(_get_dbname())
|
||||
sor.U('voucher_instance', conditions, data)
|
||||
return {'status': 'success'}
|
||||
|
||||
|
||||
async def delete_voucher_instance(conditions):
|
||||
sor = DBPools().sqlorContext(_get_dbname())
|
||||
sor.D('voucher_usage_log', {'instance_id': conditions.get('id', '')})
|
||||
sor.D('voucher_instance', conditions)
|
||||
return {'status': 'success'}
|
||||
|
||||
|
||||
# ========== Usage Log CRUD ==========
|
||||
|
||||
async def create_voucher_usage_log(data):
|
||||
sor = DBPools().sqlorContext(_get_dbname())
|
||||
data['id'] = getID()
|
||||
sor.I('voucher_usage_log', data)
|
||||
return {'status': 'success', 'data': {'id': data['id']}}
|
||||
|
||||
|
||||
async def update_voucher_usage_log(conditions, data):
|
||||
sor = DBPools().sqlorContext(_get_dbname())
|
||||
sor.U('voucher_usage_log', conditions, data)
|
||||
return {'status': 'success'}
|
||||
|
||||
|
||||
async def delete_voucher_usage_log(conditions):
|
||||
sor = DBPools().sqlorContext(_get_dbname())
|
||||
sor.D('voucher_usage_log', conditions)
|
||||
return {'status': 'success'}
|
||||
|
||||
|
||||
# ========== Options API (dropdown) ==========
|
||||
|
||||
async def get_voucher_template_options():
|
||||
sor = DBPools().sqlorContext(_get_dbname())
|
||||
rows = sor.R('voucher_template', {}, {'sort': 'name'})
|
||||
options = []
|
||||
for r in rows:
|
||||
tid = r.id if hasattr(r, 'id') else r['id']
|
||||
name = r.name if hasattr(r, 'name') else r['name']
|
||||
options.append({'value': tid, 'text': name})
|
||||
return {'status': 'success', 'data': {'options': options}}
|
||||
|
||||
|
||||
async def get_voucher_instance_options():
|
||||
sor = DBPools().sqlorContext(_get_dbname())
|
||||
rows = sor.R('voucher_instance', {}, {'sort': 'created_at desc'})
|
||||
options = []
|
||||
for r in rows:
|
||||
vid = r.id if hasattr(r, 'id') else r['id']
|
||||
code = r.code if hasattr(r, 'code') else r['code']
|
||||
options.append({'value': vid, 'text': code})
|
||||
return {'status': 'success', 'data': {'options': options}}
|
||||
|
||||
|
||||
# ========== Voucher Engine API ==========
|
||||
|
||||
async def apply_voucher_api(customer_id, order_id, voucher_ids, context):
|
||||
"""外部调用:使用代金券"""
|
||||
from voucher.rules.engine import batch_apply_vouchers
|
||||
sor = DBPools().sqlorContext(_get_dbname())
|
||||
result = batch_apply_vouchers(sor, customer_id, order_id, voucher_ids, context)
|
||||
return {'status': 'success', 'data': result}
|
||||
|
||||
# ========== 客户侧 API ==========
|
||||
|
||||
async def get_available_vouchers_api(customer_id, context=None):
|
||||
"""外部调用:查询可用代金券(含模板名称)"""
|
||||
from voucher.rules.engine import get_available_vouchers
|
||||
sor = DBPools().sqlorContext(_get_dbname())
|
||||
vouchers = get_available_vouchers(sor, customer_id, context)
|
||||
# 预加载模板名称
|
||||
template_names = {}
|
||||
for v in vouchers:
|
||||
tid = v.template_id if hasattr(v, 'template_id') else v.get('template_id', '')
|
||||
if tid and tid not in template_names:
|
||||
t = sor.R('voucher_template', {'id': tid}).first()
|
||||
if t:
|
||||
template_names[tid] = t.name if hasattr(t, 'name') else t.get('name', '')
|
||||
items = []
|
||||
for v in vouchers:
|
||||
if hasattr(v, '__dict__'):
|
||||
item = v.__dict__
|
||||
else:
|
||||
"""查询客户可用代金券(含模板名称;顺带把过期券置 expired)。"""
|
||||
db = _get_db()
|
||||
async with db.sqlorContext(_get_dbname()) as sor:
|
||||
try:
|
||||
await expire_vouchers(sor)
|
||||
except Exception as e:
|
||||
debug('get_available_vouchers_api: expire sweep skipped: {}'.format(e))
|
||||
vouchers = await get_available_vouchers(sor, customer_id, context)
|
||||
template_names = {}
|
||||
for v in vouchers:
|
||||
tid = v.template_id or ''
|
||||
if tid and tid not in template_names:
|
||||
t = await sor.R('voucher_template', {'id': tid})
|
||||
template_names[tid] = (t[0].name if t else '') or ''
|
||||
items = []
|
||||
for v in vouchers:
|
||||
item = dict(v)
|
||||
tid = item.get('template_id', '')
|
||||
item['template_name'] = template_names.get(tid, '')
|
||||
items.append(item)
|
||||
return {'status': 'success', 'data': items}
|
||||
item['template_name'] = template_names.get(v.template_id or '', '')
|
||||
items.append(_jsonable_instance(item))
|
||||
return {'status': 'success', 'data': items, 'total': len(items)}
|
||||
|
||||
|
||||
async def get_registered_rule_types():
|
||||
"""返回已注册的规则类型列表"""
|
||||
from voucher.rules.registry import get_all_rule_types
|
||||
async def apply_voucher_api(customer_id, order_id, voucher_ids, context):
|
||||
"""使用代金券(单张传一个 id 的列表即可)。"""
|
||||
db = _get_db()
|
||||
async with db.sqlorContext(_get_dbname()) as sor:
|
||||
result = await batch_apply_vouchers(sor, customer_id, order_id, voucher_ids, context)
|
||||
return {'status': 'success', 'data': result}
|
||||
|
||||
|
||||
async def validate_voucher_api(instance_id, context):
|
||||
"""只校验不核销(下单前预检)。"""
|
||||
db = _get_db()
|
||||
async with db.sqlorContext(_get_dbname()) as sor:
|
||||
ok, deductible, err = await validate_voucher(sor, instance_id, context or {})
|
||||
return {'status': 'success', 'data': {
|
||||
'ok': ok, 'deductible': float(deductible), 'error': err}}
|
||||
|
||||
|
||||
async def my_vouchers_api(request, params_kw):
|
||||
"""客户「我的代金券」列表(本机构全部券,含已用/过期/作废;Tabular 契约 {total, rows})。"""
|
||||
env = request._run_ns
|
||||
org_id = (await env.get_userorgid()) or ''
|
||||
if not org_id:
|
||||
return {'total': 0, 'rows': []}
|
||||
status = ((params_kw or {}).get('status') or '').strip()
|
||||
db = _get_db()
|
||||
async with db.sqlorContext(_get_dbname()) as sor:
|
||||
try:
|
||||
await expire_vouchers(sor)
|
||||
except Exception as e:
|
||||
debug('my_vouchers_api: expire sweep skipped: {}'.format(e))
|
||||
where = 'WHERE a.customer_id=${cid}$'
|
||||
ns = {'cid': org_id}
|
||||
if status:
|
||||
where += ' AND a.status=${st}$'
|
||||
ns['st'] = status
|
||||
sql = ("SELECT a.id, a.code, a.status, a.face_value, a.actual_deducted, "
|
||||
"a.valid_from, a.valid_to, a.issued_at, a.used_at, a.source, a.remark, "
|
||||
"b.name as template_name "
|
||||
"FROM voucher_instance a "
|
||||
"LEFT JOIN voucher_template b ON a.template_id=b.id "
|
||||
+ where + " ORDER BY a.issued_at DESC")
|
||||
recs = await sor.sqlExe(sql, ns)
|
||||
rows = [_jsonable_instance(dict(r)) for r in (recs or [])]
|
||||
return {'total': len(rows), 'rows': rows}
|
||||
|
||||
|
||||
async def invalidate_voucher_api(request, params_kw):
|
||||
"""作废一张未使用的券(管理端操作;unused → void,已用/已过期/已作废拒绝)。"""
|
||||
env = request._run_ns
|
||||
user_id = await env.get_user()
|
||||
data = dict(params_kw or {})
|
||||
vid = (data.get('id') or '').strip()
|
||||
reason = (data.get('reason') or '').strip()
|
||||
if not vid:
|
||||
return {'success': False, 'message': '缺少 id'}
|
||||
db = _get_db()
|
||||
async with db.sqlorContext(_get_dbname()) as sor:
|
||||
now = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
# 条件 UPDATE 原子作废:只有 unused 才可作废(并发防双花同款守卫)
|
||||
remark_sql = "UPDATE voucher_instance SET status='void', updated_at=${now}$"
|
||||
ns = {'now': now, 'vid': vid}
|
||||
if reason:
|
||||
remark_sql += ", remark=CONCAT(COALESCE(remark,''), ${rmark}$)"
|
||||
ns['rmark'] = ' [作废:{}]'.format(reason[:200])
|
||||
remark_sql += (" WHERE id=${vid}$ AND status='unused'")
|
||||
affected = await sor.execute(remark_sql, ns)
|
||||
if not affected:
|
||||
recs = await sor.R('voucher_instance', {'id': vid})
|
||||
if not recs:
|
||||
return {'success': False, 'message': '券不存在'}
|
||||
return {'success': False,
|
||||
'message': '券当前状态为 {},只有未使用的券可作废'.format(recs[0].status)}
|
||||
info('voucher: {} voided by {} ({})'.format(vid, user_id, reason or 'no reason'))
|
||||
return {'success': True, 'message': 'ok'}
|
||||
|
||||
|
||||
# ========== 管理侧 API ==========
|
||||
|
||||
async def create_voucher_template(request, params_kw):
|
||||
"""建模板(org_id 隔离 + code 唯一)。"""
|
||||
env = request._run_ns
|
||||
user_id = await env.get_user()
|
||||
org_id = (await env.get_userorgid()) or '0'
|
||||
data = dict(params_kw)
|
||||
name = (data.get('name') or '').strip()
|
||||
if not name:
|
||||
return {'success': False, 'message': '模板名称必填'}
|
||||
face_value = float(data.get('face_value') or 0)
|
||||
if face_value <= 0:
|
||||
return {'success': False, 'message': '面值必须大于 0'}
|
||||
code = (data.get('code') or '').strip().upper() or ('VT-' + getID()[:12].upper())
|
||||
|
||||
db = _get_db()
|
||||
async with db.sqlorContext(_get_dbname()) as sor:
|
||||
exist = await sor.R('voucher_template', {'code': code})
|
||||
if exist:
|
||||
return {'success': False, 'message': '模板编码 {} 已存在'.format(code)}
|
||||
tid = getID()
|
||||
now = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
await sor.C('voucher_template', {
|
||||
'id': tid,
|
||||
'name': name,
|
||||
'code': code,
|
||||
'face_value': face_value,
|
||||
'total_count': int(data.get('total_count') or 0),
|
||||
'issued_count': 0,
|
||||
'valid_days': int(data.get('valid_days') or 30),
|
||||
'status': data.get('status') or 'draft',
|
||||
'remark': data.get('remark') or '',
|
||||
'org_id': org_id,
|
||||
'created_by': user_id or '',
|
||||
'created_at': now,
|
||||
'updated_at': now,
|
||||
})
|
||||
return {'success': True, 'message': 'ok', 'id': tid, 'code': code}
|
||||
|
||||
|
||||
async def update_voucher_template(request, params_kw):
|
||||
env = request._run_ns
|
||||
org_id = (await env.get_userorgid()) or '0'
|
||||
data = dict(params_kw)
|
||||
tid = data.pop('id', None)
|
||||
if not tid:
|
||||
return {'success': False, 'message': '缺少 id'}
|
||||
db = _get_db()
|
||||
async with db.sqlorContext(_get_dbname()) as sor:
|
||||
recs = await sor.R('voucher_template', {'id': tid})
|
||||
if not recs:
|
||||
return {'success': False, 'message': '模板不存在'}
|
||||
if (recs[0].org_id or '0') != org_id and org_id != '0':
|
||||
return {'success': False, 'message': '无权操作该模板'}
|
||||
new_code = (data.get('code') or '').strip().upper()
|
||||
if new_code and new_code != (recs[0].code or ''):
|
||||
return {'success': False, 'message': '模板编码不可修改(已发券按编码追溯)'}
|
||||
allowed = {}
|
||||
for k in ('name', 'face_value', 'total_count', 'valid_days', 'status', 'remark'):
|
||||
if k in data and data[k] is not None:
|
||||
allowed[k] = data[k]
|
||||
if not allowed:
|
||||
return {'success': False, 'message': '无可更新字段'}
|
||||
allowed['updated_at'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
allowed['id'] = tid
|
||||
await sor.U('voucher_template', allowed)
|
||||
return {'success': True, 'message': 'ok'}
|
||||
|
||||
|
||||
async def delete_voucher_template(request, params_kw):
|
||||
"""删模板:级联删规则;已发出的实例保留(客户已持有的券不作废,
|
||||
但模板停用/删除后 validate 会拒绝使用——用「停用」代替「删除」更安全)。"""
|
||||
env = request._run_ns
|
||||
org_id = (await env.get_userorgid()) or '0'
|
||||
data = dict(params_kw)
|
||||
tid = data.get('id')
|
||||
if not tid:
|
||||
return {'success': False, 'message': '缺少 id'}
|
||||
db = _get_db()
|
||||
async with db.sqlorContext(_get_dbname()) as sor:
|
||||
recs = await sor.R('voucher_template', {'id': tid})
|
||||
if not recs:
|
||||
return {'success': False, 'message': '模板不存在'}
|
||||
if (recs[0].org_id or '0') != org_id and org_id != '0':
|
||||
return {'success': False, 'message': '无权操作该模板'}
|
||||
issued = int(recs[0].issued_count or 0)
|
||||
if issued > 0:
|
||||
return {'success': False,
|
||||
'message': '模板已发放 {} 张券,不能删除;请改用「停用」'.format(issued)}
|
||||
await sor.execute(
|
||||
'DELETE FROM voucher_rule WHERE template_id=${tid}$', {'tid': tid})
|
||||
await sor.D('voucher_template', {'id': tid})
|
||||
return {'success': True, 'message': 'ok'}
|
||||
|
||||
|
||||
async def create_voucher_rule(request, params_kw):
|
||||
"""给模板加规则(rule_config 必须是合法 JSON)。"""
|
||||
import json as _json
|
||||
data = dict(params_kw)
|
||||
template_id = data.get('template_id')
|
||||
rule_type = data.get('rule_type')
|
||||
if not template_id or not rule_type:
|
||||
return {'success': False, 'message': 'template_id 与 rule_type 必填'}
|
||||
if rule_type not in [r['rule_type'] for r in get_all_rule_types()]:
|
||||
return {'success': False, 'message': '未知规则类型: {}'.format(rule_type)}
|
||||
cfg_raw = data.get('rule_config') or '{}'
|
||||
if isinstance(cfg_raw, dict):
|
||||
cfg_str = _json.dumps(cfg_raw, ensure_ascii=False)
|
||||
else:
|
||||
try:
|
||||
_json.loads(cfg_raw)
|
||||
except (ValueError, TypeError):
|
||||
return {'success': False, 'message': 'rule_config 不是合法 JSON'}
|
||||
cfg_str = cfg_raw
|
||||
db = _get_db()
|
||||
async with db.sqlorContext(_get_dbname()) as sor:
|
||||
t = await sor.R('voucher_template', {'id': template_id})
|
||||
if not t:
|
||||
return {'success': False, 'message': '模板不存在'}
|
||||
rid = getID()
|
||||
now = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
await sor.C('voucher_rule', {
|
||||
'id': rid,
|
||||
'template_id': template_id,
|
||||
'rule_type': rule_type,
|
||||
'rule_config': cfg_str,
|
||||
'enabled': str(data.get('enabled', '1')),
|
||||
'sort_order': int(data.get('sort_order') or 0),
|
||||
'remark': data.get('remark') or '',
|
||||
'created_at': now,
|
||||
'updated_at': now,
|
||||
})
|
||||
return {'success': True, 'message': 'ok', 'id': rid}
|
||||
|
||||
|
||||
async def update_voucher_rule(request, params_kw):
|
||||
data = dict(params_kw)
|
||||
rid = data.pop('id', None)
|
||||
if not rid:
|
||||
return {'success': False, 'message': '缺少 id'}
|
||||
allowed = {'id': rid}
|
||||
for k in ('rule_type', 'rule_config', 'enabled', 'sort_order', 'remark'):
|
||||
if k in data and data[k] is not None:
|
||||
allowed[k] = data[k]
|
||||
if len(allowed) <= 1:
|
||||
return {'success': False, 'message': '无可更新字段'}
|
||||
allowed['updated_at'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
db = _get_db()
|
||||
async with db.sqlorContext(_get_dbname()) as sor:
|
||||
await sor.U('voucher_rule', allowed)
|
||||
return {'success': True, 'message': 'ok'}
|
||||
|
||||
|
||||
async def delete_voucher_rule(request, params_kw):
|
||||
rid = (dict(params_kw)).get('id')
|
||||
if not rid:
|
||||
return {'success': False, 'message': '缺少 id'}
|
||||
db = _get_db()
|
||||
async with db.sqlorContext(_get_dbname()) as sor:
|
||||
await sor.D('voucher_rule', {'id': rid})
|
||||
return {'success': True, 'message': 'ok'}
|
||||
|
||||
|
||||
async def issue_voucher_api(request, params_kw):
|
||||
"""管理端手动发券:template_id/template_code 二选一 + customer_id。"""
|
||||
env = request._run_ns
|
||||
user_id = await env.get_user()
|
||||
data = dict(params_kw)
|
||||
customer_id = data.get('customer_id')
|
||||
if not customer_id:
|
||||
return {'success': False, 'message': 'customer_id 必填'}
|
||||
template_id = data.get('template_id')
|
||||
if not template_id and data.get('template_code'):
|
||||
db0 = _get_db()
|
||||
async with db0.sqlorContext(_get_dbname()) as sor:
|
||||
recs = await sor.R('voucher_template', {'code': data['template_code']})
|
||||
template_id = recs[0].id if recs else None
|
||||
if not template_id:
|
||||
return {'success': False, 'message': '模板不存在'}
|
||||
db = _get_db()
|
||||
async with db.sqlorContext(_get_dbname()) as sor:
|
||||
result = await issue_voucher(
|
||||
sor, template_id, customer_id,
|
||||
source=data.get('source') or 'manual',
|
||||
issued_by=user_id or '',
|
||||
remark=data.get('remark') or '')
|
||||
return result
|
||||
|
||||
|
||||
async def get_voucher_template_options(request, params_kw):
|
||||
"""模板下拉选项(启用中的模板;code 组件契约:纯 [{value,text}] 数组)。"""
|
||||
db = _get_db()
|
||||
async with db.sqlorContext(_get_dbname()) as sor:
|
||||
recs = await sor.R('voucher_template', {'status': 'active', 'sort': 'name'})
|
||||
return [{'value': r.id, 'text': '{}({}元)'.format(r.name, r.face_value)}
|
||||
for r in (recs or [])]
|
||||
|
||||
|
||||
async def update_voucher_instance(request, params_kw):
|
||||
"""实例修正(管理端):仅未使用的券可改归属客户/备注。
|
||||
|
||||
状态不走这里——使用由核销 API、作废由 invalidate_voucher_api,
|
||||
防止界面直改 status 绕过原子守卫造成账实不符。
|
||||
"""
|
||||
data = dict(params_kw or {})
|
||||
vid = (data.get('id') or '').strip()
|
||||
if not vid:
|
||||
return {'success': False, 'message': '缺少 id'}
|
||||
db = _get_db()
|
||||
async with db.sqlorContext(_get_dbname()) as sor:
|
||||
recs = await sor.R('voucher_instance', {'id': vid})
|
||||
if not recs:
|
||||
return {'success': False, 'message': '券不存在'}
|
||||
if (recs[0].status or '') != 'unused':
|
||||
return {'success': False,
|
||||
'message': '券状态为 {},只有未使用的券可修改'.format(recs[0].status)}
|
||||
# 显式拒绝危险字段变更(表单 submit_changed 只送变更字段,误改要报错而非静默丢弃)
|
||||
new_tid = (data.get('template_id') or '').strip()
|
||||
if new_tid and new_tid != (recs[0].template_id or ''):
|
||||
return {'success': False, 'message': '券所属模板不可修改(面值/规则随模板)'}
|
||||
new_src = (data.get('source') or '').strip()
|
||||
if new_src and new_src != (recs[0].source or ''):
|
||||
return {'success': False, 'message': '发放来源不可修改(审计字段)'}
|
||||
allowed = {'id': vid}
|
||||
for k in ('customer_id', 'remark'):
|
||||
v = data.get(k)
|
||||
if v is not None and str(v).strip() != '':
|
||||
allowed[k] = v
|
||||
if len(allowed) <= 1:
|
||||
return {'success': False, 'message': '无可更新字段'}
|
||||
allowed['updated_at'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
await sor.U('voucher_instance', allowed)
|
||||
return {'success': True, 'message': 'ok'}
|
||||
|
||||
|
||||
async def delete_voucher_instance(request, params_kw):
|
||||
"""删除实例(管理端纠错):已使用的券有流水关联,禁删(审计完整性)。"""
|
||||
data = dict(params_kw or {})
|
||||
vid = (data.get('id') or '').strip()
|
||||
if not vid:
|
||||
return {'success': False, 'message': '缺少 id'}
|
||||
db = _get_db()
|
||||
async with db.sqlorContext(_get_dbname()) as sor:
|
||||
recs = await sor.R('voucher_instance', {'id': vid})
|
||||
if not recs:
|
||||
return {'success': False, 'message': '券不存在'}
|
||||
if (recs[0].status or '') == 'used':
|
||||
return {'success': False, 'message': '券已使用(有流水关联),不能删除'}
|
||||
await sor.D('voucher_instance', {'id': vid})
|
||||
# 发行额度回吐(未使用的券删除后名额释放)
|
||||
await sor.execute(
|
||||
"UPDATE voucher_template SET issued_count=GREATEST(issued_count-1,0) "
|
||||
"WHERE id=${tid}$", {'tid': recs[0].template_id})
|
||||
return {'success': True, 'message': 'ok'}
|
||||
|
||||
|
||||
async def get_registered_rule_types_api(request=None, params_kw=None):
|
||||
"""已注册规则类型清单(管理界面配规则用)。"""
|
||||
return {'status': 'success', 'data': get_all_rule_types()}
|
||||
|
||||
|
||||
# ========== 注册送券挂钩 ==========
|
||||
|
||||
async def on_organization_created(data):
|
||||
"""organization 表 C:after 事件 → 给新注册机构发欢迎券。
|
||||
|
||||
- 挂钩点在 sqlor 事件层(pricing/rbac 同款先例),不改 rbac 基准模块
|
||||
- 「前 N 名」由欢迎模板 total_count 原子占额控制,超发自动拒绝
|
||||
- 任何异常都只记日志(EventDispatcher continue_on_error),绝不阻断注册
|
||||
"""
|
||||
ns = data if isinstance(data, dict) else {}
|
||||
org_id = ns.get('id') or ''
|
||||
if not org_id or org_id == '0':
|
||||
return
|
||||
try:
|
||||
db = _get_db()
|
||||
async with db.sqlorContext(_get_dbname()) as sor:
|
||||
recs = await sor.R('voucher_template', {'code': WELCOME_TEMPLATE_CODE})
|
||||
if not recs:
|
||||
debug('voucher: welcome template {} not found, skip'.format(WELCOME_TEMPLATE_CODE))
|
||||
return
|
||||
tpl = recs[0]
|
||||
if (tpl.status or '') != 'active':
|
||||
return
|
||||
# 幂等:同一机构不重复送(重复触发事件/重建机构时防刷)
|
||||
exist = await sor.sqlExe(
|
||||
"SELECT id FROM voucher_instance WHERE customer_id=${cid}$ "
|
||||
"AND template_id=${tid}$ AND source='register'",
|
||||
{'cid': org_id, 'tid': tpl.id})
|
||||
if exist:
|
||||
debug('voucher: org {} already got welcome voucher'.format(org_id))
|
||||
return
|
||||
result = await issue_voucher(
|
||||
sor, tpl.id, org_id, source='register',
|
||||
issued_by='system', remark='新客户注册赠券')
|
||||
if result.get('success'):
|
||||
info('voucher: welcome voucher {} issued to org {}'.format(
|
||||
result.get('code'), org_id))
|
||||
else:
|
||||
debug('voucher: welcome issue skipped for {}: {}'.format(
|
||||
org_id, result.get('message')))
|
||||
except Exception as e:
|
||||
exception('voucher: on_organization_created failed (non-blocking): {}'.format(e))
|
||||
|
||||
|
||||
def _bind_org_event():
|
||||
"""bind organization:c:after(DBPools 是 EventDispatcher 单例)。"""
|
||||
try:
|
||||
dbname = _get_dbname()
|
||||
db = _get_db()
|
||||
db.bind('{}:organization:c:after'.format(dbname), on_organization_created)
|
||||
debug('voucher: organization:c:after bound on db {}'.format(dbname))
|
||||
except Exception as e:
|
||||
# 宿主没有事件机制/库未配置时不阻断加载
|
||||
exception('voucher: bind organization event failed: {}'.format(e))
|
||||
|
||||
|
||||
# ========== Module Load ==========
|
||||
|
||||
def load_voucher():
|
||||
"""注册所有函数到 ServerEnv"""
|
||||
"""注册所有函数到 ServerEnv + 绑定注册送券事件。"""
|
||||
env = ServerEnv()
|
||||
|
||||
# Template CRUD
|
||||
# 客户侧
|
||||
env.get_available_vouchers_api = get_available_vouchers_api
|
||||
env.apply_voucher_api = apply_voucher_api
|
||||
env.validate_voucher_api = validate_voucher_api
|
||||
env.my_vouchers_api = my_vouchers_api
|
||||
|
||||
# 管理侧
|
||||
env.create_voucher_template = create_voucher_template
|
||||
env.update_voucher_template = update_voucher_template
|
||||
env.delete_voucher_template = delete_voucher_template
|
||||
|
||||
# Rule CRUD
|
||||
env.create_voucher_rule = create_voucher_rule
|
||||
env.update_voucher_rule = update_voucher_rule
|
||||
env.delete_voucher_rule = delete_voucher_rule
|
||||
|
||||
# Instance CRUD
|
||||
env.create_voucher_instance = create_voucher_instance
|
||||
env.issue_voucher_api = issue_voucher_api
|
||||
env.invalidate_voucher_api = invalidate_voucher_api
|
||||
env.update_voucher_instance = update_voucher_instance
|
||||
env.delete_voucher_instance = delete_voucher_instance
|
||||
|
||||
# Usage Log CRUD
|
||||
env.create_voucher_usage_log = create_voucher_usage_log
|
||||
env.update_voucher_usage_log = update_voucher_usage_log
|
||||
env.delete_voucher_usage_log = delete_voucher_usage_log
|
||||
|
||||
# Options
|
||||
env.get_voucher_template_options = get_voucher_template_options
|
||||
env.get_voucher_instance_options = get_voucher_instance_options
|
||||
env.get_registered_rule_types = get_registered_rule_types_api
|
||||
|
||||
# Engine API
|
||||
env.apply_voucher_api = apply_voucher_api
|
||||
env.get_available_vouchers_api = get_available_vouchers_api
|
||||
env.get_registered_rule_types = get_registered_rule_types
|
||||
|
||||
# Import validators to trigger @register_rule decorators
|
||||
import voucher.rules.validators # noqa: F401
|
||||
# 引擎级(供其他模块 import 后直接调,如 product_management 结算联动)
|
||||
env.voucher_issue = issue_voucher
|
||||
env.voucher_apply = apply_voucher
|
||||
env.voucher_batch_apply = batch_apply_vouchers
|
||||
env.voucher_get_available = get_available_vouchers
|
||||
|
||||
_bind_org_event()
|
||||
info('voucher module loaded (v{})'.format(MODULE_VERSION))
|
||||
return True
|
||||
|
||||
4
voucher/rules/__init__.py
Normal file
4
voucher/rules/__init__.py
Normal file
@ -0,0 +1,4 @@
|
||||
from .registry import register_rule, get_all_rule_types, get_rule_validator, RULE_REGISTRY
|
||||
from . import validators # noqa: F401 触发 @register_rule 注册
|
||||
|
||||
__all__ = ['register_rule', 'get_all_rule_types', 'get_rule_validator', 'RULE_REGISTRY']
|
||||
294
voucher/rules/engine.py
Normal file
294
voucher/rules/engine.py
Normal file
@ -0,0 +1,294 @@
|
||||
"""代金券规则引擎(sqlor 风格,全异步)。
|
||||
|
||||
核心原则:
|
||||
- 一次性使用:券用后直接作废,不找零、无余额残留
|
||||
- 规则可配置:模板下挂 voucher_rule 行,按 sort_order 依次执行 validator
|
||||
- 并发安全:核销用条件 UPDATE(status='unused' 守卫)+ affected_rows 判定,
|
||||
同一张券并发双花时只有一个请求成功
|
||||
|
||||
所有函数接收 sor(已处于 async with DBPools().sqlorContext(dbname) 上下文),
|
||||
不自己开库——由调用方(init.py / dspy)决定事务边界。
|
||||
"""
|
||||
|
||||
import json
|
||||
from datetime import datetime
|
||||
from decimal import Decimal, InvalidOperation
|
||||
|
||||
from appPublic.log import debug, exception
|
||||
from appPublic.uniqueID import getID
|
||||
|
||||
from .registry import RULE_REGISTRY
|
||||
|
||||
_TIME_FMT = '%Y-%m-%d %H:%M:%S'
|
||||
|
||||
|
||||
def _dec(v, default='0'):
|
||||
try:
|
||||
return Decimal(str(v))
|
||||
except (InvalidOperation, ValueError, TypeError):
|
||||
return Decimal(default)
|
||||
|
||||
|
||||
def _now():
|
||||
return datetime.now()
|
||||
|
||||
|
||||
def _to_dt(v):
|
||||
"""DB datetime 字段 → python datetime(兼容字符串)。"""
|
||||
if v is None or v == '':
|
||||
return None
|
||||
if isinstance(v, datetime):
|
||||
return v
|
||||
try:
|
||||
return datetime.strptime(str(v)[:19], _TIME_FMT)
|
||||
except (ValueError, TypeError):
|
||||
return None
|
||||
|
||||
|
||||
def _parse_rule_config(raw):
|
||||
"""rule_config 容错解析:合法 JSON → dict;空 → {};非法 → None(视为规则损坏)。"""
|
||||
if raw is None or raw == '':
|
||||
return {}
|
||||
if isinstance(raw, dict):
|
||||
return raw
|
||||
try:
|
||||
cfg = json.loads(raw)
|
||||
return cfg if isinstance(cfg, dict) else None
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
return None
|
||||
|
||||
|
||||
async def validate_voucher(sor, instance_id, context):
|
||||
"""校验代金券是否可用。
|
||||
|
||||
Returns:
|
||||
(ok: bool, deductible: Decimal, err: str|None)
|
||||
"""
|
||||
recs = await sor.R('voucher_instance', {'id': instance_id})
|
||||
if not recs:
|
||||
return False, Decimal('0'), '代金券不存在'
|
||||
inst = recs[0]
|
||||
|
||||
if (inst.status or '') != 'unused':
|
||||
return False, Decimal('0'), '代金券已使用或失效'
|
||||
|
||||
now = _now()
|
||||
vf = _to_dt(inst.valid_from)
|
||||
vt = _to_dt(inst.valid_to)
|
||||
if vf and now < vf:
|
||||
return False, Decimal('0'), '代金券尚未生效'
|
||||
if vt and now > vt:
|
||||
return False, Decimal('0'), '代金券已过期'
|
||||
|
||||
# 模板停用则券不可用(发放后管理员下架模板的兜底)
|
||||
t_recs = await sor.R('voucher_template', {'id': inst.template_id})
|
||||
if t_recs and (t_recs[0].status or '') != 'active':
|
||||
return False, Decimal('0'), '代金券所属模板已停用'
|
||||
|
||||
# 加载模板启用规则(sort_order 升序)
|
||||
ns = {'template_id': inst.template_id, 'enabled': '1', 'sort': 'sort_order'}
|
||||
rules = await sor.R('voucher_rule', ns)
|
||||
|
||||
ctx = {
|
||||
'instance': inst,
|
||||
'used_count': int(inst.get('used_count', 0) or 0) if isinstance(inst, dict) else 0,
|
||||
'valid_from': vf,
|
||||
'valid_to': vt,
|
||||
'face_value': inst.face_value,
|
||||
}
|
||||
ctx.update(context or {})
|
||||
|
||||
for rule in rules or []:
|
||||
rule_type = rule.rule_type
|
||||
validator = RULE_REGISTRY.get(rule_type)
|
||||
if validator is None:
|
||||
debug('voucher: unknown rule_type {} skipped'.format(rule_type))
|
||||
continue
|
||||
config = _parse_rule_config(rule.rule_config)
|
||||
if config is None:
|
||||
# 规则配置损坏:拒绝使用而不是静默放行(防绕过限制)
|
||||
return False, Decimal('0'), '规则({})配置损坏,请联系管理员'.format(rule_type)
|
||||
ok, err = validator(config, ctx)
|
||||
if not ok:
|
||||
return False, Decimal('0'), err or '不满足使用条件'
|
||||
|
||||
face_value = _dec(inst.face_value)
|
||||
request_amount = _dec((context or {}).get('request_amount', 0))
|
||||
deductible = min(face_value, request_amount)
|
||||
return True, deductible, None
|
||||
|
||||
|
||||
async def apply_voucher(sor, instance_id, customer_id, order_id, context):
|
||||
"""使用一张代金券(一次性,不找零)。
|
||||
|
||||
并发安全:先条件 UPDATE 占券(status='unused' 守卫),affected_rows=0
|
||||
说明已被并发请求用掉/失效,直接失败;占券成功后再写流水。
|
||||
|
||||
Returns:
|
||||
(ok: bool, deducted: Decimal, err: str|None)
|
||||
"""
|
||||
context = context or {}
|
||||
ok, deductible, err = await validate_voucher(sor, instance_id, context)
|
||||
if not ok:
|
||||
return False, Decimal('0'), err
|
||||
|
||||
now = _now().strftime(_TIME_FMT)
|
||||
# 条件 UPDATE 原子占券:只有 status='unused' 且属于该客户时才生效
|
||||
upd = ("UPDATE voucher_instance SET status='used', "
|
||||
"actual_deducted=${ded}$, used_at=${now}$, order_id=${oid}$, "
|
||||
"updated_at=${now}$ "
|
||||
"WHERE id=${vid}$ AND status='unused' AND customer_id=${cid}$")
|
||||
affected = await sor.execute(upd, {
|
||||
'ded': float(deductible), 'now': now, 'oid': order_id or '',
|
||||
'vid': instance_id, 'cid': customer_id or '',
|
||||
})
|
||||
if not affected:
|
||||
return False, Decimal('0'), '代金券已被使用或不属于该客户'
|
||||
|
||||
# 占券成功 → 重读实例(拿 template_id/face_value 写流水)
|
||||
recs = await sor.R('voucher_instance', {'id': instance_id})
|
||||
inst = recs[0] if recs else None
|
||||
await sor.C('voucher_usage_log', {
|
||||
'id': getID(),
|
||||
'instance_id': instance_id,
|
||||
'template_id': (inst.template_id if inst else '') or '',
|
||||
'customer_id': customer_id or '',
|
||||
'order_id': order_id or '',
|
||||
'face_value': float(_dec(inst.face_value if inst else 0)),
|
||||
'deducted_amount': float(deductible),
|
||||
'product_type': context.get('product_type') or '',
|
||||
'product_name': context.get('product_name') or '',
|
||||
'used_at': now,
|
||||
'used_by': context.get('used_by') or '',
|
||||
})
|
||||
return True, deductible, None
|
||||
|
||||
|
||||
async def batch_apply_vouchers(sor, customer_id, order_id, voucher_ids, context):
|
||||
"""批量用券:按传入顺序依次尝试,消费金额抵扣完即停。
|
||||
|
||||
Returns:
|
||||
{'total_deducted': float, 'remaining': float, 'details': [...]}
|
||||
"""
|
||||
context = dict(context or {})
|
||||
total = Decimal('0')
|
||||
remaining = _dec(context.get('request_amount', 0))
|
||||
details = []
|
||||
|
||||
for vid in voucher_ids or []:
|
||||
if remaining <= 0:
|
||||
break
|
||||
ctx = dict(context)
|
||||
ctx['request_amount'] = float(remaining)
|
||||
ok, deducted, err = await apply_voucher(sor, vid, customer_id, order_id, ctx)
|
||||
if ok:
|
||||
total += deducted
|
||||
remaining -= deducted
|
||||
details.append({'voucher_id': vid, 'deducted': float(deducted), 'error': None})
|
||||
else:
|
||||
details.append({'voucher_id': vid, 'deducted': 0, 'error': err})
|
||||
|
||||
return {
|
||||
'total_deducted': float(total),
|
||||
'remaining': float(remaining),
|
||||
'details': details,
|
||||
}
|
||||
|
||||
|
||||
async def get_available_vouchers(sor, customer_id, context=None):
|
||||
"""查询客户可用代金券(带 context 时逐张预校验过滤)。"""
|
||||
ns = {'customer_id': customer_id, 'status': 'unused', 'sort': 'valid_to'}
|
||||
recs = await sor.R('voucher_instance', ns)
|
||||
vouchers = list(recs or [])
|
||||
if not context:
|
||||
return vouchers
|
||||
|
||||
available = []
|
||||
for v in vouchers:
|
||||
ok, _, _ = await validate_voucher(sor, v.id, context)
|
||||
if ok:
|
||||
available.append(v)
|
||||
return available
|
||||
|
||||
|
||||
async def issue_voucher(sor, template_id, customer_id, source='manual',
|
||||
issued_by='', remark='', valid_days=None):
|
||||
"""按模板给客户发一张券(原子占发行额度)。
|
||||
|
||||
发行量控制:total_count>0 时用条件 UPDATE 占额
|
||||
(issued_count<total_count 守卫),并发超发时占额失败即拒发;
|
||||
total_count=0 表示不限量。
|
||||
|
||||
Returns:
|
||||
{'success': bool, 'message': str, 'instance_id': str, 'code': str}
|
||||
"""
|
||||
t_recs = await sor.R('voucher_template', {'id': template_id})
|
||||
if not t_recs:
|
||||
return {'success': False, 'message': '模板不存在'}
|
||||
tpl = t_recs[0]
|
||||
if (tpl.status or '') != 'active':
|
||||
return {'success': False, 'message': '模板未启用(status={})'.format(tpl.status)}
|
||||
|
||||
total_count = int(tpl.total_count or 0)
|
||||
now_dt = _now()
|
||||
|
||||
# ── 原子占发行额度 ──
|
||||
if total_count > 0:
|
||||
upd = ("UPDATE voucher_template SET issued_count=issued_count+1, "
|
||||
"updated_at=${now}$ "
|
||||
"WHERE id=${tid}$ AND status='active' "
|
||||
"AND (total_count=0 OR issued_count<total_count)")
|
||||
affected = await sor.execute(upd, {
|
||||
'now': now_dt.strftime(_TIME_FMT), 'tid': template_id})
|
||||
if not affected:
|
||||
return {'success': False, 'message': '发行量已发完或模板已停用'}
|
||||
else:
|
||||
await sor.execute(
|
||||
"UPDATE voucher_template SET issued_count=issued_count+1, "
|
||||
"updated_at=${now}$ WHERE id=${tid}$",
|
||||
{'now': now_dt.strftime(_TIME_FMT), 'tid': template_id})
|
||||
|
||||
days = int(valid_days if valid_days is not None else (tpl.valid_days or 30))
|
||||
from datetime import timedelta
|
||||
valid_from = now_dt
|
||||
valid_to = now_dt + timedelta(days=days)
|
||||
|
||||
vid = getID()
|
||||
code = 'VCH-' + vid.replace('-', '').replace('_', '')[:16].upper()
|
||||
try:
|
||||
await sor.C('voucher_instance', {
|
||||
'id': vid,
|
||||
'template_id': template_id,
|
||||
'customer_id': customer_id,
|
||||
'code': code,
|
||||
'status': 'unused',
|
||||
'face_value': float(_dec(tpl.face_value)),
|
||||
'valid_from': valid_from.strftime(_TIME_FMT),
|
||||
'valid_to': valid_to.strftime(_TIME_FMT),
|
||||
'issued_at': now_dt.strftime(_TIME_FMT),
|
||||
'source': source or 'manual',
|
||||
'issued_by': issued_by or '',
|
||||
'remark': remark or '',
|
||||
})
|
||||
except Exception as e:
|
||||
exception('issue_voucher: insert instance failed: {}'.format(e))
|
||||
# 回滚占额
|
||||
await sor.execute(
|
||||
"UPDATE voucher_template SET issued_count=GREATEST(issued_count-1,0) "
|
||||
"WHERE id=${tid}$", {'tid': template_id})
|
||||
return {'success': False, 'message': '发券失败: {}'.format(e)}
|
||||
|
||||
return {'success': True, 'message': 'ok', 'instance_id': vid, 'code': code}
|
||||
|
||||
|
||||
async def expire_vouchers(sor):
|
||||
"""把已过期但仍 unused 的券批量置 expired(定时/查询前兜底)。
|
||||
|
||||
Returns: 置过期张数
|
||||
"""
|
||||
now = _now().strftime(_TIME_FMT)
|
||||
affected = await sor.execute(
|
||||
"UPDATE voucher_instance SET status='expired', updated_at=${now}$ "
|
||||
"WHERE status='unused' AND valid_to < ${now}$",
|
||||
{'now': now})
|
||||
return affected or 0
|
||||
33
voucher/rules/registry.py
Normal file
33
voucher/rules/registry.py
Normal file
@ -0,0 +1,33 @@
|
||||
"""规则注册机制:新增规则只需写一个 validator 函数 + @register_rule 装饰器。
|
||||
|
||||
validator 签名:(config: dict, context: dict) -> (bool, error_msg or None)
|
||||
validator 必须是纯函数(不触库、无副作用),引擎负责取数与编排。
|
||||
"""
|
||||
|
||||
RULE_REGISTRY = {}
|
||||
RULE_DESCRIPTIONS = {}
|
||||
|
||||
|
||||
def register_rule(rule_type, description=''):
|
||||
"""装饰器:注册规则类型(description 供管理界面/接口展示)。"""
|
||||
def decorator(func):
|
||||
RULE_REGISTRY[rule_type] = func
|
||||
desc = description
|
||||
if not desc and func.__doc__:
|
||||
desc = func.__doc__.strip().splitlines()[0]
|
||||
RULE_DESCRIPTIONS[rule_type] = desc
|
||||
return func
|
||||
return decorator
|
||||
|
||||
|
||||
def get_all_rule_types():
|
||||
"""返回所有已注册规则类型(含说明)。"""
|
||||
return [
|
||||
{'rule_type': k, 'description': RULE_DESCRIPTIONS.get(k, '')}
|
||||
for k in RULE_REGISTRY
|
||||
]
|
||||
|
||||
|
||||
def get_rule_validator(rule_type):
|
||||
"""根据类型获取 validator 函数,未注册返回 None。"""
|
||||
return RULE_REGISTRY.get(rule_type)
|
||||
122
voucher/rules/validators.py
Normal file
122
voucher/rules/validators.py
Normal file
@ -0,0 +1,122 @@
|
||||
"""内置规则校验器:每种规则一个纯函数,返回 (bool, error_msg)。
|
||||
|
||||
config = voucher_rule.rule_config JSON 解析后的 dict
|
||||
context = 引擎构建的执行上下文(request_amount/product_type/product_name/
|
||||
user_level/face_value/valid_from/valid_to 等)
|
||||
|
||||
rule_type 命名注意:appcodes_kv.id = {parentid}_{k} ≤ 32 字符
|
||||
(parentid='vchr_rule_type' 14 字符),所以类型名保持短名。
|
||||
"""
|
||||
|
||||
import json
|
||||
from decimal import Decimal, InvalidOperation
|
||||
|
||||
from .registry import register_rule
|
||||
|
||||
|
||||
def _dec(v, default='0'):
|
||||
try:
|
||||
return Decimal(str(v))
|
||||
except (InvalidOperation, ValueError, TypeError):
|
||||
return Decimal(default)
|
||||
|
||||
|
||||
def _as_list(v):
|
||||
"""config 数组字段容错:JSON array / 逗号分隔字符串 / 单值 → list。"""
|
||||
if not v:
|
||||
return []
|
||||
if isinstance(v, (list, tuple)):
|
||||
return [str(x) for x in v]
|
||||
if isinstance(v, str):
|
||||
try:
|
||||
parsed = json.loads(v)
|
||||
if isinstance(parsed, list):
|
||||
return [str(x) for x in parsed]
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
pass
|
||||
return [s.strip() for s in v.split(',') if s.strip()]
|
||||
return [str(v)]
|
||||
|
||||
|
||||
@register_rule('min_amount', '最低消费门槛')
|
||||
def check_min_amount(config, context):
|
||||
"""消费金额须达到最低门槛才可用券。config: {"min_value": 100}"""
|
||||
min_val = _dec(config.get('min_value', 0))
|
||||
if min_val <= 0:
|
||||
return True, None
|
||||
request_amount = _dec(context.get('request_amount', 0))
|
||||
if request_amount < min_val:
|
||||
return False, '未达最低消费 {} 元'.format(min_val)
|
||||
return True, None
|
||||
|
||||
|
||||
@register_rule('max_amount', '最高消费限制')
|
||||
def check_max_amount(config, context):
|
||||
"""消费金额超过上限则不可用券。config: {"max_value": 1000}"""
|
||||
max_val = _dec(config.get('max_value', 0))
|
||||
if max_val <= 0:
|
||||
return True, None
|
||||
request_amount = _dec(context.get('request_amount', 0))
|
||||
if request_amount > max_val:
|
||||
return False, '超过最高消费 {} 元'.format(max_val)
|
||||
return True, None
|
||||
|
||||
|
||||
@register_rule('product_type', '限定产品类型')
|
||||
def check_product_type(config, context):
|
||||
"""仅限指定产品类型可用。config: {"product_types": ["pipeline"]}"""
|
||||
allowed = _as_list(config.get('product_types'))
|
||||
if not allowed:
|
||||
return True, None
|
||||
current = str(context.get('product_type') or '')
|
||||
if current not in allowed:
|
||||
return False, '仅限 {} 类型产品使用'.format('/'.join(allowed))
|
||||
return True, None
|
||||
|
||||
|
||||
@register_rule('product', '限定特定产品')
|
||||
def check_specific_product(config, context):
|
||||
"""仅限指定产品(编码/名称)可用。config: {"products": ["PP_PIPE_DEV"]}"""
|
||||
allowed = _as_list(config.get('products'))
|
||||
if not allowed:
|
||||
return True, None
|
||||
current = str(context.get('product_name') or '')
|
||||
if current not in allowed:
|
||||
return False, '仅限 {} 使用'.format('/'.join(allowed))
|
||||
return True, None
|
||||
|
||||
|
||||
@register_rule('exclude_product', '排除特定产品')
|
||||
def check_exclude_product(config, context):
|
||||
"""指定产品不可用此券。config: {"products": ["xxx"]}"""
|
||||
excluded = _as_list(config.get('products'))
|
||||
if not excluded:
|
||||
return True, None
|
||||
current = str(context.get('product_name') or '')
|
||||
if current and current in excluded:
|
||||
return False, '{} 不可使用此代金券'.format(current)
|
||||
return True, None
|
||||
|
||||
|
||||
@register_rule('max_usage_count', '最大使用次数')
|
||||
def check_max_usage_count(config, context):
|
||||
"""实例累计使用次数上限。config: {"max_count": 1}"""
|
||||
max_count = int(config.get('max_count', 0) or 0)
|
||||
if max_count <= 0:
|
||||
return True, None
|
||||
used_count = int(context.get('used_count', 0) or 0)
|
||||
if used_count >= max_count:
|
||||
return False, '已达最大使用次数 {} 次'.format(max_count)
|
||||
return True, None
|
||||
|
||||
|
||||
@register_rule('user_level', '用户等级限制')
|
||||
def check_user_level(config, context):
|
||||
"""客户等级须达到下限。config: {"min_level": 2}"""
|
||||
required = int(config.get('min_level', 0) or 0)
|
||||
if required <= 0:
|
||||
return True, None
|
||||
level = int(context.get('user_level', 0) or 0)
|
||||
if level < required:
|
||||
return False, '需 {} 级以上用户'.format(required)
|
||||
return True, None
|
||||
@ -1,17 +1,21 @@
|
||||
import json
|
||||
|
||||
customer_id = params_kw.get('customer_id', '')
|
||||
order_id = params_kw.get('order_id', '')
|
||||
voucher_ids = params_kw.get('voucher_ids', [])
|
||||
# apply_voucher.dspy — 核销代金券(一次性,不找零;并发安全条件 UPDATE)
|
||||
customer_id = (params_kw.get('customer_id') or '').strip()
|
||||
if not customer_id:
|
||||
customer_id = await get_userorgid() or ''
|
||||
order_id = (params_kw.get('order_id') or '').strip()
|
||||
voucher_ids = params_kw.get('voucher_ids') or []
|
||||
if isinstance(voucher_ids, str):
|
||||
voucher_ids = json.loads(voucher_ids)
|
||||
|
||||
context = {
|
||||
'request_amount': params_kw.get('request_amount', 0),
|
||||
'product_type': params_kw.get('product_type', ''),
|
||||
'product_name': params_kw.get('product_name', ''),
|
||||
'user_level': params_kw.get('user_level', 0),
|
||||
}
|
||||
|
||||
result = await apply_voucher_api(customer_id, order_id, voucher_ids, context)
|
||||
return json.dumps(result)
|
||||
try:
|
||||
voucher_ids = json.loads(voucher_ids)
|
||||
except (ValueError, TypeError):
|
||||
voucher_ids = [s.strip() for s in voucher_ids.split(',') if s.strip()]
|
||||
context = {}
|
||||
for k in ('product_type', 'product_name'):
|
||||
if params_kw.get(k):
|
||||
context[k] = params_kw.get(k)
|
||||
try:
|
||||
context['request_amount'] = float(params_kw.get('request_amount') or 0)
|
||||
except (ValueError, TypeError):
|
||||
context['request_amount'] = 0
|
||||
context['used_by'] = await get_user() or ''
|
||||
return await apply_voucher_api(customer_id, order_id, voucher_ids, context)
|
||||
|
||||
@ -1,13 +1,14 @@
|
||||
import json
|
||||
|
||||
customer_id = params_kw.get('customer_id', '')
|
||||
# get_available.dspy — 内部查询可用券(带规则预校验过滤;供结算方/调试)
|
||||
customer_id = (params_kw.get('customer_id') or '').strip()
|
||||
if not customer_id:
|
||||
customer_id = await get_userorgid() or ''
|
||||
context = {}
|
||||
if params_kw.get('product_type'):
|
||||
context['product_type'] = params_kw.get('product_type')
|
||||
if params_kw.get('product_name'):
|
||||
context['product_name'] = params_kw.get('product_name')
|
||||
for k in ('product_type', 'product_name'):
|
||||
if params_kw.get(k):
|
||||
context[k] = params_kw.get(k)
|
||||
if params_kw.get('request_amount'):
|
||||
context['request_amount'] = params_kw.get('request_amount')
|
||||
|
||||
result = await get_available_vouchers_api(customer_id, context if context else None)
|
||||
return json.dumps(result)
|
||||
try:
|
||||
context['request_amount'] = float(params_kw.get('request_amount'))
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
return await get_available_vouchers_api(customer_id, context if context else None)
|
||||
|
||||
@ -1,9 +0,0 @@
|
||||
|
||||
data = {}
|
||||
for key in ['template_id', 'customer_id', 'code', 'source', 'remark', 'valid_from', 'valid_to']:
|
||||
val = params_kw.get(key, None) if hasattr(params_kw, 'get') else None
|
||||
if val is not None:
|
||||
data[key] = val
|
||||
|
||||
result = await create_voucher_instance(data)
|
||||
return result
|
||||
@ -1,4 +1,5 @@
|
||||
|
||||
conditions = {'id': params_kw.get('id', '')}
|
||||
result = await delete_voucher_instance(conditions)
|
||||
return result
|
||||
# voucher_instance_delete.dspy — 实例删除(已使用禁删保审计;未使用删除回吐发行额度)
|
||||
r = await delete_voucher_instance(request, params_kw)
|
||||
if r.get('success'):
|
||||
return {"widgettype": "Message", "options": {"title": "Success", "message": r.get('message') or 'ok', "timeout": 3, "cwidth": 16, "cheight": 9}}
|
||||
return {"widgettype": "Error", "options": {"title": "Error", "message": r.get('message') or 'failed', "timeout": 3, "cwidth": 16, "cheight": 9}}
|
||||
|
||||
@ -1,4 +0,0 @@
|
||||
import json
|
||||
|
||||
result = await get_voucher_instance_options()
|
||||
return json.dumps(result)
|
||||
@ -1,10 +1,5 @@
|
||||
|
||||
conditions = {'id': params_kw.get('id', '')}
|
||||
data = {}
|
||||
for key in ['template_id', 'customer_id', 'code', 'status', 'source', 'remark']:
|
||||
val = params_kw.get(key, None) if hasattr(params_kw, 'get') else None
|
||||
if val is not None:
|
||||
data[key] = val
|
||||
|
||||
result = await update_voucher_instance(conditions, data)
|
||||
return result
|
||||
# voucher_instance_update.dspy — 实例修正(仅未使用的券可改客户/备注;状态变更走发券/核销/作废 API)
|
||||
r = await update_voucher_instance(request, params_kw)
|
||||
if r.get('success'):
|
||||
return {"widgettype": "Message", "options": {"title": "Success", "message": r.get('message') or 'ok', "timeout": 3, "cwidth": 16, "cheight": 9}}
|
||||
return {"widgettype": "Error", "options": {"title": "Error", "message": r.get('message') or 'failed', "timeout": 3, "cwidth": 16, "cheight": 9}}
|
||||
|
||||
5
wwwroot/api/instance/voucher_invalidate.dspy
Normal file
5
wwwroot/api/instance/voucher_invalidate.dspy
Normal file
@ -0,0 +1,5 @@
|
||||
# voucher_invalidate.dspy — 作废券(unused → void 原子条件 UPDATE;已用/过期/已作废拒绝)
|
||||
r = await invalidate_voucher_api(request, params_kw)
|
||||
if r.get('success'):
|
||||
return {"widgettype": "Message", "options": {"title": "已作废", "message": '券已作废', "timeout": 3, "cwidth": 16, "cheight": 9}}
|
||||
return {"widgettype": "Error", "options": {"title": "Error", "message": r.get('message') or 'failed', "timeout": 3, "cwidth": 16, "cheight": 9}}
|
||||
5
wwwroot/api/instance/voucher_issue.dspy
Normal file
5
wwwroot/api/instance/voucher_issue.dspy
Normal file
@ -0,0 +1,5 @@
|
||||
# voucher_issue.dspy — 管理端手动发券(走 issue_voucher 引擎:原子占发行额度/生成券码/计算有效期)
|
||||
r = await issue_voucher_api(request, params_kw)
|
||||
if r.get('success'):
|
||||
return {"widgettype": "Message", "options": {"title": "发券成功", "message": '券码 ' + str(r.get('code') or ''), "timeout": 5, "cwidth": 18, "cheight": 9}}
|
||||
return {"widgettype": "Error", "options": {"title": "Error", "message": r.get('message') or 'failed', "timeout": 3, "cwidth": 16, "cheight": 9}}
|
||||
6
wwwroot/api/my_vouchers.dspy
Normal file
6
wwwroot/api/my_vouchers.dspy
Normal file
@ -0,0 +1,6 @@
|
||||
# my_vouchers.dspy — 客户「我的代金券」列表(本机构隔离,Tabular 契约 {total, rows})
|
||||
user_id = await get_user()
|
||||
if not user_id:
|
||||
return {"widgettype": "Error",
|
||||
"options": {"title": "Authorization Error", "message": "Please login", "timeout": 3}}
|
||||
return await my_vouchers_api(request, params_kw)
|
||||
@ -1,9 +1,5 @@
|
||||
|
||||
data = {}
|
||||
for key in ['template_id', 'rule_type', 'rule_config', 'enabled', 'sort_order', 'remark']:
|
||||
val = params_kw.get(key, None) if hasattr(params_kw, 'get') else None
|
||||
if val is not None:
|
||||
data[key] = val
|
||||
|
||||
result = await create_voucher_rule(data)
|
||||
return result
|
||||
# voucher_rule_create.dspy — 规则新增(rule_config 合法性由 helper 校验)
|
||||
r = await create_voucher_rule(request, params_kw)
|
||||
if r.get('success'):
|
||||
return {"widgettype": "Message", "options": {"title": "Success", "message": r.get('message') or 'ok', "timeout": 3, "cwidth": 16, "cheight": 9}}
|
||||
return {"widgettype": "Error", "options": {"title": "Error", "message": r.get('message') or 'failed', "timeout": 3, "cwidth": 16, "cheight": 9}}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
|
||||
conditions = {'id': params_kw.get('id', '')}
|
||||
result = await delete_voucher_rule(conditions)
|
||||
return result
|
||||
# voucher_rule_delete.dspy — 规则删除
|
||||
r = await delete_voucher_rule(request, params_kw)
|
||||
if r.get('success'):
|
||||
return {"widgettype": "Message", "options": {"title": "Success", "message": r.get('message') or 'ok', "timeout": 3, "cwidth": 16, "cheight": 9}}
|
||||
return {"widgettype": "Error", "options": {"title": "Error", "message": r.get('message') or 'failed', "timeout": 3, "cwidth": 16, "cheight": 9}}
|
||||
|
||||
@ -1,10 +1,5 @@
|
||||
|
||||
conditions = {'id': params_kw.get('id', '')}
|
||||
data = {}
|
||||
for key in ['template_id', 'rule_type', 'rule_config', 'enabled', 'sort_order', 'remark']:
|
||||
val = params_kw.get(key, None) if hasattr(params_kw, 'get') else None
|
||||
if val is not None:
|
||||
data[key] = val
|
||||
|
||||
result = await update_voucher_rule(conditions, data)
|
||||
return result
|
||||
# voucher_rule_update.dspy — 规则更新
|
||||
r = await update_voucher_rule(request, params_kw)
|
||||
if r.get('success'):
|
||||
return {"widgettype": "Message", "options": {"title": "Success", "message": r.get('message') or 'ok', "timeout": 3, "cwidth": 16, "cheight": 9}}
|
||||
return {"widgettype": "Error", "options": {"title": "Error", "message": r.get('message') or 'failed', "timeout": 3, "cwidth": 16, "cheight": 9}}
|
||||
|
||||
@ -1,4 +1,2 @@
|
||||
import json
|
||||
|
||||
result = await get_registered_rule_types()
|
||||
return json.dumps(result)
|
||||
# rule_types.dspy — 已注册规则类型清单(管理界面配规则参考)
|
||||
return await get_registered_rule_types(request, params_kw)
|
||||
|
||||
@ -1,10 +1,5 @@
|
||||
|
||||
params = params_kw if hasattr(params_kw, '__getitem__') else {}
|
||||
data = {}
|
||||
for key in ['name', 'code', 'face_value', 'total_count', 'valid_days', 'status', 'remark']:
|
||||
val = params_kw.get(key, None) if hasattr(params_kw, 'get') else None
|
||||
if val is not None:
|
||||
data[key] = val
|
||||
|
||||
result = await create_voucher_template(data)
|
||||
return result
|
||||
# voucher_template_create.dspy — 模板新增(走模块 helper:org 隔离+code 唯一)
|
||||
r = await create_voucher_template(request, params_kw)
|
||||
if r.get('success'):
|
||||
return {"widgettype": "Message", "options": {"title": "Success", "message": r.get('message') or 'ok', "timeout": 3, "cwidth": 16, "cheight": 9}}
|
||||
return {"widgettype": "Error", "options": {"title": "Error", "message": r.get('message') or 'failed', "timeout": 3, "cwidth": 16, "cheight": 9}}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
|
||||
conditions = {'id': params_kw.get('id', '')}
|
||||
result = await delete_voucher_template(conditions)
|
||||
return result
|
||||
# voucher_template_delete.dspy — 模板删除(已发放券的模板拒绝,引导用「停用」)
|
||||
r = await delete_voucher_template(request, params_kw)
|
||||
if r.get('success'):
|
||||
return {"widgettype": "Message", "options": {"title": "Success", "message": r.get('message') or 'ok', "timeout": 3, "cwidth": 16, "cheight": 9}}
|
||||
return {"widgettype": "Error", "options": {"title": "Error", "message": r.get('message') or 'failed', "timeout": 3, "cwidth": 16, "cheight": 9}}
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
import json
|
||||
|
||||
result = await get_voucher_template_options()
|
||||
return json.dumps(result)
|
||||
# voucher_template_options.dspy — 启用模板下拉(code 组件契约:纯 [{value,text}] 数组)
|
||||
try:
|
||||
return await get_voucher_template_options(request, params_kw)
|
||||
except Exception as e:
|
||||
debug('voucher_template_options error: ' + str(e))
|
||||
return []
|
||||
|
||||
@ -1,10 +1,5 @@
|
||||
|
||||
conditions = {'id': params_kw.get('id', '')}
|
||||
data = {}
|
||||
for key in ['name', 'code', 'face_value', 'total_count', 'valid_days', 'status', 'remark', 'updater']:
|
||||
val = params_kw.get(key, None) if hasattr(params_kw, 'get') else None
|
||||
if val is not None:
|
||||
data[key] = val
|
||||
|
||||
result = await update_voucher_template(conditions, data)
|
||||
return result
|
||||
# voucher_template_update.dspy — 模板更新
|
||||
r = await update_voucher_template(request, params_kw)
|
||||
if r.get('success'):
|
||||
return {"widgettype": "Message", "options": {"title": "Success", "message": r.get('message') or 'ok', "timeout": 3, "cwidth": 16, "cheight": 9}}
|
||||
return {"widgettype": "Error", "options": {"title": "Error", "message": r.get('message') or 'failed', "timeout": 3, "cwidth": 16, "cheight": 9}}
|
||||
|
||||
@ -1,9 +0,0 @@
|
||||
|
||||
data = {}
|
||||
for key in ['instance_id', 'customer_id', 'order_id', 'face_value', 'deducted_amount', 'used_at', 'used_by', 'product_type', 'product_name']:
|
||||
val = params_kw.get(key, None) if hasattr(params_kw, 'get') else None
|
||||
if val is not None:
|
||||
data[key] = val
|
||||
|
||||
result = await create_voucher_usage_log(data)
|
||||
return result
|
||||
@ -1,4 +0,0 @@
|
||||
|
||||
conditions = {'id': params_kw.get('id', '')}
|
||||
result = await delete_voucher_usage_log(conditions)
|
||||
return result
|
||||
@ -1,10 +0,0 @@
|
||||
|
||||
conditions = {'id': params_kw.get('id', '')}
|
||||
data = {}
|
||||
for key in ['instance_id', 'customer_id', 'order_id', 'face_value', 'deducted_amount', 'used_by', 'product_type', 'product_name']:
|
||||
val = params_kw.get(key, None) if hasattr(params_kw, 'get') else None
|
||||
if val is not None:
|
||||
data[key] = val
|
||||
|
||||
result = await update_voucher_usage_log(conditions, data)
|
||||
return result
|
||||
@ -1,9 +1,8 @@
|
||||
import json
|
||||
|
||||
# 获取当前登录用户的组织ID作为客户ID
|
||||
# v1/available.dspy — 客户自助查询可用券(HTTP API,登录态按机构隔离)
|
||||
customer_id = await get_userorgid()
|
||||
if not customer_id:
|
||||
return {'status': 'error', 'message': 'not logged in', 'data': [], 'total': 0}
|
||||
|
||||
# 构建查询上下文(可选过滤条件)
|
||||
context = {}
|
||||
if params_kw.get('product_type'):
|
||||
context['product_type'] = params_kw.get('product_type')
|
||||
@ -15,25 +14,15 @@ if params_kw.get('request_amount'):
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
# 查询可用代金券
|
||||
result = await get_available_vouchers_api(customer_id, context if context else None)
|
||||
|
||||
# 格式化输出
|
||||
if result.get('status') == 'success':
|
||||
vouchers = []
|
||||
for v in result.get('data', []):
|
||||
vouchers.append({
|
||||
'id': v.get('id'),
|
||||
'code': v.get('code'),
|
||||
'face_value': float(v.get('face_value', 0)),
|
||||
'valid_from': str(v.get('valid_from', '')),
|
||||
'valid_to': str(v.get('valid_to', '')),
|
||||
'template_name': v.get('template_name', ''),
|
||||
})
|
||||
return json.dumps({
|
||||
'status': 'success',
|
||||
'data': vouchers,
|
||||
'total': len(vouchers)
|
||||
vouchers = []
|
||||
for v in (result.get('data') or []):
|
||||
vouchers.append({
|
||||
'id': v.get('id'),
|
||||
'code': v.get('code'),
|
||||
'face_value': float(v.get('face_value') or 0),
|
||||
'valid_from': str(v.get('valid_from') or ''),
|
||||
'valid_to': str(v.get('valid_to') or ''),
|
||||
'template_name': v.get('template_name') or '',
|
||||
})
|
||||
else:
|
||||
return json.dumps(result)
|
||||
return {'status': 'success', 'data': vouchers, 'total': len(vouchers)}
|
||||
|
||||
197
wwwroot/index.ui
197
wwwroot/index.ui
@ -1,48 +1,159 @@
|
||||
{
|
||||
"widgettype": "VBox",
|
||||
"options": {"width": "100%", "height": "100%", "padding": "20px"},
|
||||
"options": {
|
||||
"width": "100%",
|
||||
"height": "100%",
|
||||
"padding": "20px"
|
||||
},
|
||||
"subwidgets": [
|
||||
{"widgettype": "Title3", "options": {"text": "代金券管理"}},
|
||||
{"widgettype": "ResponsableBox", "options": {"gap": "16px", "minWidth": "250px"},
|
||||
"subwidgets": [
|
||||
{"widgettype": "VBox",
|
||||
"options": {"bgcolor": "#FFFFFF", "padding": "20px", "cursor": "pointer", "borderRadius": "8px", "cwidth": "20"},
|
||||
"binds": [{
|
||||
"wid": "self", "event": "click", "actiontype": "urlwidget",
|
||||
"target": "app.voucher_content",
|
||||
"options": {"url": "{{entire_url('/voucher/voucher_template_list/index.ui')}}"},
|
||||
"mode": "replace"
|
||||
}],
|
||||
"subwidgets": [
|
||||
{"widgettype": "Text", "options": {"text": "模板管理", "fontSize": "18px", "bold": true}},
|
||||
{"widgettype": "Text", "options": {"text": "管理代金券模板和规则配置", "fontSize": "14px", "color": "#666"}}
|
||||
]},
|
||||
{"widgettype": "VBox",
|
||||
"options": {"bgcolor": "#FFFFFF", "padding": "20px", "cursor": "pointer", "borderRadius": "8px", "cwidth": "20"},
|
||||
"binds": [{
|
||||
"wid": "self", "event": "click", "actiontype": "urlwidget",
|
||||
"target": "app.voucher_content",
|
||||
"options": {"url": "{{entire_url('/voucher/voucher_instance_list/index.ui')}}"},
|
||||
"mode": "replace"
|
||||
}],
|
||||
"subwidgets": [
|
||||
{"widgettype": "Text", "options": {"text": "券实例管理", "fontSize": "18px", "bold": true}},
|
||||
{"widgettype": "Text", "options": {"text": "查看和发放代金券", "fontSize": "14px", "color": "#666"}}
|
||||
]},
|
||||
{"widgettype": "VBox",
|
||||
"options": {"bgcolor": "#FFFFFF", "padding": "20px", "cursor": "pointer", "borderRadius": "8px", "cwidth": "20"},
|
||||
"binds": [{
|
||||
"wid": "self", "event": "click", "actiontype": "urlwidget",
|
||||
"target": "app.voucher_content",
|
||||
"options": {"url": "{{entire_url('/voucher/voucher_usage_log_list/index.ui')}}"},
|
||||
"mode": "replace"
|
||||
}],
|
||||
"subwidgets": [
|
||||
{"widgettype": "Text", "options": {"text": "使用流水", "fontSize": "18px", "bold": true}},
|
||||
{"widgettype": "Text", "options": {"text": "查看代金券使用记录", "fontSize": "14px", "color": "#666"}}
|
||||
]}
|
||||
]},
|
||||
{"widgettype": "VBox", "id": "voucher_content",
|
||||
"options": {"width": "100%", "flex": "1", "marginTop": "20px"}}
|
||||
{
|
||||
"widgettype": "Title3",
|
||||
"options": {
|
||||
"otext": "代金券管理",
|
||||
"text": "代金券管理",
|
||||
"i18n": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"widgettype": "ResponsableBox",
|
||||
"options": {
|
||||
"gap": "16px",
|
||||
"minWidth": "250px"
|
||||
},
|
||||
"subwidgets": [
|
||||
{
|
||||
"widgettype": "VBox",
|
||||
"options": {
|
||||
"css": "card",
|
||||
"padding": "20px",
|
||||
"cursor": "pointer",
|
||||
"cwidth": 20
|
||||
},
|
||||
"binds": [
|
||||
{
|
||||
"wid": "self",
|
||||
"event": "click",
|
||||
"actiontype": "urlwidget",
|
||||
"target": "voucher_content",
|
||||
"options": {
|
||||
"url": "{{entire_url('/voucher/voucher_template_list/index.ui')}}"
|
||||
},
|
||||
"mode": "replace"
|
||||
}
|
||||
],
|
||||
"subwidgets": [
|
||||
{
|
||||
"widgettype": "Title4",
|
||||
"options": {
|
||||
"otext": "模板管理",
|
||||
"text": "模板管理",
|
||||
"i18n": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"widgettype": "Text",
|
||||
"options": {
|
||||
"otext": "管理代金券模板和规则配置",
|
||||
"text": "管理代金券模板和规则配置",
|
||||
"i18n": true,
|
||||
"cfontsize": 0.9,
|
||||
"color": "#666"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"widgettype": "VBox",
|
||||
"options": {
|
||||
"css": "card",
|
||||
"padding": "20px",
|
||||
"cursor": "pointer",
|
||||
"cwidth": 20
|
||||
},
|
||||
"binds": [
|
||||
{
|
||||
"wid": "self",
|
||||
"event": "click",
|
||||
"actiontype": "urlwidget",
|
||||
"target": "voucher_content",
|
||||
"options": {
|
||||
"url": "{{entire_url('/voucher/voucher_instance_list/index.ui')}}"
|
||||
},
|
||||
"mode": "replace"
|
||||
}
|
||||
],
|
||||
"subwidgets": [
|
||||
{
|
||||
"widgettype": "Title4",
|
||||
"options": {
|
||||
"otext": "券实例管理",
|
||||
"text": "券实例管理",
|
||||
"i18n": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"widgettype": "Text",
|
||||
"options": {
|
||||
"otext": "查看和发放代金券",
|
||||
"text": "查看和发放代金券",
|
||||
"i18n": true,
|
||||
"cfontsize": 0.9,
|
||||
"color": "#666"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"widgettype": "VBox",
|
||||
"options": {
|
||||
"css": "card",
|
||||
"padding": "20px",
|
||||
"cursor": "pointer",
|
||||
"cwidth": 20
|
||||
},
|
||||
"binds": [
|
||||
{
|
||||
"wid": "self",
|
||||
"event": "click",
|
||||
"actiontype": "urlwidget",
|
||||
"target": "voucher_content",
|
||||
"options": {
|
||||
"url": "{{entire_url('/voucher/voucher_usage_log_list/index.ui')}}"
|
||||
},
|
||||
"mode": "replace"
|
||||
}
|
||||
],
|
||||
"subwidgets": [
|
||||
{
|
||||
"widgettype": "Title4",
|
||||
"options": {
|
||||
"otext": "使用流水",
|
||||
"text": "使用流水",
|
||||
"i18n": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"widgettype": "Text",
|
||||
"options": {
|
||||
"otext": "查看代金券使用记录",
|
||||
"text": "查看代金券使用记录",
|
||||
"i18n": true,
|
||||
"cfontsize": 0.9,
|
||||
"color": "#666"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"widgettype": "VBox",
|
||||
"id": "voucher_content",
|
||||
"options": {
|
||||
"width": "100%",
|
||||
"flex": "1",
|
||||
"marginTop": "20px"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
43
wwwroot/invalidate_form.ui
Normal file
43
wwwroot/invalidate_form.ui
Normal file
@ -0,0 +1,43 @@
|
||||
{
|
||||
"widgettype": "Form",
|
||||
"id": "voucher_invalidate_form",
|
||||
"options": {
|
||||
"cols": 1,
|
||||
"title": "作废代金券",
|
||||
"padding": "16px",
|
||||
"submit_label": "确认作废",
|
||||
"submit_url": "{{entire_url('/voucher/api/instance/voucher_invalidate.dspy')}}",
|
||||
"method": "POST",
|
||||
"fields": [
|
||||
{
|
||||
"name": "id",
|
||||
"label": "实例ID",
|
||||
"uitype": "hide",
|
||||
"value": "{{params_kw.get('id') or ''}}"
|
||||
},
|
||||
{
|
||||
"name": "code_display",
|
||||
"label": "券码",
|
||||
"uitype": "str",
|
||||
"nonuse": true,
|
||||
"value": "{{params_kw.get('code') or ''}}"
|
||||
},
|
||||
{
|
||||
"name": "reason",
|
||||
"label": "作废原因",
|
||||
"uitype": "str",
|
||||
"required": true,
|
||||
"placeholder": "必填,将追加到券备注(审计留痕)"
|
||||
}
|
||||
]
|
||||
},
|
||||
"binds": [
|
||||
{
|
||||
"wid": "self",
|
||||
"event": "submited",
|
||||
"actiontype": "script",
|
||||
"target": "self",
|
||||
"script": "var resp=event.params; var d=null; try{d=await resp.json();}catch(e){d=null;} if(d&&d.widgettype){await bricks.widgetBuild(d,bricks.Body);} if(d&&d.widgettype==='Message'){ var t=bricks.getWidgetById('voucher_instance_tbl',bricks.app); if(t){await t.render({});} }"
|
||||
}
|
||||
]
|
||||
}
|
||||
65
wwwroot/issue_form.ui
Normal file
65
wwwroot/issue_form.ui
Normal file
@ -0,0 +1,65 @@
|
||||
{
|
||||
"widgettype": "Form",
|
||||
"id": "voucher_issue_form",
|
||||
"options": {
|
||||
"cols": 1,
|
||||
"title": "发放代金券",
|
||||
"padding": "16px",
|
||||
"submit_url": "{{entire_url('/voucher/api/instance/voucher_issue.dspy')}}",
|
||||
"method": "POST",
|
||||
"fields": [
|
||||
{
|
||||
"name": "template_id",
|
||||
"label": "模板ID",
|
||||
"uitype": "hide",
|
||||
"value": "{{params_kw.get('template_id') or params_kw.get('id') or ''}}"
|
||||
},
|
||||
{
|
||||
"name": "customer_id",
|
||||
"label": "客户机构",
|
||||
"uitype": "code",
|
||||
"required": true,
|
||||
"dataurl": "{{entire_url('/appbase/get_code.dspy')}}",
|
||||
"params": {
|
||||
"dbname": "{{get_module_dbname('voucher')}}",
|
||||
"table": "organization",
|
||||
"tblvalue": "id",
|
||||
"tbltext": "orgname",
|
||||
"valueField": "customer_id",
|
||||
"textField": "customer_id_text"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "source",
|
||||
"label": "发放来源",
|
||||
"uitype": "code",
|
||||
"value": "manual",
|
||||
"data": [
|
||||
{
|
||||
"value": "manual",
|
||||
"text": "手动发放"
|
||||
},
|
||||
{
|
||||
"value": "promo",
|
||||
"text": "促销活动"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "remark",
|
||||
"label": "备注",
|
||||
"uitype": "str",
|
||||
"placeholder": "可选"
|
||||
}
|
||||
]
|
||||
},
|
||||
"binds": [
|
||||
{
|
||||
"wid": "self",
|
||||
"event": "submited",
|
||||
"actiontype": "script",
|
||||
"target": "self",
|
||||
"script": "var resp=event.params; var d=null; try{d=await resp.json();}catch(e){d=null;} if(d&&d.widgettype){await bricks.widgetBuild(d,bricks.Body);} if(d&&d.widgettype==='Message'){ var t1=bricks.getWidgetById('voucher_instance_tbl',bricks.app); if(t1){await t1.render({});} var t2=bricks.getWidgetById('voucher_template_tbl',bricks.app); if(t2){await t2.render({});} }"
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -1,25 +0,0 @@
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"name": "voucher_template_list",
|
||||
"label": "模板管理",
|
||||
"icon": "fa fa-ticket",
|
||||
"url": "{{entire_url('/voucher/voucher_template_list/index.ui')}}",
|
||||
"target": "app.sage_main_content"
|
||||
},
|
||||
{
|
||||
"name": "voucher_instance_list",
|
||||
"label": "券实例",
|
||||
"icon": "fa fa-list",
|
||||
"url": "{{entire_url('/voucher/voucher_instance_list/index.ui')}}",
|
||||
"target": "app.sage_main_content"
|
||||
},
|
||||
{
|
||||
"name": "voucher_usage_log_list",
|
||||
"label": "使用流水",
|
||||
"icon": "fa fa-history",
|
||||
"url": "{{entire_url('/voucher/voucher_usage_log_list/index.ui')}}",
|
||||
"target": "app.sage_main_content"
|
||||
}
|
||||
]
|
||||
}
|
||||
62
wwwroot/my/index.ui
Normal file
62
wwwroot/my/index.ui
Normal file
@ -0,0 +1,62 @@
|
||||
{
|
||||
"widgettype": "VBox",
|
||||
"options": {"width": "100%", "height": "100%", "padding": "8px", "gap": "8px"},
|
||||
"subwidgets": [
|
||||
{
|
||||
"widgettype": "InlineForm",
|
||||
"id": "my_voucher_filter",
|
||||
"options": {
|
||||
"css": "card", "padding": "8px", "submit_label": "查询",
|
||||
"fields": [
|
||||
{"name": "status", "label": "状态", "uitype": "code", "cwidth": 10,
|
||||
"data": [
|
||||
{"value": "", "text": "全部"},
|
||||
{"value": "unused", "text": "未使用"},
|
||||
{"value": "used", "text": "已使用"},
|
||||
{"value": "expired", "text": "已过期"},
|
||||
{"value": "void", "text": "已作废"}
|
||||
]}
|
||||
]
|
||||
},
|
||||
"binds": [{
|
||||
"wid": "self", "event": "submit",
|
||||
"actiontype": "script", "target": "my_voucher_table",
|
||||
"script": "var tbl = bricks.getWidgetById('my_voucher_table', bricks.app.root); if(tbl) await tbl.render(params);"
|
||||
}]
|
||||
},
|
||||
{
|
||||
"widgettype": "Tabular",
|
||||
"id": "my_voucher_table",
|
||||
"options": {
|
||||
"title": "我的代金券",
|
||||
"width": "100%", "css": "card",
|
||||
"data_url": "{{entire_url('/voucher/api/my_vouchers.dspy')}}",
|
||||
"data_method": "GET", "page_rows": 20,
|
||||
"row_options": {
|
||||
"browserfields": {
|
||||
"exclouded": ["id"],
|
||||
"alters": {
|
||||
"status": {"uitype": "code", "data": [
|
||||
{"value": "unused", "text": "未使用"}, {"value": "used", "text": "已使用"},
|
||||
{"value": "expired", "text": "已过期"}, {"value": "void", "text": "已作废"}]},
|
||||
"source": {"uitype": "code", "data": [
|
||||
{"value": "manual", "text": "手动发放"}, {"value": "register", "text": "注册赠券"},
|
||||
{"value": "promo", "text": "促销活动"}]}
|
||||
}
|
||||
},
|
||||
"fields": [
|
||||
{"name": "code", "title": "券码", "type": "str", "cwidth": 16},
|
||||
{"name": "template_name", "title": "模板", "type": "str", "cwidth": 14},
|
||||
{"name": "face_value", "title": "面值(元)", "type": "double", "cwidth": 8},
|
||||
{"name": "status", "title": "状态", "type": "str", "cwidth": 8},
|
||||
{"name": "actual_deducted", "title": "已抵扣(元)", "type": "double", "cwidth": 9},
|
||||
{"name": "valid_from", "title": "生效时间", "type": "datetime", "cwidth": 13},
|
||||
{"name": "valid_to", "title": "过期时间", "type": "datetime", "cwidth": 13},
|
||||
{"name": "source", "title": "来源", "type": "str", "cwidth": 8},
|
||||
{"name": "remark", "title": "备注", "type": "str", "cwidth": 16}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user