Files
junhong_cmp_fiber/docs/需求规划/账号与佣金管理模块需求规划.md
huang 91c9bbfeb8
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 4m35s
feat: 实现账号与佣金管理模块
新增功能:
- 店铺佣金查询:店铺佣金统计、店铺佣金记录列表、店铺提现记录
- 佣金提现审批:提现申请列表、审批通过、审批拒绝
- 提现配置管理:配置列表、新增配置、获取当前生效配置
- 企业管理:企业列表、创建、更新、删除、获取详情
- 企业卡授权:授权列表、批量授权、批量取消授权、统计
- 客户账号管理:账号列表、创建、更新状态、重置密码
- 我的佣金:佣金统计、佣金记录、提现申请、提现记录

数据库变更:
- 扩展 tb_commission_withdrawal_request 新增提现单号等字段
- 扩展 tb_account 新增 is_primary 字段
- 扩展 tb_commission_record 新增 shop_id、balance_after
- 扩展 tb_commission_withdrawal_setting 新增每日提现次数限制
- 扩展 tb_iot_card、tb_device 新增 shop_id 冗余字段
- 新建 tb_enterprise_card_authorization 企业卡授权表
- 新建 tb_asset_allocation_record 资产分配记录表
- 数据迁移:owner_type 枚举值 agent 统一为 shop

测试:
- 新增 7 个单元测试文件覆盖各服务
- 修复集成测试 Redis 依赖问题
2026-01-21 18:20:44 +08:00

54 KiB
Raw Permalink Blame History

账号与佣金管理模块需求规划

文档版本v1.2 创建时间2026-01-21 更新时间2026-01-21 状态: 已确认所有核心问题


一、总体概述

1.1 模块范围

本次需求涵盖以下功能模块:

模块 说明
账号管理-代理商(店铺)管理 查看代理商佣金信息、佣金提现记录、佣金明细
账号管理-佣金提现 佣金提现申请审批流程(放款/拒绝)
佣金提现设置 全局提现规则配置(次数、金额、手续费)
账号管理-企业客户管理 企业CRUD、卡分配/回收、启用禁用
账号管理-客户账号管理 代理商+企业客户的账号统一管理
财务-我的账号 当前登录账号的佣金数据查询

1.2 核心概念说明

概念 系统模型 说明
代理商/店铺 Shop 同一概念,代码中使用 Shop
代理账号 Account (UserType=3) 店铺下的员工账号
店铺主账号 Account (is_primary=true) 创建店铺时同步创建的账号,每个店铺有且仅有一个
企业客户 Enterprise B端企业客户
企业账号 Account (UserType=4) 企业的登录账号
佣金钱包 Wallet (WalletType=commission) 店铺级别的佣金钱包

1.3 数据权限规则

角色 数据可见范围
平台用户 全部数据
代理商用户 自己店铺 + 下级店铺 + 归属的企业客户数据
企业用户 仅自己企业数据

二、数据模型变更

2.1 需要新增的字段

2.1.1 tb_commission_withdrawal_request 表新增字段

字段名 类型 说明 是否必填
withdrawal_no varchar(50) 提现单号唯一标识格式W + 时间戳 + 随机数)
applicant_id uint 申请人账号ID店铺下哪个账号提交的
shop_id uint 店铺ID冗余字段方便查询
fee_rate int64 手续费比率基点100=1%,记录申请时的费率快照)
payment_type varchar(20) 放款类型manual=人工打款)
processor_id uint 处理人ID审批/放款人)
processed_at timestamp 处理时间
remark text 备注

2.1.2 tb_account 表新增字段

字段名 类型 说明 是否必填
is_primary boolean 是否为店铺主账号(创建店铺时同步创建的账号) 默认false

说明

  • 每个店铺有且仅有一个主账号(is_primary=true
  • 创建店铺时自动创建的账号设置为主账号
  • 主账号不可删除,只能禁用
  • 未来销售系统会通过账号关联佣金归属

2.1.3 tb_commission_record 表新增字段

字段名 类型 说明 是否必填
shop_id uint 店铺ID冗余字段佣金主要跟着店铺走

说明

  • 佣金本质上跟着店铺走
  • 同时保留 agent_id账号ID未来可支持查看某销售的佣金情况
  • 佣金明细查询主要基于 shop_id

2.1.4 tb_commission_withdrawal_setting 表新增字段

字段名 类型 说明 是否必填
daily_withdrawal_limit int 每日提现次数限制(每个代理商)

2.1.5 tb_iot_card 表新增字段

字段名 类型 说明 是否必填
shop_id uint 店铺ID冗余字段owner_type=shop时等于owner_id

2.1.6 tb_device 表新增字段

字段名 类型 说明 是否必填
shop_id uint 店铺ID冗余字段owner_type=shop时等于owner_id

2.1.7 tb_commission_record 表新增字段(补充)

字段名 类型 说明 是否必填
balance_after int64 入账后佣金余额(分),创建时计算:本次佣金 + 历史累计佣金

2.1.8 tb_iot_cardtb_deviceowner_type 统一

当前值platform, agent, user, device

统一为platform, shop

旧值 新值 说明
platform platform 平台库存(不变)
agent shop 代理商持有(统一命名)
user 废弃 不再使用
device 废弃 不再使用

核心设计

  • 卡/设备只在平台和代理商之间流转
  • owner_type=shop + owner_id=店铺ID 表示代理商持有
  • 企业看到的卡是通过"授权"实现的,不改变归属

2.2 需要新增的表

2.2.1 tb_enterprise_card_authorization 企业-卡授权表(核心)

用于记录企业被授权可见的卡。这是企业查看卡的唯一途径,不改变卡的归属

CREATE TABLE tb_enterprise_card_authorization (
    id BIGSERIAL PRIMARY KEY,
    enterprise_id BIGINT NOT NULL,                       -- 企业ID
    iot_card_id BIGINT NOT NULL,                         -- 卡ID
    shop_id BIGINT NOT NULL,                             -- 卡所属店铺ID冗余方便查询和权限校验
    authorized_by BIGINT NOT NULL,                       -- 授权人账号ID
    authorized_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),-- 授权时间
    status INT DEFAULT 1,                                -- 1=有效, 0=已回收
    
    creator BIGINT,
    updater BIGINT,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    deleted_at TIMESTAMP WITH TIME ZONE,
    
    CONSTRAINT uk_enterprise_card UNIQUE(enterprise_id, iot_card_id)
);

