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 | 代金券模板(面值、有效期、发行量、状态) |
|
| 一次性使用 | 核销用条件 UPDATE(`status='unused'` 守卫)+ affected_rows 判定,并发双花只有一个成功 |
|
||||||
| voucher_rule | 规则定义(模板关联,可增删启用禁用) |
|
| 发行量控制 | `issued_count<total_count` 原子占额;total_count=0 不限量;未使用券删除回吐名额 |
|
||||||
| voucher_instance | 券实例(一次性使用,状态:unused/used/expired) |
|
| 规则可配置 | `@register_rule` 注册 validator 纯函数,模板下挂 voucher_rule 行即生效 |
|
||||||
| voucher_usage_log | 使用流水(订单、抵扣金额、产品信息) |
|
| 作废 | `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 示例 |
|
### 管理侧(logined;菜单「代金券管理」,工单三角色可见)
|
||||||
|-----------|------|------------------|
|
|
||||||
| `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}` |
|
|
||||||
|
|
||||||
### 规则执行流程
|
| 页面 | 路径 | 说明 |
|
||||||
1. 查询券实例状态(unused + 未过期)
|
|---|---|---|
|
||||||
2. 加载模板关联的所有启用规则(按 sort_order 排序)
|
| 入口 | `/voucher/index.ui` | 卡片导航:模板管理/券实例管理/使用流水 |
|
||||||
3. 依次执行 validator,任一失败即拒绝
|
| 模板管理 | `/voucher/voucher_template_list/index.ui` | CRUD + 工具栏「发券」弹窗 + 子表「规则配置」 |
|
||||||
4. 全部通过 → 计算抵扣金额 = min(面值, 消费金额)
|
| 券实例 | `/voucher/voucher_instance_list/index.ui` | 新增=发券引擎(禁裸插入)+ 工具栏「作废」弹窗 + 子表「使用记录」 |
|
||||||
5. 记录流水 + 标记券为已使用(一次性作废)
|
| 使用流水 | `/voucher/voucher_usage_log_list/index.ui` | 只读(noedit) |
|
||||||
|
|
||||||
### 新增规则步骤
|
管理 API(`wwwroot/api/`,全部薄封装 → init.py ServerEnv 函数):
|
||||||
```python
|
- template/:create/update/delete/options(options 返回纯 `[{value,text}]` 数组)
|
||||||
# rules/validators.py
|
- rule/:create/update/delete;`rule_types.dspy` 返回已注册类型清单
|
||||||
@register_rule('new_rule_type')
|
- instance/:`voucher_issue.dspy`(发券)、update(仅 unused 可改客户/备注)、delete(used 禁删)、`voucher_invalidate.dspy`(作废)
|
||||||
def check_new_rule(config, context):
|
- `apply_voucher.dspy`(核销)、`get_available.dspy`(内部查可用)
|
||||||
# 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"` 的规则记录即可。
|
|
||||||
|
|
||||||
---
|
### 客户侧(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
|
```python
|
||||||
# llmage 的计费逻辑中调用 voucher 引擎
|
# 结算联动(product_management/accounting 等):
|
||||||
from voucher.rules.engine import apply_voucher, get_available_vouchers
|
result = await env.voucher_batch_apply(sor, customer_id, order_id, voucher_ids, context)
|
||||||
|
# → {'total_deducted': 150.0, 'remaining': 50.0, 'details': [...]}
|
||||||
# 查询客户可用代金券
|
# remaining > 0 时从余额扣
|
||||||
vouchers = get_available_vouchers(sor, customer_id, context={
|
# context: {request_amount(必填), product_type, product_name, user_level, used_by}
|
||||||
'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 时从余额扣减
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### accounting 模块(余额扣减联动)
|
注册函数全集(`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`。
|
||||||
|
|
||||||
```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` + 模板管理界面 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 目录结构
|
## 目录结构
|
||||||
|
|
||||||
```
|
```
|
||||||
voucher/
|
voucher/
|
||||||
├── voucher/ # Python 包
|
├── voucher/ # Python 包
|
||||||
│ ├── __init__.py
|
│ ├── init.py # ServerEnv 注册 + 注册送券挂钩 + load_voucher()
|
||||||
│ └── init.py # 模块初始化 + ServerEnv 注册
|
│ └── rules/ # registry(@register_rule) + validators(7种) + engine(全异步)
|
||||||
├── rules/ # 规则引擎(纯 Python)
|
├── init/data.json # 字典种子(模板状态/实例状态/规则类型/来源/启用标志)
|
||||||
│ ├── registry.py # @register_rule 装饰器
|
├── models/ # 4 张表定义(建表唯一事实源,json2ddl 渲染)
|
||||||
│ ├── validators.py # 内置规则校验器
|
├── json/ # 4 个 CRUD 定义(xls2ui 生成 wwwroot/voucher_*_list/,生成物不入库)
|
||||||
│ └── engine.py # 校验/使用/批量使用
|
├── wwwroot/
|
||||||
├── wwwroot/ # 前端
|
│ ├── index.ui # 管理入口(卡片导航)
|
||||||
│ ├── index.ui # 入口页(卡片导航)
|
│ ├── my/index.ui # 客户「我的代金券」
|
||||||
│ ├── menu.json # 菜单定义
|
│ ├── issue_form.ui # 发券弹窗表单
|
||||||
│ └── api/ # API 端点
|
│ ├── invalidate_form.ui # 作废弹窗表单
|
||||||
│ ├── template/ # 模板 CRUD
|
│ └── api/ # 薄封装 dspy(零 import,委托 init.py 注册函数)
|
||||||
│ ├── 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 # 建表 + 编码初始化
|
|
||||||
├── scripts/
|
├── scripts/
|
||||||
│ └── load_path.py # RBAC 权限注册
|
│ ├── load_path.py # RBAC 注册(logined 全路径逐个精确配)
|
||||||
├── pyproject.toml
|
│ └── seed_welcome_voucher.py # WELCOME400 种子(幂等)
|
||||||
└── README.md
|
├── 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
|
```bash
|
||||||
cd ~/repos/voucher && git pull
|
cd <APP_ROOT>
|
||||||
cd ~/repos/sage/pkgs && ln -sf ~/repos/voucher voucher
|
./py3/bin/python pkgs/voucher/scripts/seed_welcome_voucher.py # WELCOME400 种子
|
||||||
cd voucher && ~/repos/sage/py3/bin/pip install -e .
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. 数据库建表
|
## Pitfalls
|
||||||
```bash
|
|
||||||
mysql -u root -p sage < ~/repos/voucher/sql/tables.sql
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Sage 集成
|
- **实例「新增」必须走发券 API**(voucher_issue.dspy → issue_voucher 引擎):裸插入会绕过原子占额/券码生成/有效期计算。CRUD json 的 `editable.new_data_url` 已指向它。
|
||||||
```python
|
- **状态字段禁止界面直改**:使用走核销 API、作废走 invalidate API(都有条件 UPDATE 原子守卫);update_voucher_instance 只允许改 customer_id/remark 且仅 unused 券。
|
||||||
# sage/app/sage.py
|
- 一次性使用:不追踪 remaining_value/partial 状态,抵扣 = min(面值, 消费金额),不找零。
|
||||||
from voucher.init import load_voucher
|
- rule_config 数组字段用 JSON array(validators 兼容逗号分隔串,但写入必须是 JSON)。
|
||||||
# ... in init():
|
- rule_type 用短名(product_type 而非 applicable_product_type)——appcodes_kv.id = `{parentid}_{k}` ≤32 字符。
|
||||||
load_voucher()
|
- 模板删除:已发放(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. 菜单入口
|
- 远端:git@git.opencomputing.cn:yumoqing/voucher.git(main)
|
||||||
```json
|
- 宿主接入提交:pipeline-app(build.sh/pipeline_app.py/import_init.py/index.ui)
|
||||||
// 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 从余额扣
|
|
||||||
```
|
|
||||||
|
|||||||
@ -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
|
Cancel=Cancel
|
||||||
Conform: Confirm
|
Conform=Confirm
|
||||||
Discard: Discard
|
Discard=Discard
|
||||||
Reset: Reset
|
Reset=Reset
|
||||||
Submit: Submit
|
Submit=Submit
|
||||||
主键ID: Primary Key ID
|
代金券=Voucher
|
||||||
产品名称: Product Name
|
代金券管理=Voucher Management
|
||||||
产品类型: Product Type
|
模板管理=Template Management
|
||||||
代金券使用流水: Voucher Usage Records
|
券实例管理=Voucher Instance Management
|
||||||
代金券实例: Voucher Instance
|
使用流水=Usage Records
|
||||||
代金券模板: Voucher Template
|
管理代金券模板和规则配置=Manage voucher templates and rule configuration
|
||||||
代金券规则定义: Voucher Rule Definition
|
查看和发放代金券=View and issue vouchers
|
||||||
使用时间: Usage Time
|
查看代金券使用记录=View voucher usage records
|
||||||
使用的订单ID: Order ID Used
|
我的代金券=My Vouchers
|
||||||
创建人: Creator
|
发放代金券=Issue Voucher
|
||||||
创建时间: Created At
|
作废=Void
|
||||||
券实例ID: Voucher Instance ID
|
确认作废=Confirm Void
|
||||||
券面值: Face Value
|
作废代金券=Void Voucher
|
||||||
发放时间: Issue Time
|
已作废=Voided
|
||||||
唯一券码: Unique Voucher Code
|
作废原因=Void Reason
|
||||||
备注: Remark
|
必填,将追加到券备注(审计留痕)=Required; will be appended to voucher remark (audit trail)
|
||||||
实际抵扣金额: Actual Deduction Amount
|
按此模板发券=Issue from this template
|
||||||
客户ID: Customer ID
|
代金券模板=Voucher Template
|
||||||
已发放量: Issued Quantity
|
代金券实例=Voucher Instance
|
||||||
总发行量: Total Issued
|
代金券规则=Voucher Rule
|
||||||
执行顺序: Execution Order
|
代金券使用流水=Voucher Usage Log
|
||||||
操作人: Operator
|
主键ID=Primary Key ID
|
||||||
是否启用: Enabled
|
模板名称=Template Name
|
||||||
更新人: Updater
|
模板编码=Template Code
|
||||||
更新时间: Updated At
|
模板ID=Template ID
|
||||||
有效期天数: Validity Days
|
模板=Template
|
||||||
来源: Source
|
面值=Face Value
|
||||||
模板ID: Template ID
|
面值(元)=Face Value (CNY)
|
||||||
模板名称: Template Name
|
已抵扣(元)=Deducted (CNY)
|
||||||
模板编码: Template Code
|
总发行量(0不限)=Total Issue Count (0=unlimited)
|
||||||
状态: Status
|
已发放量=Issued Count
|
||||||
生效时间: Effective Time
|
有效期天数=Valid Days
|
||||||
规则类型: Rule Type
|
状态=Status
|
||||||
规则配置(JSON): Rule Config (JSON)
|
草稿=Draft
|
||||||
订单/消费ID: Order/Consumption ID
|
启用=Active
|
||||||
过期时间: Expiry Time
|
停用=Inactive
|
||||||
面值: Face Value
|
备注=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: キャンセル
|
Cancel=キャンセル
|
||||||
Conform: 確認
|
Conform=確認
|
||||||
Discard: 破棄
|
Discard=破棄
|
||||||
Reset: リセット
|
Reset=リセット
|
||||||
Submit: 送信
|
Submit=送信
|
||||||
主键ID: 主キーID
|
代金券=クーポン
|
||||||
产品名称: 製品名
|
代金券管理=クーポン管理
|
||||||
产品类型: 製品タイプ
|
模板管理=テンプレート管理
|
||||||
代金券使用流水: バウチャー使用履歴
|
券实例管理=クーポンインスタンス管理
|
||||||
代金券实例: バウチャーインスタンス
|
使用流水=使用履歴
|
||||||
代金券模板: バウチャーテンプレート
|
管理代金券模板和规则配置=クーポンテンプレートとルール設定を管理
|
||||||
代金券规则定义: バウチャールール定義
|
查看和发放代金券=クーポンの確認と発行
|
||||||
使用时间: 使用日時
|
查看代金券使用记录=クーポン使用記録の確認
|
||||||
使用的订单ID: 使用注文ID
|
我的代金券=マイクーポン
|
||||||
创建人: 作成者
|
发放代金券=クーポン発行
|
||||||
创建时间: 作成日時
|
作废=無効化
|
||||||
券实例ID: バウチャーインスタンスID
|
确认作废=無効化の確認
|
||||||
券面值: 券面額
|
作废代金券=クーポン無効化
|
||||||
发放时间: 発行日時
|
已作废=無効化済み
|
||||||
唯一券码: ユニークバウチャーコード
|
作废原因=無効化理由
|
||||||
备注: 備考
|
必填,将追加到券备注(审计留痕)=必須入力。クーポン備考に追記されます(監査証跡)
|
||||||
实际抵扣金额: 実質控除額
|
按此模板发券=このテンプレートで発行
|
||||||
客户ID: 顧客ID
|
代金券模板=クーポンテンプレート
|
||||||
已发放量: 発行済み数量
|
代金券实例=クーポンインスタンス
|
||||||
总发行量: 総発行量
|
代金券规则=クーポンルール
|
||||||
执行顺序: 実行順序
|
代金券使用流水=クーポン使用履歴
|
||||||
操作人: 操作者
|
主键ID=主キーID
|
||||||
是否启用: 有効化
|
模板名称=テンプレート名
|
||||||
更新人: 更新者
|
模板编码=テンプレートコード
|
||||||
更新时间: 更新日時
|
模板ID=テンプレートID
|
||||||
有效期天数: 有効日数
|
模板=テンプレート
|
||||||
来源: ソース
|
面值=額面
|
||||||
模板ID: テンプレートID
|
面值(元)=額面(元)
|
||||||
模板名称: テンプレート名
|
已抵扣(元)=控除額(元)
|
||||||
模板编码: テンプレートコード
|
总发行量(0不限)=総発行枚数(0=無制限)
|
||||||
状态: ステータス
|
已发放量=発行済み枚数
|
||||||
生效时间: 発効日時
|
有效期天数=有効日数
|
||||||
规则类型: ルールタイプ
|
状态=ステータス
|
||||||
规则配置(JSON): ルール設定(JSON)
|
草稿=下書き
|
||||||
订单/消费ID: 注文/消費ID
|
启用=有効
|
||||||
过期时间: 有効期限
|
停用=無効
|
||||||
面值: 額面
|
备注=備考
|
||||||
|
所属机构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: 취소
|
Cancel=취소
|
||||||
Conform: 확인
|
Conform=확인
|
||||||
Discard: 폐기
|
Discard=폐기
|
||||||
Reset: 초기화
|
Reset=초기화
|
||||||
Submit: 제출
|
Submit=제출
|
||||||
主键ID: 기본키 ID
|
代金券=상품권
|
||||||
产品名称: 제품 이름
|
代金券管理=상품권 관리
|
||||||
产品类型: 제품 유형
|
模板管理=템플릿 관리
|
||||||
代金券使用流水: 바우처 사용 내역
|
券实例管理=상품권 인스턴스 관리
|
||||||
代金券实例: 바우처 인스턴스
|
使用流水=사용 내역
|
||||||
代金券模板: 바우처 템플릿
|
管理代金券模板和规则配置=상품권 템플릿 및 규칙 구성 관리
|
||||||
代金券规则定义: 바우처 규칙 정의
|
查看和发放代金券=상품권 조회 및 발급
|
||||||
使用时间: 사용 시간
|
查看代金券使用记录=상품권 사용 기록 조회
|
||||||
使用的订单ID: 사용 주문 ID
|
我的代金券=내 상품권
|
||||||
创建人: 생성자
|
发放代金券=상품권 발급
|
||||||
创建时间: 생성 시간
|
作废=무효화
|
||||||
券实例ID: 바우처 인스턴스 ID
|
确认作废=무효화 확인
|
||||||
券面值: 액면가
|
作废代金券=상품권 무효화
|
||||||
发放时间: 발급 시간
|
已作废=무효화됨
|
||||||
唯一券码: 고유 바우처 코드
|
作废原因=무효화 사유
|
||||||
备注: 비고
|
必填,将追加到券备注(审计留痕)=필수 입력, 상품권 비고에 추가됩니다(감사 추적)
|
||||||
实际抵扣金额: 실제 공제 금액
|
按此模板发券=이 템플릿으로 발급
|
||||||
客户ID: 고객 ID
|
代金券模板=상품권 템플릿
|
||||||
已发放量: 발급 수량
|
代金券实例=상품권 인스턴스
|
||||||
总发行量: 총 발행량
|
代金券规则=상품권 규칙
|
||||||
执行顺序: 실행 순서
|
代金券使用流水=상품권 사용 내역
|
||||||
操作人: 운영자
|
主键ID=기본키 ID
|
||||||
是否启用: 활성화 여부
|
模板名称=템플릿 이름
|
||||||
更新人: 업데이트 담당자
|
模板编码=템플릿 코드
|
||||||
更新时间: 업데이트 시간
|
模板ID=템플릿 ID
|
||||||
有效期天数: 유효 기간(일)
|
模板=템플릿
|
||||||
来源: 출처
|
面值=액면가
|
||||||
模板ID: 템플릿 ID
|
面值(元)=액면가(위안)
|
||||||
模板名称: 템플릿 이름
|
已抵扣(元)=차감액(위안)
|
||||||
模板编码: 템플릿 코드
|
总发行量(0不限)=총 발행량(0=무제한)
|
||||||
状态: 상태
|
已发放量=발행 수량
|
||||||
生效时间: 효력 시작 시간
|
有效期天数=유효 일수
|
||||||
规则类型: 규칙 유형
|
状态=상태
|
||||||
规则配置(JSON): 규칙 구성(JSON)
|
草稿=초안
|
||||||
订单/消费ID: 주문/소비 ID
|
启用=활성
|
||||||
过期时间: 만료 시간
|
停用=비활성
|
||||||
面值: 액면가
|
备注=비고
|
||||||
|
所属机构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
|
Cancel=Cancel
|
||||||
Conform: Conform
|
Conform=Conform
|
||||||
Discard: Discard
|
Discard=Discard
|
||||||
Reset: Reset
|
Reset=Reset
|
||||||
Submit: Submit
|
Submit=Submit
|
||||||
主键ID: 主键ID
|
代金券=代金券
|
||||||
产品名称: 产品名称
|
代金券管理=代金券管理
|
||||||
产品类型: 产品类型
|
模板管理=模板管理
|
||||||
代金券使用流水: 代金券使用流水
|
券实例管理=券实例管理
|
||||||
代金券实例: 代金券实例
|
使用流水=使用流水
|
||||||
代金券模板: 代金券模板
|
管理代金券模板和规则配置=管理代金券模板和规则配置
|
||||||
代金券规则定义: 代金券规则定义
|
查看和发放代金券=查看和发放代金券
|
||||||
使用时间: 使用时间
|
查看代金券使用记录=查看代金券使用记录
|
||||||
使用的订单ID: 使用的订单ID
|
我的代金券=我的代金券
|
||||||
创建人: 创建人
|
发放代金券=发放代金券
|
||||||
创建时间: 创建时间
|
作废=作废
|
||||||
券实例ID: 券实例ID
|
确认作废=确认作废
|
||||||
券面值: 券面值
|
作废代金券=作废代金券
|
||||||
发放时间: 发放时间
|
已作废=已作废
|
||||||
唯一券码: 唯一券码
|
作废原因=作废原因
|
||||||
备注: 备注
|
必填,将追加到券备注(审计留痕)=必填,将追加到券备注(审计留痕)
|
||||||
实际抵扣金额: 实际抵扣金额
|
按此模板发券=按此模板发券
|
||||||
客户ID: 客户ID
|
代金券模板=代金券模板
|
||||||
已发放量: 已发放量
|
代金券实例=代金券实例
|
||||||
总发行量: 总发行量
|
代金券规则=代金券规则
|
||||||
执行顺序: 执行顺序
|
代金券使用流水=代金券使用流水
|
||||||
操作人: 操作人
|
主键ID=主键ID
|
||||||
是否启用: 是否启用
|
模板名称=模板名称
|
||||||
更新人: 更新人
|
模板编码=模板编码
|
||||||
更新时间: 更新时间
|
模板ID=模板ID
|
||||||
有效期天数: 有效期天数
|
模板=模板
|
||||||
来源: 来源
|
面值=面值
|
||||||
模板ID: 模板ID
|
面值(元)=面值(元)
|
||||||
模板名称: 模板名称
|
已抵扣(元)=已抵扣(元)
|
||||||
模板编码: 模板编码
|
总发行量(0不限)=总发行量(0不限)
|
||||||
状态: 状态
|
已发放量=已发放量
|
||||||
生效时间: 生效时间
|
有效期天数=有效期天数
|
||||||
规则类型: 规则类型
|
状态=状态
|
||||||
规则配置(JSON): 规则配置(JSON)
|
草稿=草稿
|
||||||
订单/消费ID: 订单/消费ID
|
启用=启用
|
||||||
过期时间: 过期时间
|
停用=停用
|
||||||
面值: 面值
|
备注=备注
|
||||||
|
所属机构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",
|
"tblname": "voucher_instance",
|
||||||
"alias": "voucher_instance_list",
|
"alias": "voucher_instance_list",
|
||||||
"title": "代金券实例管理",
|
"title": "代金券实例",
|
||||||
"params": {
|
"params": {
|
||||||
"sortby": ["created_at desc"],
|
"sortby": [
|
||||||
"data_filter": {
|
"issued_at desc"
|
||||||
"AND": [
|
],
|
||||||
{"field": "customer_id", "op": "=", "var": "customer_id"},
|
|
||||||
{"field": "status", "op": "=", "var": "status"},
|
|
||||||
{"field": "template_id", "op": "=", "var": "template_id"}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"browserfields": {
|
"browserfields": {
|
||||||
"exclouded": [],
|
"exclouded": [
|
||||||
|
"id",
|
||||||
|
"template_id"
|
||||||
|
],
|
||||||
"alters": {
|
"alters": {
|
||||||
"template_id": {
|
|
||||||
"uitype": "code",
|
|
||||||
"dataurl": "{{entire_url('../api/template/voucher_template_options.dspy')}}",
|
|
||||||
"data_field": "options",
|
|
||||||
"textField": "text",
|
|
||||||
"valueField": "value"
|
|
||||||
},
|
|
||||||
"status": {
|
"status": {
|
||||||
"uitype": "code",
|
"uitype": "code",
|
||||||
"data": [
|
"data": [
|
||||||
{"value": "unused", "text": "未使用"},
|
{
|
||||||
{"value": "used", "text": "已使用"},
|
"value": "unused",
|
||||||
{"value": "expired", "text": "已过期"}
|
"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": {
|
"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')}}",
|
"update_data_url": "{{entire_url('../api/instance/voucher_instance_update.dspy')}}",
|
||||||
"delete_data_url": "{{entire_url('../api/instance/voucher_instance_delete.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": [
|
"subtables": [
|
||||||
{
|
{
|
||||||
"field": "id",
|
"field": "instance_id",
|
||||||
"title": "使用记录",
|
"title": "使用记录",
|
||||||
"url": "{{entire_url('../voucher_usage_log_list/')}}",
|
"url": "{{entire_url('../voucher_usage_log_list')}}",
|
||||||
"subtable": "voucher_usage_log"
|
"subtable": "voucher_usage_log"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@ -1,47 +1,91 @@
|
|||||||
{
|
{
|
||||||
"tblname": "voucher_rule",
|
"tblname": "voucher_rule",
|
||||||
"alias": "voucher_rule_list",
|
"alias": "voucher_rule_list",
|
||||||
"title": "代金券规则管理",
|
"title": "代金券规则",
|
||||||
"params": {
|
"params": {
|
||||||
"sortby": ["sort_order"],
|
"sortby": [
|
||||||
"data_filter": {
|
"sort_order"
|
||||||
"AND": [
|
],
|
||||||
{"field": "template_id", "op": "=", "var": "template_id"},
|
|
||||||
{"field": "rule_type", "op": "=", "var": "rule_type"}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"browserfields": {
|
"browserfields": {
|
||||||
"exclouded": [],
|
"exclouded": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
"alters": {
|
"alters": {
|
||||||
"template_id": {
|
|
||||||
"uitype": "code",
|
|
||||||
"dataurl": "{{entire_url('../api/template/voucher_template_options.dspy')}}",
|
|
||||||
"data_field": "options",
|
|
||||||
"textField": "text",
|
|
||||||
"valueField": "value"
|
|
||||||
},
|
|
||||||
"rule_type": {
|
"rule_type": {
|
||||||
"uitype": "code",
|
"uitype": "code",
|
||||||
"data": [
|
"data": [
|
||||||
{"value": "min_amount", "text": "最低消费"},
|
{
|
||||||
{"value": "max_amount", "text": "最高消费"},
|
"value": "min_amount",
|
||||||
{"value": "applicable_product_type", "text": "限定产品类型"},
|
"text": "最低消费"
|
||||||
{"value": "applicable_product", "text": "限定特定产品"},
|
},
|
||||||
{"value": "exclude_product", "text": "排除特定产品"},
|
{
|
||||||
{"value": "max_usage_count", "text": "最大使用次数"},
|
"value": "max_amount",
|
||||||
{"value": "valid_period", "text": "有效期"},
|
"text": "最高消费"
|
||||||
{"value": "user_level", "text": "用户等级"}
|
},
|
||||||
|
{
|
||||||
|
"value": "product_type",
|
||||||
|
"text": "限定产品类型"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "product",
|
||||||
|
"text": "限定特定产品"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "exclude_product",
|
||||||
|
"text": "排除特定产品"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "max_usage_count",
|
||||||
|
"text": "最大使用次数"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "user_level",
|
||||||
|
"text": "用户等级"
|
||||||
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"enabled": {
|
"enabled": {
|
||||||
"uitype": "code",
|
"uitype": "code",
|
||||||
"data": [
|
"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": {
|
"editable": {
|
||||||
"new_data_url": "{{entire_url('../api/rule/voucher_rule_create.dspy')}}",
|
"new_data_url": "{{entire_url('../api/rule/voucher_rule_create.dspy')}}",
|
||||||
"update_data_url": "{{entire_url('../api/rule/voucher_rule_update.dspy')}}",
|
"update_data_url": "{{entire_url('../api/rule/voucher_rule_update.dspy')}}",
|
||||||
|
|||||||
@ -1,42 +1,105 @@
|
|||||||
{
|
{
|
||||||
"tblname": "voucher_template",
|
"tblname": "voucher_template",
|
||||||
"alias": "voucher_template_list",
|
"alias": "voucher_template_list",
|
||||||
"title": "代金券模板管理",
|
"title": "代金券模板",
|
||||||
"params": {
|
"params": {
|
||||||
"sortby": ["created_at desc"],
|
"sortby": [
|
||||||
|
"created_at desc"
|
||||||
|
],
|
||||||
"browserfields": {
|
"browserfields": {
|
||||||
"exclouded": ["creator", "updater"],
|
"exclouded": [
|
||||||
|
"id",
|
||||||
|
"org_id",
|
||||||
|
"created_by"
|
||||||
|
],
|
||||||
"alters": {
|
"alters": {
|
||||||
"status": {
|
"status": {
|
||||||
"uitype": "code",
|
"uitype": "code",
|
||||||
"data": [
|
"data": [
|
||||||
{"value": "draft", "text": "草稿"},
|
{
|
||||||
{"value": "active", "text": "启用"},
|
"value": "draft",
|
||||||
{"value": "inactive", "text": "停用"}
|
"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": {
|
"data_filter": {
|
||||||
"AND": [
|
"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": {
|
"editable": {
|
||||||
"new_data_url": "{{entire_url('../api/template/voucher_template_create.dspy')}}",
|
"new_data_url": "{{entire_url('../api/template/voucher_template_create.dspy')}}",
|
||||||
"update_data_url": "{{entire_url('../api/template/voucher_template_update.dspy')}}",
|
"update_data_url": "{{entire_url('../api/template/voucher_template_update.dspy')}}",
|
||||||
"delete_data_url": "{{entire_url('../api/template/voucher_template_delete.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": [
|
"subtables": [
|
||||||
{
|
{
|
||||||
"field": "id",
|
"field": "template_id",
|
||||||
"title": "规则配置",
|
"title": "规则配置",
|
||||||
"url": "{{entire_url('../voucher_rule_list/')}}",
|
"url": "{{entire_url('../voucher_rule_list')}}",
|
||||||
"subtable": "voucher_rule"
|
"subtable": "voucher_rule"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@ -3,30 +3,40 @@
|
|||||||
"alias": "voucher_usage_log_list",
|
"alias": "voucher_usage_log_list",
|
||||||
"title": "代金券使用流水",
|
"title": "代金券使用流水",
|
||||||
"params": {
|
"params": {
|
||||||
"sortby": ["used_at desc"],
|
"sortby": [
|
||||||
|
"used_at desc"
|
||||||
|
],
|
||||||
|
"noedit": true,
|
||||||
|
"browserfields": {
|
||||||
|
"exclouded": [
|
||||||
|
"id",
|
||||||
|
"template_id"
|
||||||
|
],
|
||||||
|
"alters": {}
|
||||||
|
},
|
||||||
"data_filter": {
|
"data_filter": {
|
||||||
"AND": [
|
"AND": [
|
||||||
{"field": "instance_id", "op": "=", "var": "instance_id"},
|
{
|
||||||
{"field": "customer_id", "op": "=", "var": "customer_id"},
|
"field": "instance_id",
|
||||||
{"field": "order_id", "op": "=", "var": "order_id"}
|
"op": "=",
|
||||||
|
"var": "instance_id"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"field": "customer_id",
|
||||||
|
"op": "=",
|
||||||
|
"var": "customer_id"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"field": "order_id",
|
||||||
|
"op": "LIKE",
|
||||||
|
"var": "order_id"
|
||||||
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"browserfields": {
|
"filter_labels": {
|
||||||
"exclouded": [],
|
"instance_id": "券实例",
|
||||||
"alters": {
|
"customer_id": "客户机构",
|
||||||
"instance_id": {
|
"order_id": "订单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')}}"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,27 +10,27 @@
|
|||||||
"fields": [
|
"fields": [
|
||||||
{"name": "id", "title": "主键ID", "type": "str", "length": 32, "nullable": "no"},
|
{"name": "id", "title": "主键ID", "type": "str", "length": 32, "nullable": "no"},
|
||||||
{"name": "template_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": "customer_id", "title": "客户机构ID", "type": "str", "length": 32, "nullable": "no"},
|
||||||
{"name": "code", "title": "唯一券码", "type": "str", "length": 64, "nullable": "no"},
|
{"name": "code", "title": "券码", "type": "str", "length": 64, "nullable": "no"},
|
||||||
{"name": "status", "title": "状态", "type": "str", "length": 16, "nullable": "no", "default": "unused"},
|
{"name": "status", "title": "状态", "type": "str", "length": 16, "nullable": "no", "default": "unused"},
|
||||||
{"name": "face_value", "title": "面值", "type": "double", "length": 10, "dec": 2, "nullable": "no"},
|
{"name": "face_value", "title": "面值", "type": "double", "length": 18, "dec": 2, "nullable": "no"},
|
||||||
{"name": "actual_deducted", "title": "实际抵扣金额", "type": "double", "length": 10, "dec": 2, "nullable": "yes"},
|
{"name": "actual_deducted", "title": "实际抵扣金额", "type": "double", "length": 18, "dec": 2, "nullable": "yes"},
|
||||||
{"name": "valid_from", "title": "生效时间", "type": "datetime", "nullable": "no"},
|
{"name": "valid_from", "title": "生效时间", "type": "datetime", "nullable": "no"},
|
||||||
{"name": "valid_to", "title": "过期时间", "type": "datetime", "nullable": "no"},
|
{"name": "valid_to", "title": "过期时间", "type": "datetime", "nullable": "no"},
|
||||||
{"name": "issued_at", "title": "发放时间", "type": "datetime", "nullable": "no"},
|
{"name": "issued_at", "title": "发放时间", "type": "datetime", "nullable": "no"},
|
||||||
{"name": "used_at", "title": "使用时间", "type": "datetime", "nullable": "yes"},
|
{"name": "used_at", "title": "使用时间", "type": "datetime", "nullable": "yes"},
|
||||||
{"name": "order_id", "title": "使用的订单ID", "type": "str", "length": 64, "nullable": "yes"},
|
{"name": "order_id", "title": "使用订单ID", "type": "str", "length": 64, "nullable": "yes"},
|
||||||
{"name": "source", "title": "来源", "type": "str", "length": 32, "nullable": "yes"},
|
{"name": "source", "title": "发放来源", "type": "str", "length": 32, "nullable": "no", "default": "manual"},
|
||||||
{"name": "remark", "title": "备注", "type": "str", "length": 255, "nullable": "yes"},
|
{"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": "created_at", "title": "创建时间", "type": "timestamp", "nullable": "no"},
|
||||||
{"name": "updated_at", "title": "更新时间", "type": "timestamp", "nullable": "no"}
|
{"name": "updated_at", "title": "更新时间", "type": "timestamp", "nullable": "no"}
|
||||||
],
|
],
|
||||||
"indexes": [
|
"indexes": [
|
||||||
{"name": "idx_instance_code", "idxtype": "unique", "idxfields": ["code"]},
|
{"name": "idx_vi_code", "idxtype": "unique", "idxfields": ["code"]},
|
||||||
{"name": "idx_instance_customer", "idxtype": "index", "idxfields": ["customer_id"]},
|
{"name": "idx_vi_customer", "idxtype": "index", "idxfields": ["customer_id", "status"]},
|
||||||
{"name": "idx_instance_template", "idxtype": "index", "idxfields": ["template_id"]},
|
{"name": "idx_vi_template", "idxtype": "index", "idxfields": ["template_id"]},
|
||||||
{"name": "idx_instance_status", "idxtype": "index", "idxfields": ["status"]},
|
{"name": "idx_vi_valid_to", "idxtype": "index", "idxfields": ["valid_to"]}
|
||||||
{"name": "idx_instance_valid_to", "idxtype": "index", "idxfields": ["valid_to"]}
|
|
||||||
],
|
],
|
||||||
"codes": [
|
"codes": [
|
||||||
{
|
{
|
||||||
@ -39,12 +39,25 @@
|
|||||||
"valuefield": "id",
|
"valuefield": "id",
|
||||||
"textfield": "name"
|
"textfield": "name"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"field": "customer_id",
|
||||||
|
"table": "organization",
|
||||||
|
"valuefield": "id",
|
||||||
|
"textfield": "orgname"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"field": "status",
|
"field": "status",
|
||||||
"table": "appcodes_kv",
|
"table": "appcodes_kv",
|
||||||
"valuefield": "k",
|
"valuefield": "k",
|
||||||
"textfield": "v",
|
"textfield": "v",
|
||||||
"cond": "parentid='voucher_instance_status'"
|
"cond": "parentid='voucher_instance_status'"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"field": "source",
|
||||||
|
"table": "appcodes_kv",
|
||||||
|
"valuefield": "k",
|
||||||
|
"textfield": "v",
|
||||||
|
"cond": "parentid='vchr_source'"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
"summary": [
|
"summary": [
|
||||||
{
|
{
|
||||||
"name": "voucher_rule",
|
"name": "voucher_rule",
|
||||||
"title": "代金券规则定义",
|
"title": "代金券规则",
|
||||||
"primary": ["id"],
|
"primary": ["id"],
|
||||||
"catelog": "entity"
|
"catelog": "entity"
|
||||||
}
|
}
|
||||||
@ -10,17 +10,16 @@
|
|||||||
"fields": [
|
"fields": [
|
||||||
{"name": "id", "title": "主键ID", "type": "str", "length": 32, "nullable": "no"},
|
{"name": "id", "title": "主键ID", "type": "str", "length": 32, "nullable": "no"},
|
||||||
{"name": "template_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_type", "title": "规则类型", "type": "str", "length": 64, "nullable": "no"},
|
||||||
{"name": "rule_config", "title": "规则配置(JSON)", "type": "text", "nullable": "no"},
|
{"name": "rule_config", "title": "规则配置JSON", "type": "text", "nullable": "yes"},
|
||||||
{"name": "enabled", "title": "是否启用", "type": "short", "nullable": "no", "default": "1"},
|
{"name": "enabled", "title": "是否启用", "type": "char", "length": 1, "nullable": "no", "default": "1"},
|
||||||
{"name": "sort_order", "title": "执行顺序", "type": "int", "nullable": "no", "default": "0"},
|
{"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": "created_at", "title": "创建时间", "type": "timestamp", "nullable": "no"},
|
||||||
{"name": "updated_at", "title": "更新时间", "type": "timestamp", "nullable": "no"}
|
{"name": "updated_at", "title": "更新时间", "type": "timestamp", "nullable": "no"}
|
||||||
],
|
],
|
||||||
"indexes": [
|
"indexes": [
|
||||||
{"name": "idx_rule_template", "idxtype": "index", "idxfields": ["template_id"]},
|
{"name": "idx_vr_template", "idxtype": "index", "idxfields": ["template_id"]}
|
||||||
{"name": "idx_rule_enabled", "idxtype": "index", "idxfields": ["enabled"]}
|
|
||||||
],
|
],
|
||||||
"codes": [
|
"codes": [
|
||||||
{
|
{
|
||||||
@ -34,14 +33,14 @@
|
|||||||
"table": "appcodes_kv",
|
"table": "appcodes_kv",
|
||||||
"valuefield": "k",
|
"valuefield": "k",
|
||||||
"textfield": "v",
|
"textfield": "v",
|
||||||
"cond": "parentid='voucher_rule_type'"
|
"cond": "parentid='vchr_rule_type'"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"field": "enabled",
|
"field": "enabled",
|
||||||
"table": "appcodes_kv",
|
"table": "appcodes_kv",
|
||||||
"valuefield": "k",
|
"valuefield": "k",
|
||||||
"textfield": "v",
|
"textfield": "v",
|
||||||
"cond": "parentid='yes_no'"
|
"cond": "parentid='vchr_enabled'"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,22 +9,22 @@
|
|||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{"name": "id", "title": "主键ID", "type": "str", "length": 32, "nullable": "no"},
|
{"name": "id", "title": "主键ID", "type": "str", "length": 32, "nullable": "no"},
|
||||||
{"name": "name", "title": "模板名称", "type": "str", "length": 64, "nullable": "no"},
|
{"name": "name", "title": "模板名称", "type": "str", "length": 128, "nullable": "no"},
|
||||||
{"name": "code", "title": "模板编码", "type": "str", "length": 32, "nullable": "no"},
|
{"name": "code", "title": "模板编码", "type": "str", "length": 64, "nullable": "no"},
|
||||||
{"name": "face_value", "title": "面值", "type": "double", "length": 10, "dec": 2, "nullable": "no"},
|
{"name": "face_value", "title": "面值", "type": "double", "length": 18, "dec": 2, "nullable": "no"},
|
||||||
{"name": "total_count", "title": "总发行量", "type": "int", "nullable": "no", "default": "0"},
|
{"name": "total_count", "title": "总发行量(0不限)", "type": "int", "nullable": "no", "default": "0"},
|
||||||
{"name": "issued_count", "title": "已发放量", "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": "valid_days", "title": "有效期天数", "type": "int", "nullable": "no", "default": "30"},
|
||||||
{"name": "status", "title": "状态", "type": "str", "length": 16, "nullable": "no", "default": "draft"},
|
{"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": "created_at", "title": "创建时间", "type": "timestamp", "nullable": "no"},
|
||||||
{"name": "updated_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"}
|
|
||||||
],
|
],
|
||||||
"indexes": [
|
"indexes": [
|
||||||
{"name": "idx_template_code", "idxtype": "unique", "idxfields": ["code"]},
|
{"name": "idx_vt_code", "idxtype": "unique", "idxfields": ["code"]},
|
||||||
{"name": "idx_template_status", "idxtype": "index", "idxfields": ["status"]}
|
{"name": "idx_vt_status", "idxtype": "index", "idxfields": ["status"]}
|
||||||
],
|
],
|
||||||
"codes": [
|
"codes": [
|
||||||
{
|
{
|
||||||
|
|||||||
@ -4,25 +4,27 @@
|
|||||||
"name": "voucher_usage_log",
|
"name": "voucher_usage_log",
|
||||||
"title": "代金券使用流水",
|
"title": "代金券使用流水",
|
||||||
"primary": ["id"],
|
"primary": ["id"],
|
||||||
"catelog": "entity"
|
"catelog": "relation"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{"name": "id", "title": "主键ID", "type": "str", "length": 32, "nullable": "no"},
|
{"name": "id", "title": "主键ID", "type": "str", "length": 32, "nullable": "no"},
|
||||||
{"name": "instance_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": "template_id", "title": "模板ID", "type": "str", "length": 32, "nullable": "yes"},
|
||||||
{"name": "order_id", "title": "订单/消费ID", "type": "str", "length": 64, "nullable": "no"},
|
{"name": "customer_id", "title": "客户机构ID", "type": "str", "length": 32, "nullable": "no"},
|
||||||
{"name": "face_value", "title": "券面值", "type": "double", "length": 10, "dec": 2, "nullable": "no"},
|
{"name": "order_id", "title": "订单ID", "type": "str", "length": 64, "nullable": "no", "default": ""},
|
||||||
{"name": "deducted_amount", "title": "实际抵扣金额", "type": "double", "length": 10, "dec": 2, "nullable": "no"},
|
{"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_at", "title": "使用时间", "type": "datetime", "nullable": "no"},
|
||||||
{"name": "used_by", "title": "操作人", "type": "str", "length": 32, "nullable": "yes"},
|
{"name": "used_by", "title": "操作人", "type": "str", "length": 32, "nullable": "yes"},
|
||||||
{"name": "product_type", "title": "产品类型", "type": "str", "length": 32, "nullable": "yes"},
|
{"name": "created_at", "title": "创建时间", "type": "timestamp", "nullable": "no"}
|
||||||
{"name": "product_name", "title": "产品名称", "type": "str", "length": 64, "nullable": "yes"}
|
|
||||||
],
|
],
|
||||||
"indexes": [
|
"indexes": [
|
||||||
{"name": "idx_log_instance", "idxtype": "index", "idxfields": ["instance_id"]},
|
{"name": "idx_vul_instance", "idxtype": "index", "idxfields": ["instance_id"]},
|
||||||
{"name": "idx_log_customer", "idxtype": "index", "idxfields": ["customer_id"]},
|
{"name": "idx_vul_customer", "idxtype": "index", "idxfields": ["customer_id"]},
|
||||||
{"name": "idx_log_order", "idxtype": "index", "idxfields": ["order_id"]}
|
{"name": "idx_vul_order", "idxtype": "index", "idxfields": ["order_id"]}
|
||||||
],
|
],
|
||||||
"codes": [
|
"codes": [
|
||||||
{
|
{
|
||||||
@ -30,6 +32,12 @@
|
|||||||
"table": "voucher_instance",
|
"table": "voucher_instance",
|
||||||
"valuefield": "id",
|
"valuefield": "id",
|
||||||
"textfield": "code"
|
"textfield": "code"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"field": "customer_id",
|
||||||
|
"table": "organization",
|
||||||
|
"valuefield": "id",
|
||||||
|
"textfield": "orgname"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,17 +1,17 @@
|
|||||||
[build-system]
|
[build-system]
|
||||||
requires = ["setuptools>=45", "wheel"]
|
requires = ["setuptools>=61.0"]
|
||||||
build-backend = "setuptools.build_meta"
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "voucher"
|
name = "voucher"
|
||||||
version = "1.0.0"
|
version = "2.0.0"
|
||||||
description = "代金券模块:模板管理、可配置规则引擎、一次性使用代金券"
|
description = "代金券模块:模板/规则/实例/流水 + 可配置规则引擎 + 注册送券挂钩"
|
||||||
requires-python = ">=3.8"
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.10"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"sqlor",
|
"sqlor",
|
||||||
"bricks_for_python",
|
"bricks_for_python"
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.setuptools.packages.find]
|
[tool.setuptools]
|
||||||
where = ["."]
|
packages = ["voucher", "voucher.rules"]
|
||||||
include = ["voucher*"]
|
|
||||||
|
|||||||
@ -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
|
#!/usr/bin/env python3
|
||||||
"""voucher 模块 RBAC 权限注册"""
|
"""voucher 模块 RBAC 路径权限注册(pipeline-app 格式,仿 ticket/scripts/load_path.py)。
|
||||||
import os
|
|
||||||
|
从应用根执行: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 sys
|
||||||
import subprocess
|
import os
|
||||||
|
import asyncio
|
||||||
|
|
||||||
def find_sage_root():
|
# 应用根:本脚本在 pkgs/voucher/scripts/ 下,上溯三级
|
||||||
for candidate in [
|
APP_ROOT = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), "../../.."))
|
||||||
os.path.expanduser("~/repos/sage"),
|
sys.path.insert(0, os.path.join(APP_ROOT, "py3", "lib", "python3.10", "site-packages"))
|
||||||
os.path.expanduser("~/sage"),
|
sys.path.insert(0, APP_ROOT)
|
||||||
]:
|
|
||||||
if os.path.isdir(os.path.join(candidate, "wwwroot")):
|
|
||||||
return candidate
|
|
||||||
return None
|
|
||||||
|
|
||||||
def set_perm(sage_root, role, path):
|
from sqlor.dbpools import DBPools
|
||||||
script = os.path.join(sage_root, "set_role_perm.py")
|
from appPublic.jsonConfig import getConfig
|
||||||
cmd = [os.path.join(sage_root, "py3/bin/python"), script, role, path]
|
from appPublic.uniqueID import getID
|
||||||
print(f" {role} {path}")
|
|
||||||
subprocess.run(cmd, cwd=sage_root, capture_output=True)
|
|
||||||
|
|
||||||
def main():
|
MOD = "voucher"
|
||||||
sage_root = find_sage_root()
|
|
||||||
if not sage_root:
|
|
||||||
print("ERROR: Sage root not found")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
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 (公开)
|
PATHS_LOGINED = [
|
||||||
any_paths = [
|
# 客户侧
|
||||||
"/voucher/menu.ui",
|
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:
|
async def main():
|
||||||
set_perm(sage_root, "any", p)
|
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__":
|
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 ahserver.serverenv import ServerEnv
|
||||||
|
from appPublic.jsonConfig import getConfig
|
||||||
|
from appPublic.log import debug, exception, info
|
||||||
from appPublic.uniqueID import getID
|
from appPublic.uniqueID import getID
|
||||||
|
from sqlor.dbpools import DBPools
|
||||||
|
|
||||||
MODULE_NAME = "voucher"
|
from voucher.rules.engine import (
|
||||||
MODULE_VERSION = "1.0.0"
|
apply_voucher,
|
||||||
DBNAME = "sage"
|
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():
|
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 ==========
|
def _get_db():
|
||||||
|
db = DBPools()
|
||||||
async def create_voucher_template(data):
|
if not getattr(db, 'databases', None):
|
||||||
sor = DBPools().sqlorContext(_get_dbname())
|
config = getConfig()
|
||||||
data['id'] = getID()
|
db.databases = config.databases
|
||||||
data['issued_count'] = 0
|
return db
|
||||||
if 'status' not in data:
|
|
||||||
data['status'] = 'draft'
|
|
||||||
sor.I('voucher_template', data)
|
|
||||||
return {'status': 'success', 'data': {'id': data['id']}}
|
|
||||||
|
|
||||||
|
|
||||||
async def update_voucher_template(conditions, data):
|
# ========== 客户侧 API ==========
|
||||||
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}
|
|
||||||
|
|
||||||
|
|
||||||
async def get_available_vouchers_api(customer_id, context=None):
|
async def get_available_vouchers_api(customer_id, context=None):
|
||||||
"""外部调用:查询可用代金券(含模板名称)"""
|
"""查询客户可用代金券(含模板名称;顺带把过期券置 expired)。"""
|
||||||
from voucher.rules.engine import get_available_vouchers
|
db = _get_db()
|
||||||
sor = DBPools().sqlorContext(_get_dbname())
|
async with db.sqlorContext(_get_dbname()) as sor:
|
||||||
vouchers = get_available_vouchers(sor, customer_id, context)
|
try:
|
||||||
# 预加载模板名称
|
await expire_vouchers(sor)
|
||||||
template_names = {}
|
except Exception as e:
|
||||||
for v in vouchers:
|
debug('get_available_vouchers_api: expire sweep skipped: {}'.format(e))
|
||||||
tid = v.template_id if hasattr(v, 'template_id') else v.get('template_id', '')
|
vouchers = await get_available_vouchers(sor, customer_id, context)
|
||||||
if tid and tid not in template_names:
|
template_names = {}
|
||||||
t = sor.R('voucher_template', {'id': tid}).first()
|
for v in vouchers:
|
||||||
if t:
|
tid = v.template_id or ''
|
||||||
template_names[tid] = t.name if hasattr(t, 'name') else t.get('name', '')
|
if tid and tid not in template_names:
|
||||||
items = []
|
t = await sor.R('voucher_template', {'id': tid})
|
||||||
for v in vouchers:
|
template_names[tid] = (t[0].name if t else '') or ''
|
||||||
if hasattr(v, '__dict__'):
|
items = []
|
||||||
item = v.__dict__
|
for v in vouchers:
|
||||||
else:
|
|
||||||
item = dict(v)
|
item = dict(v)
|
||||||
tid = item.get('template_id', '')
|
item['template_name'] = template_names.get(v.template_id or '', '')
|
||||||
item['template_name'] = template_names.get(tid, '')
|
items.append(_jsonable_instance(item))
|
||||||
items.append(item)
|
return {'status': 'success', 'data': items, 'total': len(items)}
|
||||||
return {'status': 'success', 'data': items}
|
|
||||||
|
|
||||||
|
|
||||||
async def get_registered_rule_types():
|
async def apply_voucher_api(customer_id, order_id, voucher_ids, context):
|
||||||
"""返回已注册的规则类型列表"""
|
"""使用代金券(单张传一个 id 的列表即可)。"""
|
||||||
from voucher.rules.registry import get_all_rule_types
|
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()}
|
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 ==========
|
# ========== Module Load ==========
|
||||||
|
|
||||||
def load_voucher():
|
def load_voucher():
|
||||||
"""注册所有函数到 ServerEnv"""
|
"""注册所有函数到 ServerEnv + 绑定注册送券事件。"""
|
||||||
env = 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.create_voucher_template = create_voucher_template
|
||||||
env.update_voucher_template = update_voucher_template
|
env.update_voucher_template = update_voucher_template
|
||||||
env.delete_voucher_template = delete_voucher_template
|
env.delete_voucher_template = delete_voucher_template
|
||||||
|
|
||||||
# Rule CRUD
|
|
||||||
env.create_voucher_rule = create_voucher_rule
|
env.create_voucher_rule = create_voucher_rule
|
||||||
env.update_voucher_rule = update_voucher_rule
|
env.update_voucher_rule = update_voucher_rule
|
||||||
env.delete_voucher_rule = delete_voucher_rule
|
env.delete_voucher_rule = delete_voucher_rule
|
||||||
|
env.issue_voucher_api = issue_voucher_api
|
||||||
# Instance CRUD
|
env.invalidate_voucher_api = invalidate_voucher_api
|
||||||
env.create_voucher_instance = create_voucher_instance
|
|
||||||
env.update_voucher_instance = update_voucher_instance
|
env.update_voucher_instance = update_voucher_instance
|
||||||
env.delete_voucher_instance = delete_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_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
|
# 引擎级(供其他模块 import 后直接调,如 product_management 结算联动)
|
||||||
env.apply_voucher_api = apply_voucher_api
|
env.voucher_issue = issue_voucher
|
||||||
env.get_available_vouchers_api = get_available_vouchers_api
|
env.voucher_apply = apply_voucher
|
||||||
env.get_registered_rule_types = get_registered_rule_types
|
env.voucher_batch_apply = batch_apply_vouchers
|
||||||
|
env.voucher_get_available = get_available_vouchers
|
||||||
# Import validators to trigger @register_rule decorators
|
|
||||||
import voucher.rules.validators # noqa: F401
|
|
||||||
|
|
||||||
|
_bind_org_event()
|
||||||
|
info('voucher module loaded (v{})'.format(MODULE_VERSION))
|
||||||
return True
|
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
|
# apply_voucher.dspy — 核销代金券(一次性,不找零;并发安全条件 UPDATE)
|
||||||
|
customer_id = (params_kw.get('customer_id') or '').strip()
|
||||||
customer_id = params_kw.get('customer_id', '')
|
if not customer_id:
|
||||||
order_id = params_kw.get('order_id', '')
|
customer_id = await get_userorgid() or ''
|
||||||
voucher_ids = params_kw.get('voucher_ids', [])
|
order_id = (params_kw.get('order_id') or '').strip()
|
||||||
|
voucher_ids = params_kw.get('voucher_ids') or []
|
||||||
if isinstance(voucher_ids, str):
|
if isinstance(voucher_ids, str):
|
||||||
voucher_ids = json.loads(voucher_ids)
|
try:
|
||||||
|
voucher_ids = json.loads(voucher_ids)
|
||||||
context = {
|
except (ValueError, TypeError):
|
||||||
'request_amount': params_kw.get('request_amount', 0),
|
voucher_ids = [s.strip() for s in voucher_ids.split(',') if s.strip()]
|
||||||
'product_type': params_kw.get('product_type', ''),
|
context = {}
|
||||||
'product_name': params_kw.get('product_name', ''),
|
for k in ('product_type', 'product_name'):
|
||||||
'user_level': params_kw.get('user_level', 0),
|
if params_kw.get(k):
|
||||||
}
|
context[k] = params_kw.get(k)
|
||||||
|
try:
|
||||||
result = await apply_voucher_api(customer_id, order_id, voucher_ids, context)
|
context['request_amount'] = float(params_kw.get('request_amount') or 0)
|
||||||
return json.dumps(result)
|
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
|
# get_available.dspy — 内部查询可用券(带规则预校验过滤;供结算方/调试)
|
||||||
|
customer_id = (params_kw.get('customer_id') or '').strip()
|
||||||
customer_id = params_kw.get('customer_id', '')
|
if not customer_id:
|
||||||
|
customer_id = await get_userorgid() or ''
|
||||||
context = {}
|
context = {}
|
||||||
if params_kw.get('product_type'):
|
for k in ('product_type', 'product_name'):
|
||||||
context['product_type'] = params_kw.get('product_type')
|
if params_kw.get(k):
|
||||||
if params_kw.get('product_name'):
|
context[k] = params_kw.get(k)
|
||||||
context['product_name'] = params_kw.get('product_name')
|
|
||||||
if params_kw.get('request_amount'):
|
if params_kw.get('request_amount'):
|
||||||
context['request_amount'] = params_kw.get('request_amount')
|
try:
|
||||||
|
context['request_amount'] = float(params_kw.get('request_amount'))
|
||||||
result = await get_available_vouchers_api(customer_id, context if context else None)
|
except (ValueError, TypeError):
|
||||||
return json.dumps(result)
|
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 @@
|
|||||||
|
# voucher_instance_delete.dspy — 实例删除(已使用禁删保审计;未使用删除回吐发行额度)
|
||||||
conditions = {'id': params_kw.get('id', '')}
|
r = await delete_voucher_instance(request, params_kw)
|
||||||
result = await delete_voucher_instance(conditions)
|
if r.get('success'):
|
||||||
return result
|
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 @@
|
|||||||
|
# voucher_instance_update.dspy — 实例修正(仅未使用的券可改客户/备注;状态变更走发券/核销/作废 API)
|
||||||
conditions = {'id': params_kw.get('id', '')}
|
r = await update_voucher_instance(request, params_kw)
|
||||||
data = {}
|
if r.get('success'):
|
||||||
for key in ['template_id', 'customer_id', 'code', 'status', 'source', 'remark']:
|
return {"widgettype": "Message", "options": {"title": "Success", "message": r.get('message') or 'ok', "timeout": 3, "cwidth": 16, "cheight": 9}}
|
||||||
val = params_kw.get(key, None) if hasattr(params_kw, 'get') else None
|
return {"widgettype": "Error", "options": {"title": "Error", "message": r.get('message') or 'failed', "timeout": 3, "cwidth": 16, "cheight": 9}}
|
||||||
if val is not None:
|
|
||||||
data[key] = val
|
|
||||||
|
|
||||||
result = await update_voucher_instance(conditions, data)
|
|
||||||
return result
|
|
||||||
|
|||||||
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 @@
|
|||||||
|
# voucher_rule_create.dspy — 规则新增(rule_config 合法性由 helper 校验)
|
||||||
data = {}
|
r = await create_voucher_rule(request, params_kw)
|
||||||
for key in ['template_id', 'rule_type', 'rule_config', 'enabled', 'sort_order', 'remark']:
|
if r.get('success'):
|
||||||
val = params_kw.get(key, None) if hasattr(params_kw, 'get') else None
|
return {"widgettype": "Message", "options": {"title": "Success", "message": r.get('message') or 'ok', "timeout": 3, "cwidth": 16, "cheight": 9}}
|
||||||
if val is not None:
|
return {"widgettype": "Error", "options": {"title": "Error", "message": r.get('message') or 'failed', "timeout": 3, "cwidth": 16, "cheight": 9}}
|
||||||
data[key] = val
|
|
||||||
|
|
||||||
result = await create_voucher_rule(data)
|
|
||||||
return result
|
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
|
# voucher_rule_delete.dspy — 规则删除
|
||||||
conditions = {'id': params_kw.get('id', '')}
|
r = await delete_voucher_rule(request, params_kw)
|
||||||
result = await delete_voucher_rule(conditions)
|
if r.get('success'):
|
||||||
return result
|
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 @@
|
|||||||
|
# voucher_rule_update.dspy — 规则更新
|
||||||
conditions = {'id': params_kw.get('id', '')}
|
r = await update_voucher_rule(request, params_kw)
|
||||||
data = {}
|
if r.get('success'):
|
||||||
for key in ['template_id', 'rule_type', 'rule_config', 'enabled', 'sort_order', 'remark']:
|
return {"widgettype": "Message", "options": {"title": "Success", "message": r.get('message') or 'ok', "timeout": 3, "cwidth": 16, "cheight": 9}}
|
||||||
val = params_kw.get(key, None) if hasattr(params_kw, 'get') else None
|
return {"widgettype": "Error", "options": {"title": "Error", "message": r.get('message') or 'failed', "timeout": 3, "cwidth": 16, "cheight": 9}}
|
||||||
if val is not None:
|
|
||||||
data[key] = val
|
|
||||||
|
|
||||||
result = await update_voucher_rule(conditions, data)
|
|
||||||
return result
|
|
||||||
|
|||||||
@ -1,4 +1,2 @@
|
|||||||
import json
|
# rule_types.dspy — 已注册规则类型清单(管理界面配规则参考)
|
||||||
|
return await get_registered_rule_types(request, params_kw)
|
||||||
result = await get_registered_rule_types()
|
|
||||||
return json.dumps(result)
|
|
||||||
|
|||||||
@ -1,10 +1,5 @@
|
|||||||
|
# voucher_template_create.dspy — 模板新增(走模块 helper:org 隔离+code 唯一)
|
||||||
params = params_kw if hasattr(params_kw, '__getitem__') else {}
|
r = await create_voucher_template(request, params_kw)
|
||||||
data = {}
|
if r.get('success'):
|
||||||
for key in ['name', 'code', 'face_value', 'total_count', 'valid_days', 'status', 'remark']:
|
return {"widgettype": "Message", "options": {"title": "Success", "message": r.get('message') or 'ok', "timeout": 3, "cwidth": 16, "cheight": 9}}
|
||||||
val = params_kw.get(key, None) if hasattr(params_kw, 'get') else None
|
return {"widgettype": "Error", "options": {"title": "Error", "message": r.get('message') or 'failed', "timeout": 3, "cwidth": 16, "cheight": 9}}
|
||||||
if val is not None:
|
|
||||||
data[key] = val
|
|
||||||
|
|
||||||
result = await create_voucher_template(data)
|
|
||||||
return result
|
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
|
# voucher_template_delete.dspy — 模板删除(已发放券的模板拒绝,引导用「停用」)
|
||||||
conditions = {'id': params_kw.get('id', '')}
|
r = await delete_voucher_template(request, params_kw)
|
||||||
result = await delete_voucher_template(conditions)
|
if r.get('success'):
|
||||||
return result
|
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
|
# voucher_template_options.dspy — 启用模板下拉(code 组件契约:纯 [{value,text}] 数组)
|
||||||
|
try:
|
||||||
result = await get_voucher_template_options()
|
return await get_voucher_template_options(request, params_kw)
|
||||||
return json.dumps(result)
|
except Exception as e:
|
||||||
|
debug('voucher_template_options error: ' + str(e))
|
||||||
|
return []
|
||||||
|
|||||||
@ -1,10 +1,5 @@
|
|||||||
|
# voucher_template_update.dspy — 模板更新
|
||||||
conditions = {'id': params_kw.get('id', '')}
|
r = await update_voucher_template(request, params_kw)
|
||||||
data = {}
|
if r.get('success'):
|
||||||
for key in ['name', 'code', 'face_value', 'total_count', 'valid_days', 'status', 'remark', 'updater']:
|
return {"widgettype": "Message", "options": {"title": "Success", "message": r.get('message') or 'ok', "timeout": 3, "cwidth": 16, "cheight": 9}}
|
||||||
val = params_kw.get(key, None) if hasattr(params_kw, 'get') else None
|
return {"widgettype": "Error", "options": {"title": "Error", "message": r.get('message') or 'failed', "timeout": 3, "cwidth": 16, "cheight": 9}}
|
||||||
if val is not None:
|
|
||||||
data[key] = val
|
|
||||||
|
|
||||||
result = await update_voucher_template(conditions, data)
|
|
||||||
return result
|
|
||||||
|
|||||||
@ -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
|
# v1/available.dspy — 客户自助查询可用券(HTTP API,登录态按机构隔离)
|
||||||
|
|
||||||
# 获取当前登录用户的组织ID作为客户ID
|
|
||||||
customer_id = await get_userorgid()
|
customer_id = await get_userorgid()
|
||||||
|
if not customer_id:
|
||||||
|
return {'status': 'error', 'message': 'not logged in', 'data': [], 'total': 0}
|
||||||
|
|
||||||
# 构建查询上下文(可选过滤条件)
|
|
||||||
context = {}
|
context = {}
|
||||||
if params_kw.get('product_type'):
|
if params_kw.get('product_type'):
|
||||||
context['product_type'] = 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):
|
except (ValueError, TypeError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# 查询可用代金券
|
|
||||||
result = await get_available_vouchers_api(customer_id, context if context else None)
|
result = await get_available_vouchers_api(customer_id, context if context else None)
|
||||||
|
vouchers = []
|
||||||
# 格式化输出
|
for v in (result.get('data') or []):
|
||||||
if result.get('status') == 'success':
|
vouchers.append({
|
||||||
vouchers = []
|
'id': v.get('id'),
|
||||||
for v in result.get('data', []):
|
'code': v.get('code'),
|
||||||
vouchers.append({
|
'face_value': float(v.get('face_value') or 0),
|
||||||
'id': v.get('id'),
|
'valid_from': str(v.get('valid_from') or ''),
|
||||||
'code': v.get('code'),
|
'valid_to': str(v.get('valid_to') or ''),
|
||||||
'face_value': float(v.get('face_value', 0)),
|
'template_name': v.get('template_name') or '',
|
||||||
'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)
|
|
||||||
})
|
})
|
||||||
else:
|
return {'status': 'success', 'data': vouchers, 'total': len(vouchers)}
|
||||||
return json.dumps(result)
|
|
||||||
|
|||||||
197
wwwroot/index.ui
197
wwwroot/index.ui
@ -1,48 +1,159 @@
|
|||||||
{
|
{
|
||||||
"widgettype": "VBox",
|
"widgettype": "VBox",
|
||||||
"options": {"width": "100%", "height": "100%", "padding": "20px"},
|
"options": {
|
||||||
|
"width": "100%",
|
||||||
|
"height": "100%",
|
||||||
|
"padding": "20px"
|
||||||
|
},
|
||||||
"subwidgets": [
|
"subwidgets": [
|
||||||
{"widgettype": "Title3", "options": {"text": "代金券管理"}},
|
{
|
||||||
{"widgettype": "ResponsableBox", "options": {"gap": "16px", "minWidth": "250px"},
|
"widgettype": "Title3",
|
||||||
"subwidgets": [
|
"options": {
|
||||||
{"widgettype": "VBox",
|
"otext": "代金券管理",
|
||||||
"options": {"bgcolor": "#FFFFFF", "padding": "20px", "cursor": "pointer", "borderRadius": "8px", "cwidth": "20"},
|
"text": "代金券管理",
|
||||||
"binds": [{
|
"i18n": true
|
||||||
"wid": "self", "event": "click", "actiontype": "urlwidget",
|
}
|
||||||
"target": "app.voucher_content",
|
},
|
||||||
"options": {"url": "{{entire_url('/voucher/voucher_template_list/index.ui')}}"},
|
{
|
||||||
"mode": "replace"
|
"widgettype": "ResponsableBox",
|
||||||
}],
|
"options": {
|
||||||
"subwidgets": [
|
"gap": "16px",
|
||||||
{"widgettype": "Text", "options": {"text": "模板管理", "fontSize": "18px", "bold": true}},
|
"minWidth": "250px"
|
||||||
{"widgettype": "Text", "options": {"text": "管理代金券模板和规则配置", "fontSize": "14px", "color": "#666"}}
|
},
|
||||||
]},
|
"subwidgets": [
|
||||||
{"widgettype": "VBox",
|
{
|
||||||
"options": {"bgcolor": "#FFFFFF", "padding": "20px", "cursor": "pointer", "borderRadius": "8px", "cwidth": "20"},
|
"widgettype": "VBox",
|
||||||
"binds": [{
|
"options": {
|
||||||
"wid": "self", "event": "click", "actiontype": "urlwidget",
|
"css": "card",
|
||||||
"target": "app.voucher_content",
|
"padding": "20px",
|
||||||
"options": {"url": "{{entire_url('/voucher/voucher_instance_list/index.ui')}}"},
|
"cursor": "pointer",
|
||||||
"mode": "replace"
|
"cwidth": 20
|
||||||
}],
|
},
|
||||||
"subwidgets": [
|
"binds": [
|
||||||
{"widgettype": "Text", "options": {"text": "券实例管理", "fontSize": "18px", "bold": true}},
|
{
|
||||||
{"widgettype": "Text", "options": {"text": "查看和发放代金券", "fontSize": "14px", "color": "#666"}}
|
"wid": "self",
|
||||||
]},
|
"event": "click",
|
||||||
{"widgettype": "VBox",
|
"actiontype": "urlwidget",
|
||||||
"options": {"bgcolor": "#FFFFFF", "padding": "20px", "cursor": "pointer", "borderRadius": "8px", "cwidth": "20"},
|
"target": "voucher_content",
|
||||||
"binds": [{
|
"options": {
|
||||||
"wid": "self", "event": "click", "actiontype": "urlwidget",
|
"url": "{{entire_url('/voucher/voucher_template_list/index.ui')}}"
|
||||||
"target": "app.voucher_content",
|
},
|
||||||
"options": {"url": "{{entire_url('/voucher/voucher_usage_log_list/index.ui')}}"},
|
"mode": "replace"
|
||||||
"mode": "replace"
|
}
|
||||||
}],
|
],
|
||||||
"subwidgets": [
|
"subwidgets": [
|
||||||
{"widgettype": "Text", "options": {"text": "使用流水", "fontSize": "18px", "bold": true}},
|
{
|
||||||
{"widgettype": "Text", "options": {"text": "查看代金券使用记录", "fontSize": "14px", "color": "#666"}}
|
"widgettype": "Title4",
|
||||||
]}
|
"options": {
|
||||||
]},
|
"otext": "模板管理",
|
||||||
{"widgettype": "VBox", "id": "voucher_content",
|
"text": "模板管理",
|
||||||
"options": {"width": "100%", "flex": "1", "marginTop": "20px"}}
|
"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