diff --git a/.gitea/workflows/deploy.yaml b/.gitea/workflows/deploy.yaml index b7d56fb..fb52fb7 100644 --- a/.gitea/workflows/deploy.yaml +++ b/.gitea/workflows/deploy.yaml @@ -1,9 +1,9 @@ -name: 构建并部署前端到测试环境 +name: 构建并部署前端到生产环境 on: push: branches: - - main + - develop - dev - test @@ -27,7 +27,7 @@ jobs: - name: 设置镜像标签 id: tag run: | - if [ "${{ github.ref }}" = "refs/heads/main" ]; then + if [ "${{ github.ref }}" = "refs/heads/develop" ]; then echo "tag=latest" >> $GITHUB_OUTPUT elif [ "${{ github.ref }}" = "refs/heads/dev" ]; then echo "tag=dev" >> $GITHUB_OUTPUT @@ -51,15 +51,11 @@ jobs: docker push ${{ env.IMAGE_NAME }}:${{ steps.tag.outputs.tag }} docker push ${{ env.IMAGE_NAME }}:${{ github.sha }} - - name: 部署到本地(仅 main 分支) - if: github.ref == 'refs/heads/main' + - name: 部署到本地(仅 develop 分支) + if: github.ref == 'refs/heads/develop' run: | mkdir -p ${{ env.DEPLOY_DIR }} - echo "当前工作目录: $(pwd)" - echo "当前目录内容:" - ls -la - echo "更新部署配置文件..." cp -v docker-compose.prod.yml ${{ env.DEPLOY_DIR }}/ diff --git a/Dockerfile b/Dockerfile index 218ed88..98a0e61 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,25 +12,28 @@ WORKDIR /build # 安装 pnpm RUN corepack enable && corepack prepare pnpm@10.15.0 --activate -# 设置 npm 镜像源 +# 设置 pnpm 镜像源 RUN pnpm config set registry https://registry.npmmirror.com # 复制 package.json 和 pnpm-lock.yaml(利用 Docker 缓存) COPY package.json pnpm-lock.yaml ./ -# 安装依赖 +# 安装所有依赖 RUN pnpm install --frozen-lockfile +# 安装 git (用于获取 commit 信息) +RUN apk add --no-cache git + # 复制源代码 COPY . . -# 构建 H5 测试版本 -RUN pnpm build:h5-test +# 使用 development 环境构建 H5 +RUN pnpm run build:h5 --mode default # ================================ # 阶段 2: 运行阶段 # ================================ -FROM --platform=linux/amd64 nginx:alpine +FROM --platform=linux/amd64 nginx:1.31.2-alpine # 使用阿里云镜像源加速 RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 6bc4bd3..0b3bd1f 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -9,6 +9,9 @@ services: - '3002:80' networks: - junhong-network + tmpfs: + - /run:rw + - /tmp:rw healthcheck: test: ['CMD', 'wget', '--no-verbose', '--tries=1', '--spider', 'http://127.0.0.1:80/health'] interval: 30s diff --git a/docker/nginx.conf b/docker/nginx.conf index 807571c..8a75535 100644 --- a/docker/nginx.conf +++ b/docker/nginx.conf @@ -17,6 +17,16 @@ server { add_header Content-Type text/plain; } + location /api/ { + proxy_pass https://cmp-api.boss160.cn; + proxy_ssl_server_name on; + proxy_ssl_name cmp-api.boss160.cn; + proxy_set_header Host cmp-api.boss160.cn; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + location /h5/ { try_files $uri $uri/ /h5/index.html; } diff --git a/docs/通知.md b/docs/通知.md new file mode 100644 index 0000000..05a5270 --- /dev/null +++ b/docs/通知.md @@ -0,0 +1,305 @@ +# 通知接口 + +服务地址:`https://cmp-api.boss160.cn` + +认证方式:`Bearer ` + +## 通知枚举 + +### 通知类别 + +| 值 | 说明 | +| ---------- | --- | +| `approval` | 审批 | +| `expiry` | 临期 | +| `sync` | 同步 | +| `system` | 系统 | + +### 通知级别 + +| 值 | 说明 | +| ---------- | --- | +| `info` | 提示 | +| `warning` | 警告 | +| `error` | 错误 | +| `critical` | 严重 | + +## 查询通知未读数 + +### GET /api/admin/notifications/unread-count + +查询当前认证后台账号的未过期未读通知数量,超过 99 条时显示 `99+`。 + +#### 成功响应 + +```json +{ + "code": 0, + "data": { + "count": 120, + "display_count": "99+" + }, + "msg": "success", + "timestamp": "2026-07-25T00:00:00Z" +} +``` + +| 字段 | 类型 | 说明 | +| ------------------ | ------- | --------------------- | +| data.count | integer | 未读通知数量 | +| data.display_count | string | 徽标显示文本,超过 99 时为 `99+` | + +## 查询通知未读分类汇总 + +### GET /api/admin/notifications/unread-summary + +查询当前账号未过期通知的总未读数及分类数量。 + +#### 成功响应 + +```json +{ + "code": 0, + "data": { + "approval": 2, + "expiry": 3, + "sync": 1, + "system": 0, + "total": 6 + }, + "msg": "success", + "timestamp": "2026-07-25T00:00:00Z" +} +``` + +| 字段 | 类型 | 说明 | +| ------------- | ------- | ------- | +| data.approval | integer | 审批类未读数量 | +| data.expiry | integer | 临期类未读数量 | +| data.sync | integer | 同步类未读数量 | +| data.system | integer | 系统类未读数量 | +| data.total | integer | 未读通知总数 | + +## 查询通知列表 + +### GET /api/admin/notifications + +查询当前认证后台账号的未过期通知,按创建时间和通知 ID 倒序返回。 + +#### 请求参数 + +| 参数 | 类型 | 必填 | 说明 | +| --------- | ------- | --- | ---------------------------------------- | +| category | string | 否 | 通知类别:`approval`、`expiry`、`sync`、`system` | +| type | string | 否 | 稳定通知类型 | +| severity | string | 否 | 通知级别:`info`、`warning`、`error`、`critical` | +| is_read | boolean | 否 | 已读状态;不传查询全部 | +| page | integer | 否 | 页码,默认 1,范围 1~10000 | +| page_size | integer | 否 | 每页数量,默认 20,范围 1~50 | + +#### 成功响应 + +```json +{ + "code": 0, + "data": { + "items": [ + { + "id": 1, + "category": "approval", + "type": "refund_approval_pending", + "severity": "warning", + "title": "退款审批待处理", + "body": "退款单 REFUND202607250001 正在审批中", + "is_read": false, + "read_at": null, + "ref_type": "refund", + "ref_id": "100", + "ref_key": "REFUND202607250001", + "created_at": "2026-07-25T00:00:00Z" + } + ], + "page": 1, + "size": 20, + "total": 1 + }, + "msg": "success", + "timestamp": "2026-07-25T00:00:00Z" +} +``` + +#### 返回字段 + +| 字段 | 类型 | 说明 | +| ------------------ | ----------- | ---------------- | +| data.items | array | 通知列表 | +| data.page | integer | 当前页码 | +| data.size | integer | 每页数量 | +| data.total | integer | 总数量 | +| items[].id | integer | 通知 ID | +| items[].category | string | 通知类别 | +| items[].type | string | 稳定通知类型 | +| items[].severity | string | 通知级别 | +| items[].title | string | 纯文本标题 | +| items[].body | string | 纯文本正文 | +| items[].is_read | boolean | 是否已读 | +| items[].read_at | string/null | 首次已读时间 | +| items[].ref_type | string | 受控资源类型,不是前端路由 | +| items[].ref_id | string | 受控资源数字 ID 字符串 | +| items[].ref_key | string | 受控资源稳定 Key 或展示快照 | +| items[].created_at | string | 创建时间 | + +## 标记单条通知已读 + +### PUT /api/admin/notifications/{id}/read + +标记当前账号的一条通知为已读。通知不存在、属于其他账号或已经已读时均幂等成功。 + +#### 路径参数 + +| 参数 | 类型 | 必填 | 说明 | +| --- | ------- | --- | ------------ | +| id | integer | 是 | 通知 ID,最小值为 0 | + +#### 请求体 + +| 字段 | 类型 | 必填 | 说明 | +| --- | ------- | --- | --------------- | +| id | integer | 是 | 通知 ID,必须与路径参数一致 | + +请求示例: + +```json +{ + "id": 1 +} +``` + +#### 成功响应 + +```json +{ + "code": 0, + "data": { + "success": true + }, + "msg": "success", + "timestamp": "2026-07-25T00:00:00Z" +} +``` + +## 批量标记通知已读 + +### PUT /api/admin/notifications/read-all + +批量标记当前账号通知为已读。未传类别时更新全部未过期未读通知,传类别时只更新对应类别。 + +#### 请求体 + +| 字段 | 类型 | 必填 | 说明 | +| -------- | ------ | --- | ---------------------------------------- | +| category | string | 否 | 通知类别:`approval`、`expiry`、`sync`、`system` | + +请求示例: + +```json +{ + "category": "approval" +} +``` + +#### 成功响应 + +```json +{ + "code": 0, + "data": { + "updated_count": 2 + }, + "msg": "success", + "timestamp": "2026-07-25T00:00:00Z" +} +``` + +| 字段 | 类型 | 说明 | +| ------------------ | ------- | ----------- | +| data.updated_count | integer | 本次实际更新的通知数量 | + +## 解析通知受控目标 + +### GET /api/admin/notifications/{id}/target + +校验通知属于当前账号后,复核目标资源当前权限,返回白名单结构化目标。不会返回任意 URL。 + +#### 路径参数 + +| 参数 | 类型 | 必填 | 说明 | +| --- | ------- | --- | ------------ | +| id | integer | 是 | 通知 ID,最小值为 0 | + +#### 成功响应 + +```json +{ + "code": 0, + "data": { + "available": true, + "target_type": "refund_detail", + "target_id": 100, + "target_key": "REFUND202607250001" + }, + "msg": "success", + "timestamp": "2026-07-25T00:00:00Z" +} +``` + +| 字段 | 类型 | 说明 | +| ---------------- | ------------ | ------------------ | +| data.available | boolean | 当前账号是否仍可访问目标 | +| data.target_type | string | 前端白名单目标类型,空表示不支持跳转 | +| data.target_id | integer/null | ID 型目标业务主键 | +| data.target_key | string | Key 型目标稳定定位值 | + +### target_type 白名单 + +| 值 | 说明 | +| ----------------------- | ------- | +| `refund_detail` | 退款详情 | +| `agent_recharge_detail` | 代理充值详情 | +| `wecom_approval_detail` | 企微审批详情 | +| `iot_card_detail` | IoT 卡详情 | +| `device_detail` | 设备详情 | +| `expiring_asset_list` | 临期资产列表 | +| `shop_fund_summary` | 店铺资金概况 | +| `system_config` | 系统配置 | + +## 业务规则 + +- 通知只返回当前认证后台账号的未过期通知。 +- `ref_type`、`ref_id` 和 `ref_key` 是受控资源引用,不是前端 URL。 +- 点击通知后必须调用目标解析接口,由 `target_type` 和 `available` 决定是否跳转。 +- `available=false` 或 `target_type` 为空时,只展示通知正文,不执行跳转。 +- 前端维护 `target_type` 到页面的白名单映射,不得根据通知字段拼接任意 URL。 +- 已读操作必须支持幂等调用。 + +## 错误响应 + +适用于以上接口: + +| HTTP 状态码 | 说明 | +| -------- | --------- | +| 400 | 请求参数错误 | +| 401 | 未认证或认证已过期 | +| 403 | 无权访问 | +| 500 | 服务器内部错误 | + +错误响应示例: + +```json +{ + "code": 1001, + "data": {}, + "msg": "参数验证失败", + "timestamp": "2026-07-25T00:00:00Z" +} +``` diff --git a/openspec/changes/add-notification-entry-and-unread-reminder/design.md b/openspec/changes/add-notification-entry-and-unread-reminder/design.md new file mode 100644 index 0000000..bb6b40d --- /dev/null +++ b/openspec/changes/add-notification-entry-and-unread-reminder/design.md @@ -0,0 +1,29 @@ +## Context + +The notification API returns the response payload through the existing request wrapper, so frontend API methods should expose the typed `data` payload directly. The home page is shared by agent and enterprise accounts and is loaded after login through `/pages/agent-system/home/index`. + +## Goals / Non-Goals + +- Goals: expose notifications, show the newest unread message after login, and keep server read state aligned with what the user has actually seen. +- Non-Goals: notification target resolution, controlled-resource routing, push notifications, or a separate manual read button. + +## Decisions + +- Decision: add a dedicated notification page for the notification entry. The entry is placed in the home page's top user-information card, and the page renders notification content only; notification rows have no click navigation. +- Decision: the login page writes a short-lived pending-reminder marker after storing a valid session. The home page consumes the marker after loading the authenticated user and requests the newest unread notification. +- Decision: use `GET /api/admin/notifications/unread-count` for the entry badge and `GET /api/admin/notifications?is_read=false&page=1&page_size=1` for the login reminder. The notification page requests the newest notification page with `page_size=50`. +- Decision: call `PUT /api/admin/notifications/{id}/read` as soon as an unread notification becomes visible in the reminder or notification list, update local state optimistically, and treat the API as idempotent. +- Decision: if the unread-count or reminder request fails, keep the home page usable and do not show a fabricated notification. The normal request interceptor continues to handle authentication failures. + +## Risks / Trade-offs + +- Marking on render means a message can become read before the user finishes reading it; this follows the requested “seen means read” behavior. +- A notification page displays the first 50 newest records; additional pagination is outside this change unless the API data requires it. + +## Migration Plan + +No data migration is needed. Deploy the frontend after the notification endpoints and icon asset are available. Rolling back removes the entry and UI without changing notification records. + +## Open Questions + +- None for the stated behavior. The entry opens the notification page, while notification content itself never navigates to a controlled target. diff --git a/openspec/changes/add-notification-entry-and-unread-reminder/proposal.md b/openspec/changes/add-notification-entry-and-unread-reminder/proposal.md new file mode 100644 index 0000000..294fba1 --- /dev/null +++ b/openspec/changes/add-notification-entry-and-unread-reminder/proposal.md @@ -0,0 +1,20 @@ +# Change: Add notification entry and unread reminder + +## Why + +The backend notification contract is documented in `docs/通知.md`, but the frontend does not expose notifications to authenticated agent or enterprise users. Users also have no immediate way to notice newly issued unread messages after signing in. + +## What Changes + +- Add a notification shortcut to the home page using `src/static/icons/通知.png`. +- Add a notification page that lists the current account's notifications in newest-first order. +- Show the newest unread notification in a reminder modal after a successful login redirects to the home page. +- Mark a notification as read when it is rendered as visible to the user; no manual “mark as read” action is required. +- Do not resolve `target` data and do not navigate from a notification to any controlled resource. +- Display the unread count on the home-page notification entry when it is available. + +## Impact + +- Affected specs: `notifications` (new capability) +- Affected code: `src/api/notifications.ts`, `src/pages/agent-system/home/index.vue`, `src/pages/agent-system/notifications/index.vue`, `src/pages.json`, and the login-to-home session flow +- Existing user-supplied assets remain in place: `docs/通知.md` and `src/static/icons/通知.png` diff --git a/openspec/changes/add-notification-entry-and-unread-reminder/specs/notifications/spec.md b/openspec/changes/add-notification-entry-and-unread-reminder/specs/notifications/spec.md new file mode 100644 index 0000000..7cfee9f --- /dev/null +++ b/openspec/changes/add-notification-entry-and-unread-reminder/specs/notifications/spec.md @@ -0,0 +1,91 @@ +## ADDED Requirements + +### Requirement: Home notification entry + +The authenticated home page SHALL expose a notification entry for both supported account types. The entry MUST use `src/static/icons/通知.png`, display the current unread count when available, and open the notification page. Notification content MUST NOT resolve controlled targets or navigate to business resources. + +#### Scenario: Agent sees notification entry + +- **GIVEN** an authenticated agent is viewing the home page +- **WHEN** the unread-count request succeeds +- **THEN** the home page shows the notification icon and the returned unread badge text +- **AND** activating the entry opens the notification page without resolving a target + +#### Scenario: Enterprise sees notification entry + +- **GIVEN** an authenticated enterprise user is viewing the home page +- **WHEN** the home page renders its shortcuts +- **THEN** the notification entry is available with the same icon and behavior + +#### Scenario: Unread-count failure does not block home + +- **GIVEN** the authenticated home page has loaded +- **WHEN** the unread-count request fails +- **THEN** the home page remains usable +- **AND** no fabricated unread count is displayed + +### Requirement: Notification list display + +The notification page SHALL request the current account's non-expired notifications from `GET /api/admin/notifications`, display them newest first using the server order, and show each notification's title, body, category, severity, and creation time. Notification rows MUST be display-only and MUST NOT invoke `GET /api/admin/notifications/{id}/target` or navigate to another business page. + +#### Scenario: Latest notifications are displayed + +- **GIVEN** the notification page is opened +- **WHEN** the notification list request succeeds +- **THEN** the newest notification is rendered first +- **AND** each rendered notification shows its text content and read state + +#### Scenario: No notifications + +- **GIVEN** the notification page is opened +- **WHEN** the server returns an empty item list +- **THEN** the page shows an empty state +- **AND** it does not attempt target resolution + +### Requirement: Post-login unread reminder + +After a successful login redirects to the home page, the system SHALL request the newest unread notification and show it in a reminder modal when one exists. The newest unread notification MUST be the default displayed message. The reminder MUST not navigate to a controlled target. + +#### Scenario: Newest unread notification is shown after login + +- **GIVEN** a user has successfully logged in and is redirected to the home page +- **AND** at least one unread notification exists +- **WHEN** the home page finishes loading the authenticated session +- **THEN** a reminder modal displays the newest unread notification's title and body by default + +#### Scenario: No unread notification + +- **GIVEN** a user has successfully logged in and is redirected to the home page +- **WHEN** the unread notification query returns no items +- **THEN** no reminder modal is shown + +#### Scenario: Reminder query failure + +- **GIVEN** a user has successfully logged in and is redirected to the home page +- **WHEN** the newest unread notification query fails +- **THEN** the home page remains usable +- **AND** no reminder modal is shown + +### Requirement: Read on visibility + +The system SHALL mark an unread notification as read when its content becomes visible in the reminder modal or notification list. The UI MUST NOT require a manual read button, and it MUST update the local read state after initiating the read request. + +#### Scenario: Reminder marks visible notification read + +- **GIVEN** the newest unread notification is shown in the post-login reminder modal +- **WHEN** the modal becomes visible +- **THEN** the client calls `PUT /api/admin/notifications/{id}/read` for that notification +- **AND** the notification is treated as read locally without a manual action + +#### Scenario: List marks visible unread notifications read + +- **GIVEN** the notification page renders one or more unread notifications +- **WHEN** those notification rows become visible +- **THEN** the client initiates an idempotent read request for each visible unread notification +- **AND** no manual read control is rendered + +#### Scenario: Read request is idempotent + +- **GIVEN** a notification has already been marked read +- **WHEN** the client repeats the read request +- **THEN** the UI remains in the read state and the repeated request does not create a user-visible error diff --git a/openspec/changes/add-notification-entry-and-unread-reminder/tasks.md b/openspec/changes/add-notification-entry-and-unread-reminder/tasks.md new file mode 100644 index 0000000..58dcf7b --- /dev/null +++ b/openspec/changes/add-notification-entry-and-unread-reminder/tasks.md @@ -0,0 +1,18 @@ +## 1. API and session flow + +- [x] 1.1 Add typed notification models and list, unread-count, and single-read API methods from `docs/通知.md`. +- [x] 1.2 Set a pending notification-reminder marker after a valid login session is stored. + +## 2. Notification UI + +- [x] 2.1 Add the notification page and register it in `src/pages.json`. +- [x] 2.2 Add the notification shortcut and unread badge to the top user-information card using `src/static/icons/通知.png`. +- [x] 2.3 Render notification content without target resolution or controlled-resource navigation. +- [x] 2.4 Show the newest unread notification in a post-login home-page reminder modal. +- [x] 2.5 Mark each notification read when it becomes visible and remove the manual read action. + +## 3. Verification + +- [x] 3.1 Run `pnpm type-check`. +- [x] 3.2 Run the relevant ESLint and style checks. +- [x] 3.3 Verify the login-to-home reminder, newest-first display, entry badge, and read-on-visible behavior. diff --git a/src/api/notifications.ts b/src/api/notifications.ts new file mode 100644 index 0000000..79a86a6 --- /dev/null +++ b/src/api/notifications.ts @@ -0,0 +1,72 @@ +/** + * 通知模块 API + */ +import { get, put } from '@/utils/request'; + +export type NotificationCategory = 'approval' | 'expiry' | 'sync' | 'system'; +export type NotificationSeverity = 'info' | 'warning' | 'error' | 'critical'; + +/** + * 通知记录 + */ +export interface NotificationItem { + id: number; + category: NotificationCategory; + type: string; + severity: NotificationSeverity; + title: string; + body: string; + is_read: boolean; + read_at: string | null; + ref_type?: string | null; + ref_id?: string | null; + ref_key?: string | null; + created_at: string; +} + +export interface NotificationListParams { + category?: NotificationCategory; + type?: string; + severity?: NotificationSeverity; + is_read?: boolean; + page?: number; + page_size?: number; +} + +export interface NotificationListResponse { + items: NotificationItem[]; + page: number; + size: number; + total: number; +} + +export interface NotificationUnreadCountResponse { + count: number; + display_count: string; +} + +/** + * 查询通知列表 + * GET /api/admin/notifications + */ +export function getNotifications(params?: NotificationListParams) { + return get('/api/admin/notifications', { params }); +} + +/** + * 查询通知未读数 + * GET /api/admin/notifications/unread-count + */ +export function getNotificationUnreadCount() { + return get('/api/admin/notifications/unread-count'); +} + +/** + * 标记单条通知已读 + * PUT /api/admin/notifications/{id}/read + */ +export function markNotificationRead(id: number) { + return put<{ success: boolean }>(`/api/admin/notifications/${id}/read`, { + data: { id }, + }); +} diff --git a/src/components/AgentFundSummaryCard.vue b/src/components/AgentFundSummaryCard.vue index ddafca6..d6a13e8 100644 --- a/src/components/AgentFundSummaryCard.vue +++ b/src/components/AgentFundSummaryCard.vue @@ -1,24 +1,142 @@ - - - diff --git a/src/components/ShopCascadePicker.vue b/src/components/ShopCascadePicker.vue index 293ee2d..db57606 100644 --- a/src/components/ShopCascadePicker.vue +++ b/src/components/ShopCascadePicker.vue @@ -1,107 +1,3 @@ - -