feat: 代金券模块初始实现

- 4张表: voucher_template, voucher_rule, voucher_instance, voucher_usage_log
- 可配置规则引擎: registry + validators + engine
- 8种内置规则: min_amount, max_amount, applicable_product_type,
  applicable_product, exclude_product, max_usage_count, valid_period, user_level
- CRUD定义 + API接口 + 前端页面
- SQL建表脚本 + RBAC权限配置
- 一次性使用,不找零
This commit is contained in:
yumoqing 2026-05-29 00:24:02 +08:00
parent 424fb91d0e
commit 2c56aa904a
38 changed files with 1561 additions and 1 deletions

233
README.md
View File

@ -1,2 +1,233 @@
# voucher # voucher — 代金券模块
独立代金券管理模块,支持**可配置规则引擎**,一次性使用,不找零。
## 核心架构
```
模板(类别) → 定义规则集
实例(代金券)→ 属于某个模板,继承规则
使用流水 → 记录每次消费抵扣
```
### 表结构
| 表 | 说明 |
|---|---|
| voucher_template | 代金券模板(面值、有效期、发行量、状态) |
| voucher_rule | 规则定义(模板关联,可增删启用禁用) |
| voucher_instance | 券实例一次性使用状态unused/used/expired |
| voucher_usage_log | 使用流水(订单、抵扣金额、产品信息) |
---
## 可配置规则引擎
规则通过 `@register_rule` 装饰器注册到 `RULE_REGISTRY`,新增规则只需写 validator 函数,无需修改引擎代码。
### 内置规则类型
| rule_type | 说明 | rule_config 示例 |
|-----------|------|------------------|
| `min_amount` | 最低消费门槛 | `{"min_value": 100}` |
| `max_amount` | 最高消费限制 | `{"max_value": 1000}` |
| `applicable_product_type` | 限定产品类型llm/image/video/audio | `{"product_types": ["llm", "image"]}` |
| `applicable_product` | 限定特定产品(具体模型名) | `{"products": ["gpt-4", "claude-3"]}` |
| `exclude_product` | 排除特定产品 | `{"products": ["gpt-4"]}` |
| `max_usage_count` | 最大使用次数 | `{"max_count": 1}` |
| `valid_period` | 有效期检查 | `{}` (由实例 valid_from/valid_to 处理) |
| `user_level` | 用户等级限制 | `{"min_level": 2}` |
### 规则执行流程
1. 查询券实例状态unused + 未过期)
2. 加载模板关联的所有启用规则(按 sort_order 排序)
3. 依次执行 validator任一失败即拒绝
4. 全部通过 → 计算抵扣金额 = min(面值, 消费金额)
5. 记录流水 + 标记券为已使用(一次性作废)
### 新增规则步骤
```python
# rules/validators.py
@register_rule('new_rule_type')
def check_new_rule(config, context):
# config: 从 rule_config JSON 解析
# context: 包含 request_amount, product_type, product_name, user_level 等
if not some_condition:
return False, "不满足条件"
return True, None
```
然后在模板管理界面添加 `rule_type: "new_rule_type"` 的规则记录即可。
---
## 与其他模块交互
### llmage 模块(消费时使用代金券)
```python
# llmage 的计费逻辑中调用 voucher 引擎
from voucher.rules.engine import apply_voucher, get_available_vouchers
# 查询客户可用代金券
vouchers = get_available_vouchers(sor, customer_id, context={
'product_type': 'llm', # 从 llm.catelog 获取
'product_name': 'gpt-4', # 具体模型名
'request_amount': amount,
'user_level': customer.level
})
# 使用代金券抵扣
ok, deducted, err = apply_voucher(sor, instance_id, customer_id, order_id, context={
'request_amount': amount,
'product_type': 'llm',
'product_name': 'gpt-4'
})
# 批量使用多张券
from voucher.rules.engine import batch_apply_vouchers
result = batch_apply_vouchers(sor, customer_id, order_id, voucher_ids, context)
# result: {total_deducted: 150.0, remaining: 50.0, details: [...]}
# remaining > 0 时从余额扣减
```
### accounting 模块(余额扣减联动)
```python
# accounting 消费流程中先尝试代金券,剩余从余额扣
total_amount = calculate_consumption(customer_id, period)
# Step 1: 尝试代金券抵扣
result = batch_apply_vouchers(sor, customer_id, order_id, voucher_ids, context)
# Step 2: 剩余金额从余额扣减
if result['remaining'] > 0:
accounting.deduct_balance(customer_id, result['remaining'], order_id)
# Step 3: 记录完整账单
accounting.record_order(order_id, total_amount,
voucher_deducted=result['total_deducted'],
balance_deducted=result['remaining'])
```
### dapi 模块(客户自助查询)
```python
# 客户通过 API 查询自己的可用代金券
# dapi 端点调用 voucher 引擎
available = get_available_vouchers(sor, customer_id)
```
### 集成点总结
| 调用方 | 场景 | 调用函数 |
|--------|------|----------|
| llmage | 推理/生成时抵扣 | `apply_voucher()` |
| accounting | 月度账单结算 | `batch_apply_vouchers()` |
| dapi | 客户查询可用券 | `get_available_vouchers()` |
| 任意模块 | 新增规则类型 | `@register_rule` + 模板管理界面 |
---
## 目录结构
```
voucher/
├── voucher/ # Python 包
│ ├── __init__.py
│ └── init.py # 模块初始化 + ServerEnv 注册
├── rules/ # 规则引擎(纯 Python
│ ├── registry.py # @register_rule 装饰器
│ ├── validators.py # 内置规则校验器
│ └── engine.py # 校验/使用/批量使用
├── wwwroot/ # 前端
│ ├── index.ui # 入口页(卡片导航)
│ ├── menu.json # 菜单定义
│ └── api/ # API 端点
│ ├── template/ # 模板 CRUD
│ ├── rule/ # 规则 CRUD
│ ├── instance/ # 实例 CRUD
│ ├── usage/ # 流水 CRUD
│ ├── apply_voucher.dspy # 使用代金券
│ ├── get_available.dspy # 查询可用券
│ └── rule_types.dspy # 获取规则类型列表
├── models/ # 表定义 JSON
├── json/ # CRUD 定义 JSON
├── sql/
│ └── tables.sql # 建表 + 编码初始化
├── scripts/
│ └── load_path.py # RBAC 权限注册
├── pyproject.toml
└── README.md
```
---
## 部署步骤
### 1. 代码部署
```bash
cd ~/repos/voucher && git pull
cd ~/repos/sage/pkgs && ln -sf ~/repos/voucher voucher
cd voucher && ~/repos/sage/py3/bin/pip install -e .
```
### 2. 数据库建表
```bash
mysql -u root -p sage < ~/repos/voucher/sql/tables.sql
```
### 3. Sage 集成
```python
# sage/app/sage.py
from voucher.init import load_voucher
# ... in init():
load_voucher()
```
```bash
# sage/build.sh 安装循环
for m in ... voucher
```
### 4. 菜单入口
```json
// sage/wwwroot/global_menu.ui items 数组
,{
"name": "voucher",
"label": "代金券",
"icon": "fa fa-ticket",
"url": "{{entire_url('/voucher/index.ui')}}",
"target": "app.sage_main_content"
}
```
### 5. RBAC 权限
```bash
cd ~/repos/sage && ./py3/bin/python ~/repos/voucher/scripts/load_path.py
```
### 6. 重启
```bash
cd ~/repos/sage && ./stop.sh && ./start.sh
```
---
## 使用示例
### 创建模板 + 配置规则
1. 模板管理 → 新增 → 名称"新用户满减券",面值 50有效期 30 天
2. 点击模板 → 规则配置 → 添加规则:
- 规则类型: `min_amount`,配置: `{"min_value": 100}`
- 规则类型: `applicable_product_type`,配置: `{"product_types": ["llm"]}`
### 发放代金券
1. 券实例管理 → 新增 → 选择模板,填写客户 ID
2. 系统自动生成券码,设置有效期
### 消费时抵扣
```python
# llmage 推理完成后
context = {'request_amount': 120.0, 'product_type': 'llm', 'product_name': 'gpt-4'}
result = batch_apply_vouchers(sor, customer_id, order_id, [vid1, vid2], context)
# → 抵扣 50满100可用剩余 70 从余额扣
```

