134 lines
8.2 KiB
Markdown
134 lines
8.2 KiB
Markdown
# 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<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/options(options 返回纯 `[{value,text}]` 数组)
|
||
- rule/:create/update/delete;`rule_types.dspy` 返回已注册类型清单
|
||
- instance/:`voucher_issue.dspy`(发券)、update(仅 unused 可改客户/备注)、delete(used 禁删)、`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 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)
|