feat: 新增文档
All checks were successful
构建并部署前端到生产环境 / build-and-deploy (push) Successful in 1m2s

This commit is contained in:
luo
2026-07-27 16:21:34 +08:00
parent 861ffa371b
commit 6a794d88af
6 changed files with 810 additions and 3 deletions

View File

@@ -1,11 +1,15 @@
import request from '@/utils/request.js';
export const notificationApi = {
getList(page = 1, page_size = 20) {
getList(page = 1, page_size = 20, is_read) {
return request({
url: '/api/c/v1/notifications',
method: 'GET',
data: { page, page_size }
data: {
page,
page_size,
...(is_read === undefined ? {} : { is_read })
}
});
},

View File

@@ -0,0 +1,152 @@
<template>
<view v-if="show" class="notification-popup" @tap.stop>
<view class="notification-popup-card">
<view class="popup-header">
<view class="popup-heading">新通知</view>
<view class="popup-close" role="button" aria-label="关闭" @tap="$emit('close')">×</view>
</view>
<swiper class="notification-swiper" :current="current" @change="$emit('change', $event)">
<swiper-item v-for="item in items" :key="item.id">
<view class="notification-content">
<view class="notification-title">{{ item.title || '业务通知' }}</view>
<view class="notification-time">{{ formatDate(item.created_at) }}</view>
<scroll-view scroll-y class="notification-body">
<text>{{ item.body || '暂无通知内容' }}</text>
</scroll-view>
</view>
</swiper-item>
</swiper>
<view v-if="items.length > 1" class="popup-footer">
<view class="popup-dots">
<view v-for="(_, index) in items" :key="index" class="popup-dot" :class="{ active: index === current }"></view>
</view>
<view class="popup-counter">{{ current + 1 }}/{{ items.length }} 左右滑动查看</view>
</view>
</view>
</view>
</template>
<script setup>
defineProps({
show: Boolean,
items: {
type: Array,
default: () => []
},
current: {
type: Number,
default: 0
}
});
defineEmits(['close', 'change']);
const formatDate = (value) => {
if (!value) return '-';
return value.replace('T', ' ').slice(0, 16);
};
</script>
<style scoped lang="scss">
.notification-popup {
position: fixed;
z-index: 1000;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
padding: 40rpx;
background: rgba(0, 0, 0, 0.48);
}
.notification-popup-card {
width: 100%;
max-width: 640rpx;
overflow: hidden;
border-radius: 24rpx;
background: #fff;
box-shadow: 0 20rpx 60rpx rgba(0, 0, 0, 0.18);
}
.popup-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 30rpx 32rpx 18rpx;
}
.popup-heading {
color: var(--text-primary);
font-size: 34rpx;
font-weight: 600;
}
.popup-close {
width: 52rpx;
height: 52rpx;
color: var(--text-tertiary);
font-size: 48rpx;
font-weight: 300;
line-height: 46rpx;
text-align: center;
}
.notification-swiper { height: 360rpx; }
.notification-content {
height: 100%;
box-sizing: border-box;
padding: 18rpx 32rpx 28rpx;
}
.notification-title {
color: var(--text-primary);
font-size: 32rpx;
font-weight: 600;
line-height: 1.45;
}
.notification-time {
margin-top: 12rpx;
color: var(--text-tertiary);
font-size: 22rpx;
}
.notification-body {
box-sizing: border-box;
height: 230rpx;
margin-top: 28rpx;
color: var(--text-secondary);
font-size: 28rpx;
line-height: 1.7;
white-space: pre-wrap;
}
.popup-footer {
padding: 0 32rpx 28rpx;
text-align: center;
}
.popup-dots {
display: flex;
justify-content: center;
gap: 10rpx;
}
.popup-dot {
width: 12rpx;
height: 12rpx;
border-radius: 50%;
background: #d8dce3;
}
.popup-dot.active { width: 28rpx; border-radius: 8rpx; background: var(--primary); }
.popup-counter {
margin-top: 16rpx;
color: var(--text-tertiary);
font-size: 22rpx;
}
</style>

View File

@@ -0,0 +1,308 @@
# 七月迭代 H5/C 端改动说明
本文根据《七月迭代实现与接口对接说明》整理,重点说明本期 H5/C 端需要调整的页面、接口和预期效果。
## 一、改动总览
本期 H5/C 端主要涉及以下功能:
1. 店铺 C 端登录限制。
2. 实名认证流程和实名状态展示。
3. 支付方式展示与支付提交。
4. 下架套餐老客户续费。
5. 预计套餐最终到期时间展示。
6. 套餐临期提醒。
7. 换货通知提醒。
8. C 端订单字段展示调整。
## 二、具体改动说明
### 1. 店铺 C 端登录限制(#41
#### 前端改动
资产验证后,根据接口返回结果判断是否允许登录。如果接口返回店铺已禁止 C 端登录,应直接展示后端错误提示,不继续获取或保存资产 Token。
#### 接口
```http
POST /api/c/v1/auth/verify-asset
```
管理后台相关接口:
```http
PUT /api/admin/shops/:id
GET /api/admin/shops
GET /api/admin/shops/:id
```
店铺字段:
```json
{
"client_login_disabled": true
}
```
#### 预期效果
- 被限制的店铺无法新登录 H5/C 端。
- 前端展示后端返回的业务错误信息。
- 已经签发的 Token 不会被强制吊销。
### 2. 实名认证流程(#62
#### 前端改动
资产初始化时只使用后端返回的实名策略和实名状态,不要根据卡、设备或前端本地规则自行推断。
重点使用以下字段:
- `effective_realname_policy`
- `realname_required`
- `real_name_status`
#### 接口
```http
GET /api/c/v1/asset/info?identifier=...
```
#### 预期效果
- `none`:不需要实名。
- `before_order`:下单前要求实名。
- `after_order`:下单后要求实名。
- 卡和设备存在策略冲突时,以设备最终生效策略为准。
- 设备只要有一张有效绑定卡已实名,即视为设备已实名。
### 3. 支付方式展示和提交(#48
#### 前端改动
支付按钮和支付方式列表必须使用后端返回的 `allowed_payment_methods`,不能在前端写死卡、设备的微信、支付宝或钱包规则。
下单或充值时必须将用户选择的 `payment_method` 传给后端;微信支付场景按接口要求传递 `app_type`
#### 接口
资产初始化:
```http
GET /api/c/v1/asset/info
```
充值前校验:
```http
GET /api/c/v1/wallet/recharge-check
```
创建订单:
```http
POST /api/c/v1/orders/create
```
钱包充值:
```http
POST /api/c/v1/wallet/recharge
```
#### 预期效果
- 不同资产类型显示正确的支付方式。
- 强充和普通充值遵循后端允许的支付配置。
- 前端无法绕过后端支付限制。
- 支付方式变化后,前端无需重新发布即可按接口结果生效。
### 4. 下架套餐老客户续费(#40
#### 前端改动
普通套餐列表中不展示下架套餐,但当前正在使用下架套餐的老客户仍可通过资产信息或历史订单获取套餐 ID并继续调用现有创建订单接口。
创建订单时传递:
- 选中的套餐 ID
- 资产 `identifier`
- 当前允许的 `payment_method`
#### 接口
获取当前资产套餐:
```http
GET /api/c/v1/asset/info
```
获取历史订单:
```http
GET /api/c/v1/orders
GET /api/c/v1/orders/:id
```
创建续费订单:
```http
POST /api/c/v1/orders/create
```
相关返回字段:
- 资产信息中的 `current_package_id`
- 历史订单中的 `package_ids`
#### 预期效果
- 老客户可以继续续费已下架套餐。
- 不新增专用续费接口,仍复用普通创建订单接口。
- 新客户和代理代购仍不能购买下架套餐。
- 历史订单数据不被修改。
### 5. 预计套餐最终到期时间(#46
#### 前端改动
资产详情、资产列表等页面展示后端返回的预计最终到期时间,不要只展示当前套餐的到期时间。
重点使用:
- `estimated_final_expires_at`
- `is_expiring`
- 剩余天数及临期等级字段(如接口返回)
#### 接口
```http
GET /api/c/v1/asset/info
```
后台资产解析接口也会返回相关字段:
```http
GET /api/admin/assets/resolve/:identifier
```
#### 预期效果
- 用户看到资产综合计算后的最终到期时间。
- 临期资产可以按后端返回的临期字段进行高亮。
- 避免因只显示当前套餐到期时间导致到期日期不准确。
### 6. 套餐临期提醒(#33
#### 前端改动
接入 C 端现有站内通知能力,读取未读通知并在合适时机展示弹窗或提醒入口。
#### 接口
获取未读数量:
```http
GET /api/c/v1/notifications/unread-count
```
获取通知列表:
```http
GET /api/c/v1/notifications
```
标记已读:
```http
PUT /api/c/v1/notifications/:id/read
```
#### 预期效果
- 在套餐剩余 15 天、7 天、3 天时触发提醒。
- 前端可按后端返回的临期等级进行展示。
- 03 天的临期提醒优先级最高。
- 通知通过 C 端站内消息展示,不新增企微业务员提醒。
### 7. 换货通知提醒(#188
#### 前端改动
继续使用现有 C 端通知接口,展示换货相关的未读通知,并支持点击后标记已读。
#### 接口
```http
GET /api/c/v1/notifications/unread-count
GET /api/c/v1/notifications
PUT /api/c/v1/notifications/:id/read
```
#### 预期效果
- 换货创建后,用户可以在 H5/C 端收到站内通知。
- 首页或通知入口能够展示未读数量。
- 用户查看后可以正常标记已读。
- 不新增营销投放、ERP 或其他业务单据。
### 8. C 端订单字段展示(#181
#### 前端改动
订单列表和订单详情使用后端返回的订单角色及资产标识字段。
重点字段:
- `purchase_role`
- `asset_identifier`
设备资产标识的显示规则:
1. 优先显示 `VirtualNo`
2. `VirtualNo` 为空时显示 `IMEI`
3. 不使用 SN 冒充订单设备标识;
4. 历史空数据不需要前端伪造。
#### 接口
```http
GET /api/c/v1/orders
GET /api/c/v1/orders/:id
```
#### 预期效果
- 订单中的购买角色显示准确。
- 卡展示正确的 ICCID 或资产标识。
- 设备展示 VirtualNo缺失时使用 IMEI。
- 订单资产标识与退款、换货等业务中的固化快照保持一致。
## 三、H5/C 端不需要改动的内容
以下事项本期明确不需要 H5/C 端新增功能或接口:
- #84 H5 首页隐藏设备下 ICCID本期确认不做。
- #73 行业卡未实名复机:保持现有逻辑。
- #94 状态同步和运营商回调:继续读取现有资产状态字段,无新增前端调用。
- 企业微信审批回调:由企微服务器调用,前端禁止调用。
- 原路退款、聚水潭、跨品类换货、分销码/佣金提现:本期不做。
## 四、前端联调注意事项
- 金额接口字段默认单位为“分”,页面展示时转换为“元”,提交时仍传整数分。
- `effective_realname_policy``allowed_payment_methods` 必须以后端返回值为准。
- 企微审批业务只读展示 `approval_provider``approval_status``approval_status_name`,不能再显示旧的人工通过、驳回或线下充值确认按钮。
- 企微回调接口由企微服务器调用H5/C 端不调用:
```http
GET/POST /api/callback/wecom/approval/:application_id
```
- 前端不要根据接口名称自行假设存在“下架套餐续费接口”,续费仍调用:
```http
POST /api/c/v1/orders/create
```

View File

@@ -0,0 +1,284 @@
# 七月迭代实现与接口对接说明
> 面向:产品、前端、测试和联调人员
> 范围:`deliver-july-iteration-confirmed-scope` 及本期确认“后端已完成,只需前端联调”的需求
> 接口细节:以 [`docs/admin-openapi.yaml`](../admin-openapi.yaml) 为准,本文只说明关键调用和字段变化。
## 一、先看这几个关键结论
1. **设备没有限速接口**:限速只允许对 IoT 卡 ICCID 操作,固定档位 `-18`,不通过设备绑定卡间接限速。
2. **企微模板在企微后台创建**:系统只配置应用、默认发起人、账号 userid 绑定、`template_id` 和控件映射,不在本系统设计审批节点。
3. **代理发起审批走默认发起人**:企微单据使用配置的默认成员发起,但退款、充值等本地业务单仍记录真实代理提交人。
4. **下架套餐续费不新增专用接口**:历史订单和资产信息返回稳定套餐 ID前端仍调用现有 C 端创建订单接口生成一张新订单。
5. **实名流程由后端返回值决定**:前端使用 `effective_realname_policy`,不能按卡或设备自行推断;设备有任意一张有效绑定卡已实名即视为已实名。
6. **支付按钮由后端返回值决定**:前端使用 `allowed_payment_methods`,不要自行写死卡/设备的微信、支付宝或钱包规则。
7. **设备批量分配只复用导入任务外壳**:复用任务表、队列、进度和结果页;不会进入原 Excel 创建设备逻辑。
8. **系列套餐批量授权后端原本就支持多选**:前端把多选套餐组装为 `packages[]` 调现有接口即可,不需要新后端接口。
## 二、本期需求怎么实现
### 2.1 本期新增或修改后端的需求
| 需求 | 实现方式 | 关键口径检查 |
| --- | --- | --- |
| #189 换货后退款套餐未失效 | 退款处理不再只按旧资产查套餐,而是按原订单和换货迁移关系定位新资产上的对应套餐权益 | 只失效该退款订单产生的权益,不影响其他订单套餐;无前端改动 |
| #188 换货 C 端提醒 | 创建物流换货单时写可靠通知事件,继续走现有 C 端站内通知和未读弹窗 | 不做营销投放、ERP、自动创建其他单据 |
| #182/#44 提交人和审批展示 | 退款、代理充值、换货列表/详情批量解析提交人 ID 和名称;企微详情已具备将审批节点 userid 批量映射系统账号的内部能力 | 当前退款/充值业务 DTO 只稳定返回提交人和审批状态,尚未直接返回审批节点人员列表;如页面必须展示具体审批人,仍需把已有投影接入业务查询 |
| #181 订单渠道和资产标识 | C 端新订单保存正确 `purchase_role`;卡返回 ICCID设备优先 VirtualNo、为空时返回 IMEI | 不用 SN 冒充订单设备标识;历史空数据不伪造 |
| #41 店铺 C 端登录限制 | 店铺增加 `client_login_disabled`C 端验证资产后、签发短期令牌前检查所属店铺 | 只阻止新登录,不吊销已有 Token平台库存保持原行为 |
| #53 卡/设备实名筛选 | 卡按自身实名状态过滤;设备通过有效绑定卡 `EXISTS` 实时判断 | 设备任意一张有效绑定卡已实名即为已实名;未建投影或 Worker |
| #57 退款中禁止换货 | 在现有换货创建入口前检查资产未终结退款 | 拒绝文案为“该资产存在退款申请”;不依赖企微实时接口 |
| #62 三种实名顺序 | 保留 `none/before_order/after_order`,补齐卡和设备最多 500 条批量修改C 端返回生效策略 | 批量全成全败;设备和下卡冲突时以设备策略为准 |
| #97 主钱包低余额预警 | 消费主钱包扣款事实,余额首次从不少于 100 元跌破 100 元时通知店铺业务员 | 阈值以下不重复;恢复后再次跌破可再次提醒;无业务员不猜接收人 |
| #33 套餐临期提醒 | 每日计算 15/7/3 天节点,提供后台/代理临期列表及数量,并向个人客户发送站内通知 | 高亮为 815、47、03 天03 天优先;不发企微业务员提醒 |
| #34 员工线下代充值 | 原充值创建入口在 `offline` 场景创建通用审批实例和企微提交事件;通过后幂等入主钱包 | 在线扫码充值后置;真实提交人、金额和凭证明文业务快照保留 |
| #35 退款企微审批 | 原退款申请关联唯一通用审批实例,企微标准终态驱动现有退款处理 | 不做原路退款;重复回调/轮询不会重复退款或失效套餐 |
| #37 企业微信审批 | 完成应用连接、通讯录同步、账号绑定、默认发起人、模板映射、提交、回调、详情查询和轮询恢复 | 模板/审批人规则由企微维护;超时结果未知不盲目重提 |
| #36 批量订购套餐 | 单列 CSV整批选择一个套餐和支付方式走独立异步任务逐行复用现有订单和钱包规则 | 不选择代理,不在 CSV 中逐行指定套餐;部分失败不影响其他行 |
| #40 下架套餐老客户续费 | 普通可购列表排除下架套餐;当前使用者可用历史订单或当前资产返回的套餐 ID 调现有下单接口创建新订单 | 无专用续费接口;新客户和代理代购仍拒绝;历史订单不修改 |
| #42 六类导出 | 在现有导出任务框架新增 IoT 卡、套餐、钱包流水、代理充值、退款、换货 datasource | 按现有数据权限和字段来源导出,不建设新字段权限平台 |
| #47 卡固定档位限速 | 后台选择固定档位,按 ICCID 调用 Gateway并记录操作审计和 Integration Log | 仅 IoT 卡;设备无接口;超时返回结果未知并人工核对 |
| #48 按资产类型配置支付方式 | `system_config` 分别保存卡、设备允许的 `wallet/wechat/alipay` 集合C 端返回业务场景交集,订单端再次校验 | 至少保留一种;强充和普通充值剔除钱包;前端不可绕过 |
| #49 设备批量分配 | 扩展现有设备导入任务的 `operation_type`,新增分配代理和设置套餐系列两个 CSV 分支 | CSV 单列 VirtualNo/IMEI/SN复用现有权限、分配、系列绑定和幂等规则 |
### 2.2 后端此前已经完成,主要由前端正确调用或展示
| 需求 | 后端现状 | 前端要做什么 |
| --- | --- | --- |
| #45 换货新旧资产展示/搜索 | 换货列表已分别返回新旧资产,并支持两个独立搜索参数 | 分别提供“旧资产”和“新资产”搜索框,不要继续共用一个字段 |
| #46 预计套餐到期 | 资产详情、C 端资产信息和相关列表已返回预计最终到期字段 | 展示 `estimated_final_expires_at`;按 `is_expiring` 或剩余天数高亮,不要只显示当前套餐到期时间 |
| #55 套餐分配生效条件 | 套餐和分配接口已支持默认值、覆盖值和最终生效值 | 创建/编辑套餐传 `expiry_base`;分配时传 `expiry_base_override`,展示 `effective_expiry_base` |
| #60 店铺联系电话搜索 | 店铺列表已支持 11 位联系电话精确查询 | 将输入值作为 `contact_phone` 查询参数传给店铺列表接口 |
| #86 资产换货标识和跳转 | 资产解析接口已返回 `exchange_trace.previous_asset/next_asset``can_view` | 仅 `can_view=true` 且存在资产 ID 时允许跳转;该需求前端已对接可保持现状 |
| #38 代理信用额度 | 角色默认额度、店铺实际额度、资金概况和负可用余额均已有接口 | 使用分单位字段;更新时携带钱包 `version`;余额为负数时正常展示 |
| #94 状态同步和运营商回调 | 后端回调、定时触发和原轮询链路已装配 | 通常无前端新调用;状态页面继续读取现有资产状态字段 |
| #96 店铺业务员 | 店铺创建/更新、候选人、列表/详情和筛选都已支持业务员 | 创建/编辑店铺选择 `business_owner_account_id`;列表可按该 ID 筛选并展示名称 |
| #98 换货新资产继承旧店铺 | 换货完成时后端自动继承旧资产店铺归属 | 前端继续调用原换货完成接口,不新增分配步骤 |
| #43 系列套餐批量授权 | 创建授权和管理套餐接口均支持 `packages[]`,单次 1100 项 | 页面实现套餐多选,一次提交整个数组;删除项使用 `remove=true` |
> 注意:#44 的“提交人”已经可以直接对接;“审批人”目前不是退款、充值列表的稳定返回字段。后端已经能从企微详情快照解析并映射 userid但当前前端不能把 `processor_id` 当作完整企微审批人列表。
### 2.3 本期明确不由后端处理
- #84 H5 首页隐藏设备下 ICCID纯前端显示调整但本期范围确认标记为“不做”。
- #63 授权列表滚动条和字段顺序:纯前端页面调整,需求已关闭。
- #73 行业卡未实名复机:后端保持原有行业卡放行逻辑,不改接口、不改前端。
- #99 原路退款、#52 聚水潭、#51 跨品类换货、#39 分销码/佣金提现:本期不做。
- #168#90#75#64:已关闭,本期不重新修改。
## 三、需求与接口对接表
### 3.1 资产、店铺、订单和换货
| 需求 | 接口 | 参数或返回变化 / 前端调用说明 |
| --- | --- | --- |
| #41 登录限制 | `PUT /api/admin/shops/:id``GET /api/admin/shops``GET /api/admin/shops/:id` | 更新请求增加可选 `client_login_disabled`;列表/详情返回同名布尔值。C 端仍调 `POST /api/c/v1/auth/verify-asset`,受限时直接展示后端错误,不会返回资产令牌 |
| #53 实名筛选 | `GET /api/admin/iot-cards/standalone``GET /api/admin/devices` | 查询参数增加 `real_name_status=0|1`;响应已有 `real_name_status``real_name_status_name` |
| #62 实名顺序 | `PATCH /api/admin/assets/:identifier/realname-mode``POST /api/admin/iot-cards/batch-update-realname-policy``POST /api/admin/devices/batch-update-realname-policy` | 单条传 `realname_policy`;批量传 `asset_ids[] + realname_policy`,最多 500 条 |
| #62/#48 C 端初始化 | `GET /api/c/v1/asset/info?identifier=...` | 使用 `effective_realname_policy``realname_required``real_name_status``allowed_payment_methods`;前端不要自行覆盖 |
| #45 换货搜索 | `GET /api/admin/exchanges` | 使用 `old_asset_keyword``new_asset_keyword` 两个独立参数,可同时传并按 AND 组合 |
| #57/#98 换货 | `POST /api/admin/exchanges``POST /api/admin/exchanges/:id/complete` | 请求结构不变;存在退款时创建接口返回业务错误;完成后店铺归属由后端继承 |
| #188 换货提醒 | `GET /api/c/v1/notifications/unread-count``GET /api/c/v1/notifications``PUT /api/c/v1/notifications/:id/read` | 无新增通知接口;前端继续使用现有未读数、列表和已读接口弹窗展示 |
| #181 订单字段 | `GET /api/admin/orders``GET /api/admin/orders/:id``GET /api/c/v1/orders``GET /api/c/v1/orders/:id` | 返回正确 `purchase_role``asset_identifier`;设备标识为 VirtualNo 优先、IMEI 兜底 |
| #40 下架套餐续费 | `GET /api/c/v1/asset/info``GET /api/c/v1/orders``GET /api/c/v1/orders/:id``POST /api/c/v1/orders/create` | 资产信息返回 `current_package_id`,历史订单返回 `package_ids`;前端把选中的套餐 ID、资产 `identifier` 和当前允许的 `payment_method` 传给原创建订单接口 |
| #46/#86 资产展示 | `GET /api/admin/assets/resolve/:identifier``GET /api/c/v1/asset/info` | 返回预计最终到期字段;后台解析额外返回 `exchange_trace`,跳转前检查 `can_view` |
### 3.2 店铺业务员、信用和套餐授权
| 需求 | 接口 | 参数或返回变化 / 前端调用说明 |
| --- | --- | --- |
| #60 联系电话搜索 | `GET /api/admin/shops?contact_phone=11位号码` | 精确查询;可与店铺名称、编号等条件组合 |
| #96 店铺业务员 | `GET /api/admin/shops/business-owner-candidates``POST /api/admin/shops``PUT /api/admin/shops/:id``GET /api/admin/shops` | 创建/更新传 `business_owner_account_id`;列表可用同名参数筛选,响应展示账号 ID、名称和可用状态 |
| #38 信用额度 | `PUT /api/admin/roles/:id/default-credit``PUT /api/admin/shops/:id/credit-limit``GET /api/admin/shops/fund-summary` | 角色接口配置新建代理默认值;店铺接口传 `credit_enabled + credit_limit + version`;金额单位均为分 |
| #55 套餐默认生效条件 | `POST /api/admin/packages``PUT /api/admin/packages/:id``GET /api/admin/packages/:id` | 请求使用 `expiry_base=from_activation|from_purchase`;响应展示默认生效条件名称 |
| #55 分配覆盖 | `POST /api/admin/shop-package-batch-allocations``PATCH /api/admin/shop-package-allocations/:id/expiry-base` | 分配时使用 `expiry_base_override``null` 表示跟随套餐默认值 |
| #43 系列套餐多选 | `POST /api/admin/shop-series-grants``PUT /api/admin/shop-series-grants/:id/packages` | `packages` 是 1100 项数组,每项包含 `package_id``cost_price`,删除时传 `remove=true` |
#### 3.2.1 #43 套餐价格展示和已授权/未授权区分
#43 不需要新增后端接口,前端按下面两个现有接口组合数据:
1. 调用 `GET /api/admin/packages?series_id={series_id}&page=1&page_size=100` 分页取得该系列全部套餐。列表项直接使用:
- `suggested_retail_price`:建议售价,单位分;未配置时为空。
- `cost_price`:公司成本价,单位分。
2. 编辑已有授权时,调用 `GET /api/admin/shop-series-grants/{grant_id}`,注意路径参数是授权记录 ID不是系列 ID。
3. 将授权详情的 `packages[].package_id` 组成已授权套餐 ID 集合。
4. 套餐列表项的 `id` 在该集合中显示“已授权”,否则显示“未授权”。授权详情 `packages[].cost_price` 是该次店铺授权成本价,不要拿它替代套餐列表的公司成本价。
5. 用户提交多选结果时,创建授权调用 `POST /api/admin/shop-series-grants`;编辑授权调用 `PUT /api/admin/shop-series-grants/{grant_id}/packages`。新增/调价项传 `package_id + cost_price`,删除项传 `package_id + remove=true`
套餐接口是分页接口;一个系列超过 100 个套餐时,前端必须继续请求后续页,再完成已授权集合标记。
### 3.3 审批、退款和员工线下代充值
| 需求 | 接口 | 参数或返回变化 / 前端调用说明 |
| --- | --- | --- |
| #181/#182/#44 退款 | `GET /api/admin/refunds``GET /api/admin/refunds/:id` | 新增/补齐 `asset_identifier``submitter_id``submitter_name``approval_provider``approval_status``approval_status_name`;资产标识是退款创建时固化的快照,卡为 ICCID设备按 VirtualNo 优先、IMEI 兜底;历史空快照不做兼容 |
| #182/#44 充值 | `GET /api/admin/agent-recharges``GET /api/admin/agent-recharges/:id` | 同上;线下代充值的审批状态只读展示 |
| #44 换货 | `GET /api/admin/exchanges``GET /api/admin/exchanges/:id` | 新增/补齐 `submitter_id``submitter_name` |
| #34 员工线下代充 | `POST /api/admin/agent-recharges` | 仍用原入口;`payment_method=offline` 时传目标 `shop_id`、金额、15 个 `payment_voucher_key` 和备注,创建后等待企微审批 |
| #35 退款申请 | `POST /api/admin/refunds` | 仍用原入口;请求结构保持订单、实收金额、申请金额、凭证、原因等业务字段,创建后等待企微审批 |
| #34/#35 旧按钮 | `POST /api/admin/agent-recharges/:id/offline-pay``POST /api/admin/refunds/:id/approve|reject` | 仅兼容存量旧审批记录;企微记录前端不得显示这些操作按钮,最终按发布配置停用 |
| #37 企微应用 | `POST/GET /api/admin/wecom/applications``POST /api/admin/wecom/applications/:id/test` | 配置 corp_id、agent_id、Secret、回调 Token、EncodingAESKey管理端按明文填写 |
| #37 默认发起人和成员 | `POST /api/admin/wecom/applications/:id/members/sync``GET /api/admin/wecom/applications/:id/members``PUT /api/admin/wecom/applications/:id/default-creator` | 先同步成员,再从可见成员中选择默认 `userid` |
| #37 账号绑定 | `PUT /api/admin/accounts/:id/wecom-binding` | 管理员选择应用可见成员,绑定 `(corp_id,userid)`;不做扫码绑定 |
| #37 场景模板 | `PUT /api/admin/wecom/scenes/:business_type``GET /api/admin/wecom/scenes` | `business_type``refund_approval``offline_recharge_approval`;传 `application_id``template_id``control_mapping[]` |
| #37 回调 | `GET/POST /api/callback/wecom/approval/:application_id` | 由企微服务器调用,前端无需调用 |
#### 3.3.1 企业微信审批 8 个接口的前端调用流程
企业微信配置页使用 8 个 `/api/admin/wecom` 接口账号绑定属于账号模块因此完整闭环是“8 个企微配置接口 + 1 个账号绑定接口”。应用凭据和场景写操作应只向超级管理员开放;前端收到 403 时不要降级绕过。
| 顺序 | 接口 | 页面动作与调用说明 |
| --- | --- | --- |
| 1 | `POST /api/admin/wecom/applications` | 保存应用。传 `corp_id``agent_id``name``secret``callback_token`、43 位 `encoding_aes_key``status=1`。相同 `corp_id + agent_id` 再次提交表示更新;保存响应中的 `id` 是后续 `application_id` |
| 2 | `GET /api/admin/wecom/applications?page=1&page_size=20` | 进入配置页或保存后刷新应用列表,展示连接时间、启用状态、默认发起人和凭据是否完整 |
| 3 | `POST /api/admin/wecom/applications/{id}/test` | 用户点击“测试连接”时调用;`data.success=true` 只表示成功取得 access_token不表示通讯录、模板和回调均已配置完成 |
| 4 | `POST /api/admin/wecom/applications/{id}/members/sync` | 连接成功后点击“同步成员”;后端拉取该自建应用可见范围,返回 `synced_count``synced_at` |
| 5 | `GET /api/admin/wecom/applications/{id}/members?page=1&page_size=20&keyword=...` | 查询最近同步的本地成员快照,`keyword` 可按姓名或 userid 搜索;用于默认发起人和账号绑定的选择器,不要允许手输一个未同步 userid |
| 6 | `PUT /api/admin/wecom/applications/{id}/default-creator` | 从成员选择器取 `userid`,请求体为 `{"userid":"zhangsan"}`。代理等非企微账号提交业务时,企微审批由该成员代为发起,但本地业务提交人仍保持真实账号 |
| 7 | `PUT /api/admin/wecom/scenes/{business_type}` | 分别保存退款和线下代充值模板映射;后端会实时读取企微模板详情并校验控件 ID、类型、必填控件和选择项 key校验失败时页面应保留用户输入并展示后端错误 |
| 8 | `GET /api/admin/wecom/scenes?page=1&page_size=20` | 进入场景页或保存后刷新,展示 `business_type_name`、模板名称、状态、最近校验时间和控件映射 |
应用保存请求示例:
```json
{
"corp_id": "wwxxxxxxxxxxxxxxxx",
"agent_id": 1000002,
"name": "测试环境审批应用",
"secret": "企微应用Secret",
"callback_token": "企微后台配置的回调Token",
"encoding_aes_key": "43位EncodingAESKey",
"status": 1
}
```
场景路径只支持:
- `refund_approval`:退款审批。
- `offline_recharge_approval`:员工线下代充值审批。
场景保存请求示例:
```json
{
"application_id": 1,
"template_id": "企微模板ID",
"control_mapping": [
{
"business_field": "refund_no",
"control_id": "Text-xxxxxxxx",
"control_type": "Text",
"option_mapping": {}
}
],
"status": 1
}
```
`control_mapping``control_id``control_type` 和选择项 key 必须来自企微后台已经创建的模板。退款可映射业务字段为 `refund_no``order_id``order_no``asset_identifier``asset_type``actual_received_amount``requested_refund_amount``refund_voucher_key``refund_reason``package_usage_id``submitter_id``submitter_name`;线下代充值可映射 `recharge_no``shop_id``shop_name``amount``amount_cent``payment_voucher_key``remark``submitter_id``submitter_name`。企微模板中的必填控件必须全部映射。
完成成员同步后,账号管理页还要调用:
```http
PUT /api/admin/accounts/{account_id}/wecom-binding
```
```json
{
"application_id": 1,
"userid": "zhangsan"
}
```
前端完整配置顺序为:保存应用 → 测试连接 → 同步成员 → 查询成员 → 设置默认发起人 → 给需要本人发起审批的系统账号绑定成员 → 保存两个业务场景 → 查询场景确认均已启用。应用可见范围变化后,应重新同步成员并检查默认发起人和账号绑定。
#### 3.3.2 配置完成后的业务审批流程
1. 前端继续调用原业务接口创建退款或员工线下代充值,不直接调用企微发起审批接口。
2. 后端保存业务单、通用审批实例和提交事件,并异步向企微发起审批;前端根据业务列表/详情的 `approval_provider``approval_status``approval_status_name` 只读展示进度。
3. `approval_status` 可能为:`0` 提交中、`1` 审批中、`2` 已通过、`3` 已拒绝、`4` 已撤销、`5` 通过后撤销、`6` 已删除、`7` 提交失败、`8` 提交结果未知。页面名称优先直接使用 `approval_status_name`
4. 企微回调和 Worker 轮询共同同步最终状态;`GET/POST /api/callback/wecom/approval/{application_id}` 只由企微服务器调用,前端禁止调用。
5. `approval_provider=wecom` 或存在 `approval_instance_id` 时,前端不得显示退款通过/驳回、线下充值确认等旧人工按钮。企微通过后,退款终结或钱包入账由后端自动幂等执行。
### 3.4 通知、批量任务、导出和 Gateway
| 需求 | 接口 | 参数或返回变化 / 前端调用说明 |
| --- | --- | --- |
| #97 后台低余额通知 | `GET /api/admin/notifications/unread-count``GET /api/admin/notifications``PUT /api/admin/notifications/:id/read` | 复用现有后台通知接口;业务员账号正常展示低余额通知 |
| #33 临期列表 | `GET /api/admin/expiring-assets` | 支持资产类型、关键词、店铺、套餐、剩余天数和日期范围;响应含 `summary``expiry_level``is_priority` |
| #33 C 端提醒 | `GET /api/c/v1/notifications/unread-count``GET /api/c/v1/notifications` | 复用现有站内通知,前端在 15/7/3 天节点按未读通知弹窗 |
| #36 批量订购上传 | `POST /api/admin/storage/upload-url` | `purpose=batch_purchase`,上传 UTF-8 单列 CSV 后取得 `file_key` |
| #36 批量订购任务 | `POST /api/admin/asset-package-batch-orders``GET /api/admin/asset-package-batch-orders``GET /api/admin/asset-package-batch-orders/:id` | 创建传 `file_key + package_id + payment_method`,线下支付另传 `voucher_keys[]`;详情返回逐行结果 |
| #42 业务导出 | `POST /api/admin/export-tasks``GET /api/admin/export-tasks``GET /api/admin/export-tasks/:id` | `scene` 使用 `iot_card/package/agent_wallet_transaction/agent_recharge/refund/exchange`,筛选条件放 `query` |
| #47 卡限速 | `PUT /api/admin/iot-cards/:iccid/speed-tier` | 请求只传 `code=-1..8`;设备页面不要展示限速入口 |
| #48 后台支付配置 | `GET /api/admin/system-configs``PUT /api/admin/system-configs/:key` | Key 为 `c2b.payment.card_allowed_methods``c2b.payment.device_allowed_methods`;更新请求的 `value` 是 JSON 数组字符串 |
| #48 C 端支付 | `GET /api/c/v1/asset/info``GET /api/c/v1/wallet/recharge-check``POST /api/c/v1/orders/create``POST /api/c/v1/wallet/recharge` | 展示和提交都使用后端返回的 `allowed_payment_methods`;创建订单时 `payment_method` 必传,微信场景按接口要求传 `app_type` |
| #49 分配 CSV 上传 | `POST /api/admin/storage/upload-url` | `purpose=device_batch_allocation`,上传单列 CSV 后取得 `file_key` |
| #49 创建设备分配任务 | `POST /api/admin/devices/import/allocations` | 传 `file_key + operation_type + target_id``operation_type=assign_shop|assign_series` |
| #49 查询任务 | `GET /api/admin/devices/import/tasks``GET /api/admin/devices/import/tasks/:id` | 复用原设备导入任务页面,新增展示 `operation_type``operation_name``target_id``status_name` |
### 3.5 前端静态 CSV 模板
本期需要前端提供两份静态文件模板,**都是 CSV不接受 Excel`.xls`/`.xlsx`**。文件使用 UTF-8 编码,允许 UTF-8 BOM最大 10MB最多 1000 行数据(不含表头)。每个文件只允许一列,不要添加空行、说明行或示例外的其他列。
#### 模板一:批量订购套餐
- 建议文件名:`批量订购套餐模板.csv`
- 上传用途:`purpose=batch_purchase`
- 套餐、支付方式和线下凭证由页面另外选择,不放在 CSV 中。
| 列序号 | 固定表头 | 必填 | 填写内容 |
| --- | --- | --- | --- |
| 1 | `资产标识` | 是 | 每行一个资产标识。卡支持 ICCID、VirtualNo 或 MSISDN设备支持 VirtualNo、IMEI 或 SN |
```csv
资产标识
89860012345678901234
CARD-VIRTUAL-0001
DEVICE-VIRTUAL-0001
860123456789012
```
#### 模板二:设备批量分配
- 建议文件名:`设备批量分配模板.csv`
- 上传用途:`purpose=device_batch_allocation`
- 目标代理或套餐系列由页面另外选择,不放在 CSV 中。“分配代理”和“设置套餐系列”可以共用这一份模板。
| 列序号 | 固定表头 | 必填 | 填写内容 |
| --- | --- | --- | --- |
| 1 | `设备标识` | 是 | 每行一个设备标识,支持 VirtualNo、IMEI 或 SN |
```csv
设备标识
DEVICE-VIRTUAL-0001
860123456789012
SN202607250001
```
> 前端下载的静态模板可以只保留表头,上述数据行仅用于说明格式。资产标识必须按文本原样保存,不得转换为科学计数法、浮点数或截断前导零。
## 四、前端本期最容易漏掉的工作
- 换货列表拆成新、旧资产两个搜索参数。
- 资产预计到期展示使用“预计最终到期”,并按临期字段高亮。
- 套餐分配页面传递默认/覆盖生效条件。
- 店铺页面接入联系电话查询、业务员选择和 C 端登录限制开关。
- 系列套餐授权页面真正使用现有 `packages[]` 做多选提交。
- C 端实名流程只读取 `effective_realname_policy`
- C 端支付按钮只读取 `allowed_payment_methods`
- 历史订单续费继续调用原创建订单接口,不等待新续费接口。
- 企微退款和线下代充值只读展示审批状态,隐藏本地人工审批按钮。
- 卡页面提供固定档位限速;设备页面不得出现限速入口。
- 设备批量分配继续复用原设备导入任务列表/详情页面,但根据 `operation_type` 改标题和结果说明。
## 五、联调和验收边界
- 当前有两个需要在联调时明确的接口边界:
- #44:退款/充值列表已经返回提交人和审批状态,但没有直接返回企微审批节点人员列表;现有 userid 映射能力尚未接入这两个业务 DTO。
- #49:设备批量分配创建服务允许平台和代理账号,但复用的设备导入任务列表/详情 Handler 目前仍沿用“仅平台用户可查看”的旧限制。若本期只允许平台操作则前端应隐藏代理入口;若要求代理自行查看任务,需要再统一后端权限。
- OpenAPI 已生成,新增路径和 DTO 已检查;设备限速旧契约不存在。
- 本次没有执行数据库迁移、完整构建、自动化测试、LSP 或真实企微/Gateway 闭环。
- 企微、Gateway、Redis/Asynq、对象存储配置和回滚步骤见 [`七月迭代联调交付说明.md`](七月迭代联调交付说明.md)。
- 涉及金额的字段默认单位为分;前端展示时统一转换,提交时不要传浮点元金额。

View File

@@ -1,6 +1,6 @@
# 个人客户站内通知
为个人客户增加通知入口,支持查询通知、查看未读数以及标记已读。所有接口均需要登录,使用 `Bearer Token` 鉴权。
为个人客户增加通知入口,支持查询通知、查看未读数以及标记已读。进入首页时自动查询未读通知并弹窗展示;多条通知使用左右滑动查看,弹窗打开时第一条自动标记已读,滑动查看其他通知时立即标记对应通知已读。弹窗支持关闭。所有接口均需要登录,使用 `Bearer Token` 鉴权。
## 查询通知列表
@@ -12,6 +12,7 @@
| --- | --- | --- |
| `page` | 否 | 页码,默认 1范围 110000 |
| `page_size` | 否 | 每页数量,默认 20范围 150 |
| `is_read` | 否 | 已读状态;`false` 仅查询未读,`true` 仅查询已读,不传时查询全部 |
返回:包含通知列表 `items`、当前页 `page`、每页数量 `size` 和总数 `total`。通知项包含标题、正文、类型、类别、级别、关联资源、已读状态及创建/已读时间。
@@ -39,6 +40,14 @@
返回:`{ "count": 3, "display_count": "3" }`,数量超过 99 时 `display_count``99+`
## 首页未读通知弹窗
- 用户进入首页后,调用 `GET /api/c/v1/notifications`,传入 `is_read=false``page=1``page_size=50`,只展示当前未读通知。
- 有多条未读通知时默认展示第一条,弹窗打开后立即调用单条已读接口标记第一条。
- 用户左右滑动切换到其他通知时,视为已查看并立即调用单条已读接口标记当前通知。
- 点击关闭图标只关闭弹窗,不会将未查看的其他通知批量标记为已读;下次进入首页仍可继续展示剩余未读通知。
- 通知的 `ref_type``ref_id``ref_key` 仅用于资源定位或展示,不直接拼接页面路由。
## 通用约定
- 所有接口均使用 `Bearer Token` 鉴权。

View File

@@ -95,6 +95,9 @@
<view class="bottom-spacer"></view>
<NotificationPopup :show="notificationPopupShow" :items="notificationItems"
:current="notificationPopupCurrent" @change="onNotificationChange" @close="closeNotificationPopup" />
<!-- 日期选择器弹窗 -->
<up-datetime-picker v-if="!userInfo.isDevice" :show="showStartDatePicker" v-model="startDateTimestamp"
mode="date" @confirm="onStartDateConfirm" @cancel="showStartDatePicker=false"
@@ -123,6 +126,7 @@
import WifiCard from '@/components/WifiCard.vue';
import FunctionCard from '@/components/FunctionCard.vue';
import FloatingButton from '@/components/FloatingButton.vue';
import NotificationPopup from '@/components/NotificationPopup.vue';
import {
assetApi,
deviceApi,
@@ -213,6 +217,10 @@
let smsCode = ref('');
let indexEntryChecking = ref(false);
let notificationUnreadCount = ref(0);
let notificationPopupShow = ref(false);
let notificationItems = ref([]);
let notificationPopupCurrent = ref(0);
const notificationReadPending = new Set();
const formatDate = (dateStr) => {
if (!dateStr) return '-';
@@ -784,6 +792,47 @@
}
};
const markNotificationRead = async (item) => {
if (!item || item.is_read || notificationReadPending.has(item.id)) return;
notificationReadPending.add(item.id);
try {
await notificationApi.markRead(item.id);
item.is_read = true;
item.read_at = new Date().toISOString();
notificationUnreadCount.value = Math.max(notificationUnreadCount.value - 1, 0);
} catch (error) {
console.error('标记首页通知已读失败', error);
} finally {
notificationReadPending.delete(item.id);
}
};
const loadUnreadNotifications = async () => {
try {
const data = await notificationApi.getList(1, 50, false);
const items = (data?.items || []).filter(item => !item.is_read);
if (!items.length) return;
notificationItems.value = items;
notificationPopupCurrent.value = 0;
notificationPopupShow.value = true;
// 弹窗打开即视为用户已看到第一条通知。
markNotificationRead(items[0]);
} catch (error) {
console.error('加载首页未读通知失败', error);
}
};
const onNotificationChange = (event) => {
const index = Number(event?.detail?.current ?? event?.current ?? 0);
notificationPopupCurrent.value = index;
markNotificationRead(notificationItems.value[index]);
};
const closeNotificationPopup = () => {
notificationPopupShow.value = false;
};
onMounted(() => {
initCurrentMonth();
});
@@ -791,6 +840,7 @@
onShow(() => {
handleIndexEntry();
loadNotificationUnreadCount();
loadUnreadNotifications();
});
</script>