12
conf/config.json Normal file
View File

@ -0,0 +1,12 @@
{
"module": "voucher",
"databases": {
"sage": {
"host": "${DB_HOST}",
"port": "${DB_PORT}",
"user": "${DB_USER}",
"passwd": "${DB_PASSWD}",
"db": "sage"
}
}
}

View File

@ -0,0 +1,49 @@
{
"tblname": "voucher_instance",
"alias": "voucher_instance_list",
"title": "代金券实例管理",
"params": {
"sortby": ["created_at desc"],
"data_filter": {
"AND": [
{"field": "customer_id", "op": "=", "var": "customer_id"},
{"field": "status", "op": "=", "var": "status"},
{"field": "template_id", "op": "=", "var": "template_id"}
]
},
"browserfields": {
"exclouded": [],
"alters": {
"template_id": {
"uitype": "code",
"dataurl": "{{entire_url('../api/template/voucher_template_options.dspy')}}",
"data_field": "options",
"textField": "text",
"valueField": "value"
},
"status": {
"uitype": "code",
"data": [
{"value": "unused", "text": "未使用"},
{"value": "used", "text": "已使用"},
{"value": "expired", "text": "已过期"}
]
}
}
},
"editexclouded": ["used_at", "order_id", "actual_deducted"],
"editable": {
"new_data_url": "{{entire_url('../api/instance/voucher_instance_create.dspy')}}",
"update_data_url": "{{entire_url('../api/instance/voucher_instance_update.dspy')}}",
"delete_data_url": "{{entire_url('../api/instance/voucher_instance_delete.dspy')}}"
},
"subtables": [
{
"field": "id",
"title": "使用记录",
"url": "{{entire_url('../voucher_usage_log_list/')}}",
"subtable": "voucher_usage_log"
}
]
}
}

