# 租客成长等级与主动短信触达方案

## 一、租客成长等级

### 功能范围

- 只统计租号订单，即 `rental_orders`。
- 不统计撞车订单，即 `mohong_orders`。
- 积分只按已完成租号订单的实付租金累计，不含押金。
- 等级三档：铂金、钻石、巅峰。
- 下单租号时按用户当前等级使用折扣。
- 折扣和升级门槛后台可配置。
- 历史订单保存价格、等级、折扣快照，后台修改配置不影响历史订单。

### 积分规则

推荐规则：

```text
成长积分 = 已完成租号订单实付租金金额
1 元实付租金 = 1 积分
```

不建议统计押金，原因是押金可能退款、免押、暂扣或赔付，直接计入成长积分会让财务口径变复杂。

积分发放时机建议放在租号订单完成后，而不是支付成功后。这样可以避免用户付款后退款、取消或关闭订单导致刷积分。

### 后台配置

使用现有 `system_configs`，新增配置项：

```text
renter.growth_level_rules
```

建议默认值：

```json
{
  "enabled": true,
  "points_per_yuan": 1,
  "levels": [
    { "code": "platinum", "name": "铂金", "min_points": 300, "discount_bps": 9900 },
    { "code": "diamond", "name": "钻石", "min_points": 1000, "discount_bps": 9800 },
    { "code": "peak", "name": "巅峰", "min_points": 5000, "discount_bps": 9500 }
  ]
}
```

`discount_bps` 使用万分比，避免小数误差。例如 `9800` 表示 9.8 折。

### 数据库字段

`users` 建议新增：

- `renter_growth_points`：租客成长积分。
- `renter_growth_level`：当前租客成长等级。

`rental_orders` 建议新增：

- `rent_original_amount_cent`：原始租金。
- `rent_discount_amount_cent`：租金优惠金额。
- `renter_growth_level`：下单时命中的等级编码。
- `renter_growth_level_name`：下单时命中的等级名称。
- `renter_discount_bps`：下单时命中的折扣。
- `growth_points_awarded`：本单已发放积分，用于防重复。

### 下单折扣

在订单创建阶段应用折扣，支付阶段不重新计算。

现有租号订单创建位置：

```text
backend/internal/modules/order/lifecycle.go
```

现有支付金额来自：

```text
backend/internal/modules/payment/payment_start.go
amountCent := row.RentAmountCent + row.DepositAmountCent
```

因此订单创建时应直接把 `RentAmountCent` 写成折后租金，后续支付、退款、结算、仲裁都沿用订单金额。

推荐折扣范围：

- 只折扣租金。
- 押金不参与折扣。
- 折扣优先扣平台费，不影响号主收入。
- 如果折扣金额超过平台费，第一版建议限制为最多扣到平台费为 0，避免平台费出现负数。

### 自动升级

租号订单首次完成时：

1. 校验 `growth_points_awarded = 0`，避免重复发放。
2. 根据折后租金计算本单积分。
3. 累加到 `users.renter_growth_points`。
4. 根据 `min_points` 自动计算用户当前等级。
5. 更新 `users.renter_growth_level`。
6. 回写 `rental_orders.growth_points_awarded`。

默认只升级不降级。若后台未来调整门槛，可以选择只影响后续升级判断，不主动批量降级。

### 前端范围

后台系统配置页：

- 配置是否启用。
- 配置每元积分。
- 配置三个等级的门槛积分。
- 配置三个等级的折扣。

后台用户管理：

- 展示成长等级。
- 展示成长积分。
- 支持后台调整积分或等级，操作写审计日志。

用户端订单页：

- 展示租金原价。
- 展示等级折扣。
- 展示优惠金额。
- 展示折后租金。
- 展示押金。
- 展示应付合计。

### 测试范围

- 铂金、钻石、巅峰不同等级折扣计算正确。
- 押金不打折。
- 折扣不影响号主收入。
- 支付金额等于折后租金加实付押金。
- 撞车订单完成不增加成长积分。
- 租号订单完成只发放一次成长积分。
- 积分达到门槛后自动升级。
- 后台修改配置不影响历史订单快照。

## 二、主动短信触达

### 业务目标

用户完成一笔租号订单后，平台可以主动发送短信，告知用户本月已经累计完成多少笔租号订单，以及距离领取奖励还差多少笔。

示例文案：

```text
尊敬的 用户名:
您好，您已经在 大锤商行 累计完成 1 笔租号订单，再完成 x 笔即可领取 xxxx。
```

### 统计口径

- 只统计 `rental_orders`。
- 只统计状态为 `completed` 的租号订单。
- 不统计 `mohong_orders` 撞车订单。
- 按自然月统计，建议使用订单完成时间 `settled_at`。
- 月统计区间为当前月第一天 00:00:00 到下月第一天 00:00:00。

