voucher/rules/engine.py
yumoqing 2c56aa904a 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权限配置
- 一次性使用,不找零
2026-05-29 00:28:01 +08:00

199 lines
6.0 KiB
Python

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