View File

@ -0,0 +1,51 @@
{
"tblname": "voucher_rule",
"alias": "voucher_rule_list",
"title": "代金券规则管理",
"params": {
"sortby": ["sort_order"],
"data_filter": {
"AND": [
{"field": "template_id", "op": "=", "var": "template_id"},
{"field": "rule_type", "op": "=", "var": "rule_type"}
]
},
"browserfields": {
"exclouded": [],
"alters": {
"template_id": {
"uitype": "code",
"dataurl": "{{entire_url('../api/template/voucher_template_options.dspy')}}",
"data_field": "options",
"textField": "text",
"valueField": "value"
},
"rule_type": {
"uitype": "code",
"data": [
{"value": "min_amount", "text": "最低消费"},
{"value": "max_amount", "text": "最高消费"},
{"value": "applicable_product_type", "text": "限定产品类型"},
{"value": "applicable_product", "text": "限定特定产品"},
{"value": "exclude_product", "text": "排除特定产品"},
{"value": "max_usage_count", "text": "最大使用次数"},
{"value": "valid_period", "text": "有效期"},
{"value": "user_level", "text": "用户等级"}
]
},
"enabled": {
"uitype": "code",
"data": [
{"value": "1", "text": "启用"},
{"value": "0", "text": "停用"}
]
}
}
},
"editable": {
"new_data_url": "{{entire_url('../api/rule/voucher_rule_create.dspy')}}",
"update_data_url": "{{entire_url('../api/rule/voucher_rule_update.dspy')}}",
"delete_data_url": "{{entire_url('../api/rule/voucher_rule_delete.dspy')}}"
}
}
}

View File

@ -0,0 +1,44 @@
{
"tblname": "voucher_template",
"alias": "voucher_template_list",
"title": "代金券模板管理",
"params": {
"sortby": ["created_at desc"],
"browserfields": {
"exclouded": ["creator", "updater"],
"alters": {
"status": {
"uitype": "code",
"data": [
{"value": "draft", "text": "草稿"},
{"value": "active", "text": "启用"},
{"value": "inactive", "text": "停用"}
]
},
"face_value": {
"uitype": "code",
"data": []
}
}
},
"data_filter": {
"AND": [
{"field": "name", "op": "LIKE", "var": "name"},
{"field": "status", "op": "=", "var": "status"}
]
},
"editable": {
"new_data_url": "{{entire_url('../api/template/voucher_template_create.dspy')}}",
"update_data_url": "{{entire_url('../api/template/voucher_template_update.dspy')}}",
"delete_data_url": "{{entire_url('../api/template/voucher_template_delete.dspy')}}"
},
"subtables": [
{
"field": "id",
"title": "规则配置",
"url": "{{entire_url('../voucher_rule_list/')}}",
"subtable": "voucher_rule"
}
]
}
}

View File

@ -0,0 +1,32 @@
{
"tblname": "voucher_usage_log",
"alias": "voucher_usage_log_list",
"title": "代金券使用流水",
"params": {
"sortby": ["used_at desc"],
"data_filter": {
"AND": [
{"field": "instance_id", "op": "=", "var": "instance_id"},
{"field": "customer_id", "op": "=", "var": "customer_id"},
{"field": "order_id", "op": "=", "var": "order_id"}
]
},
"browserfields": {
"exclouded": [],
"alters": {
"instance_id": {
"uitype": "code",
"dataurl": "{{entire_url('../api/instance/voucher_instance_options.dspy')}}",
"data_field": "options",
"textField": "text",
"valueField": "value"
}
}
},
"editable": {
"new_data_url": "{{entire_url('../api/usage/voucher_usage_log_create.dspy')}}",
"update_data_url": "{{entire_url('../api/usage/voucher_usage_log_update.dspy')}}",
"delete_data_url": "{{entire_url('../api/usage/voucher_usage_log_delete.dspy')}}"
}
}
}

View File

