- 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权限配置 - 一次性使用,不找零
199 lines
6.0 KiB
Python
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
|