CREATE INDEX idx_eca_enterprise ON tb_enterprise_card_authorization(enterprise_id, status) WHERE deleted_at IS NULL;
CREATE INDEX idx_eca_card ON tb_enterprise_card_authorization(iot_card_id) WHERE deleted_at IS NULL;
CREATE INDEX idx_eca_shop ON tb_enterprise_card_authorization(shop_id) WHERE deleted_at IS NULL;

核心设计说明

  • 卡的归属owner始终是代理商店铺不会变成企业
  • 企业通过授权表"看到"被授权的卡
  • 授权是永久的,回收时更新 status=0
  • 设备通过卡的授权间接可见(不需要单独的设备授权表)

2.2.2 tb_asset_allocation_record 资产分配记录表

用于记录卡/设备在平台和代理商之间流转的历史。

CREATE TABLE tb_asset_allocation_record (
    id BIGSERIAL PRIMARY KEY,
    allocation_no VARCHAR(50) NOT NULL UNIQUE,           -- 分配单号
    allocation_type VARCHAR(20) NOT NULL,                -- 分配类型allocate=分配, recall=回收
    asset_type VARCHAR(20) NOT NULL,                     -- 资产类型iot_card, device
    asset_id BIGINT NOT NULL,                            -- 资产ID
    asset_identifier VARCHAR(50) NOT NULL,               -- 资产标识ICCID或设备号方便查询
    
    from_owner_type VARCHAR(20),                         -- 原归属类型
    from_owner_id BIGINT,                                -- 原归属ID
    to_owner_type VARCHAR(20) NOT NULL,                  -- 目标归属类型platform/shop
    to_owner_id BIGINT NOT NULL,                         -- 目标归属ID
    
    related_device_id BIGINT,                            -- 关联设备ID卡分配时如果绑定了设备
    related_card_ids JSONB,                              -- 关联卡ID列表设备分配时包含的卡
    
    operator_id BIGINT NOT NULL,                         -- 操作人ID
    remark TEXT,                                         -- 备注
    
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    deleted_at TIMESTAMP WITH TIME ZONE
);

CREATE INDEX idx_asset_allocation_asset ON tb_asset_allocation_record(asset_type, asset_id);
CREATE INDEX idx_asset_allocation_to_owner ON tb_asset_allocation_record(to_owner_type, to_owner_id);
CREATE INDEX idx_asset_allocation_created ON tb_asset_allocation_record(created_at);

2.3 卡/设备归属与授权体系(已确认)

2.3.1 核心设计原则

┌─────────────────────────────────────────────────────────────────┐
│                    归属 vs 授权 - 核心区别                        │
└─────────────────────────────────────────────────────────────────┘

【归属流转】改变 owner_type/owner_id涉及真实的资产转移
    平台 (platform) ──分配──→ 代理商 (shop)

【授权可见】不改变归属,只是让企业"能看到"
    代理商的卡 ──授权──→ 企业可见(通过授权表)

owner_type 只有两种值

  • platform - 平台库存
  • shop - 代理商持有

企业不拥有卡

  • 企业看到的卡仍然属于代理商
  • 企业通过 tb_enterprise_card_authorization 表获得可见性
  • 授权是永久的,回收时更新状态

2.3.2 数据权限过滤修改

需要修改 pkg/gorm/callback.go,对 tb_iot_card 表做特殊处理:

// 企业用户查询 IotCard 时的特殊处理
if userType == constants.UserTypeEnterprise && tableName == "tb_iot_card" {
    enterpriseID := middleware.GetEnterpriseIDFromContext(ctx)
    if enterpriseID != 0 {
        // 企业用户只能看到被授权的卡
        tx.Where("id IN (SELECT iot_card_id FROM tb_enterprise_card_authorization WHERE enterprise_id = ? AND status = 1 AND deleted_at IS NULL)", enterpriseID)
    } else {
        tx.Where("1 = 0")
    }
    return
}

// 代理用户查询 IotCard 时,使用 shop_id 字段过滤
if userType == constants.UserTypeAgent && tableName == "tb_iot_card" {
    // 使用新增的 shop_id 冗余字段
    tx.Where("shop_id IN ?", subordinateShopIDs)
    return
}

2.3.3 分配/授权规则

场景 操作 说明
平台 → 代理商 修改 owner_type=shop, owner_id=shop_id, shop_id=shop_id 真实的归属转移
代理商 → 企业 创建 tb_enterprise_card_authorization 记录 只是授权可见,不改变归属
企业 → 代理商回收 更新授权表 status=0 取消授权
代理商 → 平台回收 修改 owner_type=platform, shop_id=NULL 同时清理相关授权记录

2.3.4 设备可见性(通过卡间接查询)

企业查询设备时,不直接查设备表,而是:

  1. 查询企业被授权的卡
  2. 通过 DeviceSimBinding 表关联到设备
  3. 返回设备信息
-- 企业可见的设备(通过卡间接查询)
SELECT DISTINCT d.*
FROM tb_device d
INNER JOIN tb_device_sim_binding dsb ON d.id = dsb.device_id AND dsb.bind_status = 1
INNER JOIN tb_enterprise_card_authorization eca ON dsb.iot_card_id = eca.iot_card_id
WHERE eca.enterprise_id = ? AND eca.status = 1 AND eca.deleted_at IS NULL;

2.4 卡/设备分配详细方案(已确认)

2.4.1 分配交互流程

┌─────────────────────────────────────────────────────────────────┐
│                    卡分配给企业流程                              │
└─────────────────────────────────────────────────────────────────┘

用户选择ICCID列表 → 调用预检接口 → 展示分配预览 → 用户确认 → 执行分配
                         │
                         ▼
              ┌─────────────────────┐
              │ 检查每张卡是否绑定设备 │
              └──────────┬──────────┘
                         │
         ┌───────────────┼───────────────┐
         │               │               │
    ┌────▼────┐    ┌─────▼─────┐   ┌─────▼─────┐
    │ 未绑定设备 │    │ 绑定了设备  │   │ 卡不存在   │
    │          │    │           │   │ /无权限    │
    └────┬────┘    └─────┬─────┘   └─────┬─────┘
         │               │               │
         │               ▼               │
         │    ┌─────────────────┐        │
         │    │ 查询设备绑定的   │        │
         │    │ 所有卡列表       │        │
         │    └────────┬────────┘        │
         │             │                 │
         ▼             ▼                 ▼
    ┌─────────────────────────────────────────┐
    │           返回分配预览结果               │
    │ - 可直接分配的卡                         │
    │ - 需要整体分配的设备(含所有卡)          │
    │ - 失败的卡(不存在/无权限)              │
    └─────────────────────────────────────────┘

