voucher — 代金券模块pipeline-app 宿主)

独立代金券管理模块:可配置规则引擎、一次性使用、不找零、并发安全。

  • 宿主pipeline-app库名走 get_module_dbname('voucher')pipeline 宿主下= pipeline
  • 版本2.0.02026-09-12 sqlor 风格全异步重写,替代 1.x sage 时代废代码)

核心架构

模板 voucher_template面值/发行量/有效期/状态org_id 隔离)
  └─ 规则集 voucher_rule可配置增删不改代码按 sort_order 依次校验)
       └─ 实例 voucher_instance券码/状态 unused→used|expired|void一次性
            └─ 流水 voucher_usage_log订单/抵扣金额/产品,只读审计)

关键机制

机制 实现
一次性使用 核销用条件 UPDATEstatus='unused' 守卫)+ affected_rows 判定,并发双花只有一个成功
发行量控制 issued_count<total_count 原子占额total_count=0 不限量;未使用券删除回吐名额
规则可配置 @register_rule 注册 validator 纯函数,模板下挂 voucher_rule 行即生效
作废 invalidate_voucher_apiunused→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} 用户等级下限

新增规则:voucher/rules/validators.py 加一个 @register_rule('xxx') 纯函数 + data.json 字典补一行,无需改引擎。

页面与 API

管理侧logined菜单「代金券管理」工单三角色可见

页面 路径 说明
入口 /voucher/index.ui 卡片导航:模板管理/券实例管理/使用流水
模板管理 /voucher/voucher_template_list/index.ui CRUD + 工具栏「发券」弹窗 + 子表「规则配置」
券实例 /voucher/voucher_instance_list/index.ui 新增=发券引擎(禁裸插入)+ 工具栏「作废」弹窗 + 子表「使用记录」
使用流水 /voucher/voucher_usage_log_list/index.ui 只读noedit

管理 APIwwwroot/api/,全部薄封装 → init.py ServerEnv 函数):

  • template/create/update/delete/optionsoptions 返回纯 [{value,text}] 数组)
  • rule/create/update/deleterule_types.dspy 返回已注册类型清单
  • instance/voucher_issue.dspy发券、update仅 unused 可改客户/备注、deleteused 禁删)、voucher_invalidate.dspy(作废)
  • apply_voucher.dspy(核销)、get_available.dspy(内部查可用)

客户侧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}

其他模块调用ServerEnv

# 结算联动product_management/accounting 等):
result = await env.voucher_batch_apply(sor, customer_id, order_id, voucher_ids, context)
# → {'total_deducted': 150.0, 'remaining': 50.0, 'details': [...]}
# remaining > 0 时从余额扣
# context: {request_amount(必填), product_type, product_name, user_level, used_by}

注册函数全集(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

目录结构

voucher/
├── voucher/                 # Python 包
│   ├── init.py              # ServerEnv 注册 + 注册送券挂钩 + load_voucher()
│   └── rules/               # registry(@register_rule) + validators(7种) + engine(全异步)
├── init/data.json           # 字典种子(模板状态/实例状态/规则类型/来源/启用标志)
├── models/                  # 4 张表定义建表唯一事实源json2ddl 渲染)
├── json/                    # 4 个 CRUD 定义xls2ui 生成 wwwroot/voucher_*_list/,生成物不入库)
├── wwwroot/
│   ├── index.ui             # 管理入口(卡片导航)
│   ├── my/index.ui          # 客户「我的代金券」
│   ├── issue_form.ui        # 发券弹窗表单
│   ├── invalidate_form.ui   # 作废弹窗表单
│   └── api/                 # 薄封装 dspy零 import委托 init.py 注册函数)
├── scripts/
│   ├── load_path.py         # RBAC 注册logined 全路径逐个精确配)
│   └── seed_welcome_voucher.py  # WELCOME400 种子(幂等)
├── i18n/{zh,en,jp,ko}/msg.txt   # key=value 格式merge_i18n 消费)
└── pyproject.toml           # packages = ["voucher", "voucher.rules"]

部署清单pipeline-app 宿主,六处登记)

  1. build.shclone/pip install/xls2ui/软链四个清单加 voucher(已登记)
  2. app/pipeline_app.pyimport + load_voucher()已登记ticket 之后)
  3. scripts/import_init.pyINIT_MODULES 加 voucher 行(已登记)
  4. 建表:create_tables.py 动态扫描 pkgs/*/models 自动覆盖(无需改)
  5. wwwroot/index.ui:客户「我的代金券」+ 管理「代金券管理」菜单已登记is_ticket_staff 门禁)
  6. RBAC./load_path.sh(自动扫 pkgs/*/scripts/load_path.pyredis-cli -n 0 FLUSHDB → 重启

部署后一次性操作:

cd <APP_ROOT>
./py3/bin/python pkgs/voucher/scripts/seed_welcome_voucher.py   # WELCOME400 种子

Pitfalls

  • 实例「新增」必须走发券 APIvoucher_issue.dspy → issue_voucher 引擎):裸插入会绕过原子占额/券码生成/有效期计算。CRUD json 的 editable.new_data_url 已指向它。
  • 状态字段禁止界面直改:使用走核销 API、作废走 invalidate API都有条件 UPDATE 原子守卫update_voucher_instance 只允许改 customer_id/remark 且仅 unused 券。
  • 一次性使用:不追踪 remaining_value/partial 状态,抵扣 = min(面值, 消费金额),不找零。
  • rule_config 数组字段用 JSON arrayvalidators 兼容逗号分隔串,但写入必须是 JSON
  • rule_type 用短名product_type 而非 applicable_product_type——appcodes_kv.id = {parentid}_{k} ≤32 字符。
  • 模板删除已发放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() 后再 widgetBuildform.js dispatch 原始 resp

仓库

  • 远端git@git.opencomputing.cn:yumoqing/voucher.gitmain
  • 宿主接入提交pipeline-appbuild.sh/pipeline_app.py/import_init.py/index.ui
Description
No description provided
Readme 66 KiB
Languages
Python 99.7%
Shell 0.3%