hi-server/doc/invite-purchase-flow.md
shanshanzhong 0ec0e2b9d2
Some checks failed
Build docker and publish / build (20.15.1) (push) Has been cancelled
fix(order): align invite gift ownership
2026-04-28 05:19:57 -07:00

863 lines
22 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.

# 邀请赠送与购买订阅逻辑说明
本文档说明当前代码中的购买订阅、订单激活、邀请佣金、邀请赠送时间、家庭组归属逻辑。重点覆盖每个主要分支,方便排查“重复订单/重复订阅/邀请未赠时/赠时落点错误”等问题。
涉及核心文件:
- `internal/logic/public/order/purchaseLogic.go`
- `queue/logic/order/activateOrderLogic.go`
- `internal/logic/common/familyEntitlement.go`
- `internal/model/user/model.go`
## 1. 核心概念
### 1.1 订单状态
| 状态 | 含义 |
| --- | --- |
| `1` | pending已创建未支付 |
| `2` | paid已支付待激活 |
| `3` | close已关闭 |
| `4` | failed/claimed代码里同时用于失败和 worker 临时领取 |
| `5` | finished激活完成 |
### 1.2 订单类型
| 类型 | 含义 |
| --- | --- |
| `1` | 新购套餐 |
| `2` | 续费/换套餐 |
| `3` | 重置流量 |
| `4` | 余额充值 |
| `5` | 兑换码激活 |
### 1.3 用户 ID 与订阅归属
订单有两个重要用户字段:
| 字段 | 含义 |
| --- | --- |
| `user_id` | 发起订单/付款的用户 |
| `subscription_user_id` | 订阅权益归属用户;`0` 表示同 `user_id` |
家庭组规则:
- 普通用户下单:`subscription_user_id = user_id`
- 家庭组成员下单:`subscription_user_id = 家主用户 ID`
- 家庭组主账号下单:`subscription_user_id = 家主用户 ID`
当前代码使用 `ResolveEntitlementUser` 判断家庭归属:
- 只有有效家庭组、有效成员关系、角色为 member 时,权益归家主。
- 家主本人不会被改写到别人名下。
## 2. 购买订阅下单逻辑
入口:`Purchase(req *types.PurchaseOrderRequest)`
这里只是创建订单和安排关闭任务,不直接发放订阅。真正发放订阅在订单支付后由队列激活处理。
### 2.1 登录用户检查
分支:
- 上下文没有当前用户:返回 `InvalidAccess`
- 当前用户存在:继续。
### 2.2 解析订阅权益归属
调用 `ResolveEntitlementUser`
| 场景 | `effective_user_id` | 结果 |
| --- | --- | --- |
| 普通用户 | 本人 ID | 订阅归本人 |
| 家庭组主账号 | 本人 ID | 订阅归本人 |
| 家庭组成员 | 家主 ID | 订阅归家主 |
后续查询已有订阅、quota、创建订单里的 `subscription_user_id` 都会使用这个归属结果。
### 2.3 数量校验
分支:
- `quantity <= 0`:自动改成 `1`
- `quantity > MaxQuantity`:返回参数错误。
- 合法:继续。
### 2.4 单订阅模式路由
先默认:
```text
order_type = 1
target_subscribe_id = req.subscribe_id
parent_order_id = 0
subscribe_token = ""
```
如果开启 `Subscribe.SingleModel`
| 查询结果 | 行为 |
| --- | --- |
| 找到已有 anchor 订阅 | 下单路由为续费:`order_type=2`,保留新请求套餐 ID设置 parent/order token |
| 没找到已有订阅 | 保持新购:`order_type=1` |
| 查询异常 | 返回数据库错误 |
说明:即使是换套餐,只要单订阅模式已有订阅,也走续费语义,后续激活会更新套餐 ID 和流量配置。
### 2.5 非 SingleModel 的全局单订阅兜底
如果未开启 `SingleModel`,且当前还是新购 `order_type=1`
- 查询 `effective_user_id` 名下已有付费订阅:
```sql
user_id = effective_user_id
AND token != ''
AND (order_id > 0 OR token LIKE 'iap:%')
```
| 查询结果 | 行为 |
| --- | --- |
| 找到已有订阅 | 路由为续费:`order_type=2`,用已有订阅 token |
| 没找到 | 仍然新购 |
目的:避免同一个权益归属用户购买不同套餐后出现多条订阅权益。
### 2.6 pending 订单处理
当前只有一个分支会主动关闭旧 pending 单:
```text
SingleModel = true
AND order_type = 1
AND 存在同 user_id + subscribe_id + status=1 的订单
```
行为:
- 关闭旧 pending 订单。
- 继续创建新订单。
注意:
- 如果订单已被路由为 `order_type=2`,这里不会关闭旧 pending。
-`SingleModel` 下也不会走这段 pending 关闭逻辑。
### 2.7 套餐校验
分支:
| 条件 | 行为 |
| --- | --- |
| 套餐不存在 | 返回数据库错误 |
| `sell=false` | 返回套餐不可售 |
| 新购且库存为 `0` | 返回库存不足 |
| 续费/换套餐 | 不检查库存为 0 的拦截分支 |
### 2.8 新用户优惠与新用户限定
调用 `resolveNewUserDiscountEligibility`
| 分支 | 行为 |
| --- | --- |
| 解析失败 | 返回错误 |
| 有折扣且符合条件 | 按折扣计算金额 |
| 有折扣但不符合条件 | 按原价 |
| 新用户限定且不是新用户窗口 | 新购事务内再次校验,不通过则失败 |
### 2.9 优惠券逻辑
如果 `req.coupon` 为空:跳过。
如果不为空:
| 校验 | 不通过行为 |
| --- | --- |
| 优惠券存在 | 返回 `CouponNotExist` |
| 总使用次数未超限 | 返回使用次数不足 |
| 用户使用次数未超限 | 返回用户次数不足 |
| 套餐适用 | 返回不适用 |
通过后计算 `coupon_discount`,从订单金额中扣除。
### 2.10 支付手续费与礼品余额抵扣
流程:
1. 找支付方式。
2. 如果金额大于 0计算手续费并加到订单金额。
3. 如果用户 `gift_amount > 0`,继续抵扣订单金额。
4. 抵扣金额记录到订单 `gift_amount`
事务内如果有礼品余额抵扣:
- 扣减用户 `gift_amount`
-`system_logs` 的 gift reduce 日志。
### 2.11 `is_new` 首单标记
创建订单前调用:
```sql
SELECT COUNT(*)
FROM `order`
WHERE user_id = 当前付款用户
AND status IN (2, 5)
```
| 结果 | `is_new` |
| --- | --- |
| count = 0 | `true` |
| count > 0 | `false` |
注意:
- 判断口径是付款用户 `user_id`,不是 `subscription_user_id`
- pending/closed 订单不影响 `is_new`
- 续费订单也可能是 `is_new=true`,例如单订阅模式下首次购买被路由为续费。
### 2.12 事务内创建订单
事务内执行:
1. 新购且套餐有 quota 时,再查一次 `effective_user_id` 名下订阅数量防并发。
2. 新购且新用户限定时,再查一次新用户资格。
3. 如有礼品余额抵扣,扣减余额并写日志。
4. 新购且库存不是 `-1` 时扣库存。
5. 插入订单。
订单核心字段:
| 字段 | 值 |
| --- | --- |
| `user_id` | 当前付款用户 |
| `subscription_user_id` | 权益归属用户 |
| `type` | 新购 `1` 或续费 `2` |
| `subscribe_id` | 本次购买的套餐 ID |
| `subscribe_token` | 续费时已有订阅 token |
| `is_new` | 首单标记 |
| `status` | `1` pending |
### 2.13 延迟关单任务
订单创建成功后,发送 `DeferCloseOrder` 任务:
- 延迟时间15 分钟。
- 作用:未支付订单自动关闭。
## 3. 订单支付后的激活逻辑
入口:`ActivateOrderLogic.ProcessTask`
### 3.1 任务解析和订单领取
分支:
| 分支 | 行为 |
| --- | --- |
| payload 解析失败 | 记录错误,不重试 |
| 订单不存在 | 返回错误,允许重试 |
| 订单已 finished | 幂等跳过 |
| 订单不是 paid | 跳过 |
| paid 订单 | 原子更新为 claimed 状态后处理 |
### 3.2 按订单类型分发
| 订单类型 | 处理函数 |
| --- | --- |
| 新购 `1` | `NewPurchase` |
| 续费 `2` | `Renewal` |
| 重置流量 `3` | `ResetTraffic` |
| 充值 `4` | `Recharge` |
| 兑换码 `5` | `RedemptionActivate` |
处理成功后:
1. 执行订阅合并兜底 `reconcilePostOrderSubscriptions`
2. 更新优惠券使用次数。
3. 更新订单为 `finished`
如果处理失败:
- 把订单从 claimed 释放回 paid。
- 返回错误给队列重试。
## 4. 新购订单激活与订阅发放
入口:`NewPurchase`
### 4.1 获取用户
分支:
| 订单 user_id | 行为 |
| --- | --- |
| 不为 0 | 查询已有用户 |
| 为 0 | 从 Redis 临时订单创建游客用户 |
游客订单创建用户时:
- 创建用户和 auth method。
- 生成 refer code。
- 把订单 `user_id` 更新为新用户 ID。
- 如果临时订单有邀请码,绑定 `referer_id`
### 4.2 新用户限定激活时复查
如果套餐是新用户限定,激活时再次校验:
- 不符合则激活失败,订单回到 paid 等待重试/处理。
- 符合继续。
### 4.3 SingleModel 下复用 anchor 订阅
如果 `Subscribe.SingleModel=true`
| 分支 | 行为 |
| --- | --- |
| 找到 anchor 订阅 | 更新订单 parent_id用续费逻辑延长/换套餐 |
| 找不到 | 继续后续分支 |
| 查询异常 | 记录错误,继续后续分支 |
家庭组场景下查 anchor 的用户 ID
- 优先 `subscription_user_id`
- 没有则用 `user_id`
### 4.4 复用赠送订阅
如果还没有可复用订阅:
- 查找权益归属用户名下 `order_id=0` 的赠送订阅。
- 找到后将它升级为付费订阅:
- `order_id` 改为当前订单 ID。
- 延长到期时间。
- 状态改为 active。
- 如果套餐变更,更新套餐 ID、流量额度并清空已用流量。
### 4.5 兜底复用已有订阅
如果仍未复用到订阅:
- 候选用户 ID
- `order.user_id`
- 如果 `subscription_user_id` 存在且不同,也加入候选。
- 查找这些用户名下 `token != ''` 的订阅。
找到后:
- 如果订阅 owner 不是当前 `subscription_user_id`,先把 `user_id` 修正为权益归属用户。
- 用续费逻辑延长/换套餐。
目的:家庭组绑定前后 owner 变化时,也尽量复用旧记录,避免创建重复订阅。
### 4.6 创建新订阅
如果以上都没有复用成功,才创建新 `user_subscribe`
| 字段 | 值 |
| --- | --- |
| `user_id` | `subscription_user_id`,没有则 `order.user_id` |
| `order_id` | 当前订单 ID |
| `subscribe_id` | 当前订单套餐 ID |
| `start_time` | 当前时间 |
| `expire_time` | 按套餐时间单位和数量计算 |
| `traffic` | 套餐流量 |
| `token` | 基于订单号生成 |
| `uuid` | 新 UUID |
| `status` | `1` active |
创建前如果套餐有 quota会再按订阅 owner 统计数量。
### 4.7 新购激活后的异步逻辑
订阅发放后:
1. 后台触发用户分组重算。
2. 后台异步处理邀请佣金和赠送时间。
3. 清套餐缓存。
注意:邀请逻辑在 goroutine 中执行,不阻塞订单激活。
## 5. 续费/换套餐激活逻辑
入口:`Renewal`
### 5.1 获取用户和订阅
- 查询订单 `user_id` 对应用户。
- 通过 `subscribe_token` 查订阅。
- 查询订单 `subscribe_id` 对应套餐。
### 5.2 Apple IAP 与普通续费分支
| 分支 | 行为 |
| --- | --- |
| `iap_expire_at > 0` | 使用 IAP 到期时间兜底,但仍按累计加时语义 |
| 普通续费 | `updateSubscriptionForRenewal` |
### 5.3 普通续费/换套餐规则
`updateSubscriptionForRenewal`
- 如果当前订阅已过期,先把基准时间改为现在。
- 如果套餐 ID 变化:
- 更新订阅套餐 ID。
- 更新流量额度。
- 清空已用流量。
- 如果套餐没变:
- 如果套餐设置 renewal reset或今天是重置日则清空已用流量。
- 清理 `finished_at`
- `order_id` 改为当前订单 ID。
- 按套餐时间单位和数量延长到期时间。
- 状态改为 active。
- 清空过期流量字段。
### 5.4 续费后的邀请逻辑
续费成功后也会调用 `handleCommission`
- 是否发佣金由邀请配置和 `order.is_new` 决定。
- 是否赠时同样由邀请配置和 `order.is_new` 决定。
注意:如果 `OnlyFirstPurchase=true`,非首单续费通常不会发佣金,也不会赠首单时间。
## 6. 邀请关系绑定逻辑
### 6.1 注册/登录时的邀请码
新用户注册、游客订单创建用户时,如果带邀请码:
- 根据邀请码查邀请人。
- 设置新用户 `referer_id = 邀请人 ID`
### 6.2 用户后绑邀请码
入口:`BindInviteCode`
分支:
| 分支 | 行为 |
| --- | --- |
| 当前用户不存在 | 返回无权限 |
| 当前用户已有 `referer_id` | 返回已绑定 |
| 邀请码不存在 | 返回邀请码错误 |
| 邀请码属于自己 | 返回不允许绑定自己 |
| 通过 | 更新当前用户 `referer_id` |
注意:
- `referer_id` 始终记录实际邀请码所有者。
- 邀请人是家庭成员时,`referer_id` 仍然是该成员 ID不自动改为家主 ID。
## 7. 邀请佣金与赠送时间逻辑
入口:`handleCommission(userInfo, orderInfo)`
这里的 `userInfo` 是订单付款用户,也就是被邀请人。
### 7.1 总入口分支
先调用 `shouldProcessCommission(userInfo, orderInfo.IsNew)`
| 结果 | 行为 |
| --- | --- |
| `false` | 不发佣金;如果 `is_new=true`,走双方赠时 |
| `true` | 发佣金;如果 `is_new=true`,被邀请人赠时 |
### 7.2 什么时候发佣金
`shouldProcessCommission` 规则:
| 条件 | 结果 |
| --- | --- |
| 被邀请人为空 | 不发 |
| 被邀请人 `referer_id=0` | 不发 |
| 查不到邀请人 | 不发 |
| 邀请人自定义 `referral_percentage > 0`,且只首购但不是首单 | 不发 |
| 邀请人自定义 `referral_percentage > 0`,且通过首购限制 | 发佣金 |
| 邀请人无自定义比例,系统 `ReferralPercentage=0` | 不发 |
| 系统 `OnlyFirstPurchase=true` 且不是首单 | 不发 |
| 系统有比例且通过首购限制 | 发佣金 |
### 7.3 发佣金路径
如果 `shouldProcessCommission=true`
1. 查询邀请人,也就是 `userInfo.referer_id` 对应用户。
2. 佣金比例:
- 邀请人自定义比例优先。
- 否则用系统配置 `Invite.ReferralPercentage`
3. 佣金金额:
```text
(order.amount - order.fee_amount) * referral_percentage / 100
```
4. 事务内幂等检查:
- 如果已有同订单佣金日志,则跳过。
- 否则增加邀请人的 `commission`
-`system_logs type=33` 佣金日志。
5. 更新邀请人缓存。
6. 如果 `order.is_new=true`
- 给被邀请人赠送订阅时间。
当前保持不变的行为:
- 邀请人是家庭成员时,佣金仍然给实际邀请人成员本人。
- 佣金不归并到家主。
- 有佣金路径下,邀请人不额外赠送订阅时间。
### 7.4 不发佣金路径
如果 `shouldProcessCommission=false`
| `order.is_new` | 行为 |
| --- | --- |
| `true` | 被邀请人和邀请人双方赠送订阅时间 |
| `false` | 不赠送时间 |
双方赠时具体为:
1. 被邀请人赠时:
- 如果被邀请人是家庭成员,加到被邀请人家主套餐。
- 否则加到被邀请人本人套餐。
2. 邀请人赠时:
- 如果邀请人是家庭成员,加到邀请人家主套餐。
- 否则加到邀请人本人套餐。
## 8. 赠送时间目标解析
入口:`resolveGiftTargetUser(source, forcedOwnerID)`
### 8.1 强制 owner 分支
如果 `forcedOwnerID > 0`
- 赠送目标直接使用 `forcedOwnerID`
- 典型场景:订单里已有 `subscription_user_id`
- 这保证了家庭成员购买时,被邀请人的赠时落到家主。
### 8.2 自动家庭组解析分支
如果没有强制 owner
- 调用 `ResolveEntitlementUser(source.Id)`
- 如果 source 是有效家庭成员,目标改为家主。
- 否则目标为本人。
典型场景:
- 无佣金路径下,邀请人也赠时。
- 邀请人如果是家庭成员,赠时会加到邀请人家主套餐。
### 8.3 目标用户查询失败
如果解析出来的目标用户查不到:
- 记录错误日志。
- 回退为 source 本人。
## 9. 赠送时间落库逻辑
入口:`grantGiftDays(u, days, orderNo, remark)`
### 9.1 空值和配置分支
| 条件 | 行为 |
| --- | --- |
| 目标用户为空 | 直接返回,不写日志 |
| `days <= 0` | 直接返回,不写日志 |
### 9.2 幂等检查
按下面条件查 gift 日志:
```sql
type = 34
AND object_id = 目标用户 ID
AND content LIKE '%订单号%'
```
| 结果 | 行为 |
| --- | --- |
| 已存在 | 跳过,不重复赠时 |
| 不存在 | 继续 |
### 9.3 查目标用户活跃订阅
调用 `FindActiveSubscribe`
当前活跃口径:
```sql
user_id = 目标用户 ID
AND status IN (0, 1)
AND (
expire_time > NOW()
OR expire_time = FROM_UNIXTIME(0)
)
```
说明:
- `expire_time > NOW()` 是普通未过期订阅。
- `expire_time = FROM_UNIXTIME(0)` 是永久/不限时订阅。
### 9.4 没有活跃订阅
如果查不到活跃订阅:
- 不创建新订阅。
- 写一条 `system_logs type=34` 日志。
- 日志 remark 为:
```text
邀请赠送 skipped: no active subscription
```
这表示邀请赠时触发过,但目标用户当时没有可加时的套餐。
### 9.5 找到普通活跃订阅
如果目标订阅不是永久订阅:
- `expire_time += days * 24h`
- 更新订阅。
-`system_logs type=34` gift increase 日志。
### 9.6 找到永久订阅
如果目标订阅 `expire_time = FROM_UNIXTIME(0)`
- 不改变 `expire_time`,因为永久订阅没有可延长的到期时间。
- 仍写 `system_logs type=34` gift increase 日志,表示赠送逻辑已识别并处理。
### 9.7 赠时失败日志
发佣金路径和无佣金路径都会检查 `grantGiftDays` 返回错误。
如果出错,会写应用日志:
```text
Grant invite gift days failed
```
附带字段:
- `stage`
- `target_user_id`
- `order_no`
- `error`
## 10. 家庭组下的完整分支示例
### 10.1 被邀请人是普通用户,邀请人普通用户,有佣金
条件:
- 被邀请人 `referer_id != 0`
- 系统或邀请人佣金比例大于 0
- `order.is_new=true`
结果:
- 佣金给邀请人本人。
- 被邀请人本人套餐加赠送时间。
- 邀请人不加赠送时间。
### 10.2 被邀请人是家庭成员,邀请人普通用户,有佣金
结果:
- 佣金给邀请人本人。
- 被邀请人的赠送时间加到被邀请人家主套餐。
- 被邀请人成员本人不单独加订阅时间。
### 10.3 被邀请人普通用户,邀请人是家庭成员,有佣金
结果:
- 佣金给邀请人成员本人。
- 被邀请人本人套餐加赠送时间。
- 邀请人不加赠送时间。
- 邀请人家主不拿佣金,也不因该佣金路径加赠时。
### 10.4 被邀请人是家庭成员,邀请人也是家庭成员,有佣金
结果:
- 佣金给邀请人成员本人。
- 被邀请人的赠送时间加到被邀请人家主套餐。
- 邀请人不加赠送时间。
- 邀请人家主不拿佣金。
### 10.5 无佣金路径,被邀请人普通用户,邀请人普通用户
触发条件示例:
- `ReferralPercentage=0`
- 或因首购限制导致不发佣金
-`order.is_new=true`
结果:
- 被邀请人本人套餐加赠送时间。
- 邀请人本人套餐加赠送时间。
### 10.6 无佣金路径,被邀请人是家庭成员
结果:
- 被邀请人的赠送时间加到被邀请人家主套餐。
- 邀请人的赠时按邀请人自己的家庭归属解析。
### 10.7 无佣金路径,邀请人是家庭成员
结果:
- 被邀请人的赠时按被邀请人的家庭归属解析。
- 邀请人的赠送时间加到邀请人家主套餐。
- 邀请人成员本人不单独加订阅时间。
### 10.8 被邀请人没有活跃订阅
结果:
- 不创建新订阅。
- 写 skipped gift 日志。
- 后续即使用户后来有订阅,也不会自动补赠,除非另行补偿。
### 10.9 被邀请人或目标家主是永久订阅
结果:
- 识别为活跃订阅。
- 不改变到期时间。
- 写 gift increase 日志。
## 11. 排查 SQL
### 11.1 查邀请配置
```sql
SELECT `key`, `value`, `updated_at`
FROM system
WHERE category = 'invite'
AND `key` IN ('GiftDays', 'OnlyFirstPurchase', 'ReferralPercentage');
```
### 11.2 查某邀请人的被邀请用户
```sql
SELECT id, referer_id, created_at
FROM `user`
WHERE referer_id = 23944
ORDER BY id DESC
LIMIT 100;
```
### 11.3 查被邀请人的订单和首单标记
```sql
SELECT u.id AS invited_user_id,
o.id AS order_id,
o.order_no,
o.type,
o.status,
o.amount,
o.is_new,
o.subscribe_id,
o.subscription_user_id,
o.created_at
FROM `user` u
LEFT JOIN `order` o
ON o.user_id = u.id
AND o.type IN (1, 2)
WHERE u.referer_id = 23944
ORDER BY u.id DESC, o.id ASC
LIMIT 200;
```
### 11.4 查某订单佣金和赠时日志
```sql
SELECT id, type, object_id, content, created_at
FROM system_logs
WHERE content LIKE '%202604281812556044982351822%'
ORDER BY id DESC;
```
### 11.5 查某用户订阅
```sql
SELECT id, user_id, order_id, subscribe_id, status,
expire_time, finished_at, token, created_at, updated_at
FROM user_subscribe
WHERE user_id = 24425
ORDER BY id DESC;
```
### 11.6 查首单但没有赠时日志的被邀请人
```sql
SELECT first_orders.user_id AS invited_user_id,
first_orders.order_no,
first_orders.is_new,
first_orders.status,
first_orders.subscription_user_id,
first_orders.created_at,
(
SELECT COUNT(*)
FROM system_logs sl
WHERE sl.type = 34
AND sl.content LIKE CONCAT('%', first_orders.order_no, '%')
) AS gift_log_count,
(
SELECT COUNT(*)
FROM system_logs sl
WHERE sl.type = 33
AND sl.content LIKE CONCAT('%', first_orders.order_no, '%')
) AS commission_log_count
FROM (
SELECT o.*
FROM `order` o
JOIN (
SELECT user_id, MIN(id) AS first_order_id
FROM `order`
WHERE type IN (1, 2)
AND status IN (2, 5)
GROUP BY user_id
) fo ON fo.first_order_id = o.id
) first_orders
JOIN `user` u ON u.id = first_orders.user_id
WHERE u.referer_id = 23944
ORDER BY first_orders.created_at DESC
LIMIT 100;
```
## 12. 部署注意事项
邀请配置存在两层状态:
1. Redis 缓存:`system:invite_config`
2. 服务进程内存:`svc.Config.Invite`
如果直接修改数据库或 Redis已经运行的 `ppanel-server` 进程不会自动刷新内存配置。订单激活和赠时发生在服务进程/队列 worker 内,所以修改邀请配置或部署赠时逻辑后,需要重启服务。
推荐步骤:
```bash
docker exec ppanel-redis redis-cli DEL system:invite_config system:global_config
docker restart ppanel-server
```
确认启动时间:
```bash
docker inspect --format '{{.Name}} {{.State.StartedAt}} {{.Config.Image}}' ppanel-server
docker ps --filter name=ppanel-server
```