2.4.2 新增接口:分配预检

接口路径POST /api/admin/enterprises/:id/allocate-cards/preview

接口说明:预检要分配的卡,返回分配预览信息供用户确认

请求参数

参数名 类型 必填 说明
id uint 企业ID路径参数
iccids []string 需要分配的ICCID列表

返回参数

{
  "code": 0,
  "message": "success",
  "data": {
    "standalone_cards": [                         // 可直接分配的卡(未绑定设备)
      {
        "iccid": "89860001234567890123",
        "msisdn": "1440012345678",
        "carrier_name": "中国移动"
      }
    ],
    "device_bundles": [                           // 需要整体分配的设备包
      {
        "device_no": "DEV001",
        "device_name": "测试设备1",
        "trigger_iccid": "89860001234567890124",  // 触发整体分配的卡
        "cards": [                                 // 设备下所有卡
          {
            "iccid": "89860001234567890124",
            "msisdn": "1440012345679",
            "is_trigger": true                    // 是用户选择的卡
          },
          {
            "iccid": "89860001234567890125",
            "msisdn": "1440012345680",
            "is_trigger": false                   // 连带分配的卡
          },
          {
            "iccid": "89860001234567890126",
            "msisdn": "1440012345681",
            "is_trigger": false
          }
        ]
      }
    ],
    "failed_items": [                             // 失败的卡
      {
        "iccid": "89860001234567890127",
        "reason": "卡不存在"
      },
      {
        "iccid": "89860001234567890128",
        "reason": "无权限操作该卡"
      }
    ],
    "summary": {
      "standalone_card_count": 1,
      "device_count": 1,
      "device_card_count": 3,
      "total_card_count": 4,                      // 将要分配的总卡数
      "failed_count": 2
    }
  }
}

前端交互建议

  1. 用户选择ICCID后先调用预检接口
  2. 展示分配预览:
    • "将分配 1 张独立卡"
    • "将分配 1 台设备(含 3 张卡),因为您选择的卡 xxx 绑定在该设备上"
    • "2 张卡分配失败xxx卡不存在、xxx无权限"
  3. 用户确认后,调用正式分配接口

2.4.3 授权确认接口调整

接口路径POST /api/admin/enterprises/:id/allocate-cards

请求参数

参数名 类型 必填 说明
id uint 企业ID路径参数
iccids []string 需要授权的ICCID列表与预检相同
confirm_device_bundles bool 确认整体授权设备下所有卡必须为true才执行

说明

  • 这是"授权"操作,不是"转移归属"
  • 授权后卡仍然属于代理商,企业只是能看到和操作
  • confirm_device_bundles=true 表示用户已确认整体授权设备下所有卡

