会员权益 — 技术设计(商城 oemsaas)
状态:与业务需求 requirements.md 对齐;配置 UI 在 oemsaasapp,表在商城。
冲突处理:与代码冲突时以代码为准;表结构以本文 +docs/sql/membership_tables.sql为准。
1. 职责边界
| 侧 | 职责 |
|---|---|
| oemsaasapp | 配置界面;升降级规则表(应用库);订阅 orders/paid Webhook(延迟 5 分钟);计算级别并回写 customer.level_id |
| oemsaas 商城 | 表存储与运行时读表;积分加权;会员价(购物车/结账/前台);运费会员日+级别过滤;o_customer_level CRUD |
配置不下发,商城运行时直接查 MySQL(配合 Redis 缓存可选,后续实现阶段再定)。
2. 数据表一览
| 存储 | 表 / 位置 | 说明 |
|---|---|---|
| 升降级规则 | 应用库 o_membership_upgrade_rule(level_id=0 店铺行 + >0 级别行) | 见 design-app.md |
| 应用安装 / Webhook | 商城 o_my_app、o_webhook | 应用库不建表 |
| 积分加权 | o_point_rule.member_points_weight | JSON,按 level_id |
| 会员日 | o_store_config key=membership_member_day | JSON |
| 会员折扣 | o_customer_level_product_discount(新建) | product_id=0 全场;>0 指定商品 |
| 运费扩展 | o_shipping_discount.member_day_only | 是否仅会员日有效 |
| 运费适用级别 | o_shipping_discount_customer | obj_type=customer_level |
| 顾客级别 | o_customer_level + customer.level_id | 已有 |
3. 表结构说明
3.1 o_customer_level_product_discount(新建)
| 字段 | 类型 | 说明 |
|---|---|---|
id | int unsigned PK | |
store_id | int unsigned | 店铺 |
level_id | int unsigned | o_customer_level.id |
product_id | int unsigned | 0 = 全场;>0 = 指定商品 |
discount | decimal(5,2) | 平常日支付比例 %(100=原价,80=8 折) |
member_day_discount | decimal(5,2) | 会员日支付比例 % |
created_at / updated_at | int unsigned |
约束:
UNIQUE KEY uk_store_level_product (store_id, level_id, product_id)- 每级别指定商品 ≤ 9 条(
product_id > 0),由应用/API 校验 - 每级别至少 0~1 条
product_id=0表示全场默认 - 指定商品折扣优先于全场(同
level_id下命中product_id则不用 0 行)
3.2 o_point_rule.member_points_weight(新增字段)
类型:varchar(4096),JSON。
{
"enabled": 1,
"levels": [
{
"level_id": 1,
"normal_percent": 100,
"member_day_percent": 110
},
{
"level_id": 2,
"normal_percent": 100,
"member_day_percent": 120
}
]
}| 字段 | 说明 |
|---|---|
enabled | 0 关闭加权;1 开启 |
levels[].level_id | 与 o_customer_level.id 对应 |
normal_percent / member_day_percent | 在现有积分规则应得积分上乘以 percent/100;结果 保留 1 位小数向上取整 |
应用写配置;商城 PointsBalanceService::orderPaid() 读。
3.3 会员日 — o_store_config
Key:membership_member_day(常量建议:StoreConfigModel::KEY_MEMBERSHIP_MEMBER_DAY)
Value:JSON(≤4096 字符,与字段长度一致)
{
"weekday_rules": [
{
"frequency": "weekly",
"weekdays": [1, 2, 5]
},
{
"frequency": "first_week",
"weekdays": [6]
},
{
"frequency": "last_week",
"weekdays": [7]
}
],
"month_dates": [1, 8, 15, 28]
}| 字段 | 说明 |
|---|---|
frequency | weekly 每周 / first_week 每月首周 / last_week 每月末周 |
weekdays | 1=周一 … 7=周日,多选 |
month_dates | 每月 1–28 号,多选 |
判定:
- 使用店铺时区
o_store_config.time_zone(已有KEY_STORE_TIME_ZONE) - 当日命中 weekday_rules 任一条 或 month_dates 任一号 即为会员日
- 实现:
MembershipMemberDayService::isMemberDay(int $storeId, ?int $timestamp = null): bool
3.4 运费 — o_shipping_discount
新增字段:
| 字段 | 类型 | 默认 | 说明 |
|---|---|---|---|
member_day_only | tinyint unsigned | 0 | 0=活动时段内始终有效;1=活动时段内仅会员日当天对命中级别顾客生效 |
3.5 运费 — o_shipping_discount_customer
扩展 obj_type 枚举,增加 customer_level:
obj_id= 字符串形式的level_id(与现有customer/tag用法一致)- 一条运费活动可关联多个级别(多行)
ShippingDiscountService 过滤逻辑(实现阶段):
- 若配置了
customer_level行,则当前顾客level_id须在关联列表中 - 若
member_day_only=1且当日非会员日 → 该活动不生效 - 其余仍走现有 tag/customer/visitor/all 逻辑
4. Webhook:升级(应用侧)
| 项 | 约定 |
|---|---|
| 事件 | orders/paid(已有 OrderHandler + 全量订单体) |
| 延迟 | delay_time = 300(5 分钟),避免 ES/从库延迟导致应用读不到最新 total_spent |
| 注册 | 应用安装时在 o_webhook 写入;delay_step 与现网其它 Webhook 一致 |
| 回写 | 应用调商城 api / openapi / mixapi 更新 customer.level_id |
商城不实现自动升级 Listener。
5. 商城运行时挂点(实现阶段)
| 能力 | 挂点 | 读表 |
|---|---|---|
| 购物车/结账会员价 | CartService::cartDataComposition() 等 | o_level_product_discount + 会员日 |
| 前台商品价展示 | ProductService / Liquid 变量 | 同上 |
| 积分加权 | PointsBalanceService::orderPaid() | o_point_rule.member_points_weight |
| 运费 | ShippingDiscountService | member_day_only + customer_level 关联 |
| 会员日 | 公共 Service | o_store_config.membership_member_day |
应用门禁:MyAppService::isInstall('app_xxx')(auth_key 与应用中心对齐后补常量)。
6. 配置写入(应用 → 商城)
应用通过 api/openapi/mixapi 调用商城 Service,无商城配置 UI:
| 配置 | 写入方式 |
|---|---|
| 积分加权 | 现有 POST /points(Points/addOrUpdate),字段 member_points_weight |
| 会员日 | 现有 StoreConfig/update 或 setConfig,key=membership_member_day |
| 会员折扣 | PUT /membership/discount/:level_id |
| 运费 | 现有运费 API + member_day_only + member_level_ids |
| 升降级 | 不写商城表 |
7. 需求完整性检查
7.1 已覆盖(可进入 DDL + 开发)
- 表在商城、应用写、商城读
- 升降级在应用 + Webhook 5 分钟延迟
- 积分 JSON 按级别 + 会员日
- 会员日 store_config JSON(星期 + 月日)
- 会员折扣表 + 全场/指定商品
- 运费会员日 + 级别
- 购物车/结账会员价(实现阶段)
- 支付成功积分(实现阶段)
7.2 不阻塞表设计、实现阶段再定
| 项 | 说明 |
|---|---|
| 会员价 vs diy_offer/promotion/coupon 顺序 | 原型未定义,Cart 接入时与产品确认 |
| 商品详情页会员价 | 与列表共用 o_level_product_discount,字段一致 |
| 五种结账形态 COD 是否全开 | 与 promotions-by-checkout-type.md 对齐逐形态接入 |
应用 auth_key 常量名 | 与应用中心注册名一致后补代码常量 |
discount 是否允许 0 | 建议 (0, 100],0 表示免费需产品确认 |
8. 部署
各节点业务库执行:
# 见 docs/sql/membership_tables.sql执行后同步更新 Model $schema(PointsRuleModel、ShippingDiscountModel、ShippingDiscountCustomerModel,新建 CustomerLevelProductDiscountModel)。
9. 应用库(oemsaasapp)
升降级规则、升级流水在 应用独立库(2 张表);安装与 Webhook 在商城 o_my_app / o_webhook。
10. 变更记录
| 日期 | 说明 |
|---|---|
| 2026-06-08 | 初版:表结构、JSON 约定、Webhook 延迟、商城挂点 |
| 2026-06-08 | 补充应用库文档链接 |