# voucher — 代金券模块(pipeline-app 宿主) 独立代金券管理模块:**可配置规则引擎**、一次性使用、不找零、并发安全。 - 宿主:pipeline-app(库名走 `get_module_dbname('voucher')`,pipeline 宿主下= `pipeline`) - 版本:2.0.0(2026-09-12 sqlor 风格全异步重写,替代 1.x sage 时代废代码) ## 核心架构 ``` 模板 voucher_template(面值/发行量/有效期/状态,org_id 隔离) └─ 规则集 voucher_rule(可配置,增删不改代码;按 sort_order 依次校验) └─ 实例 voucher_instance(券码/状态 unused→used|expired|void,一次性) └─ 流水 voucher_usage_log(订单/抵扣金额/产品,只读审计) ``` ### 关键机制 | 机制 | 实现 | |---|---| | 一次性使用 | 核销用条件 UPDATE(`status='unused'` 守卫)+ affected_rows 判定,并发双花只有一个成功 | | 发行量控制 | `issued_count 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.sh`:clone/pip install/xls2ui/软链四个清单加 `voucher`(已登记) 2. `app/pipeline_app.py`:import + `load_voucher()`(已登记,ticket 之后) 3. `scripts/import_init.py`:INIT_MODULES 加 voucher 行(已登记) 4. 建表:`create_tables.py` 动态扫描 pkgs/*/models 自动覆盖(无需改) 5. `wwwroot/index.ui`:客户「我的代金券」+ 管理「代金券管理」菜单(已登记,is_ticket_staff 门禁) 6. RBAC:`./load_path.sh`(自动扫 pkgs/*/scripts/load_path.py)→ `redis-cli -n 0 FLUSHDB` → 重启 部署后一次性操作: ```bash cd ./py3/bin/python pkgs/voucher/scripts/seed_welcome_voucher.py # WELCOME400 种子 ``` ## Pitfalls - **实例「新增」必须走发券 API**(voucher_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 array(validators 兼容逗号分隔串,但写入必须是 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()` 后再 widgetBuild(form.js dispatch 原始 resp)。 ## 仓库 - 远端:git@git.opencomputing.cn:yumoqing/voucher.git(main) - 宿主接入提交:pipeline-app(build.sh/pipeline_app.py/import_init.py/index.ui)