voucher/README.md

134 lines
8.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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订单/抵扣金额/产品,只读审计)
```
### 关键机制
| 机制 | 实现 |
|---|---|
| 一次性使用 | 核销用条件 UPDATE`status='unused'` 守卫)+ affected_rows 判定,并发双花只有一个成功 |
| 发行量控制 | `issued_count<total_count` 原子占额total_count=0 不限量;未使用券删除回吐名额 |
| 规则可配置 | `@register_rule` 注册 validator 纯函数,模板下挂 voucher_rule 行即生效 |
| 作废 | `invalidate_voucher_api`unused→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 |
管理 API`wwwroot/api/`,全部薄封装 → init.py ServerEnv 函数):
- template/create/update/delete/optionsoptions 返回纯 `[{value,text}]` 数组)
- rule/create/update/delete`rule_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
```python
# 结算联动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.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 <APP_ROOT>
./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 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