@ -0,0 +1,50 @@
{
"summary": [
{
"name": "voucher_instance",
"title": "代金券实例",
"primary": ["id"],
"catelog": "entity"
}
],
"fields": [
{"name": "id", "title": "主键ID", "type": "str", "length": 32, "nullable": "no"},
{"name": "template_id", "title": "模板ID", "type": "str", "length": 32, "nullable": "no"},
{"name": "customer_id", "title": "客户ID", "type": "str", "length": 32, "nullable": "no"},
{"name": "code", "title": "唯一券码", "type": "str", "length": 64, "nullable": "no"},
{"name": "status", "title": "状态", "type": "str", "length": 16, "nullable": "no", "default": "unused"},
{"name": "face_value", "title": "面值", "type": "double", "length": 10, "dec": 2, "nullable": "no"},
{"name": "actual_deducted", "title": "实际抵扣金额", "type": "double", "length": 10, "dec": 2, "nullable": "yes"},
{"name": "valid_from", "title": "生效时间", "type": "datetime", "nullable": "no"},
{"name": "valid_to", "title": "过期时间", "type": "datetime", "nullable": "no"},
{"name": "issued_at", "title": "发放时间", "type": "datetime", "nullable": "no"},
{"name": "used_at", "title": "使用时间", "type": "datetime", "nullable": "yes"},
{"name": "order_id", "title": "使用的订单ID", "type": "str", "length": 64, "nullable": "yes"},
{"name": "source", "title": "来源", "type": "str", "length": 32, "nullable": "yes"},
{"name": "remark", "title": "备注", "type": "str", "length": 255, "nullable": "yes"},
{"name": "created_at", "title": "创建时间", "type": "timestamp", "nullable": "no"},
{"name": "updated_at", "title": "更新时间", "type": "timestamp", "nullable": "no"}
],
"indexes": [
{"name": "idx_instance_code", "idxtype": "unique", "idxfields": ["code"]},
{"name": "idx_instance_customer", "idxtype": "index", "idxfields": ["customer_id"]},
{"name": "idx_instance_template", "idxtype": "index", "idxfields": ["template_id"]},
{"name": "idx_instance_status", "idxtype": "index", "idxfields": ["status"]},
{"name": "idx_instance_valid_to", "idxtype": "index", "idxfields": ["valid_to"]}
],
"codes": [
{
"field": "template_id",
"table": "voucher_template",
"valuefield": "id",
"textfield": "name"
},
{
"field": "status",
"table": "appcodes_kv",
"valuefield": "k",
"textfield": "v",
"cond": "parentid='voucher_instance_status'"
}
]
}

47
models/voucher_rule.json Normal file
View File

@ -0,0 +1,47 @@
{
"summary": [
{
"name": "voucher_rule",
"title": "代金券规则定义",
"primary": ["id"],
"catelog": "entity"
}
],
"fields": [
{"name": "id", "title": "主键ID", "type": "str", "length": 32, "nullable": "no"},
{"name": "template_id", "title": "模板ID", "type": "str", "length": 32, "nullable": "no"},
{"name": "rule_type", "title": "规则类型", "type": "str", "length": 32, "nullable": "no"},
{"name": "rule_config", "title": "规则配置(JSON)", "type": "text", "nullable": "no"},
{"name": "enabled", "title": "是否启用", "type": "short", "nullable": "no", "default": "1"},
{"name": "sort_order", "title": "执行顺序", "type": "int", "nullable": "no", "default": "0"},
{"name": "remark", "title": "备注", "type": "str", "length": 255, "nullable": "yes"},
{"name": "created_at", "title": "创建时间", "type": "timestamp", "nullable": "no"},
{"name": "updated_at", "title": "更新时间", "type": "timestamp", "nullable": "no"}
],
"indexes": [
{"name": "idx_rule_template", "idxtype": "index", "idxfields": ["template_id"]},
{"name": "idx_rule_enabled", "idxtype": "index", "idxfields": ["enabled"]}
],
"codes": [
{
"field": "template_id",
"table": "voucher_template",
"valuefield": "id",
"textfield": "name"
},
{
"field": "rule_type",
"table": "appcodes_kv",
"valuefield": "k",
"textfield": "v",
"cond": "parentid='voucher_rule_type'"
},
{
"field": "enabled",
"table": "appcodes_kv",
"valuefield": "k",
"textfield": "v",
"cond": "parentid='yes_no'"
}
]
}

View File

@ -0,0 +1,38 @@
{
"summary": [
{
"name": "voucher_template",
"title": "代金券模板",
"primary": ["id"],
"catelog": "entity"
}
],
"fields": [
{"name": "id", "title": "主键ID", "type": "str", "length": 32, "nullable": "no"},
{"name": "name", "title": "模板名称", "type": "str", "length": 64, "nullable": "no"},
{"name": "code", "title": "模板编码", "type": "str", "length": 32, "nullable": "no"},
{"name": "face_value", "title": "面值", "type": "double", "length": 10, "dec": 2, "nullable": "no"},
{"name": "total_count", "title": "总发行量", "type": "int", "nullable": "no", "default": "0"},
{"name": "issued_count", "title": "已发放量", "type": "int", "nullable": "no", "default": "0"},
{"name": "valid_days", "title": "有效期天数", "type": "int", "nullable": "no", "default": "30"},
{"name": "status", "title": "状态", "type": "str", "length": 16, "nullable": "no", "default": "draft"},
{"name": "remark", "title": "备注", "type": "str", "length": 255, "nullable": "yes"},
{"name": "created_at", "title": "创建时间", "type": "timestamp", "nullable": "no"},
{"name": "updated_at", "title": "更新时间", "type": "timestamp", "nullable": "no"},
{"name": "creator", "title": "创建人", "type": "str", "length": 32, "nullable": "yes"},
{"name": "updater", "title": "更新人", "type": "str", "length": 32, "nullable": "yes"}
],
"indexes": [
{"name": "idx_template_code", "idxtype": "unique", "idxfields": ["code"]},
{"name": "idx_template_status", "idxtype": "index", "idxfields": ["status"]}
],
"codes": [
{
"field": "status",
"table": "appcodes_kv",
"valuefield": "k",
"textfield": "v",
"cond": "parentid='voucher_template_status'"
}
]
}

