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