接口逻辑调整

  1. 验证企业存在且归属于当前代理商(或其下级)
  2. 验证卡属于当前代理商(shop_id 在可见范围内)
  3. 如果卡绑定了设备,获取设备下所有卡一起授权
  4. 创建授权记录tb_enterprise_card_authorization 表(不修改卡的 owner
  5. 返回授权结果

三、接口设计

3.1 账号管理-代理商(店铺)管理

3.1.1 代理商分页列表查询

接口路径GET /api/admin/shops/commission-summary

接口说明:查询代理商列表及其佣金汇总信息

请求参数

参数名 类型 必填 说明
page int 页码默认1
page_size int 每页数量默认20最大100
shop_name string 店铺名称(模糊查询)
username string 代理商账号用户名(模糊查询)

返回参数

{
  "code": 0,
  "message": "success",
  "data": {
    "list": [
      {
        "shop_id": 1,
        "shop_name": "某某代理商",
        "shop_code": "SHOP001",
        "username": "agent001",                    // 店铺主账号用户名
        "phone": "13800138000",                    // 店铺主账号手机号
        "total_commission": 100000,                // 总佣金(分)
        "withdrawn_commission": 50000,             // 已提现佣金(分)
        "unwithdraw_commission": 50000,            // 未提现佣金(分)= 总佣金 - 已提现
        "frozen_commission": 10000,                // 冻结中佣金(分)
        "withdrawing_commission": 5000,            // 提现中佣金(分)= 待审批的提现申请金额
        "available_commission": 35000              // 可提现佣金(分)= 未提现 - 冻结 - 提现中
      }
    ],
    "total": 100,
    "page": 1,
    "page_size": 20
  }
}

接口逻辑

  1. 根据当前用户权限过滤店铺列表(平台看全部,代理看自己+下级)
  2. 查询店铺基本信息
  3. 关联查询店铺的主账号(is_primary=true 的账号)
  4. 计算佣金汇总:
    • total_commission:从 Wallet (shop类型, commission钱包) 的 balance + frozen_balance + 已提现金额
    • withdrawn_commission:从 CommissionWithdrawalRequest 统计 status=2(已通过) 的总金额
    • frozen_commissionWallet.frozen_balance
    • withdrawing_commission:从 CommissionWithdrawalRequest 统计 status=1(待审批) 的总金额
    • available_commissionWallet.balance - withdrawing_commission

主账号说明

  • 每个店铺有且仅有一个主账号(is_primary=true
  • 创建店铺时自动创建的账号即为主账号
  • 这里显示主账号的信息username、phone

3.1.2 佣金提现分页列表(代理商维度)

接口路径GET /api/admin/shops/:shop_id/withdrawal-requests

接口说明:查询指定代理商的佣金提现记录

请求参数

参数名 类型 必填 说明
shop_id uint 店铺ID路径参数
page int 页码默认1
page_size int 每页数量默认20
withdrawal_no string 提现单号(精确查询)
start_time string 申请开始时间ISO8601
end_time string 申请结束时间ISO8601

返回参数

{
  "code": 0,
  "message": "success",
  "data": {
    "list": [
      {
        "id": 1,
        "withdrawal_no": "W20260121143000001",     // 提现单号
        "shop_name": "某某代理商",
        "shop_hierarchy": "上上级代理_上级代理_某某代理商",  // 层级路径:本身往上最多两层(不含平台)
        "applicant_name": "张三",                   // 申请人姓名账号username
        "amount": 10000,                           // 提现金额(分)
        "fee_rate": 100,                           // 手续费比率基点100=1%
        "fee": 100,                                // 手续费金额(分)
        "actual_amount": 9900,                     // 实际到账金额(分)
        "status": 1,                               // 状态1=待审批,2=已通过,3=已拒绝,4=已放款
        "status_name": "待审批",
        "created_at": "2026-01-21T14:30:00+08:00", // 申请时间
        "withdrawal_method": "alipay",             // 收款类型
        "account_name": "张三",                    // 收款人姓名
        "account_number": "zhangsan@alipay.com",   // 支付宝账号
        "payment_type": "manual",                  // 放款类型
        "payment_type_name": "人工打款",
        "processor_name": "管理员",                 // 处理人姓名
        "processed_at": "2026-01-21T15:00:00+08:00", // 处理时间
        "remark": "备注信息"
      }
    ],
    "total": 50,
    "page": 1,
    "page_size": 20
  }
}

接口逻辑

  1. 验证当前用户有权限查看该店铺
  2. 查询 CommissionWithdrawalRequest 表,过滤 shop_id
  3. 关联查询店铺信息、申请人信息、处理人信息
  4. 计算店铺层级路径:
    • 从当前店铺往上最多查两层
    • 格式:上上级_上级_本身(用下划线分隔)
    • 如果只有一层上级:上级_本身
    • 如果是一级代理(无上级):本身
    • 不包含平台

3.1.3 佣金明细分页查询(代理商维度)

接口路径GET /api/admin/shops/:shop_id/commission-records

接口说明:查询指定代理商的佣金入账明细

请求参数

参数名 类型 必填 说明
shop_id uint 店铺ID路径参数
page int 页码默认1
page_size int 每页数量默认20
commission_type string 佣金类型one_time/long_term
iccid string ICCID模糊查询
device_no string 设备号(模糊查询)
order_no string 订单号(模糊查询)

返回参数

{
  "code": 0,
  "message": "success",
  "data": {
    "list": [
      {
        "id": 1,
        "shop_name": "某某代理商",
        "order_no": "ORD20260121001",              // 订单号
        "device_no": "DEV001",                     // 设备号(可能为空)
        "iccid": "89860001234567890123",           // ICCID可能为空
        "order_created_at": "2026-01-20T10:00:00+08:00", // 下单时间
        "commission_type": "one_time",             // 佣金类型
        "commission_type_name": "一次性佣金",
        "amount": 500,                             // 入账佣金(分)
        "balance_after": 10500,                    // 入账后佣金余额(分)
        "status": 3,                               // 状态1=冻结,2=解冻中,3=已发放,4=已失效
        "status_name": "已发放",
        "created_at": "2026-01-21T10:00:00+08:00"  // 佣金记录创建时间
      }
    ],
    "total": 200,
    "page": 1,
    "page_size": 20
  }
}

接口逻辑

  1. 验证当前用户有权限查看该店铺
  2. 查询 CommissionRecord 表,直接通过 shop_id 过滤(已确认新增该字段)
  3. 关联查询 Order 表获取订单号、下单时间
  4. 通过 Order.iot_card_id 关联 IotCard 获取 ICCID
  5. 通过 Order.device_id 关联 Device 获取设备号
  6. balance_after 需要从 WalletTransaction 中获取

数据关联说明

  • CommissionRecord 同时存储 agent_id账号IDshop_id店铺ID
  • 佣金明细查询主要基于 shop_id
  • 保留 agent_id 用于未来支持查看某销售的佣金情况

3.2 账号管理-佣金提现

3.2.1 佣金提现申请分页查询列表

接口路径GET /api/admin/commission/withdrawal-requests

接口说明:查询所有待处理的佣金提现申请(审批列表)

请求参数

参数名 类型 必填 说明
page int 页码默认1
page_size int 每页数量默认20
status int 状态1=待审批,2=已通过,3=已拒绝,4=已放款
withdrawal_no string 提现单号(精确查询)
shop_name string 店铺名称(模糊查询)
start_time string 申请开始时间ISO8601
end_time string 申请结束时间ISO8601

返回参数

{
  "code": 0,
  "message": "success",
  "data": {
    "list": [
      {
        "id": 1,
        "withdrawal_no": "W20260121143000001",
        "shop_id": 5,
        "shop_name": "某某代理商",
        "shop_hierarchy": "上上级代理_上级代理_某某代理商",  // 本身往上最多两层
        "applicant_id": 10,
        "applicant_name": "张三",
        "amount": 10000,
        "fee_rate": 100,
        "fee": 100,
        "actual_amount": 9900,
        "status": 1,
        "status_name": "待审批",
        "created_at": "2026-01-21T14:30:00+08:00",
        "withdrawal_method": "alipay",
        "withdrawal_method_name": "支付宝",
        "account_name": "张三",
        "account_number": "zhangsan@alipay.com",
        "payment_type": "manual",
        "payment_type_name": "人工打款",
        "processor_id": null,
        "processor_name": null,
        "processed_at": null,
        "remark": null
      }
    ],
    "total": 10,
    "page": 1,
    "page_size": 20
  }
}

接口逻辑

  1. 根据当前用户权限过滤数据
  2. 支持多条件组合查询
  3. 按申请时间倒序排列

3.2.2 审批通过

接口路径POST /api/admin/commission/withdrawal-requests/:id/approve

接口说明:审批通过提现申请(实际打款由人工线下完成)

请求参数

参数名 类型 必填 说明
id uint 提现申请ID路径参数
payment_type string 放款类型manual=人工打款
amount int64 修正后的提现金额(分),不传则使用原金额
withdrawal_method string 修正后的收款类型,不传则使用原值
account_name string 修正后的收款人姓名,不传则使用原值
account_number string 修正后的收款账号,不传则使用原值
remark string 备注

返回参数

{
  "code": 0,
  "message": "success",
  "data": {
    "id": 1,
    "withdrawal_no": "W20260121143000001",
    "status": 2,
    "status_name": "已通过",
    "processed_at": "2026-01-21T15:00:00+08:00"
  }
}

接口逻辑

  1. 验证提现申请存在且状态为待审批
  2. 验证当前用户有审批权限
  3. 如果修正了金额,重新计算手续费和实际到账金额
  4. 更新提现申请状态为已通过status=2
  5. 从店铺佣金钱包扣除对应金额(解冻并扣除)
  6. 记录钱包交易流水
  7. 记录处理人和处理时间

审批流程说明

  • 审批只有一步:待审批(1) → 已通过(2) 或 已拒绝(3)
  • 审批通过后,系统自动扣除佣金
  • 实际打款由人工在线下完成(目前只支持人工打款)
  • 状态流转1(待审批) → 2(已通过) → 人工线下打款

3.2.3 拒绝(审批拒绝)

接口路径POST /api/admin/commission/withdrawal-requests/:id/reject

接口说明:拒绝提现申请

请求参数

参数名 类型 必填 说明
id uint 提现申请ID路径参数
remark string 拒绝原因

返回参数

{
  "code": 0,
  "message": "success",
  "data": {
    "id": 1,
    "withdrawal_no": "W20260121143000001",
    "status": 3,
    "status_name": "已拒绝",
    "processed_at": "2026-01-21T15:00:00+08:00"
  }
}

接口逻辑

  1. 验证提现申请存在且状态为待审批
  2. 验证当前用户有审批权限
  3. 更新提现申请状态为已拒绝
  4. 解冻店铺佣金钱包中的冻结金额
  5. 记录钱包交易流水
  6. 记录处理人、处理时间和拒绝原因

3.3 佣金提现设置

3.3.1 新增佣金提现设置

接口路径POST /api/admin/commission/withdrawal-settings

接口说明:新增全局佣金提现配置(新配置生效后旧配置自动失效)

请求参数

参数名 类型 必填 说明
daily_withdrawal_limit int 每日提现次数限制(每个代理商)
min_withdrawal_amount int64 提现最低金额(分)
fee_rate int64 提现手续费比率基点100=1%

返回参数

{
  "code": 0,
  "message": "success",
  "data": {
    "id": 1,
    "daily_withdrawal_limit": 3,
    "min_withdrawal_amount": 10000,
    "fee_rate": 100,
    "is_active": true,
    "creator_name": "管理员",
    "created_at": "2026-01-21T14:30:00+08:00"
  }
}

接口逻辑

  1. 验证当前用户有配置权限(平台用户)
  2. 将当前生效配置的 is_active 设为 false
  3. 创建新配置,is_active 设为 true
  4. 记录创建人

3.3.2 分页查询设置记录

接口路径GET /api/admin/commission/withdrawal-settings

接口说明:查询佣金提现配置历史记录

请求参数

参数名 类型 必填 说明
page int 页码默认1
page_size int 每页数量默认20

返回参数

{
  "code": 0,
  "message": "success",
  "data": {
    "list": [
      {
        "id": 2,
        "daily_withdrawal_limit": 3,
        "min_withdrawal_amount": 10000,
        "fee_rate": 100,
        "is_active": true,
        "creator_id": 1,
        "creator_name": "管理员",
        "created_at": "2026-01-21T14:30:00+08:00"
      },
      {
        "id": 1,
        "daily_withdrawal_limit": 5,
        "min_withdrawal_amount": 5000,
        "fee_rate": 50,
        "is_active": false,
        "creator_id": 1,
        "creator_name": "管理员",
        "created_at": "2026-01-01T10:00:00+08:00"
      }
    ],
    "total": 2,
    "page": 1,
    "page_size": 20
  }
}

接口逻辑

  1. 查询所有配置记录,按创建时间倒序
  2. 关联查询创建人姓名

3.3.3 获取当前生效配置

接口路径GET /api/admin/commission/withdrawal-settings/current

接口说明:获取当前生效的提现配置

返回参数

{
  "code": 0,
  "message": "success",
  "data": {
    "id": 2,
    "daily_withdrawal_limit": 3,
    "min_withdrawal_amount": 10000,
    "fee_rate": 100,
    "is_active": true,
    "creator_name": "管理员",
    "created_at": "2026-01-21T14:30:00+08:00"
  }
}

3.4 账号管理-企业客户管理

3.4.1 新增企业

接口路径POST /api/admin/enterprises

接口说明:创建企业客户,同时自动创建企业账号

请求参数

参数名 类型 必填 说明
owner_shop_id uint 归属代理商ID不填则为平台自营
enterprise_name string 企业名称
enterprise_code string 企业编号(唯一)
legal_person string 法人代表
contact_name string 联系人姓名
contact_phone string 联系人电话
login_phone string 登录手机号(作为企业账号的登录账号)
password string 登录密码
business_license string 营业执照号
province string 省份
city string 城市
district string 区县
address string 详细地址

返回参数

{
  "code": 0,
  "message": "success",
  "data": {
    "enterprise": {
      "id": 1,
      "enterprise_name": "某某企业",
      "enterprise_code": "ENT001",
      "owner_shop_id": 5,
      "owner_shop_name": "某某代理商",
      "legal_person": "李四",
      "contact_name": "王五",
      "contact_phone": "13900139000",
      "business_license": "91110000...",
      "province": "北京市",
      "city": "北京市",
      "district": "朝阳区",
      "address": "某某路123号",
      "status": 1,
      "created_at": "2026-01-21T14:30:00+08:00"
    },
    "account": {
      "id": 10,
      "username": "某某企业",
      "phone": "13800138000",
      "user_type": 4,
      "status": 1
    }
  }
}

接口逻辑

  1. 验证企业编号唯一性
  2. 如果指定 owner_shop_id,验证店铺存在且当前用户有权限
  3. 验证 login_phone 在账号表中不存在
  4. 开启事务:
    • 创建企业记录
    • 创建企业账号UserType=4, EnterpriseID=企业ID, Phone=login_phone, Username=企业名称)
  5. 提交事务

3.4.2 分页查询企业客户

接口路径GET /api/admin/enterprises

接口说明:查询企业客户列表

请求参数

参数名 类型 必填 说明
page int 页码默认1
page_size int 每页数量默认20
enterprise_name string 企业名称(模糊查询)
login_phone string 登录手机号(模糊查询)
contact_phone string 联系人电话(模糊查询)
owner_shop_id uint 归属代理商ID
status int 状态0=禁用,1=启用

返回参数

{
  "code": 0,
  "message": "success",
  "data": {
    "list": [
      {
        "id": 1,
        "enterprise_name": "某某企业",
        "enterprise_code": "ENT001",
        "owner_shop_id": 5,
        "owner_shop_name": "某某代理商",          // NULL时显示"平台自营"
        "contact_name": "王五",
        "contact_phone": "13900139000",
        "login_phone": "13800138000",             // 从关联的Account获取
        "province": "北京市",
        "city": "北京市",
        "district": "朝阳区",
        "address": "某某路123号",
        "status": 1,
        "status_name": "启用",
        "created_at": "2026-01-21T14:30:00+08:00"
      }
    ],
    "total": 50,
    "page": 1,
    "page_size": 20
  }
}

接口逻辑

  1. 根据当前用户权限过滤:
    • 平台用户:看全部
    • 代理商用户:看 owner_shop_id 在自己+下级店铺范围内的企业
  2. 关联查询企业账号获取 login_phone
  3. 关联查询归属店铺名称

3.4.3 编辑企业

接口路径PUT /api/admin/enterprises/:id

接口说明:编辑企业信息(不影响账号)

请求参数

参数名 类型 必填 说明
id uint 企业ID路径参数
owner_shop_id uint 归属代理商ID
enterprise_name string 企业名称
enterprise_code string 企业编号
legal_person string 法人代表
contact_name string 联系人姓名
contact_phone string 联系人电话
business_license string 营业执照号
province string 省份
city string 城市
district string 区县
address string 详细地址

返回参数

{
  "code": 0,
  "message": "success",
  "data": {
    "id": 1,
    "enterprise_name": "某某企业(新)",
    "enterprise_code": "ENT001",
    "updated_at": "2026-01-21T15:00:00+08:00"
  }
}

接口逻辑

  1. 验证企业存在
  2. 验证当前用户有权限编辑该企业
  3. 如果修改了 enterprise_code,验证唯一性
  4. 如果修改了 owner_shop_id,验证店铺存在且当前用户有权限
  5. 更新企业信息
  6. 注意:修改联系人电话不影响账号的登录手机号

3.4.4 分配卡给企业客户

接口路径POST /api/admin/enterprises/:id/allocate-cards

接口说明:将卡分配给企业客户

请求参数

参数名 类型 必填 说明
id uint 企业ID路径参数
iccids []string 需要分配的ICCID列表

返回参数

{
  "code": 0,
  "message": "success",
  "data": {
    "success_count": 10,
    "fail_count": 2,
    "failed_items": [
      {
        "iccid": "89860001234567890123",
        "reason": "卡不存在"
      },
      {
        "iccid": "89860001234567890124",
        "reason": "无权限操作该卡"
      }
    ],
    "allocated_devices": [                        // 因卡分配而连带分配的设备
      {
        "device_no": "DEV001",
        "card_count": 4
      }
    ]
  }
}

接口逻辑

  1. 验证企业存在且当前用户有权限
  2. 遍历ICCID列表
    • 验证卡存在
    • 验证当前用户有权限操作该卡(卡的 owner 在用户可见范围内)
    • 检查卡是否绑定了设备
  3. 如果卡绑定了设备:
    • 将整个设备及其所有绑定的卡一起分配
    • 记录到返回的 allocated_devices
  4. 更新卡/设备的 owner_type=enterprise, owner_id=enterprise_id
  5. 创建分配记录到 tb_asset_allocation_record

⚠️ 待确认

  • 如果卡A绑定在设备X上设备X还绑定了卡B/C/D分配卡A时是否要连带分配整个设备和所有卡
  • 我的理解是:是的,需要整体分配,否则会导致归属关系混乱

3.4.5 从企业客户回收卡授权

接口路径POST /api/admin/enterprises/:id/recall-cards

接口说明:取消企业对卡的授权(卡仍属于代理商)

请求参数

参数名 类型 必填 说明
id uint 企业ID路径参数
iccids []string 需要回收授权的ICCID列表

返回参数

{
  "code": 0,
  "message": "success",
  "data": {
    "success_count": 10,
    "fail_count": 1,
    "failed_items": [
      {
        "iccid": "89860001234567890123",
        "reason": "该卡未授权给此企业"
      }
    ],
    "recalled_devices": [
      {
        "device_no": "DEV001",
        "card_count": 4
      }
    ]
  }
}

接口逻辑

  1. 验证企业存在且当前用户有权限
  2. 遍历ICCID列表
    • 验证卡存在
    • 验证卡已授权给该企业(授权表中有记录且 status=1
    • 检查卡是否绑定了设备
  3. 如果卡绑定了设备,设备下所有卡的授权一起回收
  4. 更新授权记录 status=0(不是删除,不是修改卡的 owner
  5. 卡仍然属于代理商,只是企业不再能看到

3.4.6 企业客户卡分页查询列表

接口路径GET /api/admin/enterprises/:id/cards

接口说明:查询企业被授权可见的卡列表

请求参数

参数名 类型 必填 说明
id uint 企业ID路径参数
page int 页码默认1
page_size int 每页数量默认20
status int 卡状态
carrier_id uint 运营商ID
iccid string ICCID模糊查询
device_no string 设备号(模糊查询)

返回参数

{
  "code": 0,
  "message": "success",
  "data": {
    "list": [
      {
        "id": 1,
        "iccid": "89860001234567890123",
        "msisdn": "1440012345678",                 // 接入号
        "device_id": 10,
        "device_no": "DEV001",                    // 设备号(可能为空)
        "carrier_id": 1,
        "carrier_name": "中国移动",
        "package_id": 5,
        "package_name": "月租套餐30G",             // 当前套餐名称
        "status": 3,
        "status_name": "已激活",
        "network_status": 1,                       // 网络状态0=停机,1=开机
        "network_status_name": "开机"
      }
    ],
    "total": 100,
    "page": 1,
    "page_size": 20
  }
}

接口逻辑

  1. 验证企业存在且当前用户有权限
  2. 通过授权表查询企业被授权的卡:
    SELECT c.* FROM tb_iot_card c
    INNER JOIN tb_enterprise_card_authorization eca 
      ON c.id = eca.iot_card_id
    WHERE eca.enterprise_id = ? AND eca.status = 1 AND eca.deleted_at IS NULL
    
  3. 关联查询设备信息、运营商信息、当前套餐信息

3.4.6.1 企业操作卡 - 停机

接口路径POST /api/admin/enterprises/:id/cards/:card_id/suspend

接口说明:企业对授权卡执行停机操作

请求参数

参数名 类型 必填 说明
id uint 企业ID路径参数
card_id uint 卡ID路径参数

接口逻辑

  1. 验证企业存在
  2. 验证卡已授权给该企业(授权表中有有效记录)
  3. 调用运营商接口执行停机
  4. 更新卡的 network_status = 0

3.4.6.2 企业操作卡 - 复机

接口路径POST /api/admin/enterprises/:id/cards/:card_id/resume

接口说明:企业对授权卡执行复机操作

请求参数

参数名 类型 必填 说明
id uint 企业ID路径参数
card_id uint 卡ID路径参数

接口逻辑

  1. 验证企业存在
  2. 验证卡已授权给该企业
  3. 调用运营商接口执行复机
  4. 更新卡的 network_status = 1

3.4.7 启用/禁用企业

接口路径PUT /api/admin/enterprises/:id/status

接口说明:启用或禁用企业

请求参数

参数名 类型 必填 说明
id uint 企业ID路径参数
status int 状态0=禁用,1=启用

返回参数

{
  "code": 0,
  "message": "success",
  "data": {
    "id": 1,
    "status": 0,
    "status_name": "禁用"
  }
}

接口逻辑

  1. 验证企业存在且当前用户有权限
  2. 更新企业状态
  3. 同步禁用/启用企业关联的账号

3.4.8 修改企业账号密码

接口路径PUT /api/admin/enterprises/:id/password

接口说明:重置企业账号的登录密码

请求参数

参数名 类型 必填 说明
id uint 企业ID路径参数
password string 新密码

返回参数

{
  "code": 0,
  "message": "success",
  "data": {
    "id": 1,
    "enterprise_name": "某某企业"
  }
}

接口逻辑

  1. 验证企业存在且当前用户有权限
  2. 查找企业关联的账号
  3. 更新账号密码bcrypt加密

3.5 账号管理-客户账号管理

说明统一管理代理商账号和企业账号UserType=3或4

3.5.1 分页查询客户账号

接口路径GET /api/admin/customer-accounts

接口说明:查询代理商和企业的账号列表

请求参数

参数名 类型 必填 说明
page int 页码默认1
page_size int 每页数量默认20
shop_id uint 代理商ID筛选该代理商及其下级的账号
username string 账号名称(模糊查询)
status int 账号状态0=禁用,1=启用
user_type int 账号类型3=代理,4=企业

返回参数

{
  "code": 0,
  "message": "success",
  "data": {
    "list": [
      {
        "id": 10,
        "username": "张三",
        "phone": "13800138000",
        "user_type": 3,
        "user_type_name": "代理账号",
        "shop_id": 5,
        "shop_name": "某某代理商",
        "enterprise_id": null,
        "enterprise_name": null,
        "status": 1,
        "status_name": "启用",
        "created_at": "2026-01-21T14:30:00+08:00"
      },
      {
        "id": 11,
        "username": "某某企业",
        "phone": "13900139000",
        "user_type": 4,
        "user_type_name": "企业账号",
        "shop_id": null,
        "shop_name": null,
        "enterprise_id": 1,
        "enterprise_name": "某某企业",
        "status": 1,
        "status_name": "启用",
        "created_at": "2026-01-21T14:30:00+08:00"
      }
    ],
    "total": 100,
    "page": 1,
    "page_size": 20
  }
}

接口逻辑

  1. 过滤条件:user_type IN (3, 4)
  2. 根据当前用户权限过滤:
    • 平台用户:看全部
    • 代理商用户:看自己店铺+下级店铺的代理账号 + 归属企业的账号
  3. 关联查询店铺名称、企业名称

3.5.2 新增客户账号

接口路径POST /api/admin/customer-accounts

接口说明:为代理商新增账号(企业账号通过新增企业时创建)

请求参数

参数名 类型 必填 说明
shop_id uint 代理商ID
username string 账号名称
phone string 登录手机号
password string 登录密码
status int 状态默认1=启用

返回参数

{
  "code": 0,
  "message": "success",
  "data": {
    "id": 12,
    "username": "李四",
    "phone": "13700137000",
    "user_type": 3,
    "shop_id": 5,
    "shop_name": "某某代理商",
    "status": 1,
    "created_at": "2026-01-21T14:30:00+08:00"
  }
}

接口逻辑

  1. 验证店铺存在且当前用户有权限
  2. 验证手机号在账号表中不存在
  3. 创建账号UserType=3, ShopID=shop_id

3.5.3 编辑客户账号

接口路径PUT /api/admin/customer-accounts/:id

接口说明:编辑客户账号信息

请求参数

参数名 类型 必填 说明
id uint 账号ID路径参数
username string 账号名称
phone string 登录手机号

返回参数

{
  "code": 0,
  "message": "success",
  "data": {
    "id": 12,
    "username": "李四(新)",
    "phone": "13700137001",
    "updated_at": "2026-01-21T15:00:00+08:00"
  }
}

接口逻辑

  1. 验证账号存在且类型为代理或企业3或4
  2. 验证当前用户有权限编辑该账号
  3. 如果修改了手机号,验证新手机号不存在
  4. 更新账号信息

3.5.4 修改客户账号密码

接口路径PUT /api/admin/customer-accounts/:id/password

接口说明:重置客户账号密码

请求参数

参数名 类型 必填 说明
id uint 账号ID路径参数
password string 新密码

返回参数

{
  "code": 0,
  "message": "success",
  "data": {
    "id": 12,
    "username": "李四"
  }
}

3.5.5 启用/禁用客户账号

接口路径PUT /api/admin/customer-accounts/:id/status

接口说明:启用或禁用客户账号

请求参数

参数名 类型 必填 说明
id uint 账号ID路径参数
status int 状态0=禁用,1=启用

返回参数

{
  "code": 0,
  "message": "success",
  "data": {
    "id": 12,
    "status": 0,
    "status_name": "禁用"
  }
}

3.6 财务-我的账号(代理商端)

3.6.1 获取当前账号佣金概览

接口路径GET /api/admin/my/commission-summary

接口说明:获取当前登录代理账号所属店铺的佣金汇总

返回参数

{
  "code": 0,
  "message": "success",
  "data": {
    "shop_id": 5,
    "shop_name": "某某代理商",
    "total_commission": 100000,
    "withdrawn_commission": 50000,
    "unwithdraw_commission": 50000,
    "frozen_commission": 10000,
    "withdrawing_commission": 5000,
    "available_commission": 35000
  }
}

接口逻辑

  1. 从当前用户上下文获取 shop_id
  2. 计算佣金汇总逻辑同3.1.1

3.6.2 佣金提现申请

接口路径POST /api/admin/my/withdrawal-requests

接口说明:代理商发起佣金提现申请

请求参数

参数名 类型 必填 说明
amount int64 提现金额(分)
withdrawal_method string 收款类型alipay
account_name string 收款人姓名
account_number string 支付宝账号

返回参数

{
  "code": 0,
  "message": "success",
  "data": {
    "id": 1,
    "withdrawal_no": "W20260121143000001",
    "amount": 10000,
    "fee_rate": 100,
    "fee": 100,
    "actual_amount": 9900,
    "status": 1,
    "status_name": "待审批",
    "created_at": "2026-01-21T14:30:00+08:00"
  }
}

接口逻辑

  1. 从当前用户上下文获取 shop_idaccount_id
  2. 获取当前生效的提现配置
  3. 验证:
    • 提现金额 >= 最低提现金额
    • 可提现余额 >= 提现金额
    • 今日提现次数 < 每日提现次数限制
  4. 计算手续费和实际到账金额
  5. 创建提现申请记录
  6. 冻结店铺佣金钱包中对应金额
  7. 记录钱包交易流水

3.6.3 我的提现记录

接口路径GET /api/admin/my/withdrawal-requests

接口说明:查询当前代理商的提现记录

请求参数

参数名 类型 必填 说明
page int 页码默认1
page_size int 每页数量默认20
status int 状态筛选
start_time string 申请开始时间
end_time string 申请结束时间

返回参数

与 3.2.1 相同,但仅返回当前店铺的数据


3.6.4 我的佣金明细

接口路径GET /api/admin/my/commission-records

接口说明:查询当前代理商的佣金入账明细

请求参数

与 3.1.3 相同

返回参数

与 3.1.3 相同,但仅返回当前店铺的数据


四、接口路径汇总

模块 方法 路径 说明
代理商管理 GET /api/admin/shops/commission-summary 代理商佣金列表
GET /api/admin/shops/:shop_id/withdrawal-requests 代理商提现记录
GET /api/admin/shops/:shop_id/commission-records 代理商佣金明细
佣金提现审批 GET /api/admin/commission/withdrawal-requests 提现申请列表
POST /api/admin/commission/withdrawal-requests/:id/approve 审批通过(人工线下打款)
POST /api/admin/commission/withdrawal-requests/:id/reject 审批拒绝
提现设置 POST /api/admin/commission/withdrawal-settings 新增配置
GET /api/admin/commission/withdrawal-settings 配置列表
GET /api/admin/commission/withdrawal-settings/current 当前配置
企业客户管理 POST /api/admin/enterprises 新增企业
GET /api/admin/enterprises 企业列表
PUT /api/admin/enterprises/:id 编辑企业
PUT /api/admin/enterprises/:id/status 启用禁用
PUT /api/admin/enterprises/:id/password 修改密码
POST /api/admin/enterprises/:id/allocate-cards/preview 授权预检
POST /api/admin/enterprises/:id/allocate-cards 授权卡(确认)
POST /api/admin/enterprises/:id/recall-cards 回收授权
GET /api/admin/enterprises/:id/cards 企业授权卡列表
POST /api/admin/enterprises/:id/cards/:card_id/suspend 企业操作-停机
POST /api/admin/enterprises/:id/cards/:card_id/resume 企业操作-复机
客户账号管理 GET /api/admin/customer-accounts 账号列表
POST /api/admin/customer-accounts 新增账号
PUT /api/admin/customer-accounts/:id 编辑账号
PUT /api/admin/customer-accounts/:id/password 修改密码
PUT /api/admin/customer-accounts/:id/status 启用禁用
财务-我的账号 GET /api/admin/my/commission-summary 我的佣金概览
POST /api/admin/my/withdrawal-requests 发起提现
GET /api/admin/my/withdrawal-requests 我的提现记录
GET /api/admin/my/commission-records 我的佣金明细

五、已确认事项

5.1 核心设计确认

# 问题 确认结果
1 CommissionRecord 存储什么ID 同时存储账号ID和店铺ID。佣金主要跟着店铺走但保留账号ID方便未来查看某销售的佣金。
2 店铺主账号如何定义? 新增 is_primary 字段标记。创建店铺时同步创建的账号就是主账号。
3 卡绑定设备后如何授权? 整个设备及所有卡一起授权。新增预检接口让用户确认。
4 审批流程几步? 只有一步。审批通过后状态变为"已通过",实际打款由人工线下完成。
5 代理商层级路径格式? 本身往上最多两层上上级_上级_本身

5.2 卡/设备归属与授权确认

# 问题 确认结果
6 卡的归属流转范围? 只在平台和代理商之间。企业不拥有卡,只是被授权可见。
7 owner_type 统一? 改为 platform / shop。去掉 agentuserdevice
8 企业看卡的机制? 通过授权表 tb_enterprise_card_authorization,不改变卡的 owner。
9 设备如何可见? 通过卡间接查询。不需要单独的设备授权表。
10 授权有效期? 永久授权。回收时更新 status=0
11 企业能操作什么? 能看、可以停机/复机

5.3 佣金明细确认

# 问题 确认结果
12 "入账后佣金"如何计算? 本次佣金 + 历史累计佣金。在 CommissionRecord 创建时计算并存储 balance_after 字段。

5.4 待确认事项

暂无待确认事项。如有遗漏请补充。


六、后续规划提示

本文档仅涵盖账号和佣金相关功能。以下功能待后续规划:

  • 物联网卡管理ICCID增删改查、状态管理、数据同步
  • 设备管理设备增删改查、SIM绑定管理
  • 号卡管理(虚拟产品管理)
  • 订单管理(套餐订购、支付流程)
  • 数据统计(用量统计、业务报表)

文档结束