View File

@ -0,0 +1,35 @@
{
"summary": [
{
"name": "voucher_usage_log",
"title": "代金券使用流水",
"primary": ["id"],
"catelog": "entity"
}
],
"fields": [
{"name": "id", "title": "主键ID", "type": "str", "length": 32, "nullable": "no"},
{"name": "instance_id", "title": "券实例ID", "type": "str", "length": 32, "nullable": "no"},
{"name": "customer_id", "title": "客户ID", "type": "str", "length": 32, "nullable": "no"},
{"name": "order_id", "title": "订单/消费ID", "type": "str", "length": 64, "nullable": "no"},
{"name": "face_value", "title": "券面值", "type": "double", "length": 10, "dec": 2, "nullable": "no"},
{"name": "deducted_amount", "title": "实际抵扣金额", "type": "double", "length": 10, "dec": 2, "nullable": "no"},
{"name": "used_at", "title": "使用时间", "type": "datetime", "nullable": "no"},
{"name": "used_by", "title": "操作人", "type": "str", "length": 32, "nullable": "yes"},
{"name": "product_type", "title": "产品类型", "type": "str", "length": 32, "nullable": "yes"},
{"name": "product_name", "title": "产品名称", "type": "str", "length": 64, "nullable": "yes"}
],
"indexes": [
{"name": "idx_log_instance", "idxtype": "index", "idxfields": ["instance_id"]},
{"name": "idx_log_customer", "idxtype": "index", "idxfields": ["customer_id"]},
{"name": "idx_log_order", "idxtype": "index", "idxfields": ["order_id"]}
],
"codes": [
{
"field": "instance_id",
"table": "voucher_instance",
"valuefield": "id",
"textfield": "code"
}
]
}

17
pyproject.toml Normal file
View File