统计 SQL 口径示例：

```sql
SELECT COUNT(*)
FROM rental_orders
WHERE renter_id = ?
  AND status = 'completed'
  AND settled_at >= ?
  AND settled_at < ?;
```

### 触发时机

推荐触发点：租号订单完成后。

现有完成入口：

```text
backend/internal/modules/order/checkout_finalize.go
```

`finalizeCheckout` 会把订单状态改为 `completed`。短信不建议在事务内直接发送，避免短信服务失败导致订单完成失败。

推荐流程：

1. 订单完成事务内写入短信待发送记录。
2. 事务提交后由后台任务发送短信。
3. 发送成功、失败、重试次数都落库。

### 短信通道

项目已有短信集成：

```text
backend/internal/integrations/sms
```

当前 `Provider` 只支持登录验证码：

```go
SendLoginCode(ctx context.Context, phone string, code string) error
```

主动短信需要扩展为模板短信能力，例如：

```go
SendTemplate(ctx context.Context, phone string, templateCode string, params map[string]string) error
```

阿里云短信实际发送不能随意拼接任意文案，必须使用已审核的短信模板。因此后台可配置“模板 code、奖励规则、预览文案”，真实发送时只传模板变量。

### 后台配置

使用 `system_configs`，新增配置项：

```text
sms.rental_completion_reminder
```

建议默认值：

```json
{
  "enabled": false,
  "brand_name": "大锤商行",
  "template_code": "",
  "monthly_user_limit": 4,
  "cooldown_hours": 24,
  "rewards": [
    { "target_orders": 3, "reward_name": "成长礼包", "enabled": true },
    { "target_orders": 5, "reward_name": "专属优惠券", "enabled": true }
  ],
  "preview_template": "尊敬的 {{nickname}}：您好，您已经在 {{brand_name}} 累计完成 {{completed_count}} 笔租号订单，再完成 {{remaining_count}} 笔即可领取 {{reward_name}}。"
}
```

规则说明：

- `enabled`：总开关。
- `brand_name`：短信里展示的品牌名。
- `template_code`：阿里云审核通过的营销或通知短信模板 code。
- `monthly_user_limit`：单用户单月最多发送次数。
- `cooldown_hours`：同一用户两次主动短信最小间隔。
- `rewards`：奖励门槛，从小到大配置。
- `preview_template`：后台预览使用，真实发送以短信供应商模板为准。

计算 `x` 的规则：

1. 获取用户本月完成租号订单数 `completed_count`。
2. 找到第一个 `target_orders > completed_count` 的奖励。
3. `remaining_count = target_orders - completed_count`。
4. 如果没有下一个奖励，默认不发送，或后续配置为发送达成提醒。

### 数据库表

建议新增短信发送记录表：

```text
sms_messages
```

字段建议：

- `id`
- `user_id`
- `phone`
- `biz_type`：例如 `rental_completion_reminder`
- `biz_id`：租号订单 ID。
- `template_code`
- `template_params`
- `content_preview`
- `status`：`pending`、`sent`、`failed`、`cancelled`
- `retry_count`
- `last_error`
- `sent_at`
- `created_at`
- `updated_at`

建议加唯一索引：

```text
uk_sms_messages_biz_type_biz_id_user
```

用于保证同一笔完成订单不会重复创建短信任务。

### 发送任务

新增后台任务，例如：

```text
backend/internal/jobs/smsdispatch
```

任务逻辑：

1. 查询 `pending` 且重试次数未超限的短信。
2. 调用短信 provider 发送模板短信。
3. 成功后标记 `sent`，写 `sent_at`。
4. 失败后记录 `last_error`，增加 `retry_count`。
5. 超过最大重试次数后标记 `failed`。

### 限流与防打扰

发送前需要校验：

- 系统配置已启用。
- 用户手机号有效。
- 本订单尚未创建过同类短信。
- 用户本月主动短信次数未超过 `monthly_user_limit`。
- 用户距离上次主动短信超过 `cooldown_hours`。
- 找得到下一个奖励门槛。

第一版可以先不做用户退订，但如果短信属于营销性质，建议后续加用户短信订阅状态或退订名单。

### 前端范围

后台系统配置页：

- 开关。
- 品牌名。
- 模板 code。
- 单用户月发送上限。
- 冷却时间。
- 奖励门槛和奖励名称。
- 文案预览。

后台短信记录页可以作为二期，第一版可先通过数据库和日志排查。

### 测试范围

- 租号订单完成后创建短信待发送记录。
- 撞车订单完成不创建短信。
- 本月完成订单数统计正确。
- 根据奖励门槛计算 `remaining_count` 正确。
- 找不到下一奖励时不发送。
- 单用户月上限生效。
- 冷却时间生效。
- 同一订单不会重复创建短信任务。
- 短信发送失败不影响订单完成。