@ -0,0 +1,17 @@
[build-system]
requires = ["setuptools>=45", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "voucher"
version = "1.0.0"
description = "代金券模块:模板管理、可配置规则引擎、一次性使用代金券"
requires-python = ">=3.8"
dependencies = [
"sqlor",
"bricks_for_python",
]
[tool.setuptools.packages.find]
where = ["."]
include = ["voucher*"]

1
rules/__init__.py Normal file
View File

@ -0,0 +1 @@
# voucher rules engine

198
rules/engine.py Normal file
View File

@ -0,0 +1,198 @@
"""代金券规则引擎:动态加载规则、校验、使用"""
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

21
rules/registry.py Normal file
View File

@ -0,0 +1,21 @@
"""规则注册机制:新增规则只需写一个 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)

100
rules/validators.py Normal file
View File

@ -0,0 +1,100 @@
"""内置规则校验器,每种规则一个函数,返回 (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

75
scripts/load_path.py Normal file
View File

@ -0,0 +1,75 @@
#!/usr/bin/env python3
"""voucher 模块 RBAC 权限注册"""
import os
import sys
import subprocess
def find_sage_root():
for candidate in [
os.path.expanduser("~/repos/sage"),
os.path.expanduser("~/sage"),
]:
if os.path.isdir(os.path.join(candidate, "wwwroot")):
return candidate
return None
def set_perm(sage_root, role, path):
script = os.path.join(sage_root, "set_role_perm.py")
cmd = [os.path.join(sage_root, "py3/bin/python"), script, role, path]
print(f" {role} {path}")
subprocess.run(cmd, cwd=sage_root, capture_output=True)
def main():
sage_root = find_sage_root()
if not sage_root:
print("ERROR: Sage root not found")
sys.exit(1)
print("Registering voucher RBAC permissions...")
# any (公开)
any_paths = [
"/voucher/menu.ui",
]
# 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/apply_voucher.dspy",
"/voucher/api/get_available.dspy",
"/voucher/api/rule_types.dspy",
]
for p in any_paths:
set_perm(sage_root, "any", p)
for p in logined_paths:
set_perm(sage_root, "logined", p)
print("Done.")
if __name__ == "__main__":
main()

107
sql/tables.sql Normal file
View File

@ -0,0 +1,107 @@
-- ============================================
-- 代金券模块建表脚本
-- ============================================
-- 代金券模板表
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
voucher/__init__.py Normal file
View File

@ -0,0 +1 @@
# voucher module

231
voucher/init.py Normal file
View File

@ -0,0 +1,231 @@
"""voucher 模块初始化"""
from ahserver.serverenv import ServerEnv
from appPublic.uniqueID import getID
MODULE_NAME = "voucher"
MODULE_VERSION = "1.0.0"
DBNAME = "sage"
def _get_dbname():
return DBNAME
# ========== Template CRUD ==========
async def create_voucher_template(data):
sor = DBPools().sqlorContext(_get_dbname())
data['id'] = getID()
data['issued_count'] = 0
if 'status' not in data:
data['status'] = 'draft'
sor.I('voucher_template', data)
return {'status': 'success', 'data': {'id': data['id']}}
async def update_voucher_template(conditions, data):
sor = DBPools().sqlorContext(_get_dbname())
sor.U('voucher_template', conditions, data)
return {'status': 'success'}
async def delete_voucher_template(conditions):
sor = DBPools().sqlorContext(_get_dbname())
# 级联删除关联规则
sor.D('voucher_rule', {'template_id': conditions.get('id', '')})
sor.D('voucher_template', conditions)
return {'status': 'success'}
# ========== Rule CRUD ==========
async def create_voucher_rule(data):
sor = DBPools().sqlorContext(_get_dbname())
data['id'] = getID()
if 'enabled' not in data:
data['enabled'] = 1
if 'sort_order' not in data:
data['sort_order'] = 0
sor.I('voucher_rule', data)
return {'status': 'success', 'data': {'id': data['id']}}
async def update_voucher_rule(conditions, data):
sor = DBPools().sqlorContext(_get_dbname())
sor.U('voucher_rule', conditions, data)
return {'status': 'success'}
async def delete_voucher_rule(conditions):
sor = DBPools().sqlorContext(_get_dbname())
sor.D('voucher_rule', conditions)
return {'status': 'success'}
# ========== Instance CRUD ==========
async def create_voucher_instance(data):
from datetime import datetime, timedelta
sor = DBPools().sqlorContext(_get_dbname())
data['id'] = getID()
data['status'] = 'unused'
# 从模板获取面值
template = sor.R('voucher_template', {'id': data.get('template_id', '')}).first()
if template:
data['face_value'] = template.face_value if hasattr(template, 'face_value') else template['face_value']
valid_days = template.valid_days if hasattr(template, 'valid_days') else template.get('valid_days', 30)
else:
valid_days = data.get('valid_days', 30)
# 设置有效期
now = datetime.now()
data['valid_from'] = data.get('valid_from', now.strftime('%Y-%m-%d %H:%M:%S'))
data['valid_to'] = data.get('valid_to', (now + timedelta(days=valid_days)).strftime('%Y-%m-%d %H:%M:%S'))
data['issued_at'] = now.strftime('%Y-%m-%d %H:%M:%S')
# 生成唯一券码
if 'code' not in data or not data['code']:
import hashlib
raw = f"{data['id']}-{data.get('customer_id', '')}-{now.timestamp()}"
data['code'] = 'VCH-' + hashlib.md5(raw.encode()).hexdigest()[:12].upper()
sor.I('voucher_instance', data)
# 更新模板已发放量
if template:
tid = data.get('template_id', '')
sor.db_execute(f"UPDATE voucher_template SET issued_count = issued_count + 1 WHERE id = '{tid}'")
return {'status': 'success', 'data': {'id': data['id'], 'code': data['code']}}
async def update_voucher_instance(conditions, data):
sor = DBPools().sqlorContext(_get_dbname())
sor.U('voucher_instance', conditions, data)
return {'status': 'success'}
async def delete_voucher_instance(conditions):
sor = DBPools().sqlorContext(_get_dbname())
sor.D('voucher_usage_log', {'instance_id': conditions.get('id', '')})
sor.D('voucher_instance', conditions)
return {'status': 'success'}
# ========== Usage Log CRUD ==========
async def create_voucher_usage_log(data):
sor = DBPools().sqlorContext(_get_dbname())
data['id'] = getID()
sor.I('voucher_usage_log', data)
return {'status': 'success', 'data': {'id': data['id']}}
async def update_voucher_usage_log(conditions, data):
sor = DBPools().sqlorContext(_get_dbname())
sor.U('voucher_usage_log', conditions, data)
return {'status': 'success'}
async def delete_voucher_usage_log(conditions):
sor = DBPools().sqlorContext(_get_dbname())
sor.D('voucher_usage_log', conditions)
return {'status': 'success'}
# ========== Options API (dropdown) ==========
async def get_voucher_template_options():
sor = DBPools().sqlorContext(_get_dbname())
rows = sor.R('voucher_template', {}, {'sort': 'name'})
options = []
for r in rows:
tid = r.id if hasattr(r, 'id') else r['id']
name = r.name if hasattr(r, 'name') else r['name']
options.append({'value': tid, 'text': name})
return {'status': 'success', 'data': {'options': options}}
async def get_voucher_instance_options():
sor = DBPools().sqlorContext(_get_dbname())
rows = sor.R('voucher_instance', {}, {'sort': 'created_at desc'})
options = []
for r in rows:
vid = r.id if hasattr(r, 'id') else r['id']
code = r.code if hasattr(r, 'code') else r['code']
options.append({'value': vid, 'text': code})
return {'status': 'success', 'data': {'options': options}}
# ========== Voucher Engine API ==========
async def apply_voucher_api(customer_id, order_id, voucher_ids, context):
"""外部调用:使用代金券"""
from voucher.rules.engine import batch_apply_vouchers
sor = DBPools().sqlorContext(_get_dbname())
result = batch_apply_vouchers(sor, customer_id, order_id, voucher_ids, context)
return {'status': 'success', 'data': result}
async def get_available_vouchers_api(customer_id, context=None):
"""外部调用:查询可用代金券"""
from voucher.rules.engine import get_available_vouchers
sor = DBPools().sqlorContext(_get_dbname())
vouchers = get_available_vouchers(sor, customer_id, context)
items = []
for v in vouchers:
if hasattr(v, '__dict__'):
items.append(v.__dict__)
else:
items.append(v)
return {'status': 'success', 'data': items}
async def get_registered_rule_types():
"""返回已注册的规则类型列表"""
from voucher.rules.registry import get_all_rule_types
return {'status': 'success', 'data': get_all_rule_types()}
# ========== Module Load ==========
def load_voucher():
"""注册所有函数到 ServerEnv"""
env = ServerEnv()
# Template CRUD
env.create_voucher_template = create_voucher_template
env.update_voucher_template = update_voucher_template
env.delete_voucher_template = delete_voucher_template
# Rule CRUD
env.create_voucher_rule = create_voucher_rule
env.update_voucher_rule = update_voucher_rule
env.delete_voucher_rule = delete_voucher_rule
# Instance CRUD
env.create_voucher_instance = create_voucher_instance
env.update_voucher_instance = update_voucher_instance
env.delete_voucher_instance = delete_voucher_instance
# Usage Log CRUD
env.create_voucher_usage_log = create_voucher_usage_log
env.update_voucher_usage_log = update_voucher_usage_log
env.delete_voucher_usage_log = delete_voucher_usage_log
# Options
env.get_voucher_template_options = get_voucher_template_options
env.get_voucher_instance_options = get_voucher_instance_options
# Engine API
env.apply_voucher_api = apply_voucher_api
env.get_available_vouchers_api = get_available_vouchers_api
env.get_registered_rule_types = get_registered_rule_types
# Import validators to trigger @register_rule decorators
import voucher.rules.validators # noqa: F401
return True

View File

@ -0,0 +1,17 @@
import json
customer_id = params_kw.get('customer_id', '')
order_id = params_kw.get('order_id', '')
voucher_ids = params_kw.get('voucher_ids', [])
if isinstance(voucher_ids, str):
voucher_ids = json.loads(voucher_ids)
context = {
'request_amount': params_kw.get('request_amount', 0),
'product_type': params_kw.get('product_type', ''),
'product_name': params_kw.get('product_name', ''),
'user_level': params_kw.get('user_level', 0),
}
result = await apply_voucher_api(customer_id, order_id, voucher_ids, context)
return json.dumps(result)

View File

@ -0,0 +1,13 @@
import json
customer_id = params_kw.get('customer_id', '')
context = {}
if params_kw.get('product_type'):
context['product_type'] = params_kw.get('product_type')
if params_kw.get('product_name'):
context['product_name'] = params_kw.get('product_name')
if params_kw.get('request_amount'):
context['request_amount'] = params_kw.get('request_amount')
result = await get_available_vouchers_api(customer_id, context if context else None)
return json.dumps(result)

View File

@ -0,0 +1,10 @@
import json
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 json.dumps(result)

View File

@ -0,0 +1,5 @@
import json
conditions = {'id': params_kw.get('id', '')}
result = await delete_voucher_instance(conditions)
return json.dumps(result)

View File

@ -0,0 +1,4 @@
import json
result = await get_voucher_instance_options()
return json.dumps(result)

View File

@ -0,0 +1,11 @@
import json
conditions = {'id': params_kw.get('id', '')}
data = {}
for key in ['template_id', 'customer_id', 'code', 'status', 'source', 'remark']:
val = params_kw.get(key, None) if hasattr(params_kw, 'get') else None
if val is not None:
data[key] = val
result = await update_voucher_instance(conditions, data)
return json.dumps(result)

View File

@ -0,0 +1,10 @@
import json
data = {}
for key in ['template_id', 'rule_type', 'rule_config', 'enabled', 'sort_order', 'remark']:
val = params_kw.get(key, None) if hasattr(params_kw, 'get') else None
if val is not None:
data[key] = val
result = await create_voucher_rule(data)
return json.dumps(result)

View File

@ -0,0 +1,5 @@
import json
conditions = {'id': params_kw.get('id', '')}
result = await delete_voucher_rule(conditions)
return json.dumps(result)

View File

@ -0,0 +1,11 @@
import json
conditions = {'id': params_kw.get('id', '')}
data = {}
for key in ['template_id', 'rule_type', 'rule_config', 'enabled', 'sort_order', 'remark']:
val = params_kw.get(key, None) if hasattr(params_kw, 'get') else None
if val is not None:
data[key] = val
result = await update_voucher_rule(conditions, data)
return json.dumps(result)

View File

@ -0,0 +1,4 @@
import json
result = await get_registered_rule_types()
return json.dumps(result)

View File

@ -0,0 +1,11 @@
import json
params = params_kw if hasattr(params_kw, '__getitem__') else {}
data = {}
for key in ['name', 'code', 'face_value', 'total_count', 'valid_days', 'status', 'remark']:
val = params_kw.get(key, None) if hasattr(params_kw, 'get') else None
if val is not None:
data[key] = val
result = await create_voucher_template(data)
return json.dumps(result)

View File

@ -0,0 +1,5 @@
import json
conditions = {'id': params_kw.get('id', '')}
result = await delete_voucher_template(conditions)
return json.dumps(result)

View File

@ -0,0 +1,4 @@
import json
result = await get_voucher_template_options()
return json.dumps(result)

View File

@ -0,0 +1,11 @@
import json
conditions = {'id': params_kw.get('id', '')}
data = {}
for key in ['name', 'code', 'face_value', 'total_count', 'valid_days', 'status', 'remark', 'updater']:
val = params_kw.get(key, None) if hasattr(params_kw, 'get') else None
if val is not None:
data[key] = val
result = await update_voucher_template(conditions, data)
return json.dumps(result)

View File

@ -0,0 +1,10 @@
import json
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 json.dumps(result)

View File

@ -0,0 +1,5 @@
import json
conditions = {'id': params_kw.get('id', '')}
result = await delete_voucher_usage_log(conditions)
return json.dumps(result)

View File

@ -0,0 +1,11 @@
import json
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 json.dumps(result)

48
wwwroot/index.ui Normal file
View File

@ -0,0 +1,48 @@
{
"widgettype": "VBox",
"options": {"width": "100%", "height": "100%", "padding": "20px"},
"subwidgets": [
{"widgettype": "Title3", "options": {"text": "代金券管理"}},
{"widgettype": "ResponsableBox", "options": {"gap": "16px", "minWidth": "250px"},
"subwidgets": [
{"widgettype": "VBox",
"options": {"bgcolor": "#FFFFFF", "padding": "20px", "cursor": "pointer", "borderRadius": "8px", "cwidth": "20"},
"binds": [{
"wid": "self", "event": "click", "actiontype": "urlwidget",
"target": "app.voucher_content",
"options": {"url": "{{entire_url('/voucher/voucher_template_list/index.ui')}}"},
"mode": "replace"
}],
"subwidgets": [
{"widgettype": "Text", "options": {"text": "模板管理", "fontSize": "18px", "bold": true}},
{"widgettype": "Text", "options": {"text": "管理代金券模板和规则配置", "fontSize": "14px", "color": "#666"}}
]},
{"widgettype": "VBox",
"options": {"bgcolor": "#FFFFFF", "padding": "20px", "cursor": "pointer", "borderRadius": "8px", "cwidth": "20"},
"binds": [{
"wid": "self", "event": "click", "actiontype": "urlwidget",
"target": "app.voucher_content",
"options": {"url": "{{entire_url('/voucher/voucher_instance_list/index.ui')}}"},
"mode": "replace"
}],
"subwidgets": [
{"widgettype": "Text", "options": {"text": "券实例管理", "fontSize": "18px", "bold": true}},
{"widgettype": "Text", "options": {"text": "查看和发放代金券", "fontSize": "14px", "color": "#666"}}
]},
{"widgettype": "VBox",
"options": {"bgcolor": "#FFFFFF", "padding": "20px", "cursor": "pointer", "borderRadius": "8px", "cwidth": "20"},
"binds": [{
"wid": "self", "event": "click", "actiontype": "urlwidget",
"target": "app.voucher_content",
"options": {"url": "{{entire_url('/voucher/voucher_usage_log_list/index.ui')}}"},
"mode": "replace"
}],
"subwidgets": [
{"widgettype": "Text", "options": {"text": "使用流水", "fontSize": "18px", "bold": true}},
{"widgettype": "Text", "options": {"text": "查看代金券使用记录", "fontSize": "14px", "color": "#666"}}
]}
]},
{"widgettype": "VBox", "id": "voucher_content",
"options": {"width": "100%", "flex": "1", "marginTop": "20px"}}
]
}

25
wwwroot/menu.json Normal file
View File

@ -0,0 +1,25 @@
{
"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